Tested action branch properties
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"alea",
|
||||
"armor",
|
||||
"autorun",
|
||||
"blackbox",
|
||||
"Crits",
|
||||
|
||||
@@ -129,9 +129,9 @@ async function applyAttackToTarget(
|
||||
});
|
||||
|
||||
if (criticalMiss || result < targetArmor) {
|
||||
scope['~attackMiss'] = { value: true };
|
||||
taskResult.pushScope['~attackMiss'] = { value: true };
|
||||
} else {
|
||||
scope['~attackHit'] = { value: true };
|
||||
taskResult.pushScope['~attackHit'] = { value: true };
|
||||
}
|
||||
} else {
|
||||
contents.push({
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
import { assert } from 'chai';
|
||||
import {
|
||||
allMutations,
|
||||
createTestCreature,
|
||||
getRandomIds,
|
||||
removeAllCreaturesAndProps,
|
||||
runActionById
|
||||
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
|
||||
|
||||
const [
|
||||
creatureId, targetCreatureId, ifTrueBranchId, ifFalseBranchId, indexBranchId, attackHitId, attackMissId, saveSucceedId, saveFailId, randomBranchId, targetCreature2Id, eachTargetBranchId, choiceBranchId,
|
||||
] = getRandomIds(100);
|
||||
|
||||
const actionTestCreature = {
|
||||
_id: creatureId,
|
||||
props: [
|
||||
// If branch
|
||||
{
|
||||
_id: ifTrueBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'if',
|
||||
condition: { calculation: 'true' },
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'this should run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: ifFalseBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'if',
|
||||
condition: { calculation: 'false' },
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'this should not run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// index branch
|
||||
{
|
||||
_id: indexBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'index',
|
||||
condition: { calculation: '1 + 1' },
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'FAIL: index child 1 should not run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'Child 2 should run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'FAIL: index child 3 should not run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
// Hit and miss branches
|
||||
{
|
||||
_id: attackHitId,
|
||||
type: 'action',
|
||||
attackRoll: { calculation: '1' },
|
||||
children: [
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'hit',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'attack hit branch' }
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'miss',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'attack miss branch' }
|
||||
}],
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
_id: attackMissId,
|
||||
type: 'action',
|
||||
attackRoll: { calculation: '-1' },
|
||||
children: [
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'hit',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'attack hit branch' }
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'miss',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'attack miss branch' }
|
||||
}],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
// Save and fail save branch
|
||||
{
|
||||
_id: saveSucceedId,
|
||||
type: 'savingThrow',
|
||||
dc: { calculation: '10' },
|
||||
target: 'target',
|
||||
stat: 'strengthSave',
|
||||
children: [
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'successfulSave',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'made save branch' }
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'failedSave',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'failed save branch' }
|
||||
}],
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
_id: saveFailId,
|
||||
type: 'savingThrow',
|
||||
dc: { calculation: '15' },
|
||||
target: 'target',
|
||||
stat: 'strengthSave',
|
||||
children: [
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'successfulSave',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'made save branch' }
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'branch',
|
||||
branchType: 'failedSave',
|
||||
children: [{
|
||||
type: 'note',
|
||||
summary: { text: 'failed save branch' }
|
||||
}],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
// Random branch
|
||||
{
|
||||
_id: randomBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'random',
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'FAIL: random child 1 should not run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'Random child 2 should run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'FAIL: random child 3 should not run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Each target branch
|
||||
{
|
||||
_id: eachTargetBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'eachTarget',
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'some note' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Choice branch
|
||||
{
|
||||
_id: choiceBranchId,
|
||||
type: 'branch',
|
||||
branchType: 'choice',
|
||||
children: [
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'Choice child 1 should run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'Fail: choice child 2 should not run' },
|
||||
},
|
||||
{
|
||||
type: 'note',
|
||||
summary: { text: 'Fail: choice child 3 should not run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const actionTargetCreature = {
|
||||
_id: targetCreatureId,
|
||||
props: [
|
||||
{
|
||||
type: 'attribute',
|
||||
attributeType: 'stat',
|
||||
variableName: 'armor',
|
||||
baseValue: { calculation: '10' },
|
||||
},
|
||||
{
|
||||
type: 'skill',
|
||||
skillType: 'save',
|
||||
variableName: 'strengthSave',
|
||||
baseValue: { calculation: '3' },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const actionTargetCreature2 = {
|
||||
_id: targetCreature2Id,
|
||||
props: [
|
||||
{
|
||||
type: 'attribute',
|
||||
attributeType: 'stat',
|
||||
variableName: 'armor',
|
||||
baseValue: { calculation: '15' },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
describe('Apply Branch Properties', function () {
|
||||
// Increase timeout
|
||||
this.timeout(8000);
|
||||
|
||||
before(async function () {
|
||||
await removeAllCreaturesAndProps();
|
||||
await createTestCreature(actionTestCreature);
|
||||
await createTestCreature(actionTargetCreature);
|
||||
await createTestCreature(actionTargetCreature2);
|
||||
});
|
||||
|
||||
// If branch
|
||||
it('Runs an if branch with a true condition', async function () {
|
||||
const action = await runActionById(ifTrueBranchId);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ value: 'this should run' }],
|
||||
targetIds: [],
|
||||
}]);
|
||||
});
|
||||
it('runs an if branch with a false condition', async function () {
|
||||
const action = await runActionById(ifFalseBranchId);
|
||||
assert.deepEqual(allMutations(action), []);
|
||||
});
|
||||
it('runs an if branch and chooses the correct child', async function () {
|
||||
const action = await runActionById(indexBranchId);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ value: 'Child 2 should run' }],
|
||||
targetIds: [],
|
||||
}]);
|
||||
});
|
||||
|
||||
// Hit and miss branch
|
||||
it('Runs only hit branches on an attack that hits', async function () {
|
||||
const action = await runActionById(attackHitId, [targetCreatureId]);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ name: 'Action' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ inline: true, name: 'Hit!', value: '1d20 [10] + 1\n**11**' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ value: 'attack hit branch' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}]);
|
||||
});
|
||||
it('Runs only miss branches on an attack that misses', async function () {
|
||||
const action = await runActionById(attackMissId, [targetCreatureId]);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ name: 'Action' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ inline: true, name: 'Miss!', value: '1d20 [10] − 1\n**9**' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ value: 'attack miss branch' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}]);
|
||||
});
|
||||
|
||||
// Save succeed and fail branches
|
||||
it('Runs only miss branches on an attack that misses', async function () {
|
||||
const action = await runActionById(saveSucceedId, [targetCreatureId]);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{
|
||||
name: 'Saving throw',
|
||||
value: 'DC **10**',
|
||||
inline: true
|
||||
}, {
|
||||
name: 'Successful save',
|
||||
value: '1d20 [ 10 ] + 3\n**13**',
|
||||
inline: true
|
||||
}],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ value: 'made save branch' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}]);
|
||||
});
|
||||
it('Runs only miss branches on an attack that misses', async function () {
|
||||
const action = await runActionById(saveFailId, [targetCreatureId]);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{
|
||||
name: 'Saving throw',
|
||||
value: 'DC **15**',
|
||||
inline: true
|
||||
}, {
|
||||
name: 'Failed save',
|
||||
value: '1d20 [ 10 ] + 3\n**13**',
|
||||
inline: true
|
||||
}],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ value: 'failed save branch' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}]);
|
||||
});
|
||||
|
||||
// Random branches, RNG is fixed at average for testing, so child 2 should run
|
||||
it('runs a random branch and chooses the correct child', async function () {
|
||||
const action = await runActionById(randomBranchId);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ value: 'Random child 2 should run' }],
|
||||
targetIds: [],
|
||||
}]);
|
||||
});
|
||||
|
||||
// Branches can split actions across targets
|
||||
it('Can split actions to targets using a branch', async function () {
|
||||
const action = await runActionById(eachTargetBranchId, [targetCreatureId, targetCreature2Id]);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ value: 'some note' }],
|
||||
targetIds: [targetCreatureId],
|
||||
}, {
|
||||
contents: [{ value: 'some note' }],
|
||||
targetIds: [targetCreature2Id],
|
||||
}]);
|
||||
});
|
||||
|
||||
// Choice branches, choices are fixed to first option for testing
|
||||
it('runs a choice branch and chooses the correct child', async function () {
|
||||
const action = await runActionById(choiceBranchId);
|
||||
assert.deepEqual(allMutations(action), [{
|
||||
contents: [{ value: 'Choice child 1 should run' }],
|
||||
targetIds: [],
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import recalculateCalculation from '/imports/api/engine/action/functions/recalcu
|
||||
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||
import { getPropertyChildren } from '/imports/api/engine/loadCreatures';
|
||||
import rollDice from '/imports/parser/rollDice';
|
||||
|
||||
export default async function applyBranchProperty(
|
||||
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
|
||||
@@ -33,7 +32,7 @@ export default async function applyBranchProperty(
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
result.appendLog({
|
||||
name: 'Branch Error',
|
||||
value: 'Index did not resolve into a valid number'
|
||||
value: `Index did not resolve into a valid number, got \`${prop.condition?.value}\` instead`
|
||||
}, targets);
|
||||
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||
}
|
||||
@@ -98,7 +97,7 @@ export default async function applyBranchProperty(
|
||||
case 'random': {
|
||||
const children = await getPropertyChildren(action.creatureId, prop);
|
||||
if (children.length) {
|
||||
const index = rollDice(1, children.length)[0];
|
||||
const index = (await userInput.rollDice([{ number: 1, diceSize: children.length }]))[0][0];
|
||||
const child = children[index - 1];
|
||||
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 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';
|
||||
@@ -10,6 +11,9 @@ 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
|
||||
|
||||
export default async function applySavingThrowProperty(
|
||||
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
|
||||
): Promise<void> {
|
||||
@@ -23,7 +27,7 @@ export default async function applySavingThrowProperty(
|
||||
|
||||
recalculateCalculation(prop.dc, action, 'reduce', inputProvider);
|
||||
|
||||
if (!isFiniteNode(prop.dc)) {
|
||||
if (!isFiniteNode(prop.dc?.parseNode)) {
|
||||
result.appendLog({
|
||||
name: 'Error',
|
||||
value: 'Saving throw requires a DC',
|
||||
@@ -33,7 +37,7 @@ export default async function applySavingThrowProperty(
|
||||
|
||||
const dc = (prop.dc?.value);
|
||||
if (!prop.silent) result.appendLog({
|
||||
name: prop.name,
|
||||
name: getPropertyTitle(prop),
|
||||
value: `DC **${dc}**`,
|
||||
inline: true,
|
||||
...prop.silent && { silenced: prop.silent }
|
||||
@@ -43,7 +47,7 @@ export default async function applySavingThrowProperty(
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeded and failed
|
||||
if (!saveTargetIds?.length) {
|
||||
result.scope = {
|
||||
result.pushScope = {
|
||||
['~saveFailed']: { value: true },
|
||||
['~saveSucceeded']: { value: true },
|
||||
}
|
||||
@@ -53,14 +57,14 @@ export default async function applySavingThrowProperty(
|
||||
// Each target makes the saving throw
|
||||
for (const targetId of saveTargetIds) {
|
||||
|
||||
const save = getFromScope('save', 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]);
|
||||
applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider);
|
||||
return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider);
|
||||
}
|
||||
|
||||
const rollModifierText = numberToSignedString(save.value, true);
|
||||
@@ -90,14 +94,15 @@ export default async function applySavingThrowProperty(
|
||||
value = rolledValue;
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||
}
|
||||
scope['~saveDiceRoll'] = { value };
|
||||
result.pushScope = {};
|
||||
result.pushScope['~saveDiceRoll'] = { value };
|
||||
const resultValue = value + rollModifier || 0;
|
||||
scope['~saveRoll'] = { value: resultValue };
|
||||
result.pushScope['~saveRoll'] = { value: resultValue };
|
||||
const saveSuccess = resultValue >= dc;
|
||||
if (saveSuccess) {
|
||||
scope['~saveSucceeded'] = { value: true };
|
||||
result.pushScope['~saveSucceeded'] = { value: true };
|
||||
} else {
|
||||
scope['~saveFailed'] = { value: true };
|
||||
result.pushScope['~saveFailed'] = { value: true };
|
||||
}
|
||||
if (!prop.silent) result.appendLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
|
||||
@@ -14,8 +14,9 @@ export default async function computeCreature(creatureId) {
|
||||
async function computeComputation(computation, creatureId) {
|
||||
try {
|
||||
await computeCreatureComputation(computation);
|
||||
writeAlteredProperties(computation);
|
||||
writeScope(creatureId, computation);
|
||||
const writePromise = writeAlteredProperties(computation);
|
||||
const scopeWritePromise = writeScope(creatureId, computation);
|
||||
await Promise.all([writePromise, scopeWritePromise]);
|
||||
} catch (e) {
|
||||
const errorText = e.reason || e.message || e.toString();
|
||||
computation.errors.push({
|
||||
|
||||
@@ -17,7 +17,7 @@ export function propsFromForest(
|
||||
const children = prop.children;
|
||||
// Check the property has a type
|
||||
if (!prop.type) {
|
||||
throw 'Type is required on every property, not found on above doc';
|
||||
throw new Error('Type is required on every property, not found on doc: ' + JSON.stringify(prop, null, 2));
|
||||
}
|
||||
// Create the clean doc
|
||||
const doc = {
|
||||
|
||||
Reference in New Issue
Block a user