From 5c7234ec62f619974d4866067e789f1926a87392 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:00:27 +0200 Subject: [PATCH] Adding failing saving throw tests --- .../applySavingThrowProperty.test.ts | 114 ++++++++++++++++ .../applySavingThrowProperty.ts | 126 +++++++++--------- app/imports/api/properties/SavingThrows.js | 2 +- app/imports/api/properties/Skills.js | 2 +- 4 files changed, 178 insertions(+), 66 deletions(-) create mode 100644 app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts diff --git a/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts new file mode 100644 index 00000000..78cd18e2 --- /dev/null +++ b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.test.ts @@ -0,0 +1,114 @@ +import { assert } from 'chai'; +import { + allMutations, + createTestCreature, + getRandomIds, + removeAllCreaturesAndProps, + runActionById +} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; + +const [ + creatureId, savingThrowId, targetCreatureId, targetCreature2Id +] = getRandomIds(4); + +const actionTestCreature = { + _id: creatureId, + props: [ + { + _id: savingThrowId, + type: 'savingThrow', + name: 'Strength Save', + dc: { calculation: '10 + 3' }, + stat: 'strengthSave', + children: [{ + type: 'branch', + branchType: 'successfulSave', + children: [{ + type: 'note', + summary: { text: 'note to apply on save' }, + }], + }, { + type: 'branch', + branchType: 'failedSave', + children: [{ + type: 'note', + summary: { text: 'note to apply on failed save' }, + }], + }], + }, + ], +} + +const actionTargetCreature = { + _id: targetCreatureId, + props: [ + { + type: 'skill', + variableName: 'strengthSave', + baseValue: { calculation: '3' }, + }, + ], +} +const actionTargetCreature2 = { + _id: targetCreature2Id, + props: [ + { + type: 'skill', + variableName: 'strengthSave', + baseValue: { calculation: '2' }, + }, + ], +} + +describe.only('Apply saving throw properties', function () { + // Increase timeout + this.timeout(8000); + + before(async function () { + await removeAllCreaturesAndProps(); + await createTestCreature(actionTestCreature); + await createTestCreature(actionTargetCreature); + await createTestCreature(actionTargetCreature2); + }); + + it('Makes multiple creatures make saves', async function () { + const action = await runActionById(savingThrowId, [targetCreatureId, targetCreature2Id]); + assert.exists(action); + assert.deepEqual(allMutations(action), [ + { + 'contents': [{ + 'inline': true, + 'name': 'Strength Save', + 'value': 'DC **13**', + }, { + 'inline': true, + 'name': 'Successful save', + 'value': '1d20 [ 10 ] + 3\n**13**', + }], + 'targetIds': [targetCreatureId], + }, { + 'contents': [{ + 'value': 'note to apply on save', + }], + 'targetIds': [targetCreatureId], + }, { + 'contents': [{ + 'inline': true, + 'name': 'Strength Save', + 'value': 'DC **13**', + }, { + 'inline': true, + 'name': 'Failed save', + 'value': '1d20 [ 10 ] + 2\n**12**', + }], + 'targetIds': [targetCreature2Id], + }, { + 'contents': [{ + 'value': 'note to apply on failed save', + }], + 'targetIds': [targetCreature2Id], + }, + ], + ); + }); +}); diff --git a/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts index 43667ed0..d48deb60 100644 --- a/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applySavingThrowProperty.ts @@ -1,9 +1,8 @@ import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import { EngineAction } from '/imports/api/engine/action/EngineActions'; import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; -import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; -import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; import { PropTask } from '/imports/api/engine/action/tasks/Task'; import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; @@ -11,23 +10,23 @@ import { getVariables } from '/imports/api/engine/loadCreatures'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; import { isFiniteNode } from '/imports/parser/parseTree/constant'; -// TODO saves are not split to targets correctly -// This will cause issues with triggers firing on saves on multiple targets +// FIXME saves against multiple targets contaminate later targets scope with the ~saveFailed or ~saveSucceeded variables of the previous targets export default async function applySavingThrowProperty( task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider ): Promise { const prop = task.prop; - const originalTargetIds = task.targetIds; - const saveTargetIds = prop.target === 'self' ? [action.creatureId] : originalTargetIds; + const saveTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; - if (saveTargetIds.length > 1) + if (saveTargetIds.length > 1) { + return applyTaskToEachTarget(action, task, saveTargetIds, inputProvider); + } - recalculateCalculation(prop.dc, action, 'reduce', inputProvider); + recalculateCalculation(prop.dc, action, 'reduce', inputProvider); - if (!isFiniteNode(prop.dc?.parseNode)) { + if (!isFiniteNode(prop.dc?.valueNode)) { result.appendLog({ name: 'Error', value: 'Saving throw requires a DC', @@ -36,17 +35,19 @@ export default async function applySavingThrowProperty( } const dc = (prop.dc?.value); - if (!prop.silent) result.appendLog({ + result.appendLog({ name: getPropertyTitle(prop), value: `DC **${dc}**`, inline: true, ...prop.silent && { silenced: prop.silent } }, saveTargetIds); - const scope = await getEffectiveActionScope(action); + + const targetId = saveTargetIds[0]; // If there are no save targets, apply all children as if the save both // succeeded and failed - if (!saveTargetIds?.length) { + if (!targetId) { + console.warn('no target, returning early'); result.pushScope = { ['~saveFailed']: { value: true }, ['~saveSucceeded']: { value: true }, @@ -55,60 +56,57 @@ export default async function applySavingThrowProperty( } // Each target makes the saving throw - for (const targetId of saveTargetIds) { + const save = getFromScope(prop.stat, getVariables(targetId)); - const save = getFromScope(prop.stat, getVariables(targetId)); - - if (!save) { - result.appendLog({ - name: 'Saving throw error', - value: 'No saving throw found: ' + prop.stat, - }, [targetId]); - return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider); - } - - const rollModifierText = numberToSignedString(save.value, true); - const rollModifier = save.value; - - let value, resultPrefix; - if (save.advantage === 1) { - const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); - if (a >= b) { - value = a; - resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else if (save.advantage === -1) { - const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); - if (a <= b) { - value = a; - resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; - } else { - value = b; - resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; - } - } else { - const [[rolledValue]] = await inputProvider.rollDice([{ number: 1, diceSize: 20 }]); - value = rolledValue; - resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` - } - result.pushScope = {}; - result.pushScope['~saveDiceRoll'] = { value }; - const resultValue = value + rollModifier || 0; - result.pushScope['~saveRoll'] = { value: resultValue }; - const saveSuccess = resultValue >= dc; - if (saveSuccess) { - result.pushScope['~saveSucceeded'] = { value: true }; - } else { - result.pushScope['~saveFailed'] = { value: true }; - } - if (!prop.silent) result.appendLog({ - name: saveSuccess ? 'Successful save' : 'Failed save', - value: resultPrefix + '\n**' + resultValue + '**', - inline: true, + if (!save) { + result.appendLog({ + name: 'Saving throw error', + value: 'No saving throw found: ' + prop.stat, }, [targetId]); return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider); } + + const rollModifierText = numberToSignedString(save.value, true); + const rollModifier = save.value; + + let value, resultPrefix; + if (save.advantage === 1) { + const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); + if (a >= b) { + value = a; + resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else if (save.advantage === -1) { + const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]); + if (a <= b) { + value = a; + resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + value = b; + resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else { + const [[rolledValue]] = await inputProvider.rollDice([{ number: 1, diceSize: 20 }]); + value = rolledValue; + resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` + } + result.pushScope = {}; + result.pushScope['~saveDiceRoll'] = { value }; + const resultValue = value + rollModifier || 0; + result.pushScope['~saveRoll'] = { value: resultValue }; + const saveSuccess = resultValue >= dc; + if (saveSuccess) { + result.pushScope['~saveSucceeded'] = { value: true }; + } else { + result.pushScope['~saveFailed'] = { value: true }; + } + if (!prop.silent) result.appendLog({ + name: saveSuccess ? 'Successful save' : 'Failed save', + value: resultPrefix + '\n**' + resultValue + '**', + inline: true, + }, [targetId]); + return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider); } diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index d53c7273..19442806 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -45,7 +45,7 @@ const ComputedOnlySavingThrowSchema = createPropertySchema({ }, }); -const ComputedSavingThrowSchema = new SimpleSchema() +const ComputedSavingThrowSchema = new SimpleSchema({}) .extend(SavingThrowSchema) .extend(ComputedOnlySavingThrowSchema); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index dc24ca92..a9204f9a 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -170,7 +170,7 @@ let ComputedOnlySkillSchema = createPropertySchema({ }, }) -const ComputedSkillSchema = new SimpleSchema() +const ComputedSkillSchema = new SimpleSchema({}) .extend(ComputedOnlySkillSchema) .extend(SkillSchema);