From 788cbb182d85a9337e2265f97d66e59c2f6fbff0 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 9 Mar 2022 01:31:09 +0200 Subject: [PATCH] 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 --- .../applyPropertyByType/applyAction.js | 30 +++++----- .../applyPropertyByType/applyBranch.js | 38 ++++++++++-- .../actions/applyPropertyByType/applyBuff.js | 24 ++++++++ .../applyPropertyByType/applyDamage.js | 18 +++--- .../actions/applyPropertyByType/applyRoll.js | 58 +++++++++++++++---- .../applyPropertyByType/applySavingThrow.js | 27 ++++++--- .../computeVariable/computeVariableAsSkill.js | 5 +- app/imports/api/properties/Branches.js | 2 + app/imports/api/properties/Rolls.js | 5 ++ .../character/characterSheetTabs/StatsTab.vue | 2 +- app/imports/ui/log/LogContent.vue | 11 +++- .../ui/properties/forms/BranchForm.vue | 13 ++++- app/imports/ui/properties/forms/RollForm.vue | 14 +++++ .../treeNodeViews/BranchTreeNode.vue | 1 + .../ui/properties/viewers/BranchViewer.vue | 6 ++ 15 files changed, 201 insertions(+), 53 deletions(-) diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 7b3b3a2e..0366bf49 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -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}; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index 63aa2e1d..6a46e07c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -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){ diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 32d9f35a..66857f6c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -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 diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index acf42475..0978d07a 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -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){ diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index d37aabfe..a097f977 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -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 diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js index b94ccc13..503442ab 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js @@ -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(); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index da0ed54b..92c11ca9 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -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; diff --git a/app/imports/api/properties/Branches.js b/app/imports/api/properties/Branches.js index 758d88fa..952c7715 100644 --- a/app/imports/api/properties/Branches.js +++ b/app/imports/api/properties/Branches.js @@ -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', diff --git a/app/imports/api/properties/Rolls.js b/app/imports/api/properties/Rolls.js index 5acc0ce7..03e819a4 100644 --- a/app/imports/api/properties/Rolls.js +++ b/app/imports/api/properties/Rolls.js @@ -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({ diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue index 332b5268..9862591f 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -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); }); }, diff --git a/app/imports/ui/log/LogContent.vue b/app/imports/ui/log/LogContent.vue index 658a6ec4..8c109c23 100644 --- a/app/imports/ui/log/LogContent.vue +++ b/app/imports/ui/log/LogContent.vue @@ -5,7 +5,10 @@ :key="index" class="content-line" > -

+

{{ content.name }}

+
@@ -36,7 +43,7 @@ export default {