Fixed failing tests and action engine props

This commit is contained in:
Thaum Rystra
2024-02-22 09:11:00 +02:00
parent b41d26b3ad
commit 5141704e23
18 changed files with 181 additions and 140 deletions

View File

@@ -3,8 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
import ActionContext from '/imports/api/engine/actions/ActionContext';
const damageProperty = new ValidatedMethod({ const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage', name: 'creatureProperties.damage',

View File

@@ -2,6 +2,9 @@ import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import ParseNode from '/imports/parser/parseTree/ParseNode'; import ParseNode from '/imports/parser/parseTree/ParseNode';
import array from '/imports/parser/parseTree/array'; import array from '/imports/parser/parseTree/array';
import constant, { isFiniteNode } from '/imports/parser/parseTree/constant'; import constant, { isFiniteNode } from '/imports/parser/parseTree/constant';
import resolve from '/imports/parser/resolve';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import Context from '/imports/parser/types/Context';
//set up the collection for creature variables //set up the collection for creature variables
const CreatureVariables = new Mongo.Collection('creatureVariables'); const CreatureVariables = new Mongo.Collection('creatureVariables');
@@ -45,6 +48,15 @@ export function getNumberFromScope(name, scope) {
return parseNode.value; return parseNode.value;
} }
export async function getConstantValueFromScope(
name, scope
) {
const parseNode = getParseNodeFromScope(name, scope);
if (!parseNode) return;
if (parseNode.parseType !== 'constant') return;
return parseNode.value;
}
export function getParseNodeFromScope(name, scope): ParseNode | undefined { export function getParseNodeFromScope(name, scope): ParseNode | undefined {
let value = getFromScope(name, scope); let value = getFromScope(name, scope);
if (!value) return; if (!value) return;

View File

@@ -4,8 +4,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import { union } from 'lodash'; import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
import { getFilter } from '/imports/api/parenting/parentingFunctions'; import { getFilter } from '/imports/api/parenting/parentingFunctions';

View File

@@ -9,14 +9,6 @@ import {
} from '/imports/api/engine/action/functions/actionEngineTest.testFn'; } from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult'; import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
import Alea from 'alea'; import Alea from 'alea';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
process.on('unhandledRejection', (error, p) => {
console.dir(error.stack);
console.error('Unhandled Rejection at:', p, 'reason:', error)
process.exit(1)
});
const [ const [
creatureId, targetCreatureId, targetCreature2Id, creatureId, targetCreatureId, targetCreature2Id,

View File

@@ -102,7 +102,7 @@ async function applyAttackToTarget(
const targetScope = getVariables(targetId); const targetScope = getVariables(targetId);
const targetArmor = getNumberFromScope('armor', targetScope) const targetArmor = getNumberFromScope('armor', targetScope)
if (Number.isFinite(targetArmor)) { if (targetArmor !== undefined) {
let name = criticalHit ? 'Critical Hit!' : let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' : criticalMiss ? 'Critical Miss!' :
result > targetArmor ? 'Hit!' : 'Miss!'; result > targetArmor ? 'Hit!' : 'Miss!';

View File

@@ -57,7 +57,7 @@ export default async function applyBuffProperty(
//Log the buff //Log the buff
let logValue = prop.description?.value let logValue = prop.description?.value
if (prop.description?.text) { if (prop.description?.text) {
recalculateInlineCalculations(prop.description, action, 'resolve', userInput); recalculateInlineCalculations(prop.description, action, 'reduce', userInput);
logValue = prop.description?.value; logValue = prop.description?.value;
} }
result.appendLog({ result.appendLog({

View File

@@ -1,6 +1,6 @@
import { some, includes, difference, intersection } from 'lodash'; import { some, includes, difference, intersection } from 'lodash';
import { getParseNodeFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import { getConstantValueFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { EngineAction } from '/imports/api/engine/action/EngineActions'; import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
@@ -9,15 +9,16 @@ import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { isFiniteNode } from '/imports/parser/parseTree/constant'; import { isFiniteNode } from '/imports/parser/parseTree/constant';
import resolve from '/imports/parser/resolve'; import resolve from '/imports/parser/resolve';
import Context from '../../../../parser/types/Context';
import toString from '/imports/parser/toString'; import toString from '/imports/parser/toString';
import { getPropertiesOfType } from '/imports/api/engine/loadCreatures'; import { getPropertiesOfType } from '/imports/api/engine/loadCreatures';
import applyTask from '/imports/api/engine/action/tasks/applyTask'; import applyTask from '/imports/api/engine/action/tasks/applyTask';
import InputProvider from '/imports/api/engine/action/functions/InputProvider'; import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
import Context from '/imports/parser/types/Context';
import applySavingThrowProperty from '/imports/api/engine/action/applyProperties/applySavingThrowProperty';
export default async function applyDamageProperty( export default async function applyDamageProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
) { ) {
const prop = task.prop; const prop = task.prop;
const scope = getEffectiveActionScope(action); const scope = getEffectiveActionScope(action);
@@ -28,7 +29,7 @@ export default async function applyDamageProperty(
// Choose target // Choose target
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds; const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
// Determine if the hit is critical // Determine if the hit is critical
const criticalHit = getParseNodeFromScope('~criticalHit', scope)?.value const criticalHit = await getConstantValueFromScope('~criticalHit', scope)
&& prop.damageType !== 'healing'; // Can't critically heal && prop.damageType !== 'healing'; // Can't critically heal
// Double the damage rolls if the hit is critical // Double the damage rolls if the hit is critical
const context = new Context({ const context = new Context({
@@ -40,7 +41,7 @@ export default async function applyDamageProperty(
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string // roll the dice only and store that string
recalculateCalculation(prop.amount, action, 'compile', userInput); recalculateCalculation(prop.amount, action, 'compile', inputProvider);
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context); const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context);
if (rolled.parseType !== 'constant') { if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled)); logValue.push(toString(rolled));
@@ -72,7 +73,7 @@ export default async function applyDamageProperty(
typeof damage !== 'number' typeof damage !== 'number'
|| !isFinite(damage) || !isFinite(damage)
) { ) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput); return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
} }
// Round the damage to a whole number // Round the damage to a whole number
@@ -80,7 +81,7 @@ export default async function applyDamageProperty(
scope['~damage'] = { value: damage }; scope['~damage'] = { value: damage };
// Convert extra damage into the stored type // Convert extra damage into the stored type
const lastDamageType = getParseNodeFromScope('~lastDamageType')?.value; const lastDamageType = await getConstantValueFromScope('~lastDamageType', scope);
if (prop.damageType === 'extra' && typeof lastDamageType === 'string') { if (prop.damageType === 'extra' && typeof lastDamageType === 'string') {
prop.damageType = lastDamageType; prop.damageType = lastDamageType;
} }
@@ -95,10 +96,10 @@ export default async function applyDamageProperty(
(prop.damageType !== 'healing' ? ' damage ' : ''); (prop.damageType !== 'healing' ? ' damage ' : '');
// If there is a save, calculate the save damage // If there is a save, calculate the save damage
let damageOnSave, saveNode, saveRoll; let damageOnSave, saveProp, saveRoll;
if (prop.save) { if (prop.save) {
if (prop.save.damageFunction?.calculation) { if (prop.save.damageFunction?.calculation) {
recalculateCalculation(prop.save.damageFunction, action, 'compile', userInput); recalculateCalculation(prop.save.damageFunction, action, 'compile', inputProvider);
context.errors = []; context.errors = [];
const { result: saveDamageRolled } = await resolve( const { result: saveDamageRolled } = await resolve(
'roll', prop.save.damageFunction.valueNode, scope, context 'roll', prop.save.damageFunction.valueNode, scope, context
@@ -112,14 +113,14 @@ export default async function applyDamageProperty(
if ( if (
!isFiniteNode(saveDamageResult) !isFiniteNode(saveDamageResult)
) { ) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput); return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
} }
// Round the damage to a whole number // Round the damage to a whole number
damageOnSave = Math.floor(saveDamageResult.value); damageOnSave = Math.floor(saveDamageResult.value);
} else { } else {
damageOnSave = Math.floor(damage / 2); damageOnSave = Math.floor(damage / 2);
} }
saveNode = { saveProp = {
node: { node: {
...prop.save, ...prop.save,
name: prop.save.stat, name: prop.save.stat,
@@ -136,8 +137,11 @@ export default async function applyDamageProperty(
// If there is a saving throw, apply that first // If there is a saving throw, apply that first
if (prop.save) { if (prop.save) {
await applySavingThrow(saveNode, actionContext); await applySavingThrowProperty({
if (getParseNodeFromScope('~saveSucceeded', scope)?.value) { prop: saveProp,
targetIds: task.targetIds,
}, action, result, inputProvider);
if (await getConstantValueFromScope('~saveSucceeded', scope)) {
// Log the total damage // Log the total damage
logValue.push(toString(reduced)); logValue.push(toString(reduced));
// Log the save damage // Log the save damage
@@ -165,14 +169,18 @@ export default async function applyDamageProperty(
// Deal the damage to the target // Deal the damage to the target
await dealDamage( await dealDamage(
action, prop, result, userInput, target, prop.damageType, damageToApply action, prop, result, inputProvider, target, prop.damageType, damageToApply
); );
} }
} else { } else {
// There are no targets, just log the result // There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`); logValue.push(`**${damage}** ${suffix}`);
if (prop.save) { if (prop.save) {
await applySavingThrow(saveNode, actionContext); await applySavingThrowProperty(saveProp, action, result, inputProvider);
await applySavingThrowProperty({
prop: saveProp,
targetIds: task.targetIds,
}, action, result, inputProvider);
logValue.push(`**${damageOnSave}** ${suffix} on a successful save`); logValue.push(`**${damageOnSave}** ${suffix} on a successful save`);
} }
} }
@@ -181,7 +189,7 @@ export default async function applyDamageProperty(
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,
}, damageTargets); }, damageTargets);
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput); return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
} }
function damageFunctionText(save) { function damageFunctionText(save) {

View File

@@ -1,18 +1,19 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions'; import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations'; import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
import { PropTask } from '/imports/api/engine/action/tasks/Task'; import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult, { LogContent } from '/imports/api/engine/action/tasks/TaskResult'; import TaskResult, { LogContent } from '/imports/api/engine/action/tasks/TaskResult';
export default async function applyNoteProperty( export default async function applyNoteProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
): Promise<void> { ): Promise<void> {
const prop = task.prop; const prop = task.prop;
let contents: LogContent[] | undefined = undefined; let contents: LogContent[] | undefined = undefined;
const logContent: LogContent = {}; const logContent: LogContent = {};
if (prop.name) logContent.name = prop.name; if (prop.name) logContent.name = prop.name;
if (prop.summary?.text) { if (prop.summary?.text) {
await recalculateInlineCalculations(prop.summary, action); await recalculateInlineCalculations(prop.summary, action, 'reduce', inputProvider);
logContent.value = prop.summary.value; logContent.value = prop.summary.value;
} }
@@ -21,7 +22,7 @@ export default async function applyNoteProperty(
} }
// Log description // Log description
if (prop.description?.text) { if (prop.description?.text) {
await recalculateInlineCalculations(prop.description, action); await recalculateInlineCalculations(prop.description, action, 'reduce', inputProvider);
if (!contents) contents = []; if (!contents) contents = [];
contents.push({ value: prop.description.value }); contents.push({ value: prop.description.value });
} }
@@ -31,5 +32,5 @@ export default async function applyNoteProperty(
targetIds: task.targetIds, targetIds: task.targetIds,
}); });
} }
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
} }

View File

@@ -1,17 +1,19 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions'; import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { rollAndReduceCalculation } from '/imports/api/engine/action/functions/recalculateCalculation'; import { rollAndReduceCalculation } from '/imports/api/engine/action/functions/recalculateCalculation';
import { PropTask } from '/imports/api/engine/action/tasks/Task'; import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import toString from '/imports/parser/toString'; import toString from '/imports/parser/toString';
export default async function roll( export default async function applyRollProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
): Promise<void> { ): Promise<void> {
const prop = task.prop; const prop = task.prop;
// If there isn't a calculation, just apply the children instead // If there isn't a calculation, just apply the children instead
if (!prop.roll?.calculation) { if (!prop.roll?.calculation) {
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
} }
const logValue: string[] = []; const logValue: string[] = [];
@@ -19,7 +21,7 @@ export default async function roll(
// roll the dice only and store that string // roll the dice only and store that string
const { const {
rolled, reduced, errors rolled, reduced, errors
} = await rollAndReduceCalculation(prop.roll, action); } = await rollAndReduceCalculation(prop.roll, action, inputProvider);
if (rolled.parseType !== 'constant') { if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled)); logValue.push(toString(rolled));
@@ -38,8 +40,8 @@ export default async function roll(
} }
// If we didn't end up with a constant or a number of finite value, give up // If we didn't end up with a constant or a number of finite value, give up
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) { if (reduced?.parseType !== 'constant' || !isFiniteNode(reduced)) {
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
} }
const value = reduced.value; const value = reduced.value;
@@ -54,5 +56,5 @@ export default async function roll(
}, task.targetIds); }, task.targetIds);
// Apply children // Apply children
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput); return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
} }

View File

@@ -1,57 +1,67 @@
// TODO import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { applyUnresolvedEffects } from '/imports/api/engine/action/methods/doCheck';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { getVariables } from '/imports/api/engine/loadCreatures';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
export default function applySavingThrow(node, actionContext) { export default async function applySavingThrowProperty(
applyNodeTriggers(node, 'before', actionContext); task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
const prop = node.doc ): Promise<void> {
const originalTargets = actionContext.targets;
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; const prop = task.prop;
const originalTargetIds = task.targetIds;
recalculateCalculation(prop.dc, actionContext); const saveTargetIds = prop.target === 'self' ? [action.creatureId] : originalTargetIds;
const dc = (prop.dc?.value); if (saveTargetIds.length > 1)
if (!isFinite(dc)) {
actionContext.addLog({ recalculateCalculation(prop.dc, action, 'reduce', inputProvider);
if (!isFiniteNode(prop.dc)) {
result.appendLog({
name: 'Error', name: 'Error',
value: 'Saving throw requires a DC', value: 'Saving throw requires a DC',
}); }, saveTargetIds);
return node.children.forEach(child => applyProperty(child, actionContext)); return applyDefaultAfterPropTasks(action, prop, saveTargetIds, inputProvider);
} }
if (!prop.silent) actionContext.addLog({
const dc = (prop.dc?.value);
if (!prop.silent) result.appendLog({
name: prop.name, name: prop.name,
value: `DC **${dc}**`, value: `DC **${dc}**`,
inline: true, inline: true,
}); ...prop.silent && { silenced: prop.silent }
const scope = actionContext.scope; }, saveTargetIds);
const scope = await getEffectiveActionScope(action);
// If there are no save targets, apply all children as if the save both // If there are no save targets, apply all children as if the save both
// succeeeded and failed // succeeded and failed
if (!saveTargets?.length) { if (!saveTargetIds?.length) {
scope['~saveFailed'] = { value: true }; result.scope = {
scope['~saveSucceeded'] = { value: true }; ['~saveFailed']: { value: true },
return applyChildren(node, actionContext); ['~saveSucceeded']: { value: true },
}
return applyDefaultAfterPropTasks(action, prop, saveTargetIds, inputProvider);
} }
// Each target makes the saving throw // Each target makes the saving throw
saveTargets.forEach(target => { for (const targetId of saveTargetIds) {
delete scope['~saveFailed'];
delete scope['~saveSucceeded'];
delete scope['~saveDiceRoll'];
delete scope['~saveRoll'];
const applyChildrenToTarget = function () { const save = getFromScope('save', getVariables(targetId));
actionContext.targets = [target];
return applyChildren(node, actionContext);
};
const save = target.variables[prop.stat];
if (!save) { if (!save) {
actionContext.addLog({ result.appendLog({
name: 'Saving throw error', name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat, value: 'No saving throw found: ' + prop.stat,
}); }, [targetId]);
return applyChildrenToTarget(); applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider);
} }
let rollModifierText = numberToSignedString(save.value, true); let rollModifierText = numberToSignedString(save.value, true);
@@ -60,9 +70,9 @@ export default function applySavingThrow(node, actionContext) {
rollModifierText += effectString; rollModifierText += effectString;
rollModifier += effectBonus; rollModifier += effectBonus;
let value, values, resultPrefix; let value, resultPrefix;
if (save.advantage === 1) { if (save.advantage === 1) {
const [a, b] = rollDice(2, 20); const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]);
if (a >= b) { if (a >= b) {
value = a; value = a;
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
@@ -71,7 +81,7 @@ export default function applySavingThrow(node, actionContext) {
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
} }
} else if (save.advantage === -1) { } else if (save.advantage === -1) {
const [a, b] = rollDice(2, 20); const [[a, b]] = await inputProvider.rollDice([{ number: 2, diceSize: 20 }]);
if (a <= b) { if (a <= b) {
value = a; value = a;
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
@@ -80,26 +90,24 @@ export default function applySavingThrow(node, actionContext) {
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
} }
} else { } else {
values = rollDice(1, 20); const [[rolledValue]] = await inputProvider.rollDice([{ number: 1, diceSize: 20 }]);
value = values[0]; value = rolledValue;
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
} }
scope['~saveDiceRoll'] = { value }; scope['~saveDiceRoll'] = { value };
const result = value + rollModifier || 0; const resultValue = value + rollModifier || 0;
scope['~saveRoll'] = { value: result }; scope['~saveRoll'] = { value: resultValue };
const saveSuccess = result >= dc; const saveSuccess = resultValue >= dc;
if (saveSuccess) { if (saveSuccess) {
scope['~saveSucceeded'] = { value: true }; scope['~saveSucceeded'] = { value: true };
} else { } else {
scope['~saveFailed'] = { value: true }; scope['~saveFailed'] = { value: true };
} }
if (!prop.silent) actionContext.addLog({ if (!prop.silent) result.appendLog({
name: saveSuccess ? 'Successful save' : 'Failed save', name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**', value: resultPrefix + '\n**' + resultValue + '**',
inline: true, inline: true,
}); }, [targetId]);
return applyChildrenToTarget(); return applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider);
}); }
// reset the targets after the save to each child
actionContext.targets = originalTargets;
} }

View File

@@ -1,10 +1,19 @@
// TODO import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import { applyAfterTasksSkipChildren, applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
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';
export default function applyToggle(node, actionContext) { export default async function applyToggle(
applyNodeTriggers(node, 'before', actionContext); task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
const prop = node.doc ): Promise<void> {
recalculateCalculation(prop.condition, actionContext);
const prop = task.prop;
await recalculateCalculation(prop.condition, action, 'reduce', inputProvider);
if (prop.condition?.value) { if (prop.condition?.value) {
return applyChildren(node, actionContext); return applyDefaultAfterPropTasks(action, prop, task.targetIds, inputProvider);
} else {
return applyAfterTasksSkipChildren(action, prop, task.targetIds, inputProvider);
} }
} }

View File

@@ -4,6 +4,7 @@ import { getPropertyChildren, getSingleProperty } from '/imports/api/engine/load
import { EngineAction } from '/imports/api/engine/action/EngineActions'; import { EngineAction } from '/imports/api/engine/action/EngineActions';
import applyTask from '../tasks/applyTask'; import applyTask from '../tasks/applyTask';
import { PropTask } from '../tasks/Task'; import { PropTask } from '../tasks/Task';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
/** /**
* Get all the child tasks of a given property * Get all the child tasks of a given property
@@ -13,11 +14,11 @@ import { PropTask } from '../tasks/Task';
* @returns * @returns
*/ */
export async function applyChildren( export async function applyChildren(
action: EngineAction, prop, targetIds: string[], userInput action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider
) { ) {
const children = await getPropertyChildren(action.creatureId, prop); const children = await getPropertyChildren(action.creatureId, prop);
for (const childProp of children) { for (const childProp of children) {
await applyTask(action, { prop: childProp, targetIds }, userInput); await applyTask(action, { prop: childProp, targetIds }, inputProvider);
} }
} }
@@ -28,24 +29,24 @@ export async function applyChildren(
* @returns * @returns
*/ */
export async function applyAfterChildrenTriggers( export async function applyAfterChildrenTriggers(
action: EngineAction, prop, targetIds: string[], userInput action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider
) { ) {
if (!prop.triggerIds?.afterChildren) return; if (!prop.triggerIds?.afterChildren) return;
for (const triggerId of prop.triggerIds.afterChildren) { for (const triggerId of prop.triggerIds.afterChildren) {
const trigger = await getSingleProperty(action.creatureId, triggerId); const trigger = await getSingleProperty(action.creatureId, triggerId);
if (!trigger) continue; if (!trigger) continue;
await applyTask(action, { prop: trigger, targetIds }, userInput); await applyTask(action, { prop: trigger, targetIds }, inputProvider);
} }
} }
export async function applyAfterTriggers( export async function applyAfterTriggers(
action: EngineAction, prop, targetIds: string[], userInput action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider
) { ) {
if (!prop.triggerIds?.after) return; if (!prop.triggerIds?.after) return;
for (const triggerId of prop.triggerIds.after) { for (const triggerId of prop.triggerIds.after) {
const trigger = await getSingleProperty(action.creatureId, triggerId); const trigger = await getSingleProperty(action.creatureId, triggerId);
if (!trigger) continue; if (!trigger) continue;
await applyTask(action, { prop: trigger, targetIds }, userInput); await applyTask(action, { prop: trigger, targetIds }, inputProvider);
} }
} }
@@ -60,11 +61,11 @@ export async function applyAfterTriggers(
* @returns * @returns
*/ */
export async function applyDefaultAfterPropTasks( export async function applyDefaultAfterPropTasks(
action: EngineAction, prop, targetIds: string[], userInput action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider
) { ) {
await applyAfterTriggers(action, prop, targetIds, userInput); await applyAfterTriggers(action, prop, targetIds, inputProvider);
await applyChildren(action, prop, targetIds, userInput); await applyChildren(action, prop, targetIds, inputProvider);
await applyAfterChildrenTriggers(action, prop, targetIds, userInput); await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider);
} }
/** /**
@@ -77,10 +78,10 @@ export async function applyDefaultAfterPropTasks(
* @returns * @returns
*/ */
export async function applyAfterTasksSkipChildren( export async function applyAfterTasksSkipChildren(
action: EngineAction, prop, targetIds: string[], userInput action: EngineAction, prop, targetIds: string[], inputProvider: InputProvider
) { ) {
await applyAfterTriggers(action, prop, targetIds, userInput); await applyAfterTriggers(action, prop, targetIds, inputProvider);
await applyAfterChildrenTriggers(action, prop, targetIds, userInput); await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider);
} }
/** /**
@@ -93,11 +94,11 @@ export async function applyAfterTasksSkipChildren(
* @returns * @returns
*/ */
export async function applyAfterPropTasksForSingleChild( export async function applyAfterPropTasksForSingleChild(
action: EngineAction, prop, childProp, targetIds: string[], userInput action: EngineAction, prop, childProp, targetIds: string[], inputProvider: InputProvider
) { ) {
await applyAfterTriggers(action, prop, targetIds, userInput); await applyAfterTriggers(action, prop, targetIds, inputProvider);
await applyTask(action, { prop: childProp, targetIds }, userInput); await applyTask(action, { prop: childProp, targetIds }, inputProvider);
await applyAfterChildrenTriggers(action, prop, targetIds, userInput); await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider);
} }
/** /**
@@ -110,13 +111,13 @@ export async function applyAfterPropTasksForSingleChild(
* @returns * @returns
*/ */
export async function applyAfterPropTasksForSomeChildren( export async function applyAfterPropTasksForSomeChildren(
action: EngineAction, prop, children, targetIds: string[], userInput action: EngineAction, prop, children, targetIds: string[], inputProvider: InputProvider
) { ) {
await applyAfterTriggers(action, prop, targetIds, userInput); await applyAfterTriggers(action, prop, targetIds, inputProvider);
for (const childProp of children) { for (const childProp of children) {
await applyTask(action, { prop: childProp, targetIds }, userInput); await applyTask(action, { prop: childProp, targetIds }, inputProvider);
} }
await applyAfterChildrenTriggers(action, prop, targetIds, userInput); await applyAfterChildrenTriggers(action, prop, targetIds, inputProvider);
} }
/** /**
@@ -128,14 +129,14 @@ export async function applyAfterPropTasksForSomeChildren(
* @returns * @returns
*/ */
export async function applyTriggers( export async function applyTriggers(
action: EngineAction, prop, targetIds: string[], triggerPath: string, userInput action: EngineAction, prop, targetIds: string[], triggerPath: string, inputProvider: InputProvider
) { ) {
const triggerIds = get(prop?.triggers, triggerPath); const triggerIds = get(prop?.triggers, triggerPath);
if (!triggerIds) return; if (!triggerIds) return;
for (const triggerId of triggerIds) { for (const triggerId of triggerIds) {
const trigger = await getSingleProperty(action.creatureId, triggerId); const trigger = await getSingleProperty(action.creatureId, triggerId);
if (!trigger) continue; if (!trigger) continue;
await applyTask(action, { prop: trigger, targetIds }, userInput); await applyTask(action, { prop: trigger, targetIds }, inputProvider);
} }
} }
@@ -146,7 +147,7 @@ export async function applyTriggers(
* @returns Copies of the task, but with a single target each * @returns Copies of the task, but with a single target each
*/ */
export async function applyTaskToEachTarget( export async function applyTaskToEachTarget(
action: EngineAction, task: PropTask, targetIds: string[] = task.targetIds, userInput action: EngineAction, task: PropTask, targetIds: string[] = task.targetIds, inputProvider: InputProvider
) { ) {
if (targetIds.length <= 1) throw 'Must have multiple targets to split a task'; if (targetIds.length <= 1) throw 'Must have multiple targets to split a task';
// If there are targets, apply a new task to each target // If there are targets, apply a new task to each target
@@ -154,6 +155,6 @@ export async function applyTaskToEachTarget(
await applyTask(action, { await applyTask(action, {
...task, ...task,
targetIds: [targetId] targetIds: [targetId]
}, userInput); }, inputProvider);
} }
} }

View File

@@ -1,7 +1,14 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations'; import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations';
import recalculateCalculation from './recalculateCalculation' import recalculateCalculation from './recalculateCalculation'
import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import ResolveLevel from '/imports/parser/types/ResolveLevel';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
export default async function recalculateInlineCalculations(inlineCalcObj, action, parseLevel, userInput) { export default async function recalculateInlineCalculations(
inlineCalcObj: InlineCalculation, action: EngineAction,
parseLevel: ResolveLevel, userInput: InputProvider
) {
// Skip if there are no calculations // Skip if there are no calculations
if (!inlineCalcObj?.inlineCalculations?.length) return; if (!inlineCalcObj?.inlineCalculations?.length) return;
// Recalculate each calculation with the current scope // Recalculate each calculation with the current scope

View File

@@ -5,9 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import rollDice from '/imports/parser/rollDice'; import rollDice from '/imports/parser/rollDice';
import numberToSignedString from '/imports/api/utility/numberToSignedString'; import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
import ActionContext from '/imports/api/engine/actions/ActionContext';
import recalculateCalculation from '../../actions/applyPropertyByType/shared/recalculateCalculation';
import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import { getSingleProperty } from '/imports/api/engine/loadCreatures';
// TODO Migrate this to the new action engine // TODO Migrate this to the new action engine

View File

@@ -9,16 +9,16 @@ import InputProvider from '/imports/api/engine/action/functions/InputProvider';
// DamagePropTask promises a number of actual damage done // DamagePropTask promises a number of actual damage done
export default async function applyTask( export default async function applyTask(
action: EngineAction, task: DamagePropTask, userInput: InputProvider action: EngineAction, task: DamagePropTask, inputProvider: InputProvider
): Promise<number> ): Promise<number>
// Other tasks promise nothing // Other tasks promise nothing
export default async function applyTask( export default async function applyTask(
action: EngineAction, task: PropTask | ItemAsAmmoTask, userInput: InputProvider action: EngineAction, task: PropTask | ItemAsAmmoTask, inputProvider: InputProvider
): Promise<void> ): Promise<void>
export default async function applyTask( export default async function applyTask(
action: EngineAction, task: Task, userInput: InputProvider action: EngineAction, task: Task, inputProvider: InputProvider
): Promise<void | number> { ): Promise<void | number> {
action.taskCount += 1; action.taskCount += 1;
if (action.taskCount > 100) throw 'Only 100 properties can be applied at once'; if (action.taskCount > 100) throw 'Only 100 properties can be applied at once';
@@ -28,9 +28,9 @@ export default async function applyTask(
action.results.push(result); action.results.push(result);
switch (task.subtaskFn) { switch (task.subtaskFn) {
case 'damageProp': case 'damageProp':
return applyDamagePropTask(task, action, result, userInput); return applyDamagePropTask(task, action, result, inputProvider);
case 'consumeItemAsAmmo': case 'consumeItemAsAmmo':
return applyItemAsAmmoTask(task, action, result, userInput); return applyItemAsAmmoTask(task, action, result, inputProvider);
} }
} else { } else {
// Get property // Get property
@@ -47,7 +47,7 @@ export default async function applyTask(
for (const triggerId of prop.triggerIds.before) { for (const triggerId of prop.triggerIds.before) {
const trigger = await getSingleProperty(action.creatureId, triggerId); const trigger = await getSingleProperty(action.creatureId, triggerId);
if (!trigger) continue; if (!trigger) continue;
await applyTask(action, { prop: trigger, targetIds: task.targetIds }, userInput); await applyTask(action, { prop: trigger, targetIds: task.targetIds }, inputProvider);
} }
} }
@@ -57,6 +57,6 @@ export default async function applyTask(
action.results.push(result); action.results.push(result);
// Apply the property // Apply the property
return applyProperties[prop.type]?.(task, action, result, userInput); return applyProperties[prop.type]?.(task, action, result, inputProvider);
} }
} }

View File

@@ -11,4 +11,10 @@ describe('Call Node', function () {
assert.isEmpty(context.errors) assert.isEmpty(context.errors)
assert.equal(toString(result), 'min(unknownVariable, 3, 3d30)'); assert.equal(toString(result), 'min(unknownVariable, 3, 3d30)');
}); });
it('reduces', async function () {
const callNode = parse('min( unknownVariable, 1 + 2, 3d30 )');
const { result, context } = await resolve('reduce', callNode, undefined, undefined, inputProviderForTests);
assert.isEmpty(context.errors)
assert.equal(toString(result), '0');
});
}); });

View File

@@ -173,16 +173,13 @@ const call: CallFactory = {
expectedType = argumentsExpected[index]; expectedType = argumentsExpected[index];
} }
if (expectedType === 'parseNode') return; if (expectedType === 'parseNode') return;
if ( failed = !(
node.parseType !== expectedType node.parseType === expectedType
|| ( || (node.parseType === 'constant' && node.valueType === expectedType)
node.parseType === 'constant' );
&& node.valueType !== expectedType
)
) failed = true;
if (failed && fn === 'reduce') { if (failed && fn === 'reduce') {
const typeName = typeof expectedType === 'string' ? expectedType : expectedType.constructor.name; const typeName = typeof expectedType === 'string' ? expectedType : expectedType.constructor.name;
const nodeName = node.parseType; const nodeName = node.parseType === 'constant' ? node.valueType : node.parseType;
context.error(`Incorrect arguments to ${callNode.functionName} function` + context.error(`Incorrect arguments to ${callNode.functionName} function` +
`expected ${typeName} got ${nodeName}`); `expected ${typeName} got ${nodeName}`);
} }

View File

@@ -0,0 +1,5 @@
process.on('unhandledRejection', (error, p) => {
console.dir(error.stack);
console.error('Unhandled Rejection at:', p, 'reason:', error)
process.exit(1)
});