Updated computation engine to handle multiple attributes and skills with the same variable name

This commit is contained in:
Stefan Zermatten
2021-04-22 15:11:49 +02:00
parent 727101cd63
commit 298db01e5b
8 changed files with 184 additions and 108 deletions

View File

@@ -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 (

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
);
});
}

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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()