Adding failing saving throw tests

This commit is contained in:
Thaum Rystra
2024-10-14 22:00:27 +02:00
parent 6887a46be4
commit 5c7234ec62
4 changed files with 178 additions and 66 deletions

View File

@@ -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],
},
],
);
});
});

View File

@@ -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<void> {
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);
}

View File

@@ -45,7 +45,7 @@ const ComputedOnlySavingThrowSchema = createPropertySchema({
},
});
const ComputedSavingThrowSchema = new SimpleSchema()
const ComputedSavingThrowSchema = new SimpleSchema({})
.extend(SavingThrowSchema)
.extend(ComputedOnlySavingThrowSchema);

View File

@@ -170,7 +170,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
},
})
const ComputedSkillSchema = new SimpleSchema()
const ComputedSkillSchema = new SimpleSchema({})
.extend(ComputedOnlySkillSchema)
.extend(SkillSchema);