diff --git a/app/imports/api/creature/computation/creatureComputation.disabledTest.js b/app/imports/api/creature/computation/creatureComputation.disabledTest.js deleted file mode 100644 index 783d8cae..00000000 --- a/app/imports/api/creature/computation/creatureComputation.disabledTest.js +++ /dev/null @@ -1,97 +0,0 @@ -import {computeCreature} from "./recomputeCreature.js"; -import assert from "assert"; - -const makeEffect = function(operation, value){ - let effect = {computed: false, result: 0, operation} - if (_.isFinite(value)){ - effect.value = +value; - } else { - effect.calculation = value; - } - return effect; -} - -describe('computeCreature', function () { - it('computes an aritrary creature', function () { - let char = { - atts: { - attribute1: { - computed: false, - busyComputing: false, - type: "attribute", - attributeType: "ability", - result: 0, - mod: 0, // The resulting modifier if this is an ability - base: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - effects: [ - makeEffect("base", 10), - makeEffect("add", 5), - makeEffect("mul", 2), - ], - }, - attribute2: { - computed: false, - busyComputing: false, - type: "attribute", - result: 0, - mod: 0, // The resulting modifier if this is an ability - base: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - effects: [ - makeEffect("base", "attribute1"), - makeEffect("max", 2), - ], - }, - }, - skills: { - skill1: { - computed: false, - busyComputing: false, - type: "skill", - ability: "attribute1", - result: 0, - proficiency: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - advantage: 0, - disadvantage: 0, - passiveAdd: 0, - fail: 0, - conditional: 0, - effects: [], - proficiencies: [], - }, - }, - dms: { - dm1: { - computed: false, - busyComputing: false, - type: "damageMultiplier", - result: 0, - immunityCount: 0, - ressistanceCount: 0, - vulnerabilityCount: 0, - effects: [], - } - }, - classes: { - Barbarian: { - level: 5, - }, - }, - level: 5, - }; - char = computeCreature(char); - console.log(char); - assert(true); - }); -}); diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js index 665ea71e..40d77ea5 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js @@ -10,7 +10,7 @@ export default function computeToggleDependencies(node, dependencyGraph){ prop.enabled ) return; walkDown(node.children, child => { - child.node._computationDetails.toggleAncestors.push(prop._id); + child.node._computationDetails.toggleAncestors.push(prop); dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index 06cf1a3d..d08fa7c2 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -2,7 +2,7 @@ import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; -export default function linkCalculationDependencies(dependencyGraph, prop, propsById){ +export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { // Store resolved ancestors let memo = { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index f2bea38b..ad3b231a 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -1,11 +1,14 @@ const linkDependenciesByType = { - attribute: linkVariableName, + action: linkResources, + attack: linkResources, + attribute: linkAttribute, classLevel: linkVariableName, constant: linkVariableName, damageMultiplier: linkDamageMultiplier, proficiency: linkStats, effect: linkStats, skill: linkSkill, + spell: linkResources, } export default function linkTypeDependencies(dependencyGraph, prop){ @@ -19,9 +22,39 @@ function linkVariableName(dependencyGraph, prop){ } } +function linkResources(dependencyGraph, prop, {propsById}){ + prop.resources.itemsConsumed.forEach(itemConsumed => { + if (!itemConsumed.itemId) return; + const item = propsById[itemConsumed.itemId]; + if (!item.equipped) { + itemConsumed.itemId = undefined; + return; + } + if (!item) return; + // none of these dependencies are computed, we can use them immediately + prop.available = item.quantity; + prop.itemName = item.name; + prop.itemIcon = item.icon; + prop.itemColor = item.color; + dependencyGraph.addLink(prop._id, item._id, 'inventory'); + }); + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource'); + }); +} + +function linkAttribute(dependencyGraph, prop){ + linkVariableName(dependencyGraph, prop); + // hit dice depend on constitution + if (prop.attributeType === 'hitDice'){ + dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod'); + } +} + function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { - dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, 'damageMultiplier'); + dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, prop.type); }); } @@ -29,12 +62,16 @@ function linkStats(dependencyGraph, prop){ // The stats a prop references depend on that prop prop.stats.forEach(variableName => { if (!variableName) return; - dependencyGraph.addLink(variableName, prop._id, 'statChange'); + dependencyGraph.addLink(variableName, prop._id, prop.type); }); } function linkSkill(dependencyGraph, prop){ linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability - if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); + if (prop.ability){ + dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); + } + // Skills depend on the creature's proficiencyBonus + dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index 9952f92b..df451a3b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -6,23 +6,31 @@ export default function parseCalculationFields(prop, schemas){ // For each key in the schema schemas[prop.type]._schemaKeys.forEach( key => { // that ends in '.calculation' - if (key.slice(-12) !== '.calculation') return; - const calcKey = key.sclice(0, -12); + if (key.slice(-12) === '.calculation'){ + const calcKey = key.sclice(0, -12); - // Determine the level the calculation should compute down to - let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + // Determine the level the calculation should compute down to + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + + // For all fields matching they keys + // supports `keys.$.with.$.arrays` + applyFnToKey(prop, calcKey, calcObj => { + // Store a reference to all the calculations + prop._computationDetails.calculations.push(calcObj); + // Store the level to compute down to later + calcObj._parseLevel = parseLevel; + // Parse the calculation + parseCalculation(calcObj); + }); + // Or that ends in .inlineCalculations + } else if (key.slice(-19) === '.inlineCalculations'){ + const inlineCalcKey = key.sclice(0, -19); + applyFnToKey(prop, inlineCalcKey, inlineCalcObj => { + // Store a reference to all the inline calculations + prop._computationDetails.inlineCalculations.push(inlineCalcObj); + }); + } - // For all fields matching they keys - // supports `keys.$.with.$.arrays` - applyFnToKey(prop, calcKey, calcObj => { - // Store a reference to all the calculations - prop._computationDetails.calculations.push(calcObj); - // Store the level to compute down to later - calcObj._parseLevel = parseLevel; - // Parse the calculation - parseCalculation(calcObj); - }); - }); } diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index e99df529..c1a3886e 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -23,17 +23,16 @@ import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngi * id -> id dependencies for docs that rely on other docs directly * id -> variable deps for docs that rely on a variable's value * variable -> id deps for variables that are impacted by docs - * TODO: - * Depth first traversal or dependency graph to: - * Find loops in the dependency graph - * resolve variables in dependency order + */ + +/** + * Forseen issues: Anything that computes during the build step will not obey + * computed toggles */ /** * TODO - * compute slots spaces left (after computed field of quantityExpected) - * compute damage multipliers - * compute dependencyGraph variables and properties + * compute class levels */ export default function buildCreatureComputation(creatureId){ @@ -48,6 +47,8 @@ export default function buildCreatureComputation(creatureId){ // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies // can limit the recompute to connected parts of the graph + // Each node's data represents a prop or a virtual prop like a variable + // Each link's data: {type: String, data: Object, requiresComputation: Boolean} const dependencyGraph = createGraph(); const computation = { @@ -102,8 +103,8 @@ export default function buildCreatureComputation(creatureId){ // Graph functions that rely on the props being stored first properties.forEach(prop => { - linkTypeDependencies(dependencyGraph, prop, computation.propsById); - linkCalculationDependencies(dependencyGraph, prop, computation.propsById); + linkTypeDependencies(dependencyGraph, prop, computation); + linkCalculationDependencies(dependencyGraph, prop, computation); }); return computation; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js deleted file mode 100644 index 96446353..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js +++ /dev/null @@ -1,13 +0,0 @@ - -/** - * Iterate through all the defining properties and choose the highest - * `baseValue.value` - */ -export default function aggregateBaseValue({node, linkedNode, link}){ - if (link.data !== 'definition') return; - const propBaseValue = linkedNode.data.baseValue?.value; - if (propBaseValue === undefined) return; - if (node.baseValue === undefined || propBaseValue > node.baseValue){ - node.baseValue = propBaseValue; - } -} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js deleted file mode 100644 index e792c5b7..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import definitions from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js'; -import baseValue from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js'; -import damageMultipliers from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDamageMultipliers.js'; -import effects from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateEffects.js'; -import proficiencies from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; -import skills from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; -import toggles from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateToggles.js'; - -export default Object.freeze({ - definitions, - baseValue, - damageMultipliers, - effects, - proficiencies, - skills, - toggles, -}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js new file mode 100644 index 00000000..4e3b4b3f --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js @@ -0,0 +1,10 @@ +import _variable from './computeByType/computeVariable.js'; +import action from './computeByType/computeAction.js'; +import slot from './computeByType/computeSlot.js'; + +export default Object.freeze({ + _variable, + action, + attack: action, + slot, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js new file mode 100644 index 00000000..f8d201b4 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js @@ -0,0 +1,21 @@ +import computeResources from './computeAction/computeResources.js'; + +export default function computeAction(graph, node, scope){ + const prop = node.data; + if (prop.uses){ + prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); + } + computeResources(graph, node, scope); + prop.resources.itemsConsumed.forEach(itemConsumed => { + if (!itemConsumed.itemId) return; + if (itemConsumed.available < itemConsumed.quantity.value){ + prop.insufficientResources = true; + } + }); + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + if (attConsumed.available < attConsumed.quantity.value){ + prop.insufficientResources = true; + } + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js new file mode 100644 index 00000000..ccda0235 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js @@ -0,0 +1,10 @@ +export default function computeResources(graph, node, scope){ + const prop = node.data; + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + const att = scope[attConsumed.variableName]; + attConsumed.available = att.value; + attConsumed.statId = att._id; + attConsumed.statName = att.name; + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js new file mode 100644 index 00000000..66c9e790 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js @@ -0,0 +1,6 @@ +export default function computSlot(graph, node){ + const prop = node.data; + if (prop.quantityExpected){ + prop.spaceLeft = prop.quantityExpected - prop.totalFilled; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js new file mode 100644 index 00000000..35352de6 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -0,0 +1,79 @@ +import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/aggregate/index.js'; +import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js'; +import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js'; +import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js'; +import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js'; + +export default function computeVariable(graph, node, scope){ + if (!node.data) node.data = {}; + aggregateLinks(graph, node); + combineAggregations(node, scope); + if (node.definingProp){ + // Add the defining variable to the scope + scope[node.id] = node.definingProp + } else { + // Otherwise add an implicit variable to the scope + scope[node.id] = computeImplicitVariable(node, scope); + } +} + +function aggregateLinks(graph, node){ + graph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + if (linkedNode.data.inactive) return; + // Apply all the aggregations + let arg = {node, linkedNode, link}; + aggregate.definition(arg); + aggregate.damageMultiplier(arg); + aggregate.effect(arg); + aggregate.proficiency(arg); + }, + true // enumerate only outbound links + ); +} + +function combineAggregations(node, scope){ + combineMultiplierAggregator(node); + node.overridenProps.forEach(prop => { + computeVariableProp(node, prop, scope); + }); + computeVariableProp(node, node.definingProp, scope); +} + +function computeVariableProp(node, prop, scope){ + if (prop.type === 'attribute'){ + computeVariableAsAttribute(node, prop, scope) + } else if (prop.type === 'skill'){ + computeVariableAsSkill(node, prop, scope) + } else if (prop.type === 'constant'){ + computeVariableAsConstant(node, prop, scope) + } +} + +function combineMultiplierAggregator(node){ + // get a reference to the aggregator + const aggregator = node.data.multiplierAggregator; + + // Combine + let value; + if (aggregator.immunityCount){ + value = 0; + } else if ( + aggregator.ressistanceCount && + !aggregator.vulnerabilityCount + ){ + value = 0.5; + } else if ( + !aggregator.ressistanceCount && + aggregator.vulnerabilityCount + ){ + value = 2; + } else { + value = 1; + } + + node.data.damageMultiplyValue = value; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js new file mode 100644 index 00000000..256a5c48 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js @@ -0,0 +1,26 @@ + +/** + * + */ +export default function aggregateDamageMultipliers({node, linkedNode, link}){ + if (link.data !== 'damageMultiplier') return; + const multiplierValue = linkedNode.data.value; + if (multiplierValue === undefined) return; + // Store an aggregator, its presence indicates damage multipliers target this + // variable + if (!node.data.multiplierAggregator) node.data.multiplierAggregator = { + immunityCount: 0, + resistanceCount: 0, + vulnerabilityCount: 0, + } + // Store a short reference to the aggregator + const aggregator = node.data.multiplierAggregator; + // Sum the counts of each type of multiplier + if (multiplierValue === 0){ + aggregator.immunityCount += 1; + } else if (multiplierValue === 0.5){ + aggregator.resistanceCount += 1; + } else if (multiplierValue === 2){ + aggregator.vulnerabilityCount += 1; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js similarity index 57% rename from app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js rename to app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index e21a4d14..b4fabd7f 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -1,7 +1,9 @@ -export default function aggregateDefinitions({node, linkedNode, link}){ +export default function aggregateDefinition({node, linkedNode, link}){ // Look at all definition links if (link.data !== 'definition') return; + + // Store which property is THE defining property and which are overriden const prop = linkedNode.data; // get current defining prop const definingProp = node.data.definingProp; @@ -14,11 +16,18 @@ export default function aggregateDefinitions({node, linkedNode, link}){ } else { overrideProp(prop, node); } + + // Aggregate the base value due to the defining properties + const propBaseValue = linkedNode.data.baseValue?.value; + if (propBaseValue === undefined) return; + if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ + node.data.baseValue = propBaseValue; + } } function overrideProp(prop, node){ if (!prop) return; prop.overriden = true; if (!node.data.overridenProps) node.data.overridenProps = []; - node.data.overridenProp.push(prop); + node.data.overridenProps.push(prop); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js new file mode 100644 index 00000000..5d454957 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -0,0 +1,78 @@ +export default function aggregateEffect({node, linkedNode, link}){ + if (link.data !== 'effect') return; + // store the effect aggregator, its presence indicates that the variable is + // targeted by effects + if (!node.data.effectAggregator) node.data.effectAggregator = { + base: undefined, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + advantage: 0, + disadvantage: 0, + passiveAdd: undefined, + fail: 0, + set: undefined, + conditional: [], + rollBonus: [], + }; + // 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; + // Aggregate the effect based on its operation + switch(linkedNode.data.operation){ + case 'base': + // Take the largest base value + if (Number.isFinite(result)){ + if(Number.isFinite(aggregator.base)){ + aggregator.base = Math.max(aggregator.base, result); + } else { + aggregator.base = result; + } + } + break; + case 'add': + // Add all adds together + aggregator.add += result || 0; + break; + case 'mul': + // Multiply the muls together + aggregator.mul *= result || 1; + break; + case 'min': + // Take the largest min value + aggregator.min = result > aggregator.min ? result : aggregator.min; + break; + case 'max': + // Take the smallest max value + aggregator.max = result < aggregator.max ? result : aggregator.max; + break; + case 'set': + // Take the highest set value + aggregator.set = aggregator.set === undefined || (result > aggregator.set) ? + result : + aggregator.set; + break; + case 'advantage': + // Sum number of advantages + aggregator.advantage++; + break; + case 'disadvantage': + // Sum number of disadvantages + aggregator.disadvantage++; + break; + case 'passiveAdd': + // Add all passive adds together + aggregator.passiveAdd = (aggregator.passiveAdd || 0) + result; + break; + case 'fail': + // Sum number of fails + aggregator.fail++; + break; + case 'conditional': + // Store array of conditionals + aggregator.conditional.push(linkedNode.data.text); + break; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js new file mode 100644 index 00000000..ea4383e6 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js @@ -0,0 +1,21 @@ +export default function aggregateProficiency({node, linkedNode, link}){ + if ( + link.data !== 'proficiency' && + !(link.data === 'definition' && linkedNode.data.type === 'skill') + ) return; + let proficiency; + if (link.data === 'proficiency'){ + proficiency = linkedNode.data.value || 0; + } else if (link.data === 'definition' && linkedNode.data.type === 'skill'){ + proficiency = linkedNode.data.baseProficiency || 0; + } else { + return; + } + // Store the highest proficiency + if ( + node.data.proficiency === undefined || + proficiency > node.data.proficiency + ){ + node.data.proficiency = proficiency; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js new file mode 100644 index 00000000..5cb17b1c --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -0,0 +1,11 @@ +import definition from './aggregateDefinition.js'; +import damageMultiplier from './aggregateDamageMultiplier.js'; +import effect from './aggregateEffect.js'; +import proficiency from './aggregateProficiency.js'; + +export default Object.freeze({ + definition, + damageMultiplier, + effect, + proficiency, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js new file mode 100644 index 00000000..5d1d365c --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -0,0 +1,35 @@ +import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; + +/* + * Variables with effects, proficiencies, or damage multipliers but no defining + * properties are added to the scope as implicit variables + */ + export default function computeImplicitVariable(node){ + const prop = {}; + const result = getAggregatorResult(node); + prop.total = result; + prop.value = result; + prop.proficiency = node.data.proficiency; + + // denormalise the aggregator fields + const aggregator = node.data.effectAggregator; + if (aggregator.advantage && !aggregator.disadvantage){ + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage){ + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; + + return prop; + } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js new file mode 100644 index 00000000..93b1aefe --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -0,0 +1,25 @@ +import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; + +export default function computeVariableAsAttribute(node, prop, scope){ + let result = getAggregatorResult(node); + prop.total = result; + prop.value = prop.total - (prop.damage || 0); + + // Proficiency + prop.proficiency = node.data.proficiency; + + // Ability scores get modifiers + if (prop.attributeType === 'ability'){ + prop.modifier = Math.floor((prop.currentValue - 10) / 2); + } + + // Hit dice denormalise constitution modifier + if (prop.attributeType === 'hitDice') { + prop.constitutionMod = scope['constitution']?.modifier || 0; + } + + // Stats that have no effects or base value can be hidden + prop.hide = !node.data.effectAggregator && + prop.baseValue === undefined || + undefined +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js new file mode 100644 index 00000000..c9070937 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js @@ -0,0 +1,13 @@ +import { parse } from '/imports/parser/parser.js'; + +export default function computeVariableAsConstant(node, prop){ + let string = prop.calculation; + if (!string) return; + let parseNode; + try { + parseNode = parse(string); + } catch (e) { + return; + } + prop.value = parseNode; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js new file mode 100644 index 00000000..e9ae5ae1 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -0,0 +1,63 @@ +export default function computeVariableAsSkill(node, prop, scope){ + // Skills are based on some ability Modifier + let ability = scope[prop.ability]; + prop.abilityMod = ability?.modifier || 0; + // TODO: Use this ability's skill effects/profs iff this skill is not a save + + // Proficiency + prop.proficiency = node.data.proficiency; + + // Get the character's proficiency bonus to apply + let profBonus = scope['proficiencyBonus']?.value || 0; + + // Multiply the proficiency bonus by the actual proficiency + if(prop.proficiency === 0.49){ + // Round down proficiency bonus in the special case + profBonus = Math.floor(profBonus * 0.5); + } else { + profBonus = Math.ceil(profBonus * prop.proficiency); + } + + // Combine everything to get the final result + const statBase = node.data.baseValue; + const aggregator = node.data.effectAggregator; + + // If there is no aggregator, determine if the prop can hide, then exit + if (!aggregator){ + prop.hide = statBase === undefined && + prop.proficiency == 0 || + undefined; + prop.value = statBase; + return; + } + // Combine aggregator + const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0; + let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul; + if (result < aggregator.min) result = aggregator.min; + if (result > aggregator.max) result = aggregator.max; + if (aggregator.set !== undefined) { + result = aggregator.set; + } + if (Number.isFinite(result)){ + result = Math.floor(result); + } + prop.value = result; + // Advantage/disadvantage + if (aggregator.advantage && !aggregator.disadvantage){ + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage){ + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js new file mode 100644 index 00000000..38c38a2a --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -0,0 +1,42 @@ +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; + +export default function getAggregatorResult(node){ + // Work out the base value as the greater of the deining stat value or + // the damage multiplier value + let statBase = node.data.baseValue; + const damageMultiplyValue = node.data.damageMultiplyValue; + if (statBase === undefined || damageMultiplyValue > statBase){ + statBase = damageMultiplyValue; + } + // get a reference to the aggregator + const aggregator = node.data.effectAggregator; + + // Without effects just return the defining base value + if (!aggregator) return statBase; + + let base; + if (!Number.isFinite(aggregator.base)){ + base = statBase || 0; + } else if (!Number.isFinite(statBase)){ + base = aggregator.base || 0; + } else { + base = Math.max(aggregator.base, statBase); + } + let result = (base + aggregator.add) * aggregator.mul; + if (result < aggregator.min) { + result = aggregator.min; + } + if (result > aggregator.max) { + result = aggregator.max; + } + if (aggregator.set !== undefined) { + result = aggregator.set; + } + if (!node.definingProp?.decimal && Number.isFinite(result)){ + result = Math.floor(result); + } else if (Number.isFinite(result)){ + result = stripFloatingPointOddities(result); + } + + return result; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js new file mode 100644 index 00000000..9bd83f04 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -0,0 +1,33 @@ +import { CompilationContext } from '/imports/parser/parser.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; + +export default function computeCalculations(node, scope){ + // evaluate all the calculations + node.data._computationDetails?.calculations?.forEach(calcObj => { + evaluateCalculation(calcObj, scope) + }); + node.data._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { + embedInlineCalculations(inlineCalcObj); + }); +} + +function evaluateCalculation(calculation, scope){ + const context = new CompilationContext(); + const parseNode = calculation._parsedCalculation; + const fn = calculation._parseLevel || 'reduce'; + const calculationScope = {...calculation._localScope, ...scope}; + const result = parseNode[fn](calculationScope, context); + calculation.value = result; + calculation.errors = context.errors; +} + +function embedInlineCalculations(inlineCalcObj){ + const string = inlineCalcObj.text; + const calculations = inlineCalcObj.inlineCalculations; + if (!string || !calculations) return; + let index = 0; + inlineCalcObj.value = string.replace(INLINE_CALCULATION_REGEX, substring => { + let calc = calculations[index++]; + return (calc && 'value' in calc) ? calc.value : substring; + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js new file mode 100644 index 00000000..d9f0cb26 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js @@ -0,0 +1,12 @@ +export default function evaluateToggles(node){ + let prop = node.data; + let toggles = prop._computationDetails?.toggleAncestors; + if (!toggles) return; + toggles.forEach(toggle => { + if (prop.inactive || !toggle.condition) return; + if (!toggle.condition.value){ + prop.inactive = true; + prop.deactivatedByToggle = true; + } + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js deleted file mode 100644 index 486fc817..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js +++ /dev/null @@ -1,33 +0,0 @@ -import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js'; - -export default function computeVariable(graph, node){ - if (!node.data) node.data = {}; - aggregateLinks(graph, node); -} - -function aggregateLinks(graph, node){ - let definingProp; - let overridenProps = []; - graph.forEachLinkedNode( - node.id, - (linkedNode, link) => { - if (!linkedNode.data) linkedNode.data = {}; - // Ignore inactive props - if (linkedNode.data.inactive) return; - // Apply all the aggregations - let arg = {node, linkedNode, link}; - aggregate.definitions(arg); - aggregate.baseValue(arg); - aggregate.damageMultipliers(arg); - aggregate.effects(arg); - aggregate.proficiencies(arg); - aggregate.skills(arg); - aggregate.toggles(arg); - }, - true // enumerate only outbound links - ); - // store the defining and overriden props on the node - if (!node.data) node.data = {}; - node.data.definingProp = definingProp; - node.data.overridenProps = overridenProps; -} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js deleted file mode 100644 index 81217078..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js +++ /dev/null @@ -1,11 +0,0 @@ -import { CompilationContext } from '/imports/parser/parser.js'; - -export default function evaluateCalculation(calculation, scope){ - const context = new CompilationContext(); - const parseNode = calculation._parsedCalculation; - const fn = calculation._parseLevel || 'reduce'; - const calculationScope = {...calculation._localScope, ...scope}; - const result = parseNode[fn](calculationScope, context); - calculation.value = result; - calculation.errors = context.errors; -} diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index e79620bb..e9f670b1 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -1,5 +1,6 @@ -import evaluateCalculation from '/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js'; -import computeVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js'; +import computeCalculations from '/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js'; +import computeToggles from '/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js'; +import computeByType from '/imports/api/creature/computation/newEngine/computeComputation/computeByType.js'; export default function computeCreatureComputation(computation){ const stack = []; @@ -16,20 +17,16 @@ export default function computeCreatureComputation(computation){ while (stack.length){ let top = stack[stack.length - 1]; if (top.visited){ - // The object has already + // The object has already been computed, skip stack.pop(); } else if (top.visitedChildren){ // Compute the top object of the stack compute(graph, top.node, scope); - // If the node holds a variable, store it in the scope - if (!top.node.data?.type){ - scope[top.node.id] = top.node.data; - } // Mark the object as visited and remove from stack top.visited = true; stack.pop(); } else { - // Push children to graph + // Push dependencies to graph to be computed first pushDependenciesToStack(top.node.id, graph, stack); top.visitedChildren = true; } @@ -37,32 +34,20 @@ export default function computeCreatureComputation(computation){ } function compute(graph, node, scope){ - // Get the property - let prop = node.data; - - // evaluate all the calculations - if (prop?._computationDetails?.calculations){ - prop._computationDetails.calculations.forEach(calcObj => { - evaluateCalculation(calcObj, scope) - }); - } - + // Determine the prop's active status by its toggles + computeToggles(node); + computeCalculations(node, scope); // Compute the property by type - let typeCompute = propTypeComputations[prop?.type || '_variable']; - typeCompute?.(graph, node); + computeByType[node.data?.type || '_variable']?.(graph, node, scope); } -var propTypeComputations = { - '_variable': computeVariable, -}; - function pushDependenciesToStack(nodeId, graph, stack){ graph.forEachLinkedNode( nodeId, (linkedNode, link) => { - // Ignore inventory links, they can't cause dependency loops - // and are already fully computed when they are created - if (link.data === 'inventory') return; + // Ignore inventory links, they are already fully computed when they are + // created + if (link.data === 'inventory' || link.data === 'classLevel') return; stack.push({ node: linkedNode, visited: false, diff --git a/app/imports/ui/utility/stripFloatingPointOddities.js b/app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js similarity index 100% rename from app/imports/ui/utility/stripFloatingPointOddities.js rename to app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index aff521f4..0f4591fb 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -78,15 +78,6 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ optional: true, index: 1, }, - // Denormalised list of all properties or creatures this property depends on - dependencies: { - type: Array, - defaultValue: [], - index: 1, - }, - 'dependencies.$': { - type: String, - }, }); CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 897d2139..86453014 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -90,6 +90,11 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: 'computedOnlyField', optional: true, }, + // Uses - usesUsed + usesLeft: { + type: Number, + optional: true, + }, }); const ComputedActionSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 1913b8ee..b4d98161 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -107,6 +107,12 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ type: SimpleSchema.Integer, optional: true, }, + // Attributes with proficiency grant it to all skills based on the attribute + proficiency: { + type: Number, + allowedValues: [0.49, 0.5, 1, 2], + optional: true, + }, // The computed creature constitution modifier for hit dice constitutionMod: { type: Number, diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index 8731a591..c3721458 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -34,6 +34,12 @@ let EffectSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, + // Conditional benefits store just uncomputed text + text: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, //which stats the effect is applied to stats: { type: Array, diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index 23a7bbf2..4c96b558 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -36,6 +36,7 @@ let SavingThrowSchema = createPropertySchema({ const ComputedOnlySavingThrowSchema = createPropertySchema({ dc: { type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, }); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index ef9bc852..71846ae1 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -46,6 +46,7 @@ let SkillSchema = createPropertySchema({ baseProficiency: { type: Number, optional: true, + allowedValues: [0.49, 0.5, 1, 2], }, // The starting value, before effects baseValue: { @@ -93,7 +94,7 @@ let ComputedOnlySkillSchema = createPropertySchema({ // Computed proficiency multiplier proficiency: { type: Number, - allowedValues: [0, 0.5, 1, 2], + allowedValues: [0, 0.49, 0.5, 1, 2], defaultValue: 0, }, // Compiled text of all conditional benefits @@ -103,15 +104,6 @@ let ComputedOnlySkillSchema = createPropertySchema({ }, 'conditionalBenefits.$': { type: String, - }, - // Compiled text of all roll bonuses - rollBonuses: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.rollBonusCount, - }, - 'rollBonuses.$': { - type: String, }, // Computed number of things forcing this skill to fail fail: { diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js index 63f16635..9acc8931 100644 --- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js @@ -23,6 +23,10 @@ const AttributeConsumedSchema = createPropertySchema({ }); const ComputedOnlyAttributeConsumedSchema = createPropertySchema({ + quantity: { + type: 'computedOnlyField', + optional: true, + }, available: { type: Number, optional: true, @@ -37,10 +41,6 @@ const ComputedOnlyAttributeConsumedSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, - quantity: { - type: 'computedOnlyField', - optional: true, - }, }); const ComputedAttributeConsumedSchema = new SimpleSchema() diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index 42eeb402..1bf08904 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -36,13 +36,6 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({ type: 'computedOnlyField', optional: true, }, - // This appears both in the computed and uncomputed schema because it can be - // set by both a computation or a form - itemId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true, - }, itemName: { type: String, max: STORAGE_LIMITS.name, diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js index b65a5523..8f5e4bb9 100644 --- a/app/imports/parser/parseTree/SymbolNode.js +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -9,10 +9,14 @@ export default class SymbolNode extends ParseNode { toString(){ return `${this.name}` } - compile(scope){ + compile(scope, context){ let value = scope && scope[this.name]; let type = typeof value; - // For objects, get their value + // For parse nodes, compile and return + if (value instanceof ParseNode){ + return value.compile(scope, context); + } + // For objects, default to their .value if (type === 'object'){ value = value.value; type = typeof value;