From a97be2f93a12a46a4f30bc77880013e9226fdb5b Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 1 Mar 2021 13:27:48 +0200 Subject: [PATCH] Made constants work in calculations performed after recomputation --- .../api/creature/actions/applyAdjustment.js | 33 +++++----- .../api/creature/actions/applyDamage.js | 58 +++++++++++++---- app/imports/api/creature/actions/applyRoll.js | 30 ++++----- app/imports/api/creature/actions/applySave.js | 16 ++--- .../api/creature/actions/applyToggle.js | 31 +++++---- .../afterComputation/evaluateString.js | 65 ++++++++++++++----- .../engine/writeCreatureVariables.js | 40 +++++++----- .../ui/creature/slots/SlotFillDialog.vue | 17 ++--- 8 files changed, 177 insertions(+), 113 deletions(-) diff --git a/app/imports/api/creature/actions/applyAdjustment.js b/app/imports/api/creature/actions/applyAdjustment.js index 06e664b2..88740430 100644 --- a/app/imports/api/creature/actions/applyAdjustment.js +++ b/app/imports/api/creature/actions/applyAdjustment.js @@ -13,42 +13,43 @@ export default function applyAdjustment({ ...creature.variables, ...actionContext, }; - try { - var {result, errors} = evaluateString(prop.amount, scope, 'reduce'); - if (typeof result !== 'number') { - log.content.push({ - name: 'Attribute damage', - error: errors.join(', ') || 'Something went wrong', - }); - } - } catch (e){ + var {result, context} = evaluateString({ + string: prop.amount, + scope, + fn: 'reduce' + }); + context.errors.forEach(e => { log.content.push({ - name: 'Attribute damage', - error: e.toString(), + name: 'Attribute damage error', + error: e.message || e.toString(), }); - } + }); if (damageTargets) { damageTargets.forEach(target => { if (prop.target === 'each'){ - result = evaluateString(prop.amount, scope, 'reduce'); + ({result} = evaluateString({ + string: prop.amount, + scope, + fn: 'reduce' + })); } damagePropertiesByName.call({ creatureId: target._id, variableName: prop.stat, operation: prop.operation || 'increment', - value: result + value: result.value, }); log.content.push({ name: 'Attribute damage', resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`, - result: `${-result}`, + result: `${result.isNumber ? -result.value : result.toString()}`, }); }); } else { log.content.push({ name: 'Attribute damage', resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`, - result: `${-result}`, + result: `${result.isNumber ? -result.value : result.toString()}`, }); } } diff --git a/app/imports/api/creature/actions/applyDamage.js b/app/imports/api/creature/actions/applyDamage.js index 02b9ce45..9704aa6c 100644 --- a/app/imports/api/creature/actions/applyDamage.js +++ b/app/imports/api/creature/actions/applyDamage.js @@ -15,49 +15,79 @@ export default function applyDamage({ ...creature.variables, ...actionContext, }; + // Add the target's variables to the scope if (targets.length === 1){ scope.target = targets[0].variables; } + // Determine if the hit is critical let criticalHit = !!( actionContext.criticalHit && actionContext.criticalHit.value && prop.damageType !== 'healing' // Can't critically heal ); + // Double the damage rolls if the hit is critical let context = new CompilationContext({ doubleRolls: criticalHit, }); - try { - var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context); - if (typeof result !== 'number') { - log.content.push({ - error: errors.join(', '), - }); - } - } catch (e){ + + // Compute the roll the first time, logging any errors + var {result} = evaluateString({ + string: prop.amount, + scope, + fn: 'reduce', + context + }); + + // If the result is an error bail out now + if (result.constructor.name === 'ErrorNode'){ log.content.push({ - error: e.toString(), + name: 'Damage error', + error: result.toString(), }); + return; } + + // Memoise the damage suffix for the log let suffix = (criticalHit ? ' critical ' : '') + prop.damageType + (prop.damageType !== 'healing' ? ' damage': ''); if (damageTargets && damageTargets.length) { + // Iterate through all the targets damageTargets.forEach(target => { let name = prop.damageType === 'healing' ? 'Healing' : 'Damage'; + + // Reroll the damage if needed if (prop.target === 'each'){ - result = evaluateString(prop.amount, scope, 'reduce'); + ({result, context} = evaluateString({ + string: prop.amount, + scope, + fn: 'reduce' + })); } + // If the result is an error or not a number bail out now + if (result.constructor.name === 'ErrorNode' || !result.isNumber){ + log.content.push({ + name: 'Damage error', + error: result.toString(), + }); + return; + } + + // Deal the damage to the target let damageDealt = dealDamage.call({ creatureId: target._id, damageType: prop.damageType, - amount: result, + amount: result.value, }); + + // Log the damage done if (target._id === creature._id){ + // Target is same as self, log damage as such log.content.push({ name, result: damageDealt, - details: suffix + 'to self', + details: suffix + ' to self', }); } else { log.content.push({ @@ -66,6 +96,7 @@ export default function applyDamage({ result: damageDealt, details: suffix + `${target.name && ' to '}${target.name}`, }); + // Log the damage received on that creature's log as well insertCreatureLog.call({ log: { content: [{ @@ -80,9 +111,10 @@ export default function applyDamage({ } }); } else { + // There are no targets, just log the result log.content.push({ name: prop.damageType === 'healing' ? 'Healing' : 'Damage', - result, + result: result.toString(), details: suffix, }); } diff --git a/app/imports/api/creature/actions/applyRoll.js b/app/imports/api/creature/actions/applyRoll.js index 16921e63..208cbeb1 100644 --- a/app/imports/api/creature/actions/applyRoll.js +++ b/app/imports/api/creature/actions/applyRoll.js @@ -10,23 +10,17 @@ export default function applyRoll({ ...creature.variables, ...actionContext, }; - try { - var {result, errors} = evaluateString(prop.roll, scope, 'reduce'); - actionContext[prop.variableName] = result; - log.content.push({ - name: prop.name, - resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ', - result, - }); - if (errors.length) { - log.content.push({ - error: errors.join(', '), - }); - } - - } catch (e){ - log.content.push({ - error: e.toString(), - }); + var {result} = evaluateString({ + string: prop.roll, + scope, + fn: 'reduce' + }); + if (result.isNumber){ + actionContext[prop.variableName] = result.value; } + log.content.push({ + name: prop.name, + resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ', + result: result.toString(), + }); } diff --git a/app/imports/api/creature/actions/applySave.js b/app/imports/api/creature/actions/applySave.js index 3d0e19d5..de01a5c5 100644 --- a/app/imports/api/creature/actions/applySave.js +++ b/app/imports/api/creature/actions/applySave.js @@ -14,19 +14,17 @@ export default function applySave({ }; try { // Calculate the DC - var {result, errors} = evaluateString(prop.dc, scope, 'reduce'); - let dc = result; + var {result} = evaluateString({ + string: prop.dc, + scope, + fn: 'reduce' + }); + let dc = result.value; log.content.push({ name: prop.name, resultPrefix: ' DC ', - result, + result: result.toString(), }); - if (errors.length) { - log.content.push({ - error: errors.join(', '), - }); - return false; - } if (prop.target === 'self'){ let save = CreaturesProperties.findOne({ 'ancestors.id': creature._id, diff --git a/app/imports/api/creature/actions/applyToggle.js b/app/imports/api/creature/actions/applyToggle.js index efafc168..1922978f 100644 --- a/app/imports/api/creature/actions/applyToggle.js +++ b/app/imports/api/creature/actions/applyToggle.js @@ -13,23 +13,22 @@ export default function applyToggle({ if (Number.isFinite(+prop.condition)){ return !!+prop.condition; } - try { - var {result, errors} = evaluateString(prop.condition, scope, 'reduce'); - if (typeof result !== 'number' && typeof result !== 'boolean') { - log.content.push({ - error: errors.join(', '), - }); - return false; - } + var {result} = evaluateString({ + string: prop.condition, + scope, + fn: 'reduce' + }); + if (result.constructor.name === 'ErrorNode') { log.content.push({ - name: prop.name, - resultPrefix: prop.condition + ' = ', - result, - }); - return !!result; - } catch (e){ - log.content.push({ - error: e.toString(), + name: 'Toggle error', + error: result.toString(), }); + return false; } + log.content.push({ + name: prop.name || 'Toggle', + resultPrefix: prop.condition + ' = ', + result: result.toString(), + }); + return !!result.value; } diff --git a/app/imports/api/creature/computation/afterComputation/evaluateString.js b/app/imports/api/creature/computation/afterComputation/evaluateString.js index 759d9c97..f193d341 100644 --- a/app/imports/api/creature/computation/afterComputation/evaluateString.js +++ b/app/imports/api/creature/computation/afterComputation/evaluateString.js @@ -1,32 +1,67 @@ import { parse, CompilationContext } from '/imports/parser/parser.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; +import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; //TODO replace constants with their parsed node -export default function evaluateString(string, scope, fn = 'compile', context){ - let errors = []; +export default function evaluateString({string, scope, fn = 'compile', context}){ + if (!context){ + context = new CompilationContext({}); + } if (!string){ - errors.push('No string provided'); - return {result: string, errors}; + context.storeError('No string provided'); + return {result: {value: string}, context}; } - if (!scope) errors.push('No scope provided'); + if (!scope) context.storeError('No scope provided'); // Parse the string using mathjs let node; try { node = parse(string); } catch (e) { - errors.push(e); - return {result: string, errors}; - } - if (!context){ - context = new CompilationContext({}); + context.storeError(e); + return {result: {value: string}, context}; } + node = replaceConstants({calc: node, context, scope}); let result = node[fn](scope, context); - if (result instanceof ConstantNode){ - return {result: result.value, errors: context.errors} - } else { - return {result: result.toString(), errors: context.errors}; - } + return {result, context}; +} + +// Replace constants in the calc with the right ParseNodes +function replaceConstants({calc, context, scope}){ + let constFailed = []; + calc = calc.replaceNodes(node => { + if (!(node instanceof SymbolNode)) return; + let constant = scope[node.name]; + // replace constants that aren't overridden by stats or disabled by a toggle + if (constant && constant.type === 'constant'){ + // Fail if the constant has errors + if (constant.errors && constant.errors.length){ + constFailed.push(node.name); + return; + } + let parsedConstantNode; + try { + parsedConstantNode = parse(constant.calculation); + } catch(e){ + constFailed.push(node.name); + return; + } + if (!parsedConstantNode) constFailed.push(node.name); + return parsedConstantNode; + } + }); + constFailed.forEach(name => { + context.storeError({ + type: 'error', + message: `${name} is a constant property with parsing errors` + }); + }); + let failed = !!constFailed.length; + if (failed){ + calc = new ErrorNode({error: 'Failed to replace constants'}); + } + return calc; } diff --git a/app/imports/api/creature/computation/engine/writeCreatureVariables.js b/app/imports/api/creature/computation/engine/writeCreatureVariables.js index a902004a..e558e7c3 100644 --- a/app/imports/api/creature/computation/engine/writeCreatureVariables.js +++ b/app/imports/api/creature/computation/engine/writeCreatureVariables.js @@ -4,28 +4,30 @@ import VERSION from '/imports/constants/VERSION.js'; export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) { const fields = [ - 'name', - 'attributeType', - 'baseValue', - 'spellSlotLevelValue', - 'damage', - 'decimal', - 'reset', - 'resetMultiplier', - 'value', - 'currentValue', - 'modifier', 'ability', - 'skillType', - 'baseProficiency', 'abilityMod', 'advantage', - 'passiveBonus', - 'proficiency', + 'attributeType', + 'baseProficiency', + 'baseValue', + 'calculation', 'conditionalBenefits', - 'rollBonuses', + 'currentValue', + 'damage', + 'decimal', 'fail', 'level', + 'modifier', + 'name', + 'passiveBonus', + 'proficiency', + 'reset', + 'resetMultiplier', + 'rollBonuses', + 'skillType', + 'spellSlotLevelValue', + 'type', + 'value', ]; if (fullRecompute){ @@ -34,6 +36,12 @@ export default function writeCreatureVariables(memo, creatureId, fullRecompute = let condensedStat = pick(stat, fields); memo.creatureVariables[variableName] = condensedStat; }); + forOwn(memo.constantsByVariableName, (stat, variableName) => { + let condensedStat = pick(stat, fields); + if (!memo.creatureVariables[variableName]){ + memo.creatureVariables[variableName] = condensedStat; + } + }); Creatures.update(creatureId, {$set: { variables: memo.creatureVariables, computeVersion: VERSION, diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 70aaa429..807f8938 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -143,10 +143,10 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import { getPropertyName } from '/imports/constants/PROPERTIES.js'; -import { parse, CompilationContext } from '/imports/parser/parser.js'; import PROPERTIES from '/imports/constants/PROPERTIES.js'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue' +import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; export default { components: { @@ -243,15 +243,12 @@ export default { // the quantity to fill nodes = nodes.filter(node => { if (node.slotFillerCondition){ - let context = new CompilationContext(); - let conditionResult; - try { - conditionResult = parse(node.slotFillerCondition) - .reduce(this.creature.variables, context); - } catch (e){ - console.warn(e); - } - if (conditionResult && !conditionResult.value) return false; + let {result} = evaluateString({ + string: node.slotFillerCondition, + scope: this.creature.variables, + fn: 'reduce', + }); + if (!result.value) return false; } if ( node.type === 'slotFiller' &&