Action system improvements

- Actions/spells now display their summary, not their description
- All save branches and attack branches run when there are no targets
- Improved action logging
- Index branch lets you customise a choice of children to run
This commit is contained in:
Stefan Zermatten
2022-03-09 01:31:09 +02:00
parent c68667be9c
commit 788cbb182d
15 changed files with 201 additions and 53 deletions

View File

@@ -11,11 +11,11 @@ export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
// Log the name and summary
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
@@ -33,7 +33,7 @@ export default function applyAction(node, {creature, targets, scope, log}){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
applyChildren(node, {creature, targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({attack, scope, log});
@@ -65,6 +65,13 @@ function applyAttackWithoutTarget({attack, scope, log}){
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
}
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
@@ -106,10 +113,10 @@ function applyAttackToTarget({attack, target, scope, log}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
} else {
scope['$attackMiss'] = true;
scope['$attackHit'] = {value: true};
}
} else {
log.content.push({
@@ -161,15 +168,10 @@ function applyCrits(value, scope){
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
scope['$criticalMiss'] = {value: true};
}
}
return {criticalHit, criticalMiss};

View File

@@ -16,17 +16,47 @@ export default function applyBranch(node, {
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length){
recalculateCalculation(prop.condition, scope, log);
if (!isFinite(prop.condition?.value)) {
log.content.push({
name: 'Branch Error',
value: 'Index did not resolve into a valid number'
});
break;
}
let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length;
applyProperty(node.children[index - 1], {
creature, targets, scope, log
});
}
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
if (scope['$attackHit']?.value){
if (!targets.length) log.content.push({value: '**On hit**'});
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
if (scope['$attackMiss']?.value){
if (!targets.length) log.content.push({value: '**On miss**'});
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
if (scope['$saveFailed']?.value){
if (!targets.length) log.content.push({value: '**On failed save**'});
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
if (scope['$saveSucceeded']?.value){
if (!targets.length) log.content.push({value: '**On save**',});
applyChildren();
}
break;
case 'random':
if (node.children.length){

View File

@@ -10,6 +10,7 @@ import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
@@ -32,7 +33,30 @@ export default function applyBuff(node, {creature, targets, scope, log}){
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
// Apply the buff
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
if (prop.name || prop.description?.value){
if (target._id === creature._id){
// Targeting self
log.content.push({
name: prop.name,
value: prop.description?.value,
});
} else {
// Targeting other
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name: prop.name,
value: prop.description?.value,
}],
}
});
}
}
});
// Don't apply the children of the buff, they get copied to the target instead

View File

@@ -36,15 +36,12 @@ export default function applyDamage(node, {
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
@@ -62,11 +59,10 @@ export default function applyDamage(node, {
} else {
prop.amount.value = toString(reduced);
}
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
@@ -129,10 +125,10 @@ export default function applyDamage(node, {
function applyDamageMultipliers({target, damage, damageProp, logValue}){
const damageType = damageProp?.damageType;
if (!damageType) return;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return;
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
@@ -157,8 +153,8 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
return damage;
}
return damage;
}
function multiplierAppliesTo(damageProp){

View File

@@ -1,20 +1,58 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
const applyChildren = node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
if (prop.roll?.calculation){
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, log);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.roll.value = null;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
const value = reduced.value;
scope[prop.variableName] = value;
logValue.push(`**${value}**`);
if (!prop.silent){
log.content.push({
name: prop.name,
value: logValue.join('\n'),
inline: true,
});
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
inline: true,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log

View File

@@ -22,10 +22,21 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
}
log.content.push({
name: prop.name,
value: ' DC ' + dc,
value: `DC **${dc}**`,
inline: true,
});
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length){
scope['$saveFailed'] = {value: true};
scope['$saveSucceeded'] = {value: true};
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}
// Each target makes the saving throw
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
@@ -55,24 +66,24 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
@@ -84,8 +95,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
scope['$saveFailed'] = {value: true};
}
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();

View File

@@ -29,8 +29,9 @@ export default function computeVariableAsSkill(computation, node, prop){
}
// Combine everything to get the final result
const statBase = node.data.baseValue;
const statBase = node.data.baseValue || 0;
const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator.base || 0;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
@@ -41,7 +42,7 @@ export default function computeVariableAsSkill(computation, node, prop){
return;
}
// Combine aggregator
const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
const base = statBase > aggregatorBase ? statBase : aggregatorBase;
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;

View File

@@ -18,6 +18,8 @@ let BranchSchema = createPropertySchema({
'eachTarget',
// Pick one child at random
'random',
// Pick one child based on a given index
'index',
// if it has option children, asks to select one
// Otherwise presents its own text with yes/no
//'choice',

View File

@@ -41,6 +41,11 @@ let RollSchema = createPropertySchema({
parseLevel: 'compile',
optional: true,
},
// Prevent the roll from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyRollSchema = createPropertySchema({

View File

@@ -537,7 +537,7 @@
if (!spellId) return;
doCastSpell.call({spellId, slotId}, error => {
if (!error) return;
snackbar({text: error.reason});
snackbar({text: error.reason || error.message || error.toString()});
console.error(error);
});
},

View File

@@ -5,7 +5,10 @@
:key="index"
class="content-line"
>
<h4 class="content-name">
<h4
class="content-name"
style="min-height: 12px;"
>
{{ content.name }}
</h4>
<markdown-text
@@ -13,6 +16,10 @@
class="content-value"
:markdown="content.value"
/>
<div
v-else
style="min-height: 12px;"
/>
</div>
</div>
</template>
@@ -36,7 +43,7 @@ export default {
<style lang="css" scoped>
.content-line {
min-height: 24px;
margin-top: 2px;
margin-top: 8px;
margin-bottom: 2px;
}
.content-line .details {

View File

@@ -19,6 +19,15 @@
@change="({path, value, ack}) =>
$emit('change', {path: ['condition', ...path], value, ack})"
/>
<computed-field
v-else-if="model.branchType === 'index'"
label="Index"
hint="Which child to apply. An index of 2 will choose the 2nd child."
:model="model.condition"
:error-messages="errors.condition"
@change="({path, value, ack}) =>
$emit('change', {path: ['condition', ...path], value, ack})"
/>
</v-expand-transition>
<smart-combobox
label="Tags"
@@ -51,7 +60,8 @@
{value: 'failedSave', text: 'Save failed'},
{value: 'successfulSave', text: 'Save succeeded'},
{value: 'eachTarget', text: 'Apply to each target'},
{value: 'random', text: 'Random'},
{value: 'random', text: 'Random'},
{value: 'index', text: 'Calculated index'},
],
}},
computed: {
@@ -64,6 +74,7 @@
case 'successfulSave': return 'If the parent save is made, the child properties are applied';
case 'eachTarget': return 'Applies each child property once per target';
case 'random': return 'Chooses one child property at random and applies it';
case 'index': return 'Chooses one child property to apply based on the given index';
default: return '';
}
}

View File

@@ -36,6 +36,20 @@
/>
<form-sections>
<form-section name="Advanced">
<v-row dense>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
</v-col>
</v-row>
<smart-combobox
label="Tags"
multiple

View File

@@ -28,6 +28,7 @@ export default {
case 'successfulSave': return 'On save';
case 'eachTarget': return 'Each target';
case 'random': return 'Pick one at random';
case 'index': return 'Pick one by index';
default: return '';
}
}

View File

@@ -10,6 +10,11 @@
name="Condition"
:calculation="model.condition"
/>
<property-field
v-else-if="model.branchType === 'index'"
name="Index"
:calculation="model.condition"
/>
</v-row>
</div>
</template>
@@ -30,6 +35,7 @@
case 'successfulSave': return 'On save';
case 'eachTarget': return 'Each target';
case 'random': return 'Pick one at random';
case 'index': return 'Pick one by index';
default: return '';
}
}