diff --git a/app/.meteor/packages b/app/.meteor/packages index 102191c1..8f08cd9e 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -11,7 +11,7 @@ accounts-google@1.4.0 email@2.2.5 meteor-base@1.5.1 mobile-experience@1.1.0 -mongo@1.16.6 +mongo@1.16.7 session@1.2.1 tracker@1.3.2 logging@1.3.2 diff --git a/app/.meteor/release b/app/.meteor/release index e8cfc7ec..6641d047 100644 --- a/app/.meteor/release +++ b/app/.meteor/release @@ -1 +1 @@ -METEOR@2.12 +METEOR@2.13.3 diff --git a/app/.meteor/versions b/app/.meteor/versions index 4f1ea628..3a927074 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -30,7 +30,7 @@ ddp@1.4.1 ddp-client@2.6.1 ddp-common@1.4.0 ddp-rate-limiter@1.2.0 -ddp-server@2.6.1 +ddp-server@2.6.2 diff-sequence@1.1.2 dynamic-import@0.7.3 ecmascript@0.16.7 @@ -42,7 +42,7 @@ email@2.2.5 es5-shim@4.8.0 fetch@0.1.3 geojson-utils@1.0.11 -google-oauth@1.4.3 +google-oauth@1.4.4 hot-code-push@1.0.4 html-tools@1.1.3 htmljs@1.1.1 @@ -55,7 +55,7 @@ littledata:synced-cron@1.5.1 localstorage@1.2.0 logging@1.3.2 mdg:validated-method@1.3.0 -meteor@1.11.2 +meteor@1.11.3 meteor-base@1.5.1 meteortesting:browser-tests@1.4.2 meteortesting:mocha@2.1.0 @@ -70,7 +70,7 @@ mobile-status-bar@1.1.0 modern-browsers@0.1.9 modules@0.19.0 modules-runtime@0.13.1 -mongo@1.16.6 +mongo@1.16.7 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 diff --git a/app/imports/api/creature/creatures/CreatureVariables.js b/app/imports/api/creature/creatures/CreatureVariables.js index 27fe0af7..b52805af 100644 --- a/app/imports/api/creature/creatures/CreatureVariables.js +++ b/app/imports/api/creature/creatures/CreatureVariables.js @@ -1,3 +1,5 @@ +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; + //set up the collection for creature variables let CreatureVariables = new Mongo.Collection('creatureVariables'); @@ -7,15 +9,29 @@ if (Meteor.isServer) { } /** No schema because the structure isn't known until compute time - * Expect documents to looke like: + * Expect documents to look like: * { * _id: "nE8Ngd6K4L4jSxLY2", * _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature - * explicitlyDefinedVariableName: {...some creatureProperty} + * explicitlyDefinedVariableName: {...some creatureProperty}, + * // Must be found in CreatureProperties before using: + * linkedProperty: { _propId: "nE8Ngd6K1234SxLY2" } * implicitVariableName: {value: 10}, * undefinedVariableName: {}, * } * Where top level fields that don't start with `_` are variables on the sheet **/ +/** + * Get the property from the given scope, respecting properties that are just a link to the actual + * property document + */ +export function getFromScope(name, scope) { + let value = scope?.[name]; + if (value?._propId) { + value = getSingleProperty(scope._creatureId, value._propId); + } + return value; +} + export default CreatureVariables; diff --git a/app/imports/api/engine/actions/ActionContext.ts b/app/imports/api/engine/actions/ActionContext.ts index ee87feb1..13f36596 100644 --- a/app/imports/api/engine/actions/ActionContext.ts +++ b/app/imports/api/engine/actions/ActionContext.ts @@ -1,11 +1,18 @@ import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs'; import { - getCreature, getVariables, getPropertiesOfType + getCreature, getVariables, getPropertiesOfType, replaceLinkedVariablesWithProps } from '/imports/api/engine/loadCreatures'; import { groupBy, remove } from 'lodash'; export default class ActionContext { - constructor(creatureId, targetIds = [], method) { + creature: any; + log: any; + scope: any; + targets: Array; + triggers: Array; + method: any; + + constructor(creatureId, targetIds: string[] = [], method) { // Get the creature this.creature = getCreature(creatureId) @@ -20,6 +27,7 @@ export default class ActionContext { // Get the variables of the acting creature this.creature.variables = getVariables(creatureId); + replaceLinkedVariablesWithProps(this.creature.variables); delete this.creature.variables._id; delete this.creature.variables._creatureId; // Alias as scope @@ -52,10 +60,10 @@ export default class ActionContext { // Group the triggers into triggers.. or // triggers.doActionProperty.. this.triggers = groupBy(this.triggers, 'event'); - for (let event in this.triggers) { + for (const event in this.triggers) { if (event === 'doActionProperty') { this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType'); - for (let propertyType in this.triggers[event]) { + for (const propertyType in this.triggers[event]) { this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing'); } } else { diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 6e2cf6bd..6455835a 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -1,13 +1,12 @@ import { - setLineageOfDocs, - renewDocIds + renewDocIds, } from '/imports/api/parenting/parentingFunctions'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey'; import { get } from 'lodash'; import resolve, { map, toString } from '/imports/parser/resolve'; -import symbol from '/imports/parser/parseTree/symbol'; +import accessor from '/imports/parser/parseTree/accessor'; import logErrors from './shared/logErrors'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53'; @@ -25,7 +24,7 @@ export default function applyBuff(node, actionContext) { // Then copy the descendants of the buff to the targets let propList = [prop]; - function addChildrenToPropList(children, { skipCrystalize } = {}) { + function addChildrenToPropList(children, { skipCrystalize } = { skipCrystalize: false }) { children.forEach(child => { if (skipCrystalize) child.node._skipCrystalize = true; propList.push(child.node); @@ -40,13 +39,20 @@ export default function applyBuff(node, actionContext) { crystalizeVariables({ propList, actionContext }); } - let oldParent = { - id: prop.parent.id, - collection: prop.parent.collection, - }; buffTargets.forEach(target => { + const targetPropList = EJSON.clone(propList); + // Move the properties to the target by replacing the old subtree parent and root with the ' + // target id + renewDocIds({ + docArray: targetPropList, + idMap: { + [prop.parentId]: target._id, + [prop.root.id]: target._id, + }, + collectionMap: { [prop.root.collection]: 'creatures' } + }); // Apply the buff - copyNodeListToTarget(propList, target, oldParent); + CreatureProperties.batchInsert(targetPropList); //Log the buff let logValue = prop.description?.value @@ -81,25 +87,6 @@ export default function applyBuff(node, actionContext) { // Don't apply the children of the buff, they get copied to the target instead } -function copyNodeListToTarget(propList, target, oldParent) { - let ancestry = [{ collection: 'creatures', id: target._id }]; - setLineageOfDocs({ - docArray: propList, - newAncestry: ancestry, - oldParent, - }); - renewDocIds({ - docArray: propList, - }); - /* - setDocToLastOrder({ - collection: CreatureProperties, - doc: propList[0], - }); - */ - CreatureProperties.batchInsert(propList); -} - /** * Replaces all variables with their resolved values * except variables of the form `~target.thing.total` become `thing.total` @@ -118,7 +105,7 @@ function crystalizeVariables({ propList, actionContext }) { calcObj.parseNode = map(calcObj.parseNode, node => { // Skip nodes that aren't symbols or accessors if ( - node.parseType !== 'accessor' && node.parseType !== 'symbol' + node.parseType !== 'accessor' ) return node; // Handle variables if (node.name === '~target') { @@ -126,7 +113,7 @@ function crystalizeVariables({ propList, actionContext }) { if (node.parseType === 'accessor') { node.name = node.path.shift(); if (!node.path.length) { - return symbol.create({ name: node.name }) + return accessor.create({ name: node.name }) } } else { // Can't strip symbols diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 11735f7e..5429bc55 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -3,7 +3,7 @@ import applyChildren from '/imports/api/engine/actions/applyPropertyByType/share import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs'; import resolve, { Context, toString } from '/imports/parser/resolve'; import logErrors from './shared/logErrors'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation' import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; import { getPropertiesOfType @@ -37,8 +37,8 @@ export default function applyDamage(node, actionContext) { const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; // roll the dice only and store that string - applyEffectsToCalculationParseNode(prop.amount, actionContext); - const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context); + recalculateCalculation(prop.amount, actionContext, undefined, 'compile'); + const { result: rolled } = resolve('roll', prop.amount.valueNode, scope, context); if (rolled.parseType !== 'constant') { logValue.push(toString(rolled)); } @@ -88,8 +88,8 @@ export default function applyDamage(node, actionContext) { let damageOnSave, saveNode, saveRoll; if (prop.save) { if (prop.save.damageFunction?.calculation) { - applyEffectsToCalculationParseNode(prop.save.damageFunction, actionContext); - let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.parseNode, scope, context); + recalculateCalculation(prop.save.damageFunction, actionContext, undefined, 'compile'); + let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.valueNode, scope, context); saveRoll = toString(saveDamageRolled); let { result: saveDamageResult } = resolve('reduce', saveDamageRolled, scope, context); // If we didn't end up with a constant of finite amount, give up @@ -167,7 +167,7 @@ export default function applyDamage(node, actionContext) { creatureId: target._id, content: [{ name, - value: `Recieved **${damageDealt}** ${suffix}`, + value: `Received **${damageDealt}** ${suffix}`, }], } }); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index 1289f129..63185db0 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -1,6 +1,6 @@ import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; import logErrors from './shared/logErrors'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; import resolve, { toString } from '/imports/parser/resolve'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; @@ -12,8 +12,8 @@ export default function applyRoll(node, actionContext) { const logValue = []; // roll the dice only and store that string - applyEffectsToCalculationParseNode(prop.roll, actionContext); - const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope); + recalculateCalculation(prop.roll, actionContext, undefined, 'compile'); + const { result: rolled, context } = resolve('roll', prop.roll.valueNode, actionContext.scope); if (rolled.parseType !== 'constant') { logValue.push(toString(rolled)); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js deleted file mode 100644 index cf0ae47f..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js +++ /dev/null @@ -1,46 +0,0 @@ -import operator from '/imports/parser/parseTree/operator'; -import { parse } from '/imports/parser/parser'; -import logErrors from './logErrors'; - -export default function applyEffectsToCalculationParseNode(calcObj, actionContext) { - 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], actionContext) - } - }); - // Add the highest proficiency as well - let highestProficiency; - calcObj.proficiencies?.forEach(proficiency => { - if ( - proficiency.value > highestProficiency - || (highestProficiency === undefined && Number.isFinite(proficiency.value)) - ) { - highestProficiency = proficiency.value; - } - }); - if (highestProficiency) { - try { - let profParseNode = parse(highestProficiency.toString()); - calcObj.parseNode = operator.create({ - left: calcObj.parseNode, - right: profParseNode, - operator: '+', - fn: 'add' - }); - } catch (e) { - logErrors([e], actionContext) - } - } -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index 5e4e5c6a..44dc3b76 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -1,11 +1,59 @@ -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode'; import logErrors from './logErrors'; +import { toPrimitiveOrString } from '/imports/parser/resolve'; +import { + aggregateCalculationEffects, + aggregateCalculationProficiencies, + resolveCalculationNode, +} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; +import resolve from '/imports/parser/resolve'; -export default function recalculateCalculation(calc, actionContext, context) { - if (!calc?.parseNode) return; - calc._parseLevel = 'reduce'; - applyEffectsToCalculationParseNode(calc, actionContext); - evaluateCalculation(calc, actionContext.scope, context); - logErrors(calc.errors, actionContext); +// Redo the work of imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +// But in the action scope +export default function recalculateCalculation(calcObj, actionContext, context, parseLevel = 'reduce') { + if (!calcObj?.parseNode) return; + calcObj._parseLevel = parseLevel; + // Re-resolve the parse node + resolveCalculationNode(calcObj, calcObj.parseNode, actionContext.scope, context); + // store the unaffected value + if (calcObj.effectIds || calcObj.proficiencyIds) { + calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode); + } + // Apply all the effects and proficiencies + aggregateCalculationEffects( + calcObj, + id => getSingleProperty(actionContext.creature._id, id) + ); + aggregateCalculationProficiencies( + calcObj, + id => getSingleProperty(actionContext.creature._id, id), + actionContext.scope['proficiencyBonus']?.value || 0 + ); + // Resolve the modified valueNode + resolveCalculationNode(calcObj, calcObj.valueNode, actionContext.scope, context); + + // Store the primitive value + calcObj.value = toPrimitiveOrString(calcObj.valueNode); + + logErrors(calcObj.errors, actionContext); +} + +export function rollAndReduceCalculation(calcObj, actionContext, context) { + // Compile + recalculateCalculation(calcObj, actionContext, context, 'compile'); + const compiled = calcObj.valueNode; + const compileErrors = context.errors; + + // Roll + context.errors = []; + const { result: rolled } = resolve('roll', calcObj.valueNode, actionContext.scope, context); + const rollErrors = context.errors; + + // Reduce + context.errors = []; + const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context); + const reduceErrors = context.errors; + + // Return + return { compiled, compileErrors, rolled, rollErrors, reduced, reduceErrors }; } diff --git a/app/imports/api/engine/actions/applyTriggers.ts b/app/imports/api/engine/actions/applyTriggers.ts index 60f9d555..5f71e496 100644 --- a/app/imports/api/engine/actions/applyTriggers.ts +++ b/app/imports/api/engine/actions/applyTriggers.ts @@ -39,7 +39,7 @@ export function applyTrigger(trigger, prop, actionContext) { // Prevent triggers from firing if their condition is false if (trigger.condition?.parseNode) { recalculateCalculation(trigger.condition, actionContext); - if (!trigger.condition.value) return; + if (!trigger.condition.value?.value) return; } // Prevent triggers from firing themselves in a loop diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index 8ee1096f..8be443d0 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -7,7 +7,7 @@ import rollDice from '/imports/parser/rollDice'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; import { applyTriggers } from '/imports/api/engine/actions/applyTriggers'; import ActionContext from '/imports/api/engine/actions/ActionContext'; -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; const doCheck = new ValidatedMethod({ name: 'creatureProperties.doCheck', @@ -77,7 +77,7 @@ function rollCheck(prop, actionContext) { let rollModifierText = numberToSignedString(rollModifier, true); - const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope) + const { effectBonus, effectString } = applyUnresolvedEffects(prop, actionContext) rollModifierText += effectString; rollModifier += effectBonus; @@ -117,7 +117,8 @@ function rollCheck(prop, actionContext) { }); } -export function applyUnresolvedEffects(prop, scope) { +// TODO replace this with recalculating and then rolling/reducing the value node +export function applyUnresolvedEffects(prop, actionContext) { let effectBonus = 0; let effectString = ''; if (!prop.effects) { @@ -126,8 +127,7 @@ export function applyUnresolvedEffects(prop, scope) { prop.effects.forEach(effect => { if (!effect.amount?.parseNode) return; if (effect.operation !== 'add') return; - effect.amount._parseLevel = 'reduce'; - evaluateCalculation(effect.amount, scope); + recalculateCalculation(effect.amount, actionContext, context, 'reduce'); if (typeof effect.amount?.value !== 'number') return; effectBonus += effect.amount.value; effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}` diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 5ee4766a..5e3601e7 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -12,8 +12,8 @@ export default function linkCalculationDependencies(dependencyGraph, prop, { pro // Skip empty calculations that aren't targeted by anything if ( !calcObj.calculation - && !calcObj.effects - && !calcObj.proficiencies + && !calcObj.effectIds + && !calcObj.proficiencyIds ) return; dependencyGraph.addNode(calcNodeId, calcObj); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index a57a4c25..ece395a7 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,19 +1,46 @@ -import evaluateCalculation from '../../utility/evaluateCalculation'; -import { getPropertyName } from '/imports/constants/PROPERTIES'; +import call from '/imports/parser/parseTree/call'; +import constant from '/imports/parser/parseTree/constant'; +import operator from '/imports/parser/parseTree/operator'; +import parenthesis from '/imports/parser/parseTree/parenthesis'; +import resolve, { toPrimitiveOrString } from '/imports/parser/resolve'; export default function computeCalculation(computation, node) { const calcObj = node.data; - evaluateCalculation(calcObj, computation.scope); - if (calcObj.effects || calcObj.proficiencies) { - calcObj.baseValue = calcObj.value; + if (!calcObj) return; + // resolve the parse node into the initial value + resolveCalculationNode(calcObj, calcObj.parseNode, computation.scope); + // Store the unaffected value + if (calcObj.effectIds || calcObj.proficiencyIds) { + calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode); } - aggregateCalculationEffects(node, computation); - aggregateCalculationProficiencies(node, computation); + // link and aggregate the effects and proficiencies + linkCalculationEffects(node, computation); + aggregateCalculationEffects(calcObj, id => computation.propsById[id]); + linkCalculationProficiencies(node, computation) + aggregateCalculationProficiencies(calcObj, id => computation.propsById[id], computation.scope['proficiencyBonus']?.value || 0); + + // Resolve the valueNode after effects and proficiencies have been applied to it + resolveCalculationNode(calcObj, calcObj.valueNode, computation.scope); + + // Store the value as a primitive + calcObj.value = toPrimitiveOrString(calcObj.valueNode); + + // remove the working fields + delete calcObj._parseLevel; + delete calcObj._localScope; } -function aggregateCalculationEffects(node, computation) { +export function resolveCalculationNode(calculation, parseNode, scope) { + const fn = calculation._parseLevel; + const calculationScope = { ...calculation._localScope, ...scope }; + const { result: resultNode, context } = resolve(fn, parseNode, calculationScope); + calculation.errors = context.errors; + calculation.valueNode = resultNode; +} + +function linkCalculationEffects(node, computation) { const calcObj = node.data; - delete calcObj.effects; + delete calcObj.effectIds; computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { @@ -25,35 +52,115 @@ function aggregateCalculationEffects(node, computation) { 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, - }, - }); + calcObj.effectIds = calcObj.effectIds || []; + calcObj.effectIds.push(linkedNode.data._id); }, true // enumerate only outbound links ); - if (calcObj.effects && typeof calcObj.value === 'number') { - calcObj.effects.forEach(effect => { - if ( - effect.operation === 'add' && - effect.amount && typeof effect.amount.value === 'number' - ) { - calcObj.value += effect.amount.value - } +} + +export function aggregateCalculationEffects(calcObj, getEffectFromId) { + // dictionary of {[operation]: parseNode} + const aggregator = {}; + // Store all effect values + calcObj.effectIds?.forEach(effectId => { + const effect = getEffectFromId(effectId); + const op = effect.operation; + switch (op) { + case undefined: + break; + // Conditionals stored as a list of text + case 'conditional': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.text); + break; + // Adv/Dis and Fails just count instances + case 'advantage': + case 'disadvantage': + case 'fail': + if (calcObj[op] === undefined) calcObj[op] = 0; + calcObj[op]++; + break; + // Math functions store value parseNodes + case 'base': + case 'add': + case 'mul': + case 'min': + case 'max': + case 'set': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.amount.valueNode); + break; + // No case for passiveAdd, it doesn't make sense in this context + } + }); + /** + * Aggregate the effects in a parse tree like so + * x = max(...base, unaffectedValue) + * x = x + sum(...add) + * x = x * mul(...mul) + * x = min(...min, x) + * x = max(...max, x) + * x = set(last(...set))a + */ + // Set + // If we do set, return early, nothing else matters + if (aggregator.set) { + calcObj.valueNode = aggregator.set[aggregator.set.length - 1]; + return; + } + // Base value + if (aggregator.base) { + calcObj.valueNode = call.create({ + functionName: 'max', + args: [calcObj.valueNode, aggregator.base] + }); + } + // Add + aggregator.add?.forEach(node => { + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: node, + operator: '+' + }); + }); + // Multiply + if (aggregator.mul) { + // Wrap the previous node in brackets if it's another operator + if (calcObj.parseType === 'operator') { + calcObj.valueNode = parenthesis.create({ + content: calcObj.valueNode + }); + } + // Append all multiplications + aggregator.mul.forEach(node => { + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: node, + operator: '*' + }); + }); + } + // Min + if (aggregator.min) { + calcObj.valueNode = call.create({ + functionName: 'max', + args: [calcObj.valueNode, aggregator.min] + }); + } + // Max + if (aggregator.max) { + calcObj.valueNode = call.create({ + functionName: 'min', + args: [calcObj.valueNode, aggregator.max] }); } } -function aggregateCalculationProficiencies(node, computation) { +function linkCalculationProficiencies(node, computation) { const calcObj = node.data; - delete calcObj.proficiencies; + delete calcObj.proficiencyIds; delete calcObj.proficiency; - let profBonus = computation.scope['proficiencyBonus']?.value || 0; // Go through all the links and collect them on the calculation computation.dependencyGraph.forEachLinkedNode( @@ -65,49 +172,52 @@ function aggregateCalculationProficiencies(node, computation) { if (!linkedNode.data) return; // Ignoring inactive props if (linkedNode.data.inactive) return; - // Compute the proficiency and value - let proficiency, value; - if (linkedNode.data.type === 'proficiency') { - proficiency = linkedNode.data.value || 0; - // Multiply the proficiency bonus by the actual proficiency - if (proficiency === 0.49) { - // Round down proficiency bonus in the special case - value = Math.floor(profBonus * 0.5); - } else { - value = Math.ceil(profBonus * proficiency); - } - } else if (linkedNode.data.type === 'skill') { - value = linkedNode.data.value || 0; - proficiency = linkedNode.data.proficiency || 0; - } // Collate proficiencies - calcObj.proficiencies = calcObj.proficiencies || []; - calcObj.proficiencies.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - type: linkedNode.data.type, - proficiency, - value, - }); + calcObj.proficiencyIds = calcObj.proficiencyIds || []; + calcObj.proficiencyIds.push(linkedNode.data._id); }, true // enumerate only outbound links ); - - // Apply the highest proficiency, marking all others as overridden - if (calcObj.proficiencies && typeof calcObj.value === 'number') { - calcObj.proficiency = 0; - calcObj.proficiencyBonus = 0; - let currentProf; - calcObj.proficiencies.forEach(prof => { - if (prof.value > calcObj.proficiencyBonus) { - if (currentProf) currentProf.overridden = true; - calcObj.proficiencyBonus = prof.value; - calcObj.proficiency = prof.proficiency; - currentProf = prof; - } else { - prof.overridden = true; - } - }); - calcObj.value += calcObj.proficiencyBonus; - } +} + +export function aggregateCalculationProficiencies(calcObj, getProficiencyFromId, profBonus) { + if (!calcObj.proficiencyIds) return; + // Apply the highest proficiency, marking all others as overridden + calcObj.proficiency = 0; + calcObj.proficiencyBonus = 0; + let currentProf; + calcObj.proficiencyIds.forEach(profId => { + const profProp = getProficiencyFromId(profId) + if (!profProp) { + console.warn('proficiency linked but not found ', profId); + } + // Compute the proficiency and value + let proficiency, value; + if (profProp.type === 'proficiency') { + proficiency = profProp.value || 0; + // Multiply the proficiency bonus by the actual proficiency + if (proficiency === 0.49) { + // Round down proficiency bonus in the special case + value = Math.floor(profBonus * 0.5); + } else { + value = Math.ceil(profBonus * proficiency); + } + } else if (profProp.type === 'skill') { + value = profProp.value || 0; + proficiency = profProp.proficiency || 0; + } + if (value > calcObj.proficiencyBonus) { + if (currentProf) currentProf.overridden = true; + calcObj.proficiencyBonus = value; + calcObj.proficiency = proficiency; + currentProf = profProp; + } else { + profProp.overridden = true; + } + }); + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, + right: constant.create({ value: calcObj.proficiencyBonus }), + operator: '+' + }); } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js index 5e22f138..2023759a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js @@ -1,5 +1,5 @@ import { has } from 'lodash'; -import evaluateCalculation from '../../utility/evaluateCalculation'; +import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; export default function computePointBuy(computation, node) { const prop = node.data; @@ -26,7 +26,9 @@ export default function computePointBuy(computation, node) { } // Evaluate the cost function if (!costFunction) return; - evaluateCalculation(costFunction, { ...computation.scope, value: row.value }); + resolveCalculationNode(costFunction, costFunction.parseNode, { + ...computation.scope, value: row.value + }); // Write calculation errors costFunction.errors?.forEach(error => { if (error?.message) { diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index f1422abe..aab7f030 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -32,23 +32,11 @@ export default function aggregateDefinition({ node, linkedNode, link }) { if (propBaseValue === undefined) return; // Store a summary of the definition as a base value effect - node.data.effects = node.data.effects || []; + node.data.effectIds = node.data.effectIds || []; if (prop.type === 'pointBuyRow') { - node.data.effects.push({ - _id: prop.tableId, - name: prop.tableName, - operation: 'base', - amount: { value: propBaseValue }, - type: 'pointBuy', - }); + node.data.effectIds.push(prop.tableId); } else { - node.data.effects.push({ - _id: prop._id, - name: prop.name, - operation: 'base', - amount: { value: propBaseValue }, - type: prop.type, - }); + node.data.effectIds.push(prop._id); } if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) { node.data.baseValue = propBaseValue; 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 2c20f27c..a06d3872 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 @@ -1,5 +1,3 @@ -import { pick } from 'lodash'; - export default function aggregateEffect({ node, linkedNode, link }) { if (link.data !== 'effect') return; // store the effect aggregator, its presence indicates that the variable is @@ -19,27 +17,9 @@ export default function aggregateEffect({ node, linkedNode, link }) { rollBonus: [], }; - // Store a summary of the effect itself - node.data.effects = node.data.effects || []; - // Store either just - let effectAmount; - if (!linkedNode.data.amount) { - effectAmount = undefined; - } else if (typeof linkedNode.data.amount.value === 'string') { - effectAmount = pick(linkedNode.data.amount, [ - 'calculation', 'parseNode', 'parseError', 'value' - ]); - } else { - effectAmount = pick(linkedNode.data.amount, ['value']); - } - node.data.effects.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - operation: linkedNode.data.operation, - amount: effectAmount, - type: linkedNode.data.type, - text: linkedNode.data.text, - }); + // Store a link to the effect + node.data.effectIds = node.data.effectIds || []; + node.data.effectIds.push(linkedNode.data._id); // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; 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 73cd7112..3427dae7 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -49,5 +49,5 @@ export default function computeVariableAsAttribute(computation, node, prop) { undefined // Store effects - prop.effects = node.data.effects; + prop.effectIds = node.data.effectIds; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 5dd84740..f7ffceed 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -34,7 +34,7 @@ export default function computeVariableAsSkill(computation, node, prop) { const aggregatorBase = aggregator?.base || 0; // Store effects - prop.effects = node.data.effects; + prop.effectIds = node.data.effectIds; // If there is no aggregator, determine if the prop can hide, then exit if (!aggregator) { diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js index da94c60e..7f427160 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js @@ -17,12 +17,8 @@ export default function () { prop('strengthId').modifier, -1, 'The proficiency bonus should not change the strength modifier' ); - assert.isTrue( - !!hasLink('actionId.attackRoll', 'tagTargetedProficiency'), - 'There should be a link from the proficiency to the attack roll' - ); - assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here') - assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here') + assert.exists(prop('actionId').attackRoll.proficiencyIds, 'The proficiency aggregator should be here') + assert.exists(prop('actionId').attackRoll.proficiencyIds[0], 'The proficiency should be here') // attack roll = strength.mod + proficiencyBonus/2 rounded down // = -1 + 13/2 = -1 + 6 = 5 assert.equal( diff --git a/app/imports/api/engine/computation/utility/evaluateCalculation.js b/app/imports/api/engine/computation/utility/evaluateCalculation.js deleted file mode 100644 index 71fd1e62..00000000 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ /dev/null @@ -1,19 +0,0 @@ -import resolve, { toString } from '/imports/parser/resolve'; - -export default function evaluateCalculation(calculation, scope, givenContext) { - const parseNode = calculation.parseNode; - const fn = calculation._parseLevel; - const calculationScope = { ...calculation._localScope, ...scope }; - const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext); - calculation.errors = context.errors; - if (resultNode?.parseType === 'constant') { - calculation.value = resultNode.value; - } else if (resultNode?.parseType === 'error') { - calculation.value = null; - } else { - calculation.value = toString(resultNode); - } - // remove the working fields - delete calculation._parseLevel; - delete calculation._localScope; -} diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index 90a43e99..1c2473e4 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -19,6 +19,12 @@ export default function writeScope(creatureId, computation) { // Mongo can't handle keys that start with a dollar sign if (key[0] === '$' || key[0] === '_') continue; + // Remove empty objects + if (Object.keys(scope[key]).length === 0) { + delete scope[key]; + continue; + } + // Remove large properties that aren't likely to be accessed delete scope[key].parent; @@ -29,6 +35,11 @@ export default function writeScope(creatureId, computation) { } } + // If this is a creature property, replace the property with a link + if (scope[key]._id && scope[key].type) { + scope[key] = { _propId: scope[key]._id }; + } + // Only update changed fields if (!EJSON.equals(variables[key], scope[key])) { if (!$set) $set = {}; diff --git a/app/imports/api/engine/loadCreatures.ts b/app/imports/api/engine/loadCreatures.ts index 65b51093..b213eae3 100644 --- a/app/imports/api/engine/loadCreatures.ts +++ b/app/imports/api/engine/loadCreatures.ts @@ -114,6 +114,14 @@ export function getVariables(creatureId) { return variables; } +export function replaceLinkedVariablesWithProps(variables) { + for (const key in variables) { + const propId = variables[key]?._propId; + if (!propId) continue; + variables[key] = getSingleProperty(variables._creatureId, propId); + } +} + export function getPropertyAncestors(creatureId: string, propertyId: string) { const prop = getSingleProperty(creatureId, propertyId); if (!prop) return []; diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index fe5ea9e4..22a8e630 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -199,14 +199,21 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ removeBeforeCompute: true, }, // A list of effect ids targeting this attribute - effects: { + 'effectIds': { type: Array, optional: true, removeBeforeCompute: true, }, - 'effects.$': { - type: Object, - blackbox: true, + 'effectIds.$': { + type: String, + }, + 'proficiencyIds': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'proficiencyIds.$': { + type: String, }, }); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index da9d7588..171c15f7 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -134,14 +134,21 @@ let ComputedOnlySkillSchema = createPropertySchema({ removeBeforeCompute: true, }, // A list of effect ids targeting this skill - effects: { + 'effectIds': { type: Array, optional: true, removeBeforeCompute: true, }, - 'effects.$': { - type: Object, - blackbox: true, + 'effectIds.$': { + type: String, + }, + 'proficiencyIds': { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'proficiencyIds.$': { + type: String, }, }) diff --git a/app/imports/api/properties/subSchemas/computedField.ts b/app/imports/api/properties/subSchemas/computedField.ts index 38516cea..cba0c0da 100644 --- a/app/imports/api/properties/subSchemas/computedField.ts +++ b/app/imports/api/properties/subSchemas/computedField.ts @@ -29,20 +29,40 @@ function fieldToCompute(field) { function computedOnlyField(field) { const schemaObj = { + // The value (or calculation string) before any effects/proficiencies are applied or rolls made + [`${field}.unaffected`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + blackbox: true, + }, + // The value (or calculation string) after applying all effects [`${field}.value`]: { type: SimpleSchema.oneOf(String, Number), optional: true, - removeBeforeCompute: true, + blackbox: true, }, - // A list of effects targeting this calculation - [`${field}.effects`]: { + // The value as a parse node, after applying all effects + [`${field}.valueNode`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + blackbox: true, + }, + // A list of effect Ids targeting this calculation + [`${field}.effectIds`]: { type: Array, optional: true, removeBeforeCompute: true, }, - [`${field}.effects.$`]: { - type: Object, - blackbox: true, + [`${field}.effectIds.$`]: { + type: String, + }, + [`${field}.proficiencyIds`]: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.proficiencyIds.$`]: { + type: String, }, // A cache of the parse result of the calculation [`${field}.parseNode`]: { @@ -69,6 +89,30 @@ function computedOnlyField(field) { [`${field}.errors.$`]: { type: ErrorSchema, }, + // Effect aggregations + [`${field}.advantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.disadvantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.fail`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional`]: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional.$`]: { + type: String, + }, } includeParentFields(field, schemaObj); return new SimpleSchema(schemaObj); diff --git a/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue index 94255270..e3c2c69d 100644 --- a/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue @@ -85,21 +85,15 @@