From 298db01e5b61a5ad9298f90f513a797b9c8b805e Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 22 Apr 2021 15:11:49 +0200 Subject: [PATCH] Updated computation engine to handle multiple attributes and skills with the same variable name --- .../computation/engine/ComputationMemo.js | 23 +-- .../computation/engine/EffectAggregator.js | 34 +--- .../computation/engine/combineStat.js | 18 +-- .../computation/engine/computeProficiency.js | 23 +++ .../computation/engine/computeStat.js | 147 +++++++++++++++--- .../engine/writeAlteredProperties.js | 37 ++--- app/imports/api/properties/Attributes.js | 5 + app/imports/api/properties/Skills.js | 5 + 8 files changed, 184 insertions(+), 108 deletions(-) create mode 100644 app/imports/api/creature/computation/engine/computeProficiency.js diff --git a/app/imports/api/creature/computation/engine/ComputationMemo.js b/app/imports/api/creature/computation/engine/ComputationMemo.js index 5830776c..75d270da 100644 --- a/app/imports/api/creature/computation/engine/ComputationMemo.js +++ b/app/imports/api/creature/computation/engine/ComputationMemo.js @@ -104,31 +104,10 @@ export default class ComputationMemo { let variableName = prop.variableName; if (!variableName) return; let existingStat = this.statsByVariableName[variableName]; + prop = this.registerProperty(prop); if (existingStat){ existingStat.computationDetails.idsOfSameName.push(prop._id); - this.originalPropsById[prop._id] = cloneDeep(prop); - if (prop.baseValueCalculation){ - existingStat.computationDetails.effects.push({ - operation: 'base', - calculation: prop.baseValueCalculation, - stats: [variableName], - computationDetails: propDetailsByType.effect(), - statBase: true, - dependencies: [], - }); - } - if (prop.baseProficiency){ - existingStat.computationDetails.proficiencies.push({ - value: prop.baseProficiency, - stats: [variableName], - computationDetails: propDetailsByType.proficiency(), - type: 'proficiency', - statBase: true, - dependencies: [], - }); - } } else { - prop = this.registerProperty(prop); this.statsById[prop._id] = prop; this.statsByVariableName[variableName] = prop; if ( diff --git a/app/imports/api/creature/computation/engine/EffectAggregator.js b/app/imports/api/creature/computation/engine/EffectAggregator.js index 3bfb7ace..649d6e31 100644 --- a/app/imports/api/creature/computation/engine/EffectAggregator.js +++ b/app/imports/api/creature/computation/engine/EffectAggregator.js @@ -1,31 +1,6 @@ -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import { union } from 'lodash'; - export default class EffectAggregator{ - constructor(stat, memo){ - delete this.baseValueErrors; - if (stat.baseValueCalculation){ - let { - result, - context, - dependencies - } = evaluateCalculation({ - string: stat.baseValueCalculation, - prop: stat, - memo - }); - this.statBaseValue = +result.value; - stat.dependencies = union( - stat.dependencies, - dependencies, - ); - if (context.errors.length){ - this.baseValueErrors = context.errors; - } - this.base = this.statBaseValue; - } else { - this.base = 0; - } + constructor(){ + this.base = 0; this.add = 0; this.mul = 1; this.min = Number.NEGATIVE_INFINITY; @@ -46,11 +21,6 @@ export default class EffectAggregator{ case 'base': // Take the largest base value this.base = result > this.base ? result : this.base; - if (effect.statBase){ - if (this.statBaseValue === undefined || result > this.statBaseValue){ - this.statBaseValue = result; - } - } break; case 'add': // Add all adds together diff --git a/app/imports/api/creature/computation/engine/combineStat.js b/app/imports/api/creature/computation/engine/combineStat.js index 71fa58f1..eea810e5 100644 --- a/app/imports/api/creature/computation/engine/combineStat.js +++ b/app/imports/api/creature/computation/engine/combineStat.js @@ -1,5 +1,5 @@ import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; +import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js'; import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; import { union } from 'lodash'; @@ -14,7 +14,8 @@ export default function combineStat(stat, aggregator, memo){ } function getAggregatorResult(stat, aggregator){ - let result = (aggregator.base + aggregator.add) * aggregator.mul; + let base = Math.max(aggregator.base, stat.baseValue || 0); + let result = (base + aggregator.add) * aggregator.mul; if (result < aggregator.min) { result = aggregator.min; } @@ -32,8 +33,6 @@ function getAggregatorResult(stat, aggregator){ function combineAttribute(stat, aggregator, memo){ stat.value = getAggregatorResult(stat, aggregator); - stat.baseValue = aggregator.statBaseValue; - stat.baseValueErrors = aggregator.baseValueErrors; if (stat.attributeType === 'spellSlot'){ let { result, @@ -78,9 +77,7 @@ function combineSkill(stat, aggregator, memo){ // Skills are based on some ability Modifier let ability = stat.ability && memo.statsByVariableName[stat.ability] if (stat.ability && ability){ - if (!ability.computationDetails.computed){ - computeStat(ability, memo); - } + computeStat(ability, memo); stat.abilityMod = ability.modifier; stat.dependencies = union( stat.dependencies, @@ -91,10 +88,10 @@ function combineSkill(stat, aggregator, memo){ stat.abilityMod = 0; } // Combine all the child proficiencies - stat.proficiency = stat.baseProficiency || 0; + stat.proficiency = 0; for (let i in stat.computationDetails.proficiencies){ let prof = stat.computationDetails.proficiencies[i]; - applyToggles(prof, memo); + computeProficiency(prof, memo); if ( !prof.deactivatedByToggle && prof.value > stat.proficiency @@ -130,9 +127,6 @@ function combineSkill(stat, aggregator, memo){ } // Multiply the proficiency bonus by the actual proficiency profBonus *= stat.proficiency; - // Base value - stat.baseValue = aggregator.statBaseValue; - stat.baseValueErrors = aggregator.baseValueErrors; // Combine everything to get the final result let result = (aggregator.base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul; if (result < aggregator.min) result = aggregator.min; diff --git a/app/imports/api/creature/computation/engine/computeProficiency.js b/app/imports/api/creature/computation/engine/computeProficiency.js new file mode 100644 index 00000000..112a594b --- /dev/null +++ b/app/imports/api/creature/computation/engine/computeProficiency.js @@ -0,0 +1,23 @@ +import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; + +export default function computeEffect(proficiency, memo){ + if (proficiency.computationDetails.computed) return; + if (proficiency.computationDetails.busyComputing){ + // Trying to compute this proficiency again while it is already computing. + // We must be in a dependency loop. + proficiency.computationDetails.computed = true; + proficiency.result = NaN; + proficiency.computationDetails.busyComputing = false; + proficiency.computationDetails.error = 'dependencyLoop'; + if (Meteor.isClient) console.warn('dependencyLoop', proficiency); + return; + } + // Before doing any work, mark this proficiency as busy + proficiency.computationDetails.busyComputing = true; + + // Apply any toggles + applyToggles(proficiency, memo); + + proficiency.computationDetails.computed = true; + proficiency.computationDetails.busyComputing = false; +} diff --git a/app/imports/api/creature/computation/engine/computeStat.js b/app/imports/api/creature/computation/engine/computeStat.js index fcd912c8..50928fa6 100644 --- a/app/imports/api/creature/computation/engine/computeStat.js +++ b/app/imports/api/creature/computation/engine/computeStat.js @@ -1,8 +1,9 @@ import combineStat from '/imports/api/creature/computation/engine/combineStat.js'; import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js'; import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js'; +import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; -import { each, union } from 'lodash'; +import { each, union, without } from 'lodash'; export default function computeStat(stat, memo){ // If the stat is already computed, skip it @@ -19,29 +20,135 @@ export default function computeStat(stat, memo){ } // Before doing any work, mark this stat as busy stat.computationDetails.busyComputing = true; - // Apply any toggles - applyToggles(stat, memo); + + let effects = stat.computationDetails.effects; + let proficiencies = stat.computationDetails.proficiencies; + + // Get references to all the stats that share the variable name + let sameNameStats = stat.computationDetails.idsOfSameName.map( + id => memo.propsById[id] + ); + + let allStats = [stat, ...sameNameStats]; + + // Decide which stat is the last active stat + // The last active stat is considered the cannonical stat + let lastActiveStat; + allStats.forEach(candidateStat => { + applyToggles(candidateStat, memo); + if (!candidateStat.inactive) lastActiveStat = candidateStat; + candidateStat.overridden = undefined; + }); + if (!lastActiveStat){ + delete memo.statsByVariableName[stat.variableName]; + return; + } + // Make sure the active stat has all the effects and proficiencies + lastActiveStat.computationDetails.effects = effects; + lastActiveStat.computationDetails.proficiencies = proficiencies; + + // Update the memo's stat with the chosen stat + memo.statsByVariableName[stat.variableName] = lastActiveStat; + + // Recreate list of the non-cannonical stats + sameNameStats = without(allStats, lastActiveStat); + + sameNameStats.forEach(statInstance => { + // Mark the non-cannonical stats as overridden + statInstance.overridden = true; + + // Apply the cannonical damage + statInstance.damage = lastActiveStat.damage; + }); + + let baseDependencies = []; + allStats.forEach(statInstance => { + // Add this stat and its deps to the dependencies + baseDependencies = union( + baseDependencies, + [statInstance._id], + statInstance.dependencies, + ); + + // Apply all the base proficiencies + if (statInstance.baseProficiency && !statInstance.inactive){ + proficiencies.push({ + value: statInstance.baseProficiency, + stats: [statInstance.variableName], + type: 'proficiency', + dependencies: [], + computationDetails: { + computed: true, + } + }); + } + + // Compute each active stat's baseValue calculation and apply it + if (statInstance.baseValueCalculation) { + let { + result, + context, + dependencies + } = evaluateCalculation({ + string: statInstance.baseValueCalculation, + prop: statInstance, + memo + }); + baseDependencies = union(baseDependencies, dependencies); + statInstance.baseValue = +result.value; + if (context.errors.length){ + statInstance.baseValueErrors = context.errors; + } + // Apply all the base values + if (!statInstance.inactive){ + effects.push({ + operation: 'base', + calculation: statInstance.baseValueCalculation, + result: statInstance.baseValue, + stats: [statInstance.variableName], + dependencies: [], + computationDetails: { + computed: true, + }, + }); + } + } + }); + + // Apply all the base baseDependencies + allStats.forEach(statInstance => { + statInstance.dependencies = union( + statInstance.dependencies, + without(baseDependencies, statInstance._id) + ); + }); // Compute and aggregate all the effects - let aggregator = new EffectAggregator(stat, memo) - each(stat.computationDetails.effects, (effect) => { + let aggregator = new EffectAggregator(); + let effectDeps = []; + each(effects, (effect) => { + // Compute computeEffect(effect, memo); if (effect.deactivatedByToggle) return; - if (effect._id){ - stat.dependencies = union( - stat.dependencies, - [effect._id] - ); - } - stat.dependencies = union( - stat.dependencies, - effect.dependencies - ) + + // dependencies + if (effect._id) effectDeps = [effect._id]; + effectDeps = union(effectDeps, effect.dependencies); + + // Add computed effect to aggregator aggregator.addEffect(effect); }); - // Conglomerate all the effects to compute the final stat values - combineStat(stat, aggregator, memo); - // Mark the attribute as computed - stat.computationDetails.computed = true; - stat.computationDetails.busyComputing = false; + + // Combine the effects into the stats + allStats.forEach(statInstance => { + // Conglomerate all the effects to compute the final stat values + combineStat(statInstance, aggregator, memo); + // Mark the stats as computed + statInstance.computationDetails.computed = true; + statInstance.computationDetails.busyComputing = false; + statInstance.dependencies = union( + statInstance.dependencies, + effectDeps + ); + }); } diff --git a/app/imports/api/creature/computation/engine/writeAlteredProperties.js b/app/imports/api/creature/computation/engine/writeAlteredProperties.js index 3a91c4ba..6927ea9e 100644 --- a/app/imports/api/creature/computation/engine/writeAlteredProperties.js +++ b/app/imports/api/creature/computation/engine/writeAlteredProperties.js @@ -12,29 +12,22 @@ export default function writeAlteredProperties(memo){ console.warn('No schema for ' + changed.type); return; } - let extraIds = changed.computationDetails.idsOfSameName; - let ids; - if (extraIds && extraIds.length){ - ids = [changed._id, ...extraIds]; - } else { - ids = [changed._id]; + let id = changed._id; + let op = undefined; + let original = memo.originalPropsById[id]; + let keys = [ + 'dependencies', + 'inactive', + 'deactivatedBySelf', + 'deactivatedByAncestor', + 'deactivatedByToggle', + 'damage', + ...schema.objectKeys(), + ]; + op = addChangedKeysToOp(op, keys, original, changed); + if (op){ + bulkWriteOperations.push(op); } - ids.forEach(id => { - let op = undefined; - let original = memo.originalPropsById[id]; - let keys = [ - 'dependencies', - 'inactive', - 'deactivatedBySelf', - 'deactivatedByAncestor', - 'deactivatedByToggle', - ...schema.objectKeys(), - ]; - op = addChangedKeysToOp(op, keys, original, changed); - if (op){ - bulkWriteOperations.push(op); - } - }); }); writePropertiesSequentially(bulkWriteOperations); } diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index bbb1de0f..714d9b47 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -133,6 +133,11 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({ type: Boolean, optional: true, }, + // Denormalised tag if stat is overridden by one with the same variable name + overridden: { + type: Boolean, + optional: true, + }, }); const ComputedAttributeSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index af176d84..63f9f5f0 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -121,6 +121,11 @@ let ComputedOnlySkillSchema = new SimpleSchema({ type: Boolean, optional: true, }, + // Denormalised tag if stat is overridden by one with the same variable name + overridden: { + type: Boolean, + optional: true, + }, }) const ComputedSkillSchema = new SimpleSchema()