From ea058ba65077082adc2291a47bd0b37951703403 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:17:01 +0200 Subject: [PATCH 01/11] =?UTF-8?q?Added=20=C3=9Flue,=20Embodiment=20of=20Gr?= =?UTF-8?q?eed=20to=20about=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/imports/client/ui/pages/About.vue | 6 +++++- app/public/images/paragons/blue.png | Bin 0 -> 909 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 app/public/images/paragons/blue.png diff --git a/app/imports/client/ui/pages/About.vue b/app/imports/client/ui/pages/About.vue index 7eed0225..67fa476b 100644 --- a/app/imports/client/ui/pages/About.vue +++ b/app/imports/client/ui/pages/About.vue @@ -91,7 +91,11 @@ export default { name: 'Vibes', title: 'Kell of Nothing', avatar: 'vibes' - } + }, { + name: 'ßlue', + title: 'Embodiment of Greed', + avatar: 'blue' + }, ], }}, } diff --git a/app/public/images/paragons/blue.png b/app/public/images/paragons/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..15ffd6bbc700f455fa362c806410801271c343e5 GIT binary patch literal 909 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJZqd}|q!UPIW1k8Q2NU3jWm+OXrTU=|cX2kqr^_7|=BJO^0>w^c! zldc{-8X3+W?kMnwp|G?tsAQth|LG!8M=A>L^p}5^D}T?hFfw)3O=0GcvqqOXcg#4w zzN>aUhm!*Xv&6I+E7gs=BP;sWSO54?CH^$>-t&Jlf4vT-H2ACZ8Q$H{VcwB$t~>X$ zUEz$Ws;xifR9srXEo5I5TlTZJ_+(;ZdRuQ!)jSIyooTxw-X(>Zg*=jc)UhaQivE+B zFpK=cu+^p4_Nmt=u2`@(Wrtre52ub|r-;jm6^om+bX+p8dKgYz_IF2n{gsc)PpD2_ z&E(d{dS!uA(VqwPA9hD?wy=Gw*80fdeRpns4|DBqht-LRm*eR5{zIu=Kg}b-+r{DVfm%(L4(Uh8yJbCYGe8D3oWGWGJ|M`UZqI z@`(c@x7^dkF{I+w+e?m|3=SL&2R|Q>`lhcLaj3n2CdX;FRQns>R1ZW*a3?ozG!Q$+ zl$@y3!+PuxP@tz3C;${s)B%bgib&7_3eZ#GU~&sM7)S}1N3$4(ESNS-XW3r^j3)+9 LS3j3^P6 Date: Sat, 21 Oct 2023 11:49:50 +0200 Subject: [PATCH 02/11] Added denormalized computation fields --- .../properties/subSchemas/computedField.js | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 0d7ad114..6ff3d20b 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -4,7 +4,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended -function fieldToCompute(field){ +function fieldToCompute(field) { const schemaObj = { [`${field}.calculation`]: { type: String, @@ -17,22 +17,52 @@ function fieldToCompute(field){ return new SimpleSchema(schemaObj); } -function computedOnlyField(field){ +function computedOnlyField(field) { const schemaObj = { - [`${field}.value`]: { + // The parseNode of the compiled value before any effects are applied or rolls made + [`${field}.unaffected`]: { + type: Object, + optional: true, + blackbox: true, + }, + /* + // toString(.baseValue) + [`${field}.baseValueString`]: { type: SimpleSchema.oneOf(String, Number), optional: true, removeBeforeCompute: true, }, - // A list of effects targeting this calculation - [`${field}.effects`]: { + */ + // The compiled parseNode after applying all effects + [`${field}.value`]: { + type: Object, + optional: true, + blackbox: true, + }, + /* + // toString(.value) + [`${field}.valueString`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + removeBeforeCompute: 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`]: { @@ -56,7 +86,7 @@ function computedOnlyField(field){ maxCount: STORAGE_LIMITS.errorCount, removeBeforeCompute: true, }, - [`${field}.errors.$`]:{ + [`${field}.errors.$`]: { type: ErrorSchema, }, } @@ -65,9 +95,9 @@ function computedOnlyField(field){ } // We must include parent array and object fields for the schema to be valid -function includeParentFields(field, schemaObj){ +function includeParentFields(field, schemaObj) { const splitField = field.split('.'); - if (splitField.length === 1){ + if (splitField.length === 1) { schemaObj[field] = { type: Object, optional: true, @@ -78,8 +108,8 @@ function includeParentFields(field, schemaObj){ let key = ''; splitField.push(''); splitField.forEach((value, index) => { - if (key){ - if (value === '$'){ + if (key) { + if (value === '$') { schemaObj[key] = { type: Array, optional: true @@ -90,7 +120,7 @@ function includeParentFields(field, schemaObj){ optional: true, }; // the last object is the computed field - if (index === splitField.length - 1){ + if (index === splitField.length - 1) { schemaObj[key].computedField = true; } } @@ -102,7 +132,7 @@ function includeParentFields(field, schemaObj){ // This should rarely be used, since the other two will merge correctly when // uncomputed and computedOnly schemas are merged -function computedField(field){ +function computedField(field) { return computedField(field).extend(computedOnlyField(field)); } From 243684d2064cfe9e43dcbdc17dd65431933862dd Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:50:00 +0200 Subject: [PATCH 03/11] Updated packages --- app/.meteor/packages | 2 +- app/.meteor/release | 2 +- app/.meteor/versions | 8 +++--- app/package-lock.json | 60 +++++++++++++++++++++---------------------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/.meteor/packages b/app/.meteor/packages index d2c043aa..55c83ae9 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 e5431969..977f2a7c 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/package-lock.json b/app/package-lock.json index a772ebfa..b77519e4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -241,9 +241,9 @@ } }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -338,9 +338,9 @@ } }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -365,9 +365,9 @@ }, "dependencies": { "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1213,9 +1213,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -1506,9 +1506,9 @@ } }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -1968,9 +1968,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -3368,9 +3368,9 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -3401,9 +3401,9 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -3952,9 +3952,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -4045,9 +4045,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrappy": { From 6ce7542c4b60e797281cc5340e615653d3601456 Mon Sep 17 00:00:00 2001 From: ThaumRystra <9525416+ThaumRystra@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:12:18 +0200 Subject: [PATCH 04/11] Changed aggregation schema of computed fields --- .../api/engine/actions/applyTriggers.js | 2 +- .../computeByType/computeCalculation.js | 127 +++++++++++++++--- .../aggregate/aggregateDefinition.js | 19 ++- .../aggregate/aggregateEffect.js | 21 ++- .../utility/evaluateCalculation.js | 9 +- .../properties/subSchemas/computedField.js | 36 +++-- app/imports/parser/parseTree/accessor.js | 20 ++- 7 files changed, 176 insertions(+), 58 deletions(-) diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js index cbaf2108..de9476f6 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -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/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index f695b25d..aae6cdcc 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,19 +1,25 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; -import { getPropertyName } from '/imports/constants/PROPERTIES.js'; +import call from '/imports/parser/parseTree/call.js'; +import constant from '/imports/parser/parseTree/constant.js'; +import operator from '/imports/parser/parseTree/operator.js'; +import parenthesis from '/imports/parser/parseTree/parenthesis.js'; +import { toString } from '/imports/parser/resolve.js'; export default function computeCalculation(computation, node) { const calcObj = node.data; evaluateCalculation(calcObj, computation.scope); if (calcObj.effects || calcObj.proficiencies) { - calcObj.baseValue = calcObj.value; + calcObj.unaffected = calcObj.value; + calcObj.displayUnaffected = toString(calcObj.unaffected); } aggregateCalculationEffects(node, computation); aggregateCalculationProficiencies(node, computation); + calcObj.displayValue = toString(calcObj.value); } function aggregateCalculationEffects(node, computation) { const calcObj = node.data; - delete calcObj.effects; + delete calcObj.effectIds; computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { @@ -25,29 +31,104 @@ 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, - //parseNode: linkedNode.data.amount.parseNode, - }, - // ancestors: linkedNode.data.ancestors, - }); + calcObj.effectIds = calcObj.effectIds || []; + calcObj.effectIds.push(linkedNode.data._id); }, true // enumerate only outbound links ); - if (calcObj.effects && typeof calcObj.value === 'number') { + if (calcObj.effectIds) { + // dictionary of {[operation]: parseNode} + const aggregator = {}; + // Store all effect values calcObj.effects.forEach(effect => { - if ( - effect.operation === 'add' && - effect.amount && typeof effect.amount.value === 'number' - ) { - calcObj.value += effect.amount.value + 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.value); + 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) + sum(...add) ) * mul(...mul) + * min(...min, x) + * max(...max, x) + * set(last(...set))a + */ + // Set + // If we do set, return early, nothing else matters + if (aggregator.set) { + calcObj.value = aggregator.set[aggregator.set.length - 1]; + return; + } + // Base value + if (aggregator.base) { + calcObj.value = call.create({ + functionName: 'max', + args: [calcObj.value, aggregator.base] + }); + } + // Add + aggregator.add?.forEach(node => { + calcObj.value = operator.create({ + left: calcObj.value, + right: node, + operator: '+' + }); + }); + // Multiply + if (aggregator.mul) { + // Wrap the previous node in brackets if it's another operator + if (calcObj.parseType === 'operator') { + calcObj.value = parenthesis.create({ + content: calcObj.value + }); + } + // Append all multiplications + aggregator.mul.forEach(node => { + calcObj.value = operator.create({ + left: calcObj.value, + right: node, + operator: '*' + }); + }); + } + // Min + if (aggregator.min) { + calcObj.value = call.create({ + functionName: 'max', + args: [calcObj.value, aggregator.min] + }); + } + // Max + if (aggregator.max) { + calcObj.value = call.create({ + functionName: 'min', + args: [calcObj.value, aggregator.max] + }); + } } } @@ -110,6 +191,10 @@ function aggregateCalculationProficiencies(node, computation) { prof.overridden = true; } }); - calcObj.value += calcObj.proficiencyBonus; + calcObj.value = operator.create({ + left: calcObj.value, + right: constant.create({ value: calcObj.proficiencyBonus }), + operator: '+' + }); } } 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 d665b284..9ddd0600 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 @@ -1,5 +1,5 @@ -export default function aggregateDefinition({node, linkedNode, link}){ +export default function aggregateDefinition({ node, linkedNode, link }) { // Look at all definition links if (link.data !== 'definition') return; @@ -24,7 +24,14 @@ export default function aggregateDefinition({node, linkedNode, link}){ } // Aggregate the base value due to the defining properties - let propBaseValue = prop.baseValue?.value; + let propBaseValue = undefined; + const valueNode = prop.baseValue?.value; + if ( + valueNode?.parseType === 'constant' + && valueNode?.valueType === 'number' + ) { + propBaseValue = valueNode.value; + } // Point buy rows use prop.value instead of prop.baseValue if (prop.type === 'pointBuyRow') { propBaseValue = prop.value; @@ -38,7 +45,7 @@ export default function aggregateDefinition({node, linkedNode, link}){ _id: prop.tableId, name: prop.tableName, operation: 'base', - amount: { value: propBaseValue }, + amount: propBaseValue, type: 'pointBuy', }); } else { @@ -46,16 +53,16 @@ export default function aggregateDefinition({node, linkedNode, link}){ _id: prop._id, name: prop.name, operation: 'base', - amount: { value: propBaseValue }, + amount: propBaseValue, type: prop.type, }); } - if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ + if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) { node.data.baseValue = propBaseValue; } } -function overrideProp(prop, node){ +function overrideProp(prop, node) { if (!prop) return; prop.overridden = true; if (!node.data.overriddenProps) node.data.overriddenProps = []; 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 4bbf2e3e..6546f18c 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 @@ -22,21 +22,11 @@ export default function aggregateEffect({ node, linkedNode, link }) { // 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, + amount: linkedNode.data.amount.displayValue, type: linkedNode.data.type, text: linkedNode.data.text, // ancestors: linkedNode.data.ancestors, @@ -45,7 +35,14 @@ export default function aggregateEffect({ node, linkedNode, link }) { // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; // Get the result of the effect - let result = linkedNode.data.amount?.value; + let result = undefined; + const valueNode = linkedNode.data.amount?.value; + if ( + valueNode?.parseType === 'constant' + && valueNode?.valueType === 'number' + ) { + result = valueNode.value; + } if (typeof result !== 'number') result = undefined; // Aggregate the effect based on its operation diff --git a/app/imports/api/engine/computation/utility/evaluateCalculation.js b/app/imports/api/engine/computation/utility/evaluateCalculation.js index 6d5f1f82..4323d377 100644 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ b/app/imports/api/engine/computation/utility/evaluateCalculation.js @@ -6,13 +6,8 @@ export default function evaluateCalculation(calculation, scope, givenContext) { 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); - } + calculation.value = resultNode; + calculation.displayValue = toString(resultNode); // remove the working fields delete calculation._parseLevel; delete calculation._localScope; diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 6ff3d20b..48e16760 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -25,28 +25,24 @@ function computedOnlyField(field) { optional: true, blackbox: true, }, - /* - // toString(.baseValue) - [`${field}.baseValueString`]: { + // toString(.unaffected) + [`${field}.displayUnaffected`]: { type: SimpleSchema.oneOf(String, Number), optional: true, removeBeforeCompute: true, }, - */ // The compiled parseNode after applying all effects [`${field}.value`]: { type: Object, optional: true, blackbox: true, }, - /* - // toString(.value) - [`${field}.valueString`]: { + // The displayed value of the calculation: toString(.value) + [`${field}.displayValue`]: { type: SimpleSchema.oneOf(String, Number), optional: true, removeBeforeCompute: true, }, - */ // A list of effect Ids targeting this calculation [`${field}.effectIds`]: { type: Array, @@ -89,6 +85,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/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 5ca0e325..51fbbcbf 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -17,13 +17,20 @@ const accessor = { if (value === undefined) return; value = value[name]; }); - let valueType = Array.isArray(value) ? 'array' : typeof value; + let valueType = getType(value); // If the accessor returns an objet, get the object's value instead while (valueType === 'object') { value = value.value; - valueType = Array.isArray(value) ? 'array' : typeof value; + valueType = getType(value); } - // Return a parse node based on the type returned + // Return a discovered parse node + if (valueType === 'parseNode') { + return { + result: value, + context, + }; + } + // Return a parse node based on the constant type returned if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { return { result: constant.create({ @@ -83,4 +90,11 @@ const accessor = { } } +function getType(val) { + if (!val) return typeof val; + if (Array.isArray(val)) return 'array'; + if (val.parseType) return 'parseNode'; + return typeof val; +} + export default accessor; From 9e5b6b11e17aa9130e7fa308943ca0701bdab5c8 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:08:04 +0200 Subject: [PATCH 05/11] Settling on a data structure to balance compatibility with not being wrong --- .../applyPropertyByType/applyDamage.js | 10 +- .../actions/applyPropertyByType/applyRoll.js | 6 +- .../applyEffectsToCalculationParseNode.js | 46 --- .../shared/recalculateCalculation.js | 43 ++- app/imports/api/engine/actions/doCheck.js | 10 +- .../linkCalculationDependencies.js | 4 +- .../tests/computeInactiveStatus.testFn.js | 34 +- .../computeByType/computeCalculation.js | 313 ++++++++++-------- .../computeByType/computePointBuy.js | 1 - .../aggregate/aggregateDefinition.js | 27 +- .../aggregate/aggregateEffect.js | 26 +- .../computeVariableAsAttribute.js | 2 +- .../computeVariable/computeVariableAsSkill.js | 2 +- .../tests/computeAttribute.testFn.js | 2 +- .../tests/computeProficiencies.testFn.js | 4 +- .../utility/evaluateCalculation.js | 9 +- app/imports/api/properties/Attributes.js | 15 +- app/imports/api/properties/Skills.js | 15 +- .../properties/subSchemas/computedField.js | 20 +- .../components/PrintedAction.vue | 10 +- app/imports/parser/parseTree/error.js | 10 +- app/imports/parser/resolve.js | 41 ++- 22 files changed, 312 insertions(+), 338 deletions(-) delete mode 100644 app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 3cbe225c..58385f1e 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.js'; import resolve, { Context, toString } from '/imports/parser/resolve.js'; import logErrors from './shared/logErrors.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js' import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; 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 diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index d3b7bf12..5edf2613 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.js'; import logErrors from './shared/logErrors.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; import resolve, { toString } from '/imports/parser/resolve.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; @@ -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 6ee0a765..00000000 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js +++ /dev/null @@ -1,46 +0,0 @@ -import operator from '/imports/parser/parseTree/operator.js'; -import { parse } from '/imports/parser/parser.js'; -import logErrors from './logErrors.js'; - -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 16f2cfe3..512183f7 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -1,11 +1,38 @@ -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; -import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import logErrors from './logErrors.js'; +import { toPrimitiveOrString } from '/imports/parser/resolve.js'; +import { + aggregateCalculationEffects, + aggregateCalculationProficiencies, + resolveCalculationNode, +} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js'; +import { getSingleProperty } from '/imports/api/engine/loadCreatures'; -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); } diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index 452c32eb..8614741d 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.js'; import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js'; -import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; const doCheck = new ValidatedMethod({ name: 'creatureProperties.doCheck', @@ -76,7 +76,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; @@ -116,7 +116,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) { @@ -125,8 +126,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 35ba9b60..de4a931a 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -13,8 +13,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/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js index c7565156..248d34a9 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js @@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; -export default function(){ +export default function () { let computation = buildComputationFromProps(testProperties); const bySelf = (propId, note) => assertDeactivatedBySelf(computation, propId, note); const byAncestor = (propId, note) => assertDeactivatedByAncestor(computation, propId, note); @@ -24,22 +24,22 @@ export default function(){ // Notes active('NoteId', 'Notes should be active'); - byAncestor('NoteChildId', 'children of notes should always be inactive'); + active('NoteChildId', 'children of notes should be active'); } -function assertDeactivatedBySelf(computation, propId, note){ +function assertDeactivatedBySelf(computation, propId, note) { const prop = computation.propsById[propId]; assert.isTrue(prop.deactivatedBySelf, note); assert.isTrue(prop.inactive, note + '. The property should be inactive'); } -function assertDeactivatedByAncestor(computation, propId, note){ +function assertDeactivatedByAncestor(computation, propId, note) { const prop = computation.propsById[propId]; assert.isTrue(prop.deactivatedByAncestor, note); assert.isTrue(prop.inactive, 'The property should be inactive'); } -function assertActive(computation, propId, note){ +function assertActive(computation, propId, note) { const prop = computation.propsById[propId]; assert.isNotTrue(prop.inactive, note); assert.isNotTrue(prop.deactivatedBySelf, note); @@ -51,66 +51,66 @@ var testProperties = [ clean({ _id: 'itemUnequippedId', type: 'item', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], }), clean({ _id: 'itemUnequippedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'itemUnequippedId'}], + ancestors: [{ id: 'charId' }, { id: 'itemUnequippedId' }], }), clean({ _id: 'itemEquippedId', type: 'item', equipped: true, - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], }), clean({ _id: 'itemEquippedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'itemEquippedId'}], + ancestors: [{ id: 'charId' }, { id: 'itemEquippedId' }], }), // Spells clean({ _id: 'spellPreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], prepared: true, }), clean({ _id: 'spellPreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellPreparedId'}], + ancestors: [{ id: 'charId' }, { id: 'spellPreparedId' }], }), clean({ _id: 'spellAlwaysPreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], alwaysPrepared: true, }), clean({ _id: 'spellAlwaysPreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellAlwaysPreparedId'}], + ancestors: [{ id: 'charId' }, { id: 'spellAlwaysPreparedId' }], }), clean({ _id: 'spellUnpreparedId', type: 'spell', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], }), clean({ _id: 'spellUnpreparedChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'spellUnpreparedId'}], + ancestors: [{ id: 'charId' }, { id: 'spellUnpreparedId' }], }), // Notes clean({ _id: 'NoteId', type: 'note', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], }), clean({ _id: 'NoteChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'NoteId'}], + ancestors: [{ id: 'charId' }, { id: 'NoteId' }], }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index aae6cdcc..68c9bb46 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,23 +1,44 @@ -import evaluateCalculation from '../../utility/evaluateCalculation.js'; import call from '/imports/parser/parseTree/call.js'; import constant from '/imports/parser/parseTree/constant.js'; import operator from '/imports/parser/parseTree/operator.js'; import parenthesis from '/imports/parser/parseTree/parenthesis.js'; -import { toString } from '/imports/parser/resolve.js'; +import resolve, { toPrimitiveOrString } from '/imports/parser/resolve.js'; export default function computeCalculation(computation, node) { const calcObj = node.data; - evaluateCalculation(calcObj, computation.scope); - if (calcObj.effects || calcObj.proficiencies) { - calcObj.unaffected = calcObj.value; - calcObj.displayUnaffected = toString(calcObj.unaffected); + // 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); - calcObj.displayValue = toString(calcObj.value); + // 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.effectIds; computation.dependencyGraph.forEachLinkedNode( @@ -36,107 +57,110 @@ function aggregateCalculationEffects(node, computation) { }, true // enumerate only outbound links ); - if (calcObj.effectIds) { - // dictionary of {[operation]: parseNode} - const aggregator = {}; - // Store all effect values - calcObj.effects.forEach(effect => { - 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.value); - 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) + sum(...add) ) * mul(...mul) - * min(...min, x) - * max(...max, x) - * set(last(...set))a - */ - // Set - // If we do set, return early, nothing else matters - if (aggregator.set) { - calcObj.value = aggregator.set[aggregator.set.length - 1]; - return; +} + +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 } - // Base value - if (aggregator.base) { - calcObj.value = call.create({ - functionName: 'max', - args: [calcObj.value, aggregator.base] + }); + /** + * 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 }); } - // Add - aggregator.add?.forEach(node => { - calcObj.value = operator.create({ - left: calcObj.value, + // Append all multiplications + aggregator.mul.forEach(node => { + calcObj.valueNode = operator.create({ + left: calcObj.valueNode, right: node, - operator: '+' + operator: '*' }); }); - // Multiply - if (aggregator.mul) { - // Wrap the previous node in brackets if it's another operator - if (calcObj.parseType === 'operator') { - calcObj.value = parenthesis.create({ - content: calcObj.value - }); - } - // Append all multiplications - aggregator.mul.forEach(node => { - calcObj.value = operator.create({ - left: calcObj.value, - right: node, - operator: '*' - }); - }); - } - // Min - if (aggregator.min) { - calcObj.value = call.create({ - functionName: 'max', - args: [calcObj.value, aggregator.min] - }); - } - // Max - if (aggregator.max) { - calcObj.value = call.create({ - functionName: 'min', - args: [calcObj.value, aggregator.max] - }); - } + } + // 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( @@ -148,53 +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 = operator.create({ - left: calcObj.value, - right: constant.create({ value: calcObj.proficiencyBonus }), - operator: '+' - }); - } +} + +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 a7727247..ae7577fb 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js @@ -1,5 +1,4 @@ import { has } from 'lodash'; -import evaluateCalculation from '../../utility/evaluateCalculation.js'; export default function computePointBuy(computation, node) { const prop = node.data; 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 9ddd0600..f1fed346 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 @@ -24,14 +24,7 @@ export default function aggregateDefinition({ node, linkedNode, link }) { } // Aggregate the base value due to the defining properties - let propBaseValue = undefined; - const valueNode = prop.baseValue?.value; - if ( - valueNode?.parseType === 'constant' - && valueNode?.valueType === 'number' - ) { - propBaseValue = valueNode.value; - } + let propBaseValue = prop.baseValue?.value; // Point buy rows use prop.value instead of prop.baseValue if (prop.type === 'pointBuyRow') { propBaseValue = prop.value; @@ -39,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: propBaseValue, - type: 'pointBuy', - }); + node.data.effectIds.push(prop.tableId); } else { - node.data.effects.push({ - _id: prop._id, - name: prop.name, - operation: 'base', - amount: 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 6546f18c..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,30 +17,14 @@ export default function aggregateEffect({ node, linkedNode, link }) { rollBonus: [], }; - // Store a summary of the effect itself - node.data.effects = node.data.effects || []; - // Store either just - node.data.effects.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - operation: linkedNode.data.operation, - amount: linkedNode.data.amount.displayValue, - type: linkedNode.data.type, - text: linkedNode.data.text, - // ancestors: linkedNode.data.ancestors, - }); + // 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; // Get the result of the effect - let result = undefined; - const valueNode = linkedNode.data.amount?.value; - if ( - valueNode?.parseType === 'constant' - && valueNode?.valueType === 'number' - ) { - result = valueNode.value; - } + let result = linkedNode.data.amount?.value; if (typeof result !== 'number') result = undefined; // Aggregate the effect based on its operation 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 395619a9..cfe4a4c1 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 336a5ab7..41f00188 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/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index dfc9fb86..32a3c7f9 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -3,7 +3,7 @@ import { assert } from 'chai'; import computeCreatureComputation from '../../computeCreatureComputation.js'; import clean from '../../utility/cleanProp.testFn.js'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const prop = id => computation.propsById[id]; 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 b61395da..c27b4e86 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js @@ -15,8 +15,8 @@ export default function () { prop('strengthId').modifier, -1, 'The proficiency bonus should not change the strength modifier' ); - 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 index 4323d377..83653102 100644 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ b/app/imports/api/engine/computation/utility/evaluateCalculation.js @@ -1,13 +1,14 @@ -import resolve, { toString } from '/imports/parser/resolve.js'; - +import resolve, { toPrimitiveOrString } from '/imports/parser/resolve.js'; +console.warn('evaluateCalculation is deprecated use resolveCalculationNode instead') +// TODO everywhere this is used, replace with more specific code to recalculate fields 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; - calculation.value = resultNode; - calculation.displayValue = toString(resultNode); + calculation.valueNode = resultNode; + calculation.value = toPrimitiveOrString(resultNode); // remove the working fields delete calculation._parseLevel; delete calculation._localScope; diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 8835661a..d5a46524 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 7f15df3a..dc00cdec 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.js b/app/imports/api/properties/subSchemas/computedField.js index 48e16760..9e1197e2 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -19,29 +19,23 @@ function fieldToCompute(field) { function computedOnlyField(field) { const schemaObj = { - // The parseNode of the compiled value before any effects are applied or rolls made + // The value (or calculation string) before any effects/proficiencies are applied or rolls made [`${field}.unaffected`]: { - type: Object, + type: SimpleSchema.oneOf(String, Number), optional: true, blackbox: true, }, - // toString(.unaffected) - [`${field}.displayUnaffected`]: { - type: SimpleSchema.oneOf(String, Number), - optional: true, - removeBeforeCompute: true, - }, - // The compiled parseNode after applying all effects + // The value (or calculation string) after applying all effects [`${field}.value`]: { - type: Object, + type: SimpleSchema.oneOf(String, Number), optional: true, blackbox: true, }, - // The displayed value of the calculation: toString(.value) - [`${field}.displayValue`]: { + // The value as a parse node, after applying all effects + [`${field}.valueNode`]: { type: SimpleSchema.oneOf(String, Number), optional: true, - removeBeforeCompute: true, + blackbox: true, }, // A list of effect Ids targeting this calculation [`${field}.effectIds`]: { 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 4a63daa5..f4a22e23 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 @@