From 293deaa592748576ce89fbf9d8cd292ec1bb8425 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Mon, 18 Mar 2024 20:40:20 +0200 Subject: [PATCH] More action test cases --- .../applyActionProperty.test.ts | 92 ++++++++++++++++++- .../applyProperties/applyActionProperty.ts | 21 +++-- .../engine/action/functions/InputProvider.ts | 9 ++ .../functions/inputProviderForTests.testFn.ts | 9 +- 4 files changed, 120 insertions(+), 11 deletions(-) diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts index bc2e7c37..5544423e 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts @@ -10,11 +10,13 @@ import { } from '/imports/api/engine/action/functions/actionEngineTest.testFn'; import { LogContent, Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult'; import Alea from 'alea'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; const [ creatureId, targetCreatureId, targetCreature2Id, emptyActionId, selfActionId, attackActionId, - usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId, consumeAmmoId, - consumeResourceId, noUsesActionId, insufficientResourcesActionId + usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId, + consumeAmmoId, consumeResourceId, noUsesActionId, insufficientResourcesActionId, + attributeResetByEventId, eventActionId, advantageAttackId, advantageEffectId ] = randomIds; const actionTestCreature = { @@ -44,6 +46,20 @@ const actionTestCreature = { type: 'action', attackRoll: { calculation: '-5' }, }, + // Attack that has Advantage + { + _id: advantageAttackId, + type: 'action', + attackRoll: { calculation: '0' }, + tags: ['hasAdvantage'], + }, + { + _id: advantageEffectId, + type: 'effect', + operation: 'advantage', + targetByTags: true, + targetTags: ['hasAdvantage'], + }, // Attack that has no target { _id: attackNoTargetId, @@ -121,7 +137,24 @@ const actionTestCreature = { quantity: { calculation: '9001' }, }] } - } + }, + // Events and resetting attributes + { + _id: attributeResetByEventId, + type: 'attribute', + name: 'Attribute Reset By testEvent Event', + attributeType: 'stat', + baseValue: { calculation: '27' }, + damage: 13, + variableName: 'resetByEventAtt', + reset: 'testEvent' + }, + { + _id: eventActionId, + type: 'action', + actionType: 'event', + variableName: 'testEvent', + }, ], } @@ -280,6 +313,26 @@ describe('Apply Action Properties', function () { assert.deepEqual(allMutations(action), expectedMutations); }); + it('should make attack rolls that roll with advantage', async function () { + const prop = await CreatureProperties.findOneAsync(advantageAttackId); + assert.equal(prop.attackRoll.advantage, 1, 'The attack roll should have advantage'); + const action = await runActionById(advantageAttackId, [targetCreatureId]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Hit! (Advantage)', + value: '1d20 [ ~~10~~, 11 ] + 0\n**11**', + }], + targetIds: [targetCreatureId], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + it('actions should consume resources', async function () { const action = await runActionById(usesResourcesActionId, []); const expectedMutations: Mutation[] = [ @@ -335,4 +388,37 @@ describe('Apply Action Properties', function () { assert.deepEqual(allMutations(action), expectedMutations); }); + it('should reset attributes when events happen', async function () { + const action = await runActionById(eventActionId, []); + const expectedMutations: Mutation[] = [ + { + contents: [{ + name: 'Action' + }], + targetIds: [], + }, + { + contents: [ + { + inline: true, + name: 'Attribute damaged', + value: '+13 Attribute Reset By testEvent Event', + }, + ], + targetIds: [creatureId], + updates: [ + { + inc: { + damage: -13, + value: 13, + }, + propId: attributeResetByEventId, + type: 'attribute', + }, + ], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + }); diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts index 5069236f..5af826a4 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -10,7 +10,7 @@ import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/ import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; -import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { getConstantValueFromScope, getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import InputProvider from '/imports/api/engine/action/functions/InputProvider'; import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; @@ -100,6 +100,7 @@ async function applyAttackToTarget( result, criticalHit, criticalMiss, + advantage } = await rollAttack(attack, scope, taskResult.pushScope, userInput); const targetScope = getVariables(targetId); @@ -109,9 +110,9 @@ async function applyAttackToTarget( let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : result > targetArmor ? 'Hit!' : 'Miss!'; - if (scope['~attackAdvantage']?.value === 1) { + if (advantage === 1) { name += ' (Advantage)'; - } else if (scope['~attackAdvantage']?.value === -1) { + } else if (advantage === -1) { name += ' (Disadvantage)'; } @@ -187,10 +188,16 @@ async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskRe }); } -async function rollAttack(attack, scope, resultPushScope, userInput: InputProvider) { +async function rollAttack(attack, scope: any, resultPushScope, userInput: InputProvider) { + const advantage: 0 | 1 | -1 = await userInput.advantage( + (!!attack.advantage && !attack.disadvantage) ? 1 : + (!attack.advantage && !!attack.disadvantage) ? -1 : + 0 + ); const rollModifierText = numberToSignedString(attack.value, true); let value, resultPrefix; - if (scope['~attackAdvantage']?.value === 1) { + + if (advantage === 1) { const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); if (a >= b) { value = a; @@ -199,7 +206,7 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid value = b; resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; } - } else if (scope['~attackAdvantage']?.value === -1) { + } else if (advantage === -1) { const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); if (a <= b) { value = a; @@ -216,7 +223,7 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid const result = value + attack.value; resultPushScope['~attackRoll'] = { value: result }; const { criticalHit, criticalMiss } = applyCrits(value, scope, resultPushScope); - return { resultPrefix, result, value, criticalHit, criticalMiss }; + return { resultPrefix, result, value, criticalHit, criticalMiss, advantage }; } function applyCrits(value, scope, resultPushScope) { diff --git a/app/imports/api/engine/action/functions/InputProvider.ts b/app/imports/api/engine/action/functions/InputProvider.ts index aab27246..90923766 100644 --- a/app/imports/api/engine/action/functions/InputProvider.ts +++ b/app/imports/api/engine/action/functions/InputProvider.ts @@ -1,4 +1,9 @@ type InputProvider = { + /** + * Roll dice + * @param dice How many dice + * @param diceSize How many faces per die + */ rollDice( dice: { number: number, diceSize: number }[] ): Promise; @@ -12,6 +17,10 @@ type InputProvider = { choices: ({ _id: string } & Record)[], quantity?: [min: number, max: number], ): Promise; + /** + * Get advantage, natural, or disadvantage for a d20 roll + */ + advantage(suggestedAdvantage: 0 | 1 | -1): Promise<0 | 1 | -1>; } export default InputProvider; \ No newline at end of file diff --git a/app/imports/api/engine/action/functions/inputProviderForTests.testFn.ts b/app/imports/api/engine/action/functions/inputProviderForTests.testFn.ts index c76daded..9b45948b 100644 --- a/app/imports/api/engine/action/functions/inputProviderForTests.testFn.ts +++ b/app/imports/api/engine/action/functions/inputProviderForTests.testFn.ts @@ -10,10 +10,11 @@ const inputProviderForTests: InputProvider = { const result: number[][] = []; for (const diceRoll of dice) { const averageRoll = Math.round(diceRoll.diceSize / 2); - // Return an array full of averagely rolled dice + // Return an array full of averagely rolled dice, increasing by 1 for every dice result.push( new Array(diceRoll.number) .fill(averageRoll) + .map((value, index) => (value + index - 1) % diceRoll.diceSize + 1) ) } return result; @@ -28,6 +29,12 @@ const inputProviderForTests: InputProvider = { chosen.push(choices[i]._id); } return chosen; + }, + /** + * For testing, always return the suggested advantage, as if the user never chose differently + */ + async advantage(suggestedAdvantage) { + return suggestedAdvantage; } }