Overhauled computations to allow for toggles :'( that sucked

This commit is contained in:
Thaum Rystra
2020-05-16 22:03:21 +02:00
parent 7024adecaf
commit 5c0a2a4d6c
30 changed files with 468 additions and 251 deletions

View File

@@ -1,22 +1,45 @@
import { includes, cloneDeep, has } from 'lodash';
import { includes, cloneDeep } from 'lodash';
// The computation memo is an in-memory data structure used only during the
// computation process
export default class ComputationMemo {
constructor(props){
this.statsByVariableName = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
this.propsById = {};
this.skillsByAbility = {};
this.unassignedEffects = [];
this.classes = {};
props.filter((prop) => {
// skip effects, proficiencies, and class levels for the next pass
this.classLevelsById = {};
this.togglesById = {};
this.toggleIds = new Set();
// First note all the ids of all the toggles
props.forEach((prop) => {
if (
prop.type === 'effect' ||
prop.type === 'proficiency' ||
prop.type === 'classLevel'
) return true;
// Add all the stats
this.addStat(prop);
prop.type === 'toggle'
) {
this.toggleIds.add(prop._id);
}
});
props.filter((prop) => {
if (
prop.type === 'toggle'
) {
this.addToggle(prop);
} else {
return true;
}
}).filter((prop) => {
if (
prop.type === 'attribute' ||
prop.type === 'skill'
) {
// Add all the stats
this.addStat(prop);
} else {
return true;
}
}).forEach((prop) => {
// Now add all effects and proficiencies
if (prop.type === 'effect'){
@@ -32,8 +55,14 @@ export default class ComputationMemo {
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.computationDetails = propDetails(prop);
prop.ancestors.forEach(ancestor => {
if (this.toggleIds.has(ancestor.id)){
prop.computationDetails.toggleAncestors.push(ancestor.id);
}
});
return prop;
}
/*
storeHighestClassLevel(name, prop, isBaseClass){
// Only store the highest level classLevel
let stat = this.statsByVariableName[name]
@@ -72,33 +101,51 @@ export default class ComputationMemo {
level += this.classes[name].level || 0;
}
this.statsByVariableName['level'].value = level;
}*/
addToggle(prop){
prop = this.registerProperty(prop);
this.togglesById[prop._id] = prop;
}
addClassLevel(prop){
prop = this.registerProperty(prop);
if (prop.variableName){
this.storeHighestClassLevel(prop.variableName, prop);
}
if (prop.baseClass){
this.storeHighestClassLevel(prop.baseClass, prop, true);
}
this.classLevelsById[prop._id] = prop;
}
addStat(prop){
prop = this.registerProperty(prop);
let variableName = prop.variableName;
if (!variableName) return;
if (this.statsByVariableName[variableName]){
prop.value = NaN;
prop.computationDetails.error = 'variableNameCollision';
if (Meteor.isClient) console.warn('variableNameCollision', prop);
return;
}
this.statsByVariableName[variableName] = prop;
if (
prop.type === 'skill' &&
isSkillCheck(prop) &&
prop.ability
){
this.addSkillToAbility(prop, prop.ability)
let existingStat = this.statsByVariableName[variableName];
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,
});
}
if (prop.baseProficiency){
existingStat.computationDetails.proficiencies.push({
value: prop.baseProficiency,
stats: [variableName],
computationDetails: propDetailsByType.proficiency(),
type: 'proficiency',
statBase: true,
});
}
} else {
prop = this.registerProperty(prop);
this.statsById[prop._id] = prop;
this.statsByVariableName[variableName] = prop;
if (
prop.type === 'skill' &&
isSkillCheck(prop) &&
prop.ability
){
this.addSkillToAbility(prop, prop.ability)
}
}
}
addSkillToAbility(prop, ability){
@@ -152,9 +199,14 @@ export default class ComputationMemo {
let target = this.statsByVariableName[statName];
if (!target) return;
targets.add(target);
if (isAbility(target) && isSkillCheck(prop)) {
if (isAbility(target)) {
let extras = this.skillsByAbility[statName] || [];
targets.add(...extras)
extras.forEach(ex =>{
// Only pass on ability proficiencies to skills and checks
if (ex.skillType === 'skill' || ex.skillType === 'check'){
targets.add(ex)
}
});
}
});
return targets;
@@ -188,11 +240,22 @@ function propDetails(prop){
}
const propDetailsByType = {
toggle(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
attribute(){
return {
computed: false,
busyComputing: false,
effects: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
skill(){
@@ -201,19 +264,30 @@ const propDetailsByType = {
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
effect(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
classLevel(){
return {
computed: true,
toggleAncestors: [],
disabledByToggle: false,
};
},
proficiency(){
return {};
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
}

View File

@@ -25,6 +25,9 @@ export default class EffectAggregator{
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
if (effect.statBase){
this.statBaseValue = result > this.statBaseValue ? result : this.statBaseValue;
}
break;
case 'add':
// Add all adds together

View File

@@ -0,0 +1,11 @@
import computeToggle from '/imports/api/creature/computation/computeToggle.js';
export default function applyToggles(prop, memo){
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
if (!toggle.toggleResult){
prop.computationDetails.disabledByToggle = true;
}
});
}

View File

@@ -1,5 +1,5 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
@@ -37,7 +37,13 @@ function combineSkill(stat, aggregator, memo){
stat.proficiency = stat.baseProficiency || 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
applyToggles(prof, memo);
if (
!prof.computationDetails.disabledByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
}
}
// Get the character's proficiency bonus to apply
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];

View File

@@ -1,7 +1,25 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
export default function computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
if (effect.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
effect.computationDetails.computed = true;
effect.result = NaN;
effect.computationDetails.busyComputing = false;
effect.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', effect);
return;
}
// Before doing any work, mark this effect as busy
effect.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(effect, memo);
// Determine result of effect calculation
if (!effect.calculation){
if(effect.operation === 'add' || effect.operation === 'base'){
effect.result = 0;
@@ -16,4 +34,5 @@ export default function computeEffect(effect, memo){
effect.result = evaluateCalculation(effect.calculation, memo);
}
effect.computationDetails.computed = true;
effect.computationDetails.busyComputing = false;
}

View File

@@ -1,12 +1,19 @@
import { each, forOwn } from 'lodash';
import computeStat from '/imports/api/creature/computation/computeStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
import computeToggle from '/imports/api/creature/computation/computeToggle.js';
export default function computeMemo(memo){
forOwn(memo.statsByVariableName, (stat) => {
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);
});
each(memo.unassignedEffects, (effect) => {
// Compute effects which didn't end up targeting a stat
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute class levels
}

View File

@@ -1,6 +1,7 @@
import combineStat from '/imports/api/creature/computation/combineStat.js';
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/EffectAggregator.js';
import applyToggles from '/imports/api/creature/computation/applyToggles.js';
import { each } from 'lodash';
export default function computeStat(stat, memo){
@@ -18,14 +19,21 @@ export default function computeStat(stat, memo){
}
// Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true;
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
aggregator.addEffect(effect);
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
// Apply any toggles
applyToggles(stat, memo);
if (!stat.computationDetails.disabledByToggle){
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (!effect.computationDetails.disabledByToggle){
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;

View File

@@ -0,0 +1,30 @@
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
export default function computeToggle(toggle, memo){
if (toggle.computationDetails.computed) return;
if (toggle.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
toggle.computationDetails.computed = true;
toggle.result = false;
toggle.computationDetails.busyComputing = false;
toggle.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', toggle);
return;
}
// Before doing any work, mark this toggle as busy
toggle.computationDetails.busyComputing = true;
// Do work
if (toggle.enabled){
toggle.toggleResult = true;
} else if (!toggle.condition){
toggle.toggleResult = false;
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
toggle.toggleResult = evaluateCalculation(toggle.condition, memo);
}
toggle.computationDetails.computed = true;
toggle.computationDetails.busyComputing = false;
}

View File

@@ -32,6 +32,7 @@ const calculationPropertyTypes = [
'effect',
'proficiency',
'classLevel',
'toggle',
];
/**
@@ -71,7 +72,11 @@ const calculationPropertyTypes = [
* - Write the computed results back to the database
*/
export function recomputeCreatureById(creatureId){
let props = getActiveProperties(creatureId, {type: {$in: calculationPropertyTypes}});
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: {$in: calculationPropertyTypes}},
includeUntoggled: true,
});
let computationMemo = new ComputationMemo(props);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);

View File

@@ -3,51 +3,60 @@ import { isEqual, forOwn } from 'lodash';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
const schemasByType = {
'skill': ComputedOnlySkillSchema,
'attribute': ComputedOnlyAttributeSchema,
'effect': ComputedOnlyEffectSchema,
'toggle': ComputedOnlyToggleSchema,
};
export default function writeAlteredProperties(memo){
let bulkWriteOperations = [];
// Loop through all properties on the memo
forOwn(memo.originalPropsById, (original, _id) => {
let changed = memo.propsById[_id];
let schema;
switch (changed.type){
case 'skill':
schema = ComputedOnlySkillSchema;
break;
case 'attribute':
schema = ComputedOnlyAttributeSchema;
break;
case 'effect':
schema = ComputedOnlyEffectSchema;
break;
default:
return;
forOwn(memo.propsById, changed => {
let schema = schemasByType[changed.type];
if (!schema) return;
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
if (extraIds && extraIds.length){
ids = [changed._id, ...extraIds];
} else {
ids = [changed._id];
}
let op = undefined;
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of schema.objectKeys()){
if (!isEqual(original[key], changed[key])){
if (!op) op = newOperation(_id, changed.type);
let value = changed[key];
if (value === undefined){
// Unset values that become undefined
addUnsetOp(op, key);
} else {
// Set values that changed to something else
addSetOp(op, key, value);
}
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
op = addChangedKeysToOp(op, schema.objectKeys(), original, changed);
if (op){
bulkWriteOperations.push(op);
}
}
if (op){
bulkWriteOperations.push(op);
}
});
});
bulkWriteProperties(bulkWriteOperations);
}
function addChangedKeysToOp(op, keys, original, changed) {
// Loop through all keys that can be changed by computation
// and compile an operation that sets all those keys
for (let key of keys){
if (!isEqual(original[key], changed[key])){
if (!op) op = newOperation(original._id, changed.type);
let value = changed[key];
if (value === undefined){
// Unset values that become undefined
addUnsetOp(op, key);
} else {
// Set values that changed to something else
addSetOp(op, key, value);
}
}
}
return op;
}
function newOperation(_id, type){
let newOp = {
updateOne: {
@@ -92,7 +101,8 @@ function bulkWriteProperties(bulkWriteOps){
);
} else {
bulkWriteOps.forEach(op => {
CreatureProperties.update(op.updateOne.filter, op.updateOne.update, {
let updateOneOrMany = op.updateOne || op.updateMany;
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
// The server code is bypassing collection 2 validation, so do the same
// on the client
// include this if bypass is off: