Typescript all the parser things

This commit is contained in:
Thaum Rystra
2024-02-20 23:21:12 +02:00
parent 3ea492ee78
commit ac15512bc5
86 changed files with 926 additions and 718 deletions

View File

@@ -9,6 +9,14 @@ import {
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
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 [
creatureId, targetCreatureId, targetCreature2Id,

View File

@@ -23,7 +23,7 @@ export default async function applyActionProperty(
//Log the name and summary, check that the property has enough resources to fire
const content: LogContent = { name: getPropertyTitle(prop) };
if (prop.summary?.text) {
await recalculateInlineCalculations(prop.summary, action);
await recalculateInlineCalculations(prop.summary, action, 'reduce', userInput);
content.value = prop.summary.value;
}
if (prop.silent) content.silenced = true;
@@ -188,7 +188,7 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['~attackAdvantage']?.value === 1) {
const [[a, b]] = await userInput.rollDice(attack, [{ number: 2, diceSize: 20 }]);
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a >= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
@@ -197,7 +197,7 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['~attackAdvantage']?.value === -1) {
const [[a, b]] = await userInput.rollDice(attack, [{ number: 2, diceSize: 20 }]);
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a <= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
@@ -206,7 +206,7 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
[[value]] = await userInput.rollDice(attack, [{ number: 1, diceSize: 20 }]);
[[value]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]);
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
resultPushScope['~attackDiceRoll'] = { value };
@@ -218,10 +218,12 @@ async function rollAttack(attack, scope, resultPushScope, userInput: InputProvid
function applyCrits(value, scope, resultPushScope) {
const scopeCritTarget = getNumberFromScope('~criticalHitTarget', scope);
const criticalHitTarget = Number.isFinite(scopeCritTarget) ? scopeCritTarget : 20;
const criticalHitTarget = scopeCritTarget !== undefined &&
Number.isFinite(scopeCritTarget) ? scopeCritTarget : 20;
const scopeCritMissTarget = getNumberFromScope('~criticalMissTarget', scope);
const criticalMissTarget = Number.isFinite(scopeCritMissTarget) ? scopeCritMissTarget : 1;
const criticalMissTarget = scopeCritMissTarget !== undefined &&
Number.isFinite(scopeCritMissTarget) ? scopeCritMissTarget : 1;
const criticalHit = value >= criticalHitTarget;
const criticalMiss = value <= criticalMissTarget;

View File

@@ -115,7 +115,7 @@ export default async function applyBranchProperty(
let choices: string[];
let chosenChildren: typeof children = [];
if (children.length) {
choices = await userInput.choose(action, children);
choices = await userInput.choose(children);
chosenChildren = filter(children, child => choices.includes(child._id));
}
if (!children.length || !chosenChildren.length) {

View File

@@ -3,9 +3,11 @@ import { get } from 'lodash';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
import resolve, { toString, map } from '/imports/parser/resolve';
import resolve from '/imports/parser/resolve';
import map from '/imports/parser/map';
import toString from '/imports/parser/toString';
import computedSchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey';
import applyFnToKey, { applyFnToKeyAsync } from '/imports/api/engine/computation/utility/applyFnToKey';
import accessor from '/imports/parser/parseTree/accessor';
import TaskResult, { Mutation } from '/imports/api/engine/action/tasks/TaskResult';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
@@ -55,7 +57,7 @@ export default async function applyBuffProperty(
//Log the buff
let logValue = prop.description?.value
if (prop.description?.text) {
recalculateInlineCalculations(prop.description, action);
recalculateInlineCalculations(prop.description, action, 'resolve', userInput);
logValue = prop.description?.value;
}
result.appendLog({
@@ -83,17 +85,17 @@ async function crystalizeVariables(
action: EngineAction, propList: any[], task: PropTask, result: TaskResult
) {
const scope = await getEffectiveActionScope(action);
propList.forEach(prop => {
for (const prop of propList) {
if (prop._skipCrystalize) {
delete prop._skipCrystalize;
return;
}
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
for (const calcKey of computedSchemas[prop.type].computedFields()) {
await applyFnToKeyAsync(prop, calcKey, async (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
calcObj.parseNode = map(calcObj.parseNode, node => {
calcObj.parseNode = await map(calcObj.parseNode, async node => {
// Skip nodes that aren't symbols or accessors
if (
node.parseType !== 'accessor'
@@ -117,22 +119,17 @@ async function crystalizeVariables(
return node;
} else {
// Resolve all other variables
const { result, context } = resolve('reduce', node, scope);
context.errors?.forEach(error => {
result.appendLog({
name: 'Error',
value: error,
}, task.targetIds);
});
return result;
const { result: nodeResult, context } = await resolve('reduce', node, scope);
result.appendParserContextErrors(context, task.targetIds);
return nodeResult;
}
});
calcObj.calculation = toString(calcObj.parseNode);
calcObj.hash = cyrb53(calcObj.calculation);
});
});
}
// For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
for (const calcKey of computedSchemas[prop.type].inlineCalculationFields()) {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
@@ -161,6 +158,6 @@ async function crystalizeVariables(
}
inlineCalcObj.hash = inlineCalcHash;
});
});
});
}
}
}

View File

@@ -1,66 +1,88 @@
// TODO
import { some, includes, difference, intersection } from 'lodash';
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
import { getParseNodeFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
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 { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import resolve from '/imports/parser/resolve';
import Context from '../../../../parser/types/Context';
import toString from '/imports/parser/toString';
import { getPropertiesOfType } from '/imports/api/engine/loadCreatures';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
const prop = node.doc
const scope = actionContext.scope;
export default async function applyDamageProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput
) {
const prop = task.prop;
const scope = getEffectiveActionScope(action);
// Skip if there is no parse node to work with
if (!prop.amount?.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
// Determine if the hit is critical
let criticalHit = scope['~criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
const criticalHit = getParseNodeFromScope('~criticalHit', scope)?.value
&& prop.damageType !== 'healing'; // Can't critically heal
// Double the damage rolls if the hit is critical
let context = new Context({
const context = new Context({
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
const logValue = [];
const logValue: string[] = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string
recalculateCalculation(prop.amount, actionContext, 'compile');
const { result: rolled } = resolve('roll', prop.amount.valueNode, scope, context);
recalculateCalculation(prop.amount, action, 'compile', userInput);
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
result.appendParserContextErrors(context, damageTargets);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext);
const { result: reduced } = await resolve('reduce', rolled, scope, context);
result.appendParserContextErrors(context, damageTargets);
// Store the result
let damage: number | undefined = undefined;
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
if (typeof reduced.value === 'number') {
damage = reduced.value;
}
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
}
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren(node, actionContext);
// If we didn't end up with damage of finite amount, give up
if (
typeof damage !== 'number'
|| !isFinite(damage)
) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput);
}
// Round the damage to a whole number
damage = Math.floor(damage);
scope['~damage'] = damage;
scope['~damage'] = { value: damage };
// Convert extra damage into the stored type
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
prop.damageType = scope['~lastDamageType']?.value;
const lastDamageType = getParseNodeFromScope('~lastDamageType')?.value;
if (prop.damageType === 'extra' && typeof lastDamageType === 'string') {
prop.damageType = lastDamageType;
}
// Store current damage type
if (prop.damageType !== 'healing') {
@@ -68,7 +90,7 @@ export default function applyDamage(node, actionContext) {
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
const suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ' : '');
@@ -76,17 +98,24 @@ export default function applyDamage(node, actionContext) {
let damageOnSave, saveNode, saveRoll;
if (prop.save) {
if (prop.save.damageFunction?.calculation) {
recalculateCalculation(prop.save.damageFunction, actionContext, undefined, 'compile');
let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.valueNode, scope, context);
recalculateCalculation(prop.save.damageFunction, action, 'compile', userInput);
context.errors = [];
const { result: saveDamageRolled } = await resolve(
'roll', prop.save.damageFunction.valueNode, scope, context
);
saveRoll = toString(saveDamageRolled);
let { result: saveDamageResult } = resolve('reduce', saveDamageRolled, scope, context);
const { result: saveDamageResult } = await resolve(
'reduce', saveDamageRolled, scope, context
);
result.appendParserContextErrors(context, damageTargets);
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren(node, actionContext);
if (
!isFiniteNode(saveDamageResult)
) {
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput);
}
damageOnSave = +saveDamageResult.value;
// Round the damage to a whole number
damageOnSave = Math.floor(damageOnSave);
damageOnSave = Math.floor(saveDamageResult.value);
} else {
damageOnSave = Math.floor(damage / 2);
}
@@ -102,14 +131,13 @@ export default function applyDamage(node, actionContext) {
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
actionContext.target = [target];
let damageToApply = damage;
for (const target of damageTargets) {
let damageToApply = damage || 0;
// If there is a saving throw, apply that first
if (prop.save) {
applySavingThrow(saveNode, actionContext);
if (scope['~saveSucceeded']?.value) {
await applySavingThrow(saveNode, actionContext);
if (getParseNodeFromScope('~saveSucceeded', scope)?.value) {
// Log the total damage
logValue.push(toString(reduced));
// Log the save damage
@@ -136,49 +164,28 @@ export default function applyDamage(node, actionContext) {
});
// Deal the damage to the target
let damageDealt = dealDamage({
target,
damageType: prop.damageType,
amount: damageToApply,
actionContext
});
// Log the damage done
if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
logValue.push(`Dealt **${damageDealt}** ${suffix} ${target.name && ' to '}${target.name}`);
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name,
value: `Received **${damageDealt}** ${suffix}`,
}],
}
});
}
});
await dealDamage(
action, prop, result, userInput, target, prop.damageType, damageToApply
);
}
} else {
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
if (prop.save) {
applySavingThrow(saveNode, actionContext);
await applySavingThrow(saveNode, actionContext);
logValue.push(`**${damageOnSave}** ${suffix} on a successful save`);
}
}
if (!prop.silent) actionContext.addLog({
if (logValue.length) result.appendLog({
name: logName,
value: logValue.join('\n'),
inline: true,
});
return applyChildren(node, actionContext);
}, damageTargets);
return applyDefaultAfterPropTasks(action, prop, damageTargets, userInput);
}
function damageFunctionText(save, scope, context, actionContext) {
if (!save) return [];
function damageFunctionText(save) {
if (!save) return;
if (!save.damageFunction) {
return '**Half damage on successful save**';
}
@@ -239,9 +246,12 @@ function multiplierAppliesTo(damageProp, multiplierType) {
}
}
function dealDamage({ target, damageType, amount, actionContext }) {
async function dealDamage(
action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider,
targetId: string, damageType: string, amount: number
) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
let healthBars = getPropertiesOfType(targetId, 'attribute');
// Keep only the healthbars that can take damage/healing
healthBars = healthBars.filter((bar) => {
@@ -276,24 +286,20 @@ function dealDamage({ target, damageType, amount, actionContext }) {
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
for (const healthBar of healthBars) {
if (damageLeft === 0) return;
// Replace the healthbar by the one in the action context if we can
// The damagePropertyWork function bashes the prop with the damage
// So we can use the new value in later action properties
if (healthBar.variableName) {
const targetHealthBar = target.variables[healthBar.variableName];
if (targetHealthBar?._id === healthBar._id) {
healthBar = targetHealthBar;
}
}
// Do the damage
let damageAdded = damagePropertyWork({
prop: healthBar,
operation: 'increment',
value: damageLeft,
actionContext
});
const damageAdded = await applyTask(action, {
prop,
targetIds: [targetId],
subtaskFn: 'damageProp',
params: {
operation: 'increment',
value: +damageLeft || 0,
targetProp: healthBar,
},
}, userInput);
damageLeft -= damageAdded;
// Prevent overflow
if (
@@ -303,6 +309,6 @@ function dealDamage({ target, damageType, amount, actionContext }) {
) {
damageLeft = 0;
}
});
}
return totalDamage;
}

View File

@@ -3,7 +3,7 @@ import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions
import { rollAndReduceCalculation } 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';
import { toString } from '/imports/parser/resolve';
import toString from '/imports/parser/toString';
export default async function roll(
task: PropTask, action: EngineAction, result: TaskResult, userInput

View File

@@ -1,8 +1,6 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
type InputProvider = {
rollDice(
action: EngineAction, dice: { number: number, diceSize: number }[]
dice: { number: number, diceSize: number }[]
): Promise<number[][]>;
/**
* Choose from a provided selection
@@ -11,7 +9,6 @@ type InputProvider = {
* @param quantity Number of choices to make [min, max] inclusive, where -1 means no limit
*/
choose(
action: EngineAction,
choices: ({ _id: string } & Record<string, any>)[],
quantity?: [min: number, max: number],
): Promise<string[]>;

View File

@@ -6,7 +6,7 @@ const inputProviderForTests: InputProvider = {
* rollDice function returns the average roll for every dice rolled
* [5d10, 1d4] => [[6,6,6,6,6], [3]]
*/
async rollDice(action, dice) {
async rollDice(dice = []) {
const result: number[][] = [];
for (const diceRoll of dice) {
const averageRoll = Math.round(diceRoll.diceSize / 2);
@@ -21,7 +21,7 @@ const inputProviderForTests: InputProvider = {
/**
* For testing, always return the minimum number of choices, always choosing the first options
*/
async choose(action, choices, quantity = [1, 1]) {
async choose(choices, quantity = [1, 1]) {
const chosen: string[] = [];
const choiceQuantity = quantity[0] <= 0 ? 1 : quantity[0];
for (let i = 0; i < choiceQuantity && i < choices.length; i += 1) {

View File

@@ -1,4 +1,5 @@
import { Context, toPrimitiveOrString } from '/imports/parser/resolve';
import Context from '../../../../parser/types/Context';
import toPrimitiveOrString from '/imports/parser/toPrimitiveOrString';
import {
aggregateCalculationEffects,
aggregateCalculationProficiencies,
@@ -26,7 +27,7 @@ export default async function recalculateCalculation(
const {
result: unaffectedResult,
context
} = resolve(parseLevel, calcObj.parseNode, scope);
} = await resolve(parseLevel, calcObj.parseNode, scope);
calcObj.valueNode = unaffectedResult;
// store the unaffected value
@@ -47,7 +48,7 @@ export default async function recalculateCalculation(
// Resolve the modified valueNode, use the same context
const {
result: finalResult
} = resolve(parseLevel, calcObj.parseNode, scope, context);
} = await resolve(parseLevel, calcObj.parseNode, scope, context);
// Store the errors
calcObj.errors = context.errors;
@@ -55,12 +56,12 @@ export default async function recalculateCalculation(
// Store the value and its primitive
calcObj.value = toPrimitiveOrString(finalResult);
calcObj.valueNode = finalResult;
}
export async function rollAndReduceCalculation(
calcObj: CalculatedField, action: EngineAction, userInput: InputProvider
) {
if (!calcObj) throw new Error('calcObj is required');
const context = new Context();
const scope = await getEffectiveActionScope(action);
// Compile
@@ -68,10 +69,10 @@ export async function rollAndReduceCalculation(
const compiled = calcObj.valueNode;
// Roll
const { result: rolled } = resolve('roll', calcObj.valueNode, scope, context);
const { result: rolled } = await resolve('roll', calcObj.valueNode, scope, context, userInput);
// Reduce
const { result: reduced } = resolve('reduce', rolled, scope, context);
const { result: reduced } = await resolve('reduce', rolled, scope, context, userInput);
// Return
return { compiled, rolled, reduced, errors: context.errors };

View File

@@ -1,15 +1,13 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations';
import recalculateCalculation from './recalculateCalculation'
export default async function recalculateInlineCalculations(inlineCalcObj, action) {
export default async function recalculateInlineCalculations(inlineCalcObj, action, parseLevel, userInput) {
// Skip if there are no calculations
if (!inlineCalcObj?.inlineCalculations?.length) return;
// Recalculate each calculation with the current scope
const promises = [];
for (const calc of inlineCalcObj.inlineCalculations) {
promises.push(recalculateCalculation(calc, action));
await recalculateCalculation(calc, action, undefined, userInput);
}
await Promise.all(promises);
// Embed the new calculated values
embedInlineCalculations(inlineCalcObj);
}

View File

@@ -1,6 +1,7 @@
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
@@ -31,7 +32,7 @@ export default async function spendResources(
for (const att of prop.resources.attributesConsumed) {
const scope = await getEffectiveActionScope(action);
const statToDamage = await getFromScope(att.variableName, scope);
await recalculateCalculation(att.quantity, action, 'reduce');
await recalculateCalculation(att.quantity, action, 'reduce', userInput);
await applyTask(action, {
prop,
targetIds: [action.creatureId],
@@ -48,7 +49,7 @@ export default async function spendResources(
// Iterate through all the items consumed and consume them
if (prop.resources?.itemsConsumed?.length) {
for (const itemConsumed of prop.resources.itemsConsumed) {
await recalculateCalculation(itemConsumed.quantity, action, 'reduce');
await recalculateCalculation(itemConsumed.quantity, action, 'reduce', userInput);
if (!itemConsumed.itemId) {
throw 'No ammo was selected';
}

View File

@@ -1,3 +1,5 @@
import Context from '../../../../parser/types/Context';
/**
* The result of running a task containing all the changes that need to be made to the listed
* targets
@@ -36,6 +38,22 @@ export default class TaskResult {
}
latestMutation.contents.push(content);
}
appendParserContextErrors(context: Context, targetIds) {
if (!context.errors?.length) return;
if (!this.mutations.length) {
this.mutations.push({ targetIds, contents: [] });
}
const latestMutation = this.mutations[this.mutations.length - 1]
if (!latestMutation.contents) {
latestMutation.contents = [];
}
context.errors?.forEach(error => {
latestMutation.contents?.push({
name: 'Error',
value: error.message,
});
});
}
}
export type Mutation = {

View File

@@ -8,7 +8,7 @@ import { getSingleProperty } from '/imports/api/engine/loadCreatures';
export default async function applyDamagePropTask(
task: DamagePropTask, action: EngineAction, result: TaskResult, userInput
): Promise<void> {
): Promise<number> {
const prop = task.prop;
if (task.targetIds.length > 1) {
@@ -74,7 +74,7 @@ export default async function applyDamagePropTask(
let damage, newValue, increment;
targetProp = await getSingleProperty(targetId, targetPropId);
if (!targetProp) return;
if (!targetProp) return value;
if (operation === 'set') {
const total = targetProp.total || 0;
@@ -128,4 +128,5 @@ export default async function applyDamagePropTask(
});
}
await applyTriggers(action, prop, [action.creatureId], 'damageProperty.after', userInput);
return increment;
}

View File

@@ -1,5 +1,5 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import Task from './Task';
import Task, { DamagePropTask, ItemAsAmmoTask, PropTask } from './Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyDamagePropTask from '/imports/api/engine/action/tasks/applyDamagePropTask';
import applyItemAsAmmoTask from '/imports/api/engine/action/tasks/applyItemAsAmmoTask';
@@ -7,9 +7,19 @@ import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import applyProperties from '/imports/api/engine/action/applyProperties';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
// DamagePropTask promises a number of actual damage done
export default async function applyTask(
action: EngineAction, task: DamagePropTask, userInput: InputProvider
): Promise<number>
// Other tasks promise nothing
export default async function applyTask(
action: EngineAction, task: PropTask | ItemAsAmmoTask, userInput: InputProvider
): Promise<void>
export default async function applyTask(
action: EngineAction, task: Task, userInput: InputProvider
): Promise<void> {
): Promise<void | number> {
action.taskCount += 1;
if (action.taskCount > 100) throw 'Only 100 properties can be applied at once';

View File

@@ -1,4 +1,4 @@
import { traverse } from '/imports/parser/resolve';
import traverse from '/imports/parser/traverse';
export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) {
prop._computationDetails.calculations.forEach(calcObj => {

View File

@@ -125,6 +125,6 @@ function parseCalculation(calcObj) {
message: prettifyParseError(e),
};
calcObj.parseError = error;
calcObj.parseNode = errorNode.create({ error });
calcObj.parseNode = errorNode.create({ error: error.message });
}
}

View File

@@ -1,4 +1,7 @@
export default function computeAction(computation, node) {
import CreatureComputation from '/imports/api/engine/computation/CreatureComputation';
import { Node } from 'ngraph.graph';
export default function computeAction(computation: CreatureComputation, node: Node) {
const prop = node.data;
if (Number.isFinite(prop.uses?.value)) {
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);

View File

@@ -2,27 +2,30 @@ import call from '/imports/parser/parseTree/call';
import constant from '/imports/parser/parseTree/constant';
import operator from '/imports/parser/parseTree/operator';
import parenthesis from '/imports/parser/parseTree/parenthesis';
import resolve, { toPrimitiveOrString } from '/imports/parser/resolve';
import resolve from '/imports/parser/resolve';
import toPrimitiveOrString from '/imports/parser/toPrimitiveOrString';
export default function computeCalculation(computation, node) {
export default async function computeCalculation(computation, node) {
const calcObj = node.data;
if (!calcObj) return;
// resolve the parse node into the initial value
resolveCalculationNode(calcObj, calcObj.parseNode, computation.scope);
await resolveCalculationNode(calcObj, calcObj.parseNode, computation.scope);
// link and aggregate the effects and proficiencies
// link the effects and proficiencies
linkCalculationEffects(node, computation);
aggregateCalculationEffects(calcObj, id => computation.propsById[id]);
linkCalculationProficiencies(node, computation)
aggregateCalculationProficiencies(calcObj, id => computation.propsById[id], computation.scope['proficiencyBonus']?.value || 0);
// Store the unaffected value
if (calcObj.effectIds || calcObj.proficiencyIds) {
calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode);
}
// Aggregate the effects and proficiencies
aggregateCalculationEffects(calcObj, id => computation.propsById[id]);
aggregateCalculationProficiencies(calcObj, id => computation.propsById[id], computation.scope['proficiencyBonus']?.value || 0);
// Resolve the valueNode after effects and proficiencies have been applied to it
resolveCalculationNode(calcObj, calcObj.valueNode, computation.scope);
await resolveCalculationNode(calcObj, calcObj.valueNode, computation.scope);
// Store the value as a primitive
calcObj.value = toPrimitiveOrString(calcObj.valueNode);
@@ -32,10 +35,13 @@ export default function computeCalculation(computation, node) {
delete calcObj._localScope;
}
export function resolveCalculationNode(calculation, parseNode, scope, givenContext) {
export async function resolveCalculationNode(calculation, parseNode, scope, givenContext) {
if (!parseNode) throw new Error('parseNode is required');
const fn = calculation._parseLevel;
const calculationScope = { ...calculation._localScope, ...scope };
const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext);
const { result: resultNode, context } = await resolve(fn, parseNode, calculationScope, givenContext);
if (calculation.hash === 1318417319946211 && calculation._key === 'attackRoll') console.log({ calculation, resultNode, parseNode, ers: context.errors })
calculation.errors = context.errors;
calculation.valueNode = resultNode;
}

View File

@@ -1,13 +1,12 @@
import { has } from 'lodash';
import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
export default function computePointBuy(computation, node) {
export default async function computePointBuy(computation, node) {
const prop = node.data;
const min = has(prop, 'min.value') ? prop.min.value : null;
const max = has(prop, 'max.value') ? prop.max.value : null;
prop.spent = 0;
prop.values?.forEach(row => {
for (const row of prop.values || []) {
row.spent = 0;
if (row.value === undefined) return;
const costFunction = EJSON.clone(prop.cost);
@@ -22,7 +21,7 @@ export default function computePointBuy(computation, node) {
}
// Evaluate the cost function
if (!costFunction) return;
resolveCalculationNode(costFunction, costFunction.parseNode, {
await resolveCalculationNode(costFunction, costFunction.parseNode, {
...computation.scope, value: row.value
});
// Write calculation errors
@@ -37,7 +36,7 @@ export default function computePointBuy(computation, node) {
row.spent = costFunction.value;
prop.spent += costFunction.value;
}
});
}
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
if (prop.spent > prop.total?.value) {
prop.errors = prop.errors || [];

View File

@@ -8,11 +8,9 @@ export default function computeVariableAsAttribute(computation, node, prop) {
// Apply damage in a way that respects the damage rules, modifying damage if need be
// Bound the damage
if (!prop.ignoreLowerLimit && prop.damage > prop.total) {
console.log(`reducing damage from ${prop.damage} to ${prop.total}`);
prop.damage = prop.total;
}
if (!prop.ignoreUpperLimit && prop.damage < 0) {
console.log(`increasing damage from ${prop.damage} to 0`);
prop.damage = 0;
}
// Apply damage

View File

@@ -1,7 +1,7 @@
import getAggregatorResult from './getAggregatorResult';
export default function computeVariableAsToggle(computation, node, prop) {
let result = getAggregatorResult(node, prop) || 0;
let result = getAggregatorResult(node) || 0;
prop.value = !!result || !!prop.enabled || !!prop.condition?.value;
}

View File

@@ -3,9 +3,9 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = computation.propsById['actionId'];
assert.equal(prop.summary.value, 'test summary 3 without referencing anything 7');

View File

@@ -3,9 +3,9 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
const scope = variableName => computation.scope[variableName];
assert.equal(prop('emptyId').value, 0, 'calculates empty props to zero');

View File

@@ -3,10 +3,13 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation.js';
import clean from '../../utility/cleanProp.testFn.js';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
// Tag targeted effects make complicated parse trees
console.log(prop('attackAction2'));
assert.equal(prop('attackAction2').attackRoll.value, 'min(3 + d4, d100)', 'Tag targeted effects change the attack roll correctly');
// Tags target effects on attributes
assert.equal(prop('taggedCon').value, 26, 'Tagged targeted effects affect attribute values');
assert.equal(prop('taggedCon').baseValue.value, 10, 'Tag targeted effects target the attribute itself, not the base value');
@@ -14,7 +17,6 @@ export default function () {
assert.equal(prop('attackAction').attackRoll.value, 20, 'Tag targeted effects change the attack roll correctly');
// Tag target effects can deal with rolls
assert.equal(prop('attackAction').attackRoll.value, 20, 'Tag targeted effects change the attack roll correctly');
assert.equal(prop('attackAction2').attackRoll.value, 'min(3 + d4, d100)', 'Tag targeted effects change the attack roll correctly');
}
var testProperties = [

View File

@@ -4,9 +4,9 @@ import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const scope = id => computation.scope[id];
const prop = id => computation.propsById[id];
assert.equal(scope('level').value, 5);

View File

@@ -4,9 +4,9 @@ import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(prop('attId').value, 6);
}

View File

@@ -3,9 +3,9 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const scope = id => computation.scope[id];
assert.isTrue(scope('blugeoning').vulnerability);
assert.isTrue(scope('customDamage').resistance);

View File

@@ -4,9 +4,9 @@ import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(prop('strengthId').value, 26);
}

View File

@@ -4,9 +4,9 @@ import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
const scope = id => computation.scope[id].value;

View File

@@ -3,9 +3,9 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation.js';
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn.js';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(prop('strengthId').value, 11, 'Point buys should apply a base value when active');
}

View File

@@ -4,10 +4,10 @@ import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
const hasLink = computation.dependencyGraph.hasLink;
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(
prop('strengthId').value, 8,

View File

@@ -3,9 +3,9 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation';
import clean from '../../utility/cleanProp.testFn';
export default function () {
export default async function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
const prop = id => computation.propsById[id];
assert.equal(prop('atheleticsId').proficiency, 2, 'Inherits proficiency from ability');

View File

@@ -4,7 +4,7 @@ import embedInlineCalculations from './utility/embedInlineCalculations';
import { removeEmptyCalculations } from './buildComputation/parseCalculationFields';
import path from 'ngraph.path';
export default function computeCreatureComputation(computation) {
export default async function computeCreatureComputation(computation) {
const stack = [];
// Computation scope of {variableName: prop}
const graph = computation.dependencyGraph;
@@ -31,7 +31,7 @@ export default function computeCreatureComputation(computation) {
top._visited = true;
stack.pop();
// Compute the top object of the stack
compute(computation, top);
await compute(computation, top);
} else {
top._visitedChildren = true;
// Push dependencies to graph to be computed first
@@ -40,14 +40,16 @@ export default function computeCreatureComputation(computation) {
}
// Finish the props after the dependency graph has been traversed
computation.props.forEach(finalizeProp);
for (const prop of computation.props) {
finalizeProp(prop);
}
}
function compute(computation, node) {
async function compute(computation, node) {
// Determine the prop's active status by its toggles
computeToggles(computation, node);
// Compute the property by type
computeByType[node.data?.type || '_variable']?.(computation, node);
await computeByType[node.data?.type || '_variable']?.(computation, node);
}
function pushDependenciesToStack(nodeId, graph, stack, computation) {

View File

@@ -1,16 +1,29 @@
import { get } from 'lodash';
export default function applyFnToKey(doc, key, fn){
if (key.includes('.$')){
export default function applyFnToKey(doc, key, fn) {
if (key.includes('.$')) {
applyToArrayKey(doc, key, fn);
} else {
applyToSingleKey(doc, key, fn);
}
}
function applyToSingleKey(doc, key, fn){
export async function applyFnToKeyAsync(doc, key, fn) {
if (key.includes('.$')) {
await applyToArrayKeyAsync(doc, key, fn);
} else {
await applyToSingleKeyAsync(doc, key, fn);
}
}
function applyToSingleKey(doc, key, fn) {
// call the function with the current value and document for context
fn(doc, key);
return fn(doc, key);
}
async function applyToSingleKeyAsync(doc, key, fn) {
// call the function with the current value and document for context
return await fn(doc, key);
}
/**
@@ -19,7 +32,7 @@ function applyToSingleKey(doc, key, fn){
* Warning: Order might be confusing, it will traverse the deepest array in order
* but the shallower arrays in reverse order
*/
function applyToArrayKey(doc, key, fn){
function applyToArrayKey(doc, key, fn) {
const keySplit = key.split('.$');
// Stack based depth first traversal of arrays
const array = get(doc, keySplit[0]);
@@ -30,11 +43,12 @@ function applyToArrayKey(doc, key, fn){
currentPath: keySplit[0],
indices: [],
}];
while(stack.length){
while (stack.length) {
const state = stack.pop();
for (let index in state.array){
if (!state) break;
for (let index in state.array) {
const currentPath = `${state.currentPath}[${index}]${state.paths[0]}`
if (state.paths.length == 1){
if (state.paths.length == 1) {
applyToSingleKey(doc, currentPath, fn);
} else {
const array = get(doc, currentPath);
@@ -49,3 +63,35 @@ function applyToArrayKey(doc, key, fn){
}
}
}
async function applyToArrayKeyAsync(doc, key, fn) {
const keySplit = key.split('.$');
// Stack based depth first traversal of arrays
const array = get(doc, keySplit[0]);
if (!array) return;
const stack = [{
array,
paths: keySplit.slice(1),
currentPath: keySplit[0],
indices: [],
}];
while (stack.length) {
const state = stack.pop();
if (!state) break;
for (let index in state.array) {
const currentPath = `${state.currentPath}[${index}]${state.paths[0]}`
if (state.paths.length == 1) {
await applyToSingleKey(doc, currentPath, fn);
} else {
const array = get(doc, currentPath);
if (!array) return;
stack.push({
array,
paths: state.paths.slice(1),
currentPath,
indices: [...state.indices, index],
});
}
}
}
}

View File

@@ -4,16 +4,16 @@ import writeAlteredProperties from './computation/writeComputation/writeAlteredP
import writeScope from './computation/writeComputation/writeScope';
import writeErrors from './computation/writeComputation/writeErrors';
export default function computeCreature(creatureId) {
export default async function computeCreature(creatureId) {
if (Meteor.isClient) return;
// console.log('compute ' + creatureId);
const computation = buildCreatureComputation(creatureId);
computeComputation(computation, creatureId);
await computeComputation(computation, creatureId);
}
function computeComputation(computation, creatureId) {
async function computeComputation(computation, creatureId) {
try {
computeCreatureComputation(computation);
await computeCreatureComputation(computation);
writeAlteredProperties(computation);
writeScope(creatureId, computation);
} catch (e) {