From 378da71f5dee67347b82039815c4e8035ac5a129 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 15 Feb 2022 15:59:41 +0200 Subject: [PATCH] Effects targeting calculations by tag now work in the engine and actions --- .../api/engine/actions/applyProperty.js | 5 +- .../applyPropertyByType/applyDamage.js | 8 +- .../applyEffectsToCalculationParseNode.js | 24 +++ .../shared/recalculateCalculation.js | 2 + .../engine/computation/CreatureComputation.js | 12 ++ .../buildComputation/linkTypeDependencies.js | 69 ++++++++- .../parseCalculationFields.js | 5 +- .../computeByType/computeCalculation.js | 42 +++++ .../computeByType/computeVariable.js | 2 +- .../aggregate/aggregateEffect.js | 13 ++ .../computeVariableAsAttribute.js | 3 + .../properties/subSchemas/computedField.js | 9 ++ app/imports/parser/parseTree/rollArray.js | 2 +- .../components/attributes/AttributeEffect.vue | 146 +++++++++--------- .../components/effects/InlineEffect.vue | 136 ++++++++++++++++ .../ui/properties/forms/EffectForm.vue | 9 ++ .../properties/forms/shared/ComputedField.vue | 11 +- .../ui/properties/viewers/ActionViewer.vue | 9 +- .../viewers/shared/PropertyField.vue | 45 +++++- 19 files changed, 454 insertions(+), 98 deletions(-) create mode 100644 app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js create mode 100644 app/imports/ui/properties/components/effects/InlineEffect.vue diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 23094897..32542390 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -21,6 +21,7 @@ const applyPropertyByType = { toggle, }; -export default function applyProperty(node, ...args){ - return applyPropertyByType[node.node.type]?.(node, ...args); +export default function applyProperty(node, opts, ...rest){ + opts.scope[`#${node.node.type}`] = node.node; + return applyPropertyByType[node.node.type]?.(node, opts, ...rest); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 87ef98c2..2368b0cb 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -3,6 +3,7 @@ import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js'; import resolve, { Context, toString } from '/imports/parser/resolve.js'; import logErrors from './shared/logErrors.js'; +import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; export default function applyDamage(node, { creature, targets, scope, log @@ -35,11 +36,12 @@ export default function applyDamage(node, { 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); + // 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)); logErrors(context.errors, log); diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js new file mode 100644 index 00000000..be625eea --- /dev/null +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js @@ -0,0 +1,24 @@ +import operator from '/imports/parser/parseTree/operator.js'; +import { parse } from '/imports/parser/parser.js'; +import logErrors from './logErrors.js'; + +export default function applyEffectsToCalculationParseNode(calcObj, log){ + if (!calcObj.effects) return; + calcObj.effects.forEach(effect => { + if (effect.operation !== 'add') return; + if (!effect.amount) return; + if (effect.amount.value === null) return; + let effectParseNode; + try { + effectParseNode = parse(effect.amount.value.toString()); + calcObj.parseNode = operator.create({ + left: calcObj.parseNode, + right: effectParseNode, + operator: '+', + fn: 'add' + }); + } catch (e){ + logErrors([e], log) + } + }); +} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index 4ac1565a..a10340be 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -1,9 +1,11 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; +import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import logErrors from './logErrors.js'; export default function recalculateCalculation(calc, scope, log, context){ if (!calc?.parseNode) return; calc._parseLevel = 'reduce'; + applyEffectsToCalculationParseNode(calc, log); evaluateCalculation(calc, scope, context); logErrors(calc.errors, log); } diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js index 416ef7c7..a806ddd6 100644 --- a/app/imports/api/engine/computation/CreatureComputation.js +++ b/app/imports/api/engine/computation/CreatureComputation.js @@ -6,6 +6,7 @@ export default class CreatureComputation { // Set up fields this.originalPropsById = {}; this.propsById = {}; + this.propsWithTag = {}; this.scope = {}; this.props = properties; this.dependencyGraph = createGraph(); @@ -18,6 +19,17 @@ export default class CreatureComputation { // Store by id this.propsById[prop._id] = prop; + // Store sets of ids in each tag + if (prop.tags){ + prop.tags.forEach(tag => { + if (this.propsWithTag[tag]){ + this.propsWithTag[tag].push(prop._id); + } else { + this.propsWithTag[tag] = [prop._id]; + } + }); + } + // Store the prop in the dependency graph this.dependencyGraph.addNode(prop._id, prop); }); diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 2e1b29ea..0acaf4cc 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, intersection, difference } from 'lodash'; const linkDependenciesByType = { action: linkAction, @@ -127,11 +127,11 @@ function linkEffects(dependencyGraph, prop, computation){ dependOnCalc({dependencyGraph, prop, key: 'amount'}); // The stats depend on the effect if (prop.targetByTags){ - // TODO: - getEffectTagTargets(prop, computation).forEach(targetProp => { + getEffectTagTargets(prop, computation).forEach(targetId => { + const targetProp = computation.propsById[targetId]; const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj){ + if (calcObj && calcObj.calculation){ dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect'); } }); @@ -143,6 +143,67 @@ function linkEffects(dependencyGraph, prop, computation){ } } +// Returns an array of IDs of the properties the effect targets +function getEffectTagTargets(effect, computation){ + const targets = getTargetListFromTags(effect.targetTags, computation); + const notIds = []; + if (effect.extraTags){ + effect.extraTags.forEach(ex => { + if (ex.operation === 'OR'){ + targets.push(...getTargetListFromTags(ex.tags, computation)); + } else if (ex.operation === 'NOT'){ + ex.tags.forEach(tag => { + const idList = computation.propsWithTag[tag]; + if (idList) notIds.push(...computation.propsWithTag[tag]) + }); + } + }); + } + return difference(targets, notIds); +} + +function getTargetListFromTags(tags, computation){ + const targetTagIdLists = []; + if (!tags) return []; + tags.forEach(tag => { + const idList = computation.propsWithTag[tag]; + if (idList) targetTagIdLists.push(idList); + }); + const targets = intersection(...targetTagIdLists); + return targets; +} + +function getDefaultCalculationField(prop){ + switch (prop.type){ + case 'action': return 'attackRoll'; + case 'adjustment': return 'amount'; + case 'attribute': return 'baseValue'; + case 'branch': return 'condition'; + case 'buff': return 'duration'; + case 'class': return null; + case 'classLevel': return null; + case 'constant': return null; + case 'container': return null; + case 'damageMultiplier': return null; + case 'damage': return 'amount'; + case 'effect': return 'amount'; + case 'feature': return null; + case 'folder': return null; + case 'item': return null; + case 'note': return null; + case 'proficiency': return null; + case 'reference': return null; + case 'roll': return 'roll'; + case 'savingThrow': return 'dc'; + case 'skill': return 'baseValue'; + case 'slotFiller': return null; + case 'slot': return 'quantityExpected'; + case 'spellList': return 'attackRollBonus'; + case 'spell': return null; + case 'toggle': return 'condition'; + } +} + function linkRoll(dependencyGraph, prop){ dependOnCalc({dependencyGraph, prop, key: 'roll'}); } diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index d8a7cdba..a2e19db1 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -29,7 +29,7 @@ function discoverInlineCalculationFields(prop, schemas){ // Set the value to the uncomputed string for use in calculations inlineCalcObj.value = string; - + // Has the text, if it matches the existing hash, stop const inlineCalcHash = cyrb53(inlineCalcObj.text); if (inlineCalcHash === inlineCalcObj.hash){ @@ -57,6 +57,9 @@ function parseAllCalculationFields(prop, schemas){ // Determine the level the calculation should compute down to let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; + // Special case of effects, when targeting by tags compile + if (prop.type === 'effect' && prop.targetByTags) parseLevel = 'compile'; + // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, (prop, key) => { diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 80dd0746..054fc7f9 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -3,4 +3,46 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; export default function computeCalculation(computation, node){ const calcObj = node.data; evaluateCalculation(calcObj, computation.scope); + aggregateCalculationEffects(node, computation); +} + +export function aggregateCalculationEffects(node, computation){ + const calcObj = node.data; + delete calcObj.effects; + computation.dependencyGraph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + // Only effect links + if (link.data !== 'effect') return; + // That have effect data + if (!linkedNode.data) return; + // Ignore inactive props + if (linkedNode.data.inactive) return; + + // Collate effects + calcObj.effects = calcObj.effects || []; + calcObj.effects.push({ + _id: linkedNode.data._id, + name: linkedNode.data.name, + operation: linkedNode.data.operation, + amount: linkedNode.data.amount && { + value: linkedNode.data.amount.value, + //parseNode: linkedNode.data.amount.parseNode, + }, + // ancestors: linkedNode.data.ancestors, + }); + }, + true // enumerate only outbound links + ); + if (calcObj.effects && typeof calcObj.value === 'number'){ + calcObj.baseValue = calcObj.value; + calcObj.effects.forEach(effect => { + if ( + effect.operation === 'add' && + effect.amount && typeof effect.amount.value === 'number' + ){ + calcObj.value += effect.amount.value + } + }); + } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js index 0f9bdab1..2f5f2964 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js @@ -28,7 +28,7 @@ function aggregateLinks(computation, node){ // Ignore inactive props if (linkedNode.data.inactive) return; // Apply all the aggregations - let arg = {node, linkedNode, link}; + let arg = {node, linkedNode, link, computation}; aggregate.classLevel(arg); aggregate.damageMultiplier(arg); aggregate.definition(arg); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js index 5d454957..8cf47d36 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -16,10 +16,23 @@ export default function aggregateEffect({node, linkedNode, link}){ conditional: [], rollBonus: [], }; + + // Store a summary of the effect itself + node.data.effects = node.data.effects || []; + node.data.effects.push({ + _id: linkedNode.data._id, + name: linkedNode.data.name, + operation: linkedNode.data.operation, + amount: linkedNode.data.amount && {value: linkedNode.data.amount.value}, + // ancestors: linkedNode.data.ancestors, + }); + // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; // Get the result of the effect const result = linkedNode.data.amount?.value; + // Skip aggregating if the result is not resolved completely + if (typeof result === 'string') return; // Aggregate the effect based on its operation switch(linkedNode.data.operation){ case 'base': diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 3a7c2982..21137749 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -23,4 +23,7 @@ export default function computeVariableAsAttribute(computation, node, prop){ prop.hide = !node.data.effectAggregator && prop.baseValue === undefined || undefined + + // Store effects + prop.effects = node.data.effects; } diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index e8cb1220..0f0ef37b 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -24,6 +24,15 @@ function computedOnlyField(field){ optional: true, removeBeforeCompute: true, }, + // A list of effects targeting this calculation + [`${field}.effects`]: { + type: Array, + optional: true, + }, + [`${field}.effects.$`]: { + type: Object, + blackbox: true, + }, // A cache of the parse result of the calculation [`${field}.parseNode`]: { type: Object, diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js index ba737cc4..49474acb 100644 --- a/app/imports/parser/parseTree/rollArray.js +++ b/app/imports/parser/parseTree/rollArray.js @@ -16,7 +16,7 @@ const rollArray = { }; }, toString(node){ - return `[${node.values.join(', ')}]`; + return `${node.diceNum || ''}d${node.diceSize} [${node.values.join(', ')}]`; }, reduce(node, scope, context){ const total = node.values.reduce((a, b) => a + b, 0); diff --git a/app/imports/ui/properties/components/attributes/AttributeEffect.vue b/app/imports/ui/properties/components/attributes/AttributeEffect.vue index 623a26b8..687500bc 100644 --- a/app/imports/ui/properties/components/attributes/AttributeEffect.vue +++ b/app/imports/ui/properties/components/attributes/AttributeEffect.vue @@ -27,7 +27,7 @@
{{ displayedText }}
-
+
diff --git a/app/imports/ui/properties/components/effects/InlineEffect.vue b/app/imports/ui/properties/components/effects/InlineEffect.vue new file mode 100644 index 00000000..9ac97e38 --- /dev/null +++ b/app/imports/ui/properties/components/effects/InlineEffect.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue index 514defad..bed6c6a3 100644 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ b/app/imports/ui/properties/forms/EffectForm.vue @@ -10,6 +10,7 @@ - +
@@ -29,6 +29,15 @@ export default { default: () => ({}), }, }, + computed: { + errorList(){ + if (this.model.parseError){ + return [this.model.parseError, ...this.model.errors]; + } else { + return this.model.errors; + } + } + } } diff --git a/app/imports/ui/properties/viewers/ActionViewer.vue b/app/imports/ui/properties/viewers/ActionViewer.vue index 93a43290..4a8ef728 100644 --- a/app/imports/ui/properties/viewers/ActionViewer.vue +++ b/app/imports/ui/properties/viewers/ActionViewer.vue @@ -29,7 +29,7 @@ large center signed - :value="rollBonus" + :calculation="model.attackRoll" /> 3; }, diff --git a/app/imports/ui/properties/viewers/shared/PropertyField.vue b/app/imports/ui/properties/viewers/shared/PropertyField.vue index 6c2a4d60..6047149a 100644 --- a/app/imports/ui/properties/viewers/shared/PropertyField.vue +++ b/app/imports/ui/properties/viewers/shared/PropertyField.vue @@ -19,7 +19,7 @@ {{ name }}
+
+ + +
@@ -50,14 +74,18 @@