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

@@ -20,8 +20,6 @@ ejson@1.1.3
check@1.3.2
standard-minifier-js@2.8.1
shell-server@0.5.0
ecmascript@0.16.8
es5-shim@4.8.0
service-configuration@1.3.3
dynamic-import@0.7.3
ddp-rate-limiter@1.2.1
@@ -47,8 +45,9 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
#mdg:meteor-apm-agent
typescript@4.9.5
seba:minifiers-autoprefixer
mixmax:smart-disconnect
zodern:types
zodern:fix-async-stubs
typescript
ecmascript

View File

@@ -16,14 +16,14 @@ babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.3
blaze-tools@1.1.4
boilerplate-generator@1.7.2
bozhao:link-accounts@2.8.0
caching-compiler@1.2.2
caching-html-compiler@1.2.1
caching-html-compiler@1.2.2
callback-hook@1.5.1
check@1.3.2
coffeescript@2.4.1
coffeescript@2.7.0
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.4.0
ddp@1.4.1
@@ -44,8 +44,8 @@ fetch@0.1.4
geojson-utils@1.0.11
google-oauth@1.4.4
hot-code-push@1.0.4
html-tools@1.1.3
htmljs@1.1.1
html-tools@1.1.4
htmljs@1.2.0
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
@@ -57,7 +57,7 @@ logging@1.3.3
mdg:validated-method@1.3.0
meteor@1.11.5
meteor-base@1.5.1
meteortesting:browser-tests@1.4.2
meteortesting:browser-tests@1.5.3
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
@@ -78,7 +78,7 @@ npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
ordered-dict@1.1.0
ostrio:cookies@2.7.2
ostrio:cookies@2.8.0
ostrio:files@2.3.3
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
@@ -113,10 +113,10 @@ simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.2
spacebars-compiler@1.3.1
spacebars-compiler@1.3.2
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.2
templating-tools@1.2.3
tmeasday:check-npm-versions@1.0.2
tracker@1.3.3
typescript@4.9.5

View File

@@ -1,9 +1,10 @@
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import array from '/imports/parser/parseTree/constant';
import constant from '/imports/parser/parseTree/constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import array from '/imports/parser/parseTree/array';
import constant, { isFiniteNode } from '/imports/parser/parseTree/constant';
//set up the collection for creature variables
let CreatureVariables = new Mongo.Collection('creatureVariables');
const CreatureVariables = new Mongo.Collection('creatureVariables');
// Unique index on _creatureId
if (Meteor.isServer) {
@@ -28,7 +29,7 @@ if (Meteor.isServer) {
* Get the property from the given scope, respecting properties that are just a link to the actual
* property document
*/
export function getFromScope(name, scope) {
export function getFromScope(name: string, scope) {
let value = scope?.[name];
if (value?._propId) {
value = getSingleProperty(scope._creatureId, value._propId);
@@ -38,13 +39,13 @@ export function getFromScope(name, scope) {
export function getNumberFromScope(name, scope) {
const parseNode = getParseNodeFromScope(name, scope);
if (!parseNode || parseNode.valueType !== 'number') {
if (!parseNode || !isFiniteNode(parseNode)) {
return undefined;
}
return parseNode.value;
}
export function getParseNodeFromScope(name, scope) {
export function getParseNodeFromScope(name, scope): ParseNode | undefined {
let value = getFromScope(name, scope);
if (!value) return;
let valueType = getType(value);

View File

@@ -6,7 +6,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import { parse, prettifyParseError } from '/imports/parser/parser';
import resolve, { toString } from '/imports/parser/resolve';
import resolve from '/imports/parser/resolve';
import toString from '/imports/parser/toString';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
@@ -196,7 +197,7 @@ const logRoll = new ValidatedMethod({
optional: true,
},
}).validator(),
run({ roll, tabletopId, creatureId }) {
async run({ roll, tabletopId, creatureId }) {
if (!creatureId && !tabletopId) throw new Meteor.Error('no-id',
'A creature id or tabletop id must be given'
);
@@ -230,7 +231,7 @@ const logRoll = new ValidatedMethod({
let {
result: compiled,
context
} = resolve('compile', parsedResult, variables);
} = await resolve('compile', parsedResult, variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
@@ -238,12 +239,12 @@ const logRoll = new ValidatedMethod({
logContent.push({
value: compiledString
});
let { result: rolled } = resolve('roll', compiled, variables, context);
let { result: rolled } = await resolve('roll', compiled, variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolledString
});
let { result } = resolve('reduce', rolled, variables, context);
let { result } = await resolve('reduce', rolled, variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString

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) {

View File

@@ -4,7 +4,6 @@ import getUserLibraryIds from './getUserLibraryIds';
import { intersection, union } from 'lodash';
export default function getCreatureLibraryIds(creature, userId) {
if (!userId) console.log('no userId, returning empty array');
if (!userId) return [];
// Get the ids of libraries the user is permitted to view

View File

@@ -6,7 +6,9 @@ import {
prettifyParseError,
} from '/imports/parser/parser';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import resolve, { Context, traverse } from '/imports/parser/resolve';
import resolve from '/imports/parser/resolve';
import Context from '../../parser/types/Context';
import traverse from '/imports/parser/traverse';
/*
* Constants are primitive values that can be used elsewhere in computations

View File

@@ -17,7 +17,6 @@ export function propsFromForest(
const children = prop.children;
// Check the property has a type
if (!prop.type) {
console.log(prop);
throw 'Type is required on every property, not found on above doc';
}
// Create the clean doc

View File

@@ -0,0 +1,19 @@
/**
* Async compatible map that processes all items in parallel
*/
export async function parallelMap(array: any[], fn: (doc: any) => Promise<any>): Promise<any[]> {
return await Promise.all(array.map(fn));
}
/**
* Async compatible map that processes all items in series
*/
export async function serialMap(array: any[], fn: (doc: any) => Promise<any>): Promise<any[]> {
const results: any[] = [];
for (const doc of array) {
const result = await fn(doc);
results.push(result);
}
return results;
}

View File

@@ -476,3 +476,4 @@ export default {
opacity: 0.7;
}
</style>
resolveimport { toString } from '/imports/parser/toString';

View File

@@ -558,3 +558,4 @@ export default {
opacity: 0.7;
}
</style>
resolveimport { toString } from '/imports/parser/toString';

View File

@@ -183,3 +183,4 @@ export default {
margin-bottom: 0;
}
</style>
resolveimport { toString } from '/imports/parser/toString';

View File

@@ -114,13 +114,13 @@ export default {
min(row) {
return row.min ? row.min && row.min.value : this.model.min && this.model.min.value;
},
dragSlider(row, value) {
async dragSlider(row, value) {
const currentSpent = this.model.spent;
let newSpent = currentSpent - row.spent;
const costFunction = EJSON.clone(row.cost || this.model.cost);
if (!costFunction?.parseNode) return;
if (costFunction) costFunction.parseLevel = 'reduce';
resolveCalculationNode(costFunction, costFunction.parseNode, { value });
await resolveCalculationNode(costFunction, costFunction.parseNode, { value });
if (Number.isFinite(costFunction.value)) {
newSpent += costFunction.value;
if (this.useEstimate) this.estimatedCost = newSpent;

View File

@@ -2,7 +2,7 @@
// http://github.com/Hardmath123/nearley
function id(x) { return x[0]; }
import node from './parseTree/_index';
import node from '/imports/parser/parseTree/';
import moo from 'moo';

View File

@@ -1,6 +1,6 @@
@preprocessor esmodule
@{%
import node from './parseTree/_index';
import node from '/imports/parser/parseTree/_index';
import moo from 'moo';

15
app/imports/parser/map.ts Normal file
View File

@@ -0,0 +1,15 @@
import nodeTypeIndex from './parseTree';
import ParseNode from '/imports/parser/parseTree/ParseNode';
export default async function map(node: ParseNode, fn: (ParseNode) => Promise<any>): Promise<any> {
if (!node) return;
const type = nodeTypeIndex[node.parseType];
if (!type) {
console.error(node);
throw new Meteor.Error('Not valid parse node');
}
if ('map' in type) {
return type.map(node as any, fn, map);
}
return fn(node);
}

View File

@@ -1,28 +0,0 @@
import { ParseNode } from '/imports/parser/parser';
import { ResolvedResult, Context } from '/imports/parser/resolve';
export type ResolveLevel = 'compile' | 'roll' | 'reduce';
export default interface NodeFactory {
create(node: Partial<ParseNode>): ParseNode;
compile?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
reduce?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?(
fn: ResolveLevel, node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ParseNode): string;
traverse?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}

View File

@@ -1,34 +0,0 @@
import accessor from './accessor';
import array from './array';
import call from './call';
import constant from './constant';
import error from './error';
import ifNode from './if';
import index from './indexNode';
import not from './not';
import operator from './operator';
import parenthesis from './parenthesis';
import roll from './roll';
import rollArray from './rollArray';
import unaryOperator from './unaryOperator';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
const factories: Record<string, NodeFactory> = {
accessor,
array,
call,
constant,
error,
if: ifNode,
index,
not,
operator,
parenthesis,
roll,
rollArray,
// What used to be symbols are now just treated as accessors without a path
symbol: accessor,
unaryOperator,
};
export default factories;

View File

@@ -1,28 +1,20 @@
import constant from './constant';
import array from './array';
import resolve, { Context, ResolvedResult } from '/imports/parser/resolve';
import constant from '/imports/parser/parseTree/constant';
import array from '/imports/parser/parseTree/array';
import ResolvedResult from '/imports/parser/types/ResolvedResult';
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
export type AccessorNode = {
parseType: 'accessor';
parseType: 'accessor' | 'symbol';
path?: string[];
name: string;
}
interface AccessorFactory extends NodeFactory {
type AccessorFactory = {
create(node: Partial<AccessorNode>): AccessorNode;
compile(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
resolve?: undefined;
reduce(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
compile: ResolveLevelFunction<AccessorNode>;
reduce: ResolveLevelFunction<AccessorNode>;
toString(node: AccessorNode): string;
traverse?: undefined;
map?: undefined;
}
const accessor: AccessorFactory = {
@@ -33,9 +25,7 @@ const accessor: AccessorFactory = {
name,
};
},
compile(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult {
async compile(node, scope, context) {
let value = getFromScope(node.name, scope);
// Get the value from the given path
node.path?.forEach(name => {
@@ -102,9 +92,9 @@ const accessor: AccessorFactory = {
context,
};
},
reduce(node, scope, context): ResolvedResult {
let { result } = accessor.compile(node, scope, context);
({ result } = resolve('reduce', result, scope, context));
async reduce(node, scope, context, inputProvider, resolveOthers): Promise<ResolvedResult> {
let { result } = await accessor.compile(node, scope, context, inputProvider, resolveOthers);
({ result } = await resolveOthers('reduce', result, scope, context, inputProvider));
if (result.parseType === 'accessor') {
return {
result: constant.create({

View File

@@ -1,28 +1,26 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import { serialMap } from '/imports/api/utility/asyncMap';
import constant from '/imports/parser/parseTree/constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type ArrayNode = {
parseType: 'array';
values: ParseNode[];
}
interface ArrayFactory extends NodeFactory {
type ArrayFactory = {
create(node: Partial<ArrayNode>): ArrayNode;
fromConstantArray(array: (string | number | boolean | undefined)[]): ArrayNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: ArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ArrayNode): string;
traverse(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<ArrayNode>;
toString: ToStringFunction<ArrayNode>;
traverse: TraverseFunction<ArrayNode>;
map: MapFunction<ArrayNode>;
}
const array: ArrayFactory = {
const arrayFactory: ArrayFactory = {
create({ values }: { values: ParseNode[] }) {
return {
parseType: 'array',
@@ -44,32 +42,33 @@ const array: ArrayFactory = {
return constant.create({ value: undefined });
}
});
return array.create({ values });
return arrayFactory.create({ values });
},
resolve(fn, node, scope, context): ResolvedResult {
const values = node.values.map(node => {
const { result } = resolve(fn, node, scope, context);
return result;
});
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const values: ParseNode[] = [];
for (const val of node.values) {
const { result } = await resolveOthers(fn, val, scope, context, inputProvider);
values.push(result);
}
return {
result: array.create({ values }),
result: arrayFactory.create({ values }),
context,
};
},
toString(node) {
return `[${node.values.map(value => toString(value)).join(', ')}]`;
toString(node, toStringOthers) {
return `[${node.values.map(value => toStringOthers(value)).join(', ')}]`;
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
node.values.forEach(value => traverse(value, fn));
node.values.forEach(value => traverseOthers(value, fn));
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.values = node.values.map(value => map(value, fn));
node.values = await serialMap(node.values, async value => await mapOthers(value, fn));
}
return resultingNode;
},
}
export default array;
export default arrayFactory;

View File

@@ -1,9 +1,16 @@
import error from './error';
import constant from './constant';
console.log('call.ts imports')
import error from '/imports/parser/parseTree/error';
import constant from '/imports/parser/parseTree/constant';
import functions, { ParserFunction } from '/imports/parser/functions';
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import Context from '../types/Context';
import ResolvedResult from '../types/ResolvedResult';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import { serialMap } from '/imports/api/utility/asyncMap';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import ResolveLevel from '/imports/parser/types/ResolveLevel';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type CallNode = {
parseType: 'call';
@@ -11,14 +18,12 @@ export type CallNode = {
args: ParseNode[];
}
interface CallFactory extends NodeFactory {
type CallFactory = {
create(node: Partial<CallNode>): CallNode;
resolve(
fn: ResolveLevel, node: CallNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: CallNode): string;
traverse(node: CallNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: CallNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<CallNode>;
toString: ToStringFunction<CallNode>;
traverse: TraverseFunction<CallNode>;
map: MapFunction<CallNode>;
checkArguments(node: CallNode, fn: ResolveLevel, func: ParserFunction,
resolvedArgs: ParseNode[], context: Context): boolean;
}
@@ -31,7 +36,7 @@ const call: CallFactory = {
args,
}
},
resolve(fn, node, scope, context): ResolvedResult {
async resolve(fn, node, scope, context, inputProvider, resolveOthers): Promise<ResolvedResult> {
const func = functions[node.functionName];
// Check that the function exists
if (!func) {
@@ -46,7 +51,7 @@ const call: CallFactory = {
}
// Resolve a given node to a maximum depth of resolution
const resolveToLevel = (node, maxResolveFn = 'reduce'): ResolvedResult => {
const resolveToLevel = (node, maxResolveFn = 'reduce'): Promise<ResolvedResult> => {
// Determine the actual depth to resolve to
let resolveFn: ResolveLevel = 'reduce';
if (fn === 'compile' || maxResolveFn === 'compile') {
@@ -55,14 +60,15 @@ const call: CallFactory = {
resolveFn = 'roll';
}
// Resolve
return resolve(resolveFn, node, scope, context);
return resolveOthers(resolveFn, node, scope, context, inputProvider);
}
// Resolve the arguments
const resolvedArgs = node.args.map((arg, i) => {
const { result } = resolveToLevel(arg, func.maxResolveLevels?.[i]);
return result;
});
const resolvedArgs: ParseNode[] = [];
for (const [i, arg] of node.args.entries()) {
const { result } = await resolveToLevel(arg, func.maxResolveLevels?.[i]);
resolvedArgs.push(result);
}
// Check that the arguments match what is expected
const checkFailed = call.checkArguments(node, fn, func, resolvedArgs, context);
@@ -116,7 +122,7 @@ const call: CallFactory = {
};
} else {
// Resolve the return value
return resolve(fn, value, scope, context);
return resolveOthers(fn, value, scope, context, inputProvider);
}
} catch (err) {
context.error(`Internal error: ${err.message || err}`);
@@ -129,22 +135,22 @@ const call: CallFactory = {
}
}
},
toString(node) {
return `${node.functionName}(${node.args.map(arg => toString(arg)).join(', ')})`;
toString(node, toStringOthers) {
return `${node.functionName}(${node.args.map(arg => toStringOthers(arg)).join(', ')})`;
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
node.args.forEach(arg => traverse(arg, fn));
node.args.forEach(arg => traverseOthers(arg, fn));
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.args = node.args.map(arg => map(arg, fn));
node.args = await serialMap(node.args, async arg => mapOthers(arg, fn));
}
return resultingNode;
},
checkArguments(node, fn, func, resolvedArgs, context) {
const argumentsExpected = func.arguments;
checkArguments(callNode, fn, func, resolvedArgs, context) {
const argumentsExpected = func.arguments as any;
// Check that the number of arguments matches the number expected
if (
!argumentsExpected.anyLength &&
@@ -152,7 +158,7 @@ const call: CallFactory = {
resolvedArgs.length < (func.minArguments ?? argumentsExpected.length)
) {
context.error('Incorrect number of arguments ' +
`to ${node.functionName} function, ` +
`to ${callNode.functionName} function, ` +
`expected ${argumentsExpected.length} got ${resolvedArgs.length}`);
return true;
}
@@ -160,18 +166,22 @@ const call: CallFactory = {
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type: string;
let type;
if (argumentsExpected.anyLength) {
type = argumentsExpected[0];
} else {
type = argumentsExpected[index];
}
if (type === 'parseNode') return;
if (node.parseType !== type && node.valueType !== type) failed = true;
if (
node.parseType !== type
&& node.parseType === 'constant'
&& node.valueType !== type
) failed = true;
if (failed && fn === 'reduce') {
const typeName = typeof type === 'string' ? type : type.constructor.name;
const nodeName = node.parseType;
context.error(`Incorrect arguments to ${node.functionName} function` +
context.error(`Incorrect arguments to ${callNode.functionName} function` +
`expected ${typeName} got ${nodeName}`);
}
});

View File

@@ -1,5 +1,5 @@
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import { Context, ResolvedResult } from '/imports/parser/resolve';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
export type ConstantValueType = number | string | boolean | undefined
@@ -10,17 +10,17 @@ export type ConstantNode = {
valueType: 'number' | 'string' | 'boolean' | 'undefined';
}
interface ConstantFactory extends NodeFactory {
export type FiniteNumberConstantNode = {
parseType: 'constant';
value: number;
// TODO replace all `constantNode.valueType` with `typeof constantNode.value`
valueType: 'number';
}
type ConstantFactory = {
create({ value }: { value: ConstantValueType }): ConstantNode;
compile(
node: ConstantNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce?: undefined;
resolve?: undefined;
compile: ResolveLevelFunction<ConstantNode>;
toString(node: ConstantNode): string;
traverse?: undefined;
map?: undefined;
}
const constant: ConstantFactory = {
@@ -31,7 +31,7 @@ const constant: ConstantFactory = {
value,
}
},
compile(node, scope, context) {
async compile(node, scope, context) {
return { result: node, context };
},
toString(node) {
@@ -39,4 +39,11 @@ const constant: ConstantFactory = {
},
}
export function isFiniteNode(node: ParseNode): node is FiniteNumberConstantNode {
return node.parseType === 'constant'
&& node.valueType === 'number'
&& typeof node.value === 'number'
&& isFinite(node.value);
}
export default constant;

View File

@@ -1,6 +1,6 @@
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { Context, ResolvedResult } from '/imports/parser/resolve';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type ErrorNode = {
parseType: 'error';
@@ -8,17 +8,10 @@ export type ErrorNode = {
error: string;
}
interface ErrorFactory extends NodeFactory {
interface ErrorFactory {
create(node: Partial<ErrorNode>): ErrorNode;
compile(
node: ErrorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce?: undefined;
resolve?: undefined;
toString(node: ErrorNode): string;
traverse?: undefined;
map?: undefined;
compile: ResolveLevelFunction<ErrorNode>;
toString: ToStringFunction<ErrorNode>;
}
const error: ErrorFactory = {
@@ -29,7 +22,7 @@ const error: ErrorFactory = {
error,
}
},
compile(node, scope, context) {
async compile(node, scope, context) {
return { result: node, context };
},
toString(node) {

View File

@@ -1,7 +1,9 @@
import resolve, { traverse, toString, map } from '../resolve';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { Context, ResolvedResult } from '/imports/parser/resolve';
import ResolvedResult from '../types/ResolvedResult';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type IfNode = {
parseType: 'if';
@@ -10,17 +12,12 @@ export type IfNode = {
alternative: ParseNode;
}
interface IfFactory extends NodeFactory {
type IfFactory = {
create(node: Partial<IfNode>): IfNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: IfNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: IfNode): string;
traverse(node: IfNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: IfNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<IfNode>;
toString: ToStringFunction<IfNode>;
traverse: TraverseFunction<IfNode>;
map: MapFunction<IfNode>;
}
const ifNode: IfFactory = {
@@ -35,18 +32,18 @@ const ifNode: IfFactory = {
alternative,
};
},
toString(node) {
toString(node, stringOthers) {
const { condition, consequent, alternative } = node;
condition.parseType
return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}`
return `${stringOthers(condition)} ? ${stringOthers(consequent)} : ${stringOthers(alternative)}`
},
resolve(fn, node, scope, context): ResolvedResult {
const { result: condition } = resolve(fn, node.condition, scope, context);
async resolve(fn, node, scope, context, inputProvider, resolveOthers): Promise<ResolvedResult> {
const { result: condition } = await resolveOthers(fn, node.condition, scope, context, inputProvider);
if (condition.parseType === 'constant') {
if (condition.value) {
return resolve(fn, node.consequent, scope, context);
return resolveOthers(fn, node.consequent, scope, context, inputProvider);
} else {
return resolve(fn, node.alternative, scope, context);
return resolveOthers(fn, node.alternative, scope, context, inputProvider);
}
} else {
return {
@@ -59,18 +56,18 @@ const ifNode: IfFactory = {
};
}
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.condition, fn);
traverse(node.consequent, fn);
traverse(node.alternative, fn);
traverseOthers(node.condition, fn);
traverseOthers(node.consequent, fn);
traverseOthers(node.alternative, fn);
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.condition = map(node.condition, fn);
node.consequent = map(node.consequent, fn);
node.alternative = map(node.alternative, fn);
node.condition = await mapOthers(node.condition, fn);
node.consequent = await mapOthers(node.consequent, fn);
node.alternative = await mapOthers(node.alternative, fn);
}
return resultingNode;
},

View File

@@ -0,0 +1,45 @@
console.log('index.ts imports')
import accessor from '/imports/parser/parseTree/accessor';
import array from '/imports/parser/parseTree/array';
import call from '/imports/parser/parseTree/call';
import constant from '/imports/parser/parseTree/constant';
import error from '/imports/parser/parseTree/error';
import ifNode from '/imports/parser/parseTree/if';
import index from '/imports/parser/parseTree/indexNode';
import not from '/imports/parser/parseTree/not';
import operator from '/imports/parser/parseTree/operator';
import parenthesis from '/imports/parser/parseTree/parenthesis';
import roll from '/imports/parser/parseTree/roll';
import rollArray from '/imports/parser/parseTree/rollArray';
import unaryOperator from '/imports/parser/parseTree/unaryOperator';
console.log('index.ts')
const factories = {
accessor,
array,
call,
constant,
error,
if: ifNode,
index,
not,
operator,
parenthesis,
roll,
rollArray,
// What used to be symbols are now just treated as accessors without a path
symbol: accessor,
unaryOperator,
};
console.log('---------------------');
console.log('---------------------');
console.log('---------------------');
console.log(factories.array);
console.log('---------------------');
console.log('---------------------');
console.log('---------------------');
export default factories;

View File

@@ -1,7 +1,11 @@
import resolve, { traverse, toString, map, ResolvedResult, Context } from '/imports/parser/resolve';
import error from './error';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import error from '/imports/parser/parseTree/error';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import toString from '/imports/parser/toString';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type IndexNode = {
parseType: 'index';
@@ -9,17 +13,12 @@ export type IndexNode = {
index: ParseNode;
}
interface IndexFactory extends NodeFactory {
type IndexFactory = {
create(node: Partial<IndexNode>): IndexNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: IndexNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: IndexNode): string;
traverse(node: IndexNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: IndexNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<IndexNode>;
toString: ToStringFunction<IndexNode>;
traverse: TraverseFunction<IndexNode>;
map: MapFunction<IndexNode>;
}
const indexNode: IndexFactory = {
@@ -30,25 +29,22 @@ const indexNode: IndexFactory = {
index,
}
},
resolve(fn, node, scope, context) {
const { result: index } = resolve(fn, node.index, scope, context);
const { result: array } = resolve(fn, node.array, scope, context);
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const { result: index } = await resolveOthers(fn, node.index, scope, context, inputProvider);
const { result: array } = await resolveOthers(fn, node.array, scope, context, inputProvider);
if (
index.valueType === 'number' &&
isFiniteNode(index) &&
Number.isInteger(index.value) &&
array.parseType === 'array'
) {
if (index.value < 1 || index.value > array.values.length) {
context.error({
type: 'warning',
message: `Index of ${index.value} is out of range for an array` +
` of length ${array.values.length}`,
});
context.error(`Index of ${index.value} is out of range for an array` +
` of length ${array.values.length}`);
}
const selection = array.values[index.value - 1];
if (selection) {
return resolve(fn, selection, scope, context);
return resolveOthers(fn, selection, scope, context, inputProvider);
}
} else if (fn === 'reduce') {
if (array.parseType !== 'array') {
@@ -61,7 +57,7 @@ const indexNode: IndexFactory = {
}),
context,
};
} else if (!index.isInteger) {
} else if (!isFiniteNode(index) || !Number.isInteger(index.value)) {
const message = `${toString(array)} is not an integer index of the array`
context.error(message);
return {
@@ -81,19 +77,19 @@ const indexNode: IndexFactory = {
context,
};
},
toString(node) {
return `${toString(node.array)}[${toString(node.index)}]`;
toString(node, stringOthers) {
return `${stringOthers(node.array)}[${stringOthers(node.index)}]`;
},
traverse(node, fn: (node: ParseNode) => any) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.array, fn);
traverse(node.index, fn);
traverseOthers(node.array, fn);
traverseOthers(node.index, fn);
},
map(node, fn: (node: ParseNode) => any) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.array = map(node.array, fn);
node.index = map(node.index, fn);
node.array = await mapOthers(node.array, fn);
node.index = await mapOthers(node.index, fn);
}
return resultingNode;
},

View File

@@ -1,24 +1,21 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import constant from '/imports/parser/parseTree/constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type NotNode = {
parseType: 'not';
right: ParseNode;
}
interface NotFactory extends NodeFactory {
type NotFactory = {
create(node: Partial<NotNode>): NotNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: NotNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: NotNode): string;
traverse(node: NotNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: NotNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<NotNode>;
toString: ToStringFunction<NotNode>;
traverse: TraverseFunction<NotNode>;
map: MapFunction<NotNode>;
}
const not: NotFactory = {
@@ -28,8 +25,8 @@ const not: NotFactory = {
right,
}
},
resolve(fn, node, scope, context) {
const { result: right } = resolve(fn, node.right, scope, context);
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const { result: right } = await resolveOthers(fn, node.right, scope, context, inputProvider);
if (right.parseType !== 'constant') {
return {
result: not.create({
@@ -45,17 +42,17 @@ const not: NotFactory = {
context,
};
},
toString(node) {
return `!${toString(node.right)}`;
toString(node, stringOthers) {
return `!${stringOthers(node.right)}`;
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.right, fn);
traverseOthers(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
node.right = await mapOthers(node.right, fn);
}
return resultingNode;
},

View File

@@ -1,7 +1,9 @@
import resolve, { toString, traverse, map, ResolvedResult, Context } from '/imports/parser/resolve';
import constant from './constant';
import constant, { ConstantValueType, isFiniteNode } from '/imports/parser/parseTree/constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
type OperatorSymbol = '*' | '/' | '^' | '+' | '-' | '%' | '&' | '&&' | '|' | '||' | '=' |
'==' | '===' | '!=' | '!==' | '>' | '<' | '>=' | '<=';
@@ -13,17 +15,12 @@ export type OperatorNode = {
operator: OperatorSymbol;
}
interface OperatorFactory extends NodeFactory {
type OperatorFactory = {
create(node: Partial<OperatorNode>): OperatorNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: OperatorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: OperatorNode): string;
traverse(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<OperatorNode>;
toString: ToStringFunction<OperatorNode>;
traverse: TraverseFunction<OperatorNode>;
map: MapFunction<OperatorNode>;
}
// Which operators can be considered commutative by the parser
@@ -43,10 +40,10 @@ const operator: OperatorFactory = {
operator,
};
},
resolve(fn, node, scope, context) {
const { result: leftNode } = resolve(fn, node.left, scope, context);
const { result: rightNode } = resolve(fn, node.right, scope, context);
let left, right;
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const { result: leftNode } = await resolveOthers(fn, node.left, scope, context, inputProvider);
const { result: rightNode } = await resolveOthers(fn, node.right, scope, context, inputProvider);
let left: ConstantValueType, right: ConstantValueType;
// If commutation is possible, do it and return that result
const commutatedResult = reorderCommutativeOperations(node, leftNode, rightNode);
@@ -73,32 +70,39 @@ const operator: OperatorFactory = {
context,
};
},
toString(node) {
toString(node, stringOthers) {
const { left, right, operator } = node;
// special case of adding a negative number
if (operator === '+' && right.valueType === 'number' && right.value < 0) {
return `${toString(left)} - ${-right.value}`
if (operator === '+' && isFiniteNode(right) && right.value < 0) {
return `${stringOthers(left)} - ${-right.value}`
}
return `${toString(left)} ${operator} ${toString(right)}`;
return `${stringOthers(left)} ${operator} ${stringOthers(right)}`;
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.left, fn);
traverse(node.right, fn);
traverseOthers(node.left, fn);
traverseOthers(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.left = map(node.left, fn);
node.right = map(node.right, fn);
node.left = await mapOthers(node.left, fn);
node.right = await mapOthers(node.right, fn);
}
return resultingNode;
},
}
function applyOperator(operator: OperatorSymbol, left: ParseNode, right: ParseNode) {
function applyOperator(operator: OperatorSymbol, left: ConstantValueType, right: ConstantValueType) {
let result;
if (left === undefined) {
left = 0;
}
if (right === undefined) {
right = 0;
}
switch (operator) {
// Typescript might complain about these, but they return NaN as expected
case '+': result = left + right; break;
case '-': result = left - right; break;
case '*': result = left * right; break;

View File

@@ -1,23 +1,20 @@
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
export type ParenthesisNode = {
parseType: 'parenthesis';
content: ParseNode;
}
interface ParenthesisFactory extends NodeFactory {
type ParenthesisFactory = {
create(node: Partial<ParenthesisNode>): ParenthesisNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: ParenthesisNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ParenthesisNode): string;
traverse(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<ParenthesisNode>;
toString: ToStringFunction<ParenthesisNode>;
traverse: TraverseFunction<ParenthesisNode>;
map: MapFunction<ParenthesisNode>;
}
const parenthesis: ParenthesisFactory = {
@@ -27,8 +24,8 @@ const parenthesis: ParenthesisFactory = {
content,
};
},
resolve(fn, node, scope, context) {
const { result: content } = resolve(fn, node.content, scope, context);
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const { result: content } = await resolveOthers(fn, node.content, scope, context, inputProvider);
if (
fn === 'reduce' ||
content.parseType === 'constant' ||
@@ -42,17 +39,17 @@ const parenthesis: ParenthesisFactory = {
};
}
},
toString(node) {
return `(${toString(node.content)})`;
toString(node, stringOthers) {
return `(${stringOthers(node.content)})`;
},
traverse(node, fn: (node: ParseNode) => any) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.content, fn);
traverseOthers(node.content, fn);
},
map(node, fn: (node: ParseNode) => any) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.content = map(node.content, fn);
node.content = await mapOthers(node.content, fn);
}
return resultingNode;
},

View File

@@ -1,10 +1,12 @@
import resolve, { toString, traverse, map, ResolvedResult, Context } from '../resolve';
import error from './error';
import rollArray from './rollArray';
import rollDice from '/imports/parser/rollDice';
import error from '/imports/parser/parseTree/error';
import rollArray from '/imports/parser/parseTree/rollArray';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
import Context from '/imports/parser/types/Context';
export type RollNode = {
parseType: 'roll';
@@ -12,21 +14,14 @@ export type RollNode = {
right: ParseNode;
}
interface RollNodeFactory extends NodeFactory {
type RollNodeFactory = {
create(node: Partial<RollNode>): RollNode;
compile(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
reduce(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?: undefined;
toString(node: RollNode): string;
traverse(node: RollNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: RollNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
compile: ResolveLevelFunction<RollNode>;
roll: ResolveLevelFunction<RollNode>;
reduce: ResolveLevelFunction<RollNode>;
toString: ToStringFunction<RollNode>;
traverse: TraverseFunction<RollNode>;
map: MapFunction<RollNode>;
}
const rollNode: RollNodeFactory = {
@@ -37,28 +32,28 @@ const rollNode: RollNodeFactory = {
right,
};
},
compile(node, scope, context) {
const { result: left } = resolve('compile', node.left, scope, context);
const { result: right } = resolve('compile', node.right, scope, context);
async compile(node, scope, context, inputProvider, resolveOthers) {
const { result: left } = await resolveOthers('compile', node.left, scope, context, inputProvider);
const { result: right } = await resolveOthers('compile', node.right, scope, context, inputProvider);
return {
result: rollNode.create({ left, right }),
context,
};
},
toString(node) {
toString(node, stringOthers) {
if (
node.left.parseType === 'constant'
&& typeof node.left.value === 'number'
&& node.left.value === 1
) {
return `d${toString(node.right)}`;
return `d${stringOthers(node.right)}`;
} else {
return `${toString(node.left)}d${toString(node.right)}`;
return `${stringOthers(node.left)}d${stringOthers(node.right)}`;
}
},
roll(node, scope, context) {
const { result: left } = resolve('reduce', node.left, scope, context);
const { result: right } = resolve('reduce', node.right, scope, context);
async roll(node, scope, context, inputProvider, resolveOthers) {
const { result: left } = await resolveOthers('reduce', node.left, scope, context, inputProvider);
const { result: right } = await resolveOthers('reduce', node.right, scope, context, inputProvider);
if (
left.parseType !== 'constant'
|| typeof left.value !== 'number'
@@ -82,7 +77,7 @@ const rollNode: RollNodeFactory = {
return errorResult(message, node, context);
}
const diceSize = right.value;
const values = rollDice(number, diceSize);
const [values] = await inputProvider.rollDice([{ number, diceSize }]);
if (context) {
context.rolls.push({ number, diceSize, values });
}
@@ -95,20 +90,20 @@ const rollNode: RollNodeFactory = {
context
};
},
reduce(node, scope, context) {
const { result } = rollNode.roll(node, scope, context);
return resolve('reduce', result, scope, context);
async reduce(node, scope, context, inputProvider, resolveOthers) {
const { result } = await rollNode.roll(node, scope, context, inputProvider, resolveOthers);
return resolveOthers('reduce', result, scope, context, inputProvider);
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.left, fn);
traverse(node.right, fn);
traverseOthers(node.left, fn);
traverseOthers(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.left = map(node.left, fn);
node.right = map(node.right, fn);
node.left = await mapOthers(node.left, fn);
node.right = await mapOthers(node.right, fn);
}
return resultingNode;
},

View File

@@ -1,6 +1,6 @@
import constant from './constant';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import { Context, ResolvedResult } from '/imports/parser/resolve';
import constant from '/imports/parser/parseTree/constant';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
type RollValue = {
value: number,
@@ -17,19 +17,11 @@ export type RollArrayNode = {
diceNum: number,
}
interface RollArrayFactory extends NodeFactory {
type RollArrayFactory = {
create(input: { values: number[], diceSize: number, diceNum: number }): RollArrayNode;
compile(
node: RollArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce(
node: RollArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?: undefined;
toString(node: RollArrayNode): string;
traverse?: undefined;
map?: undefined;
compile: ResolveLevelFunction<RollArrayNode>;
reduce: ResolveLevelFunction<RollArrayNode>;
toString: ToStringFunction<RollArrayNode>;
}
const rollArray: RollArrayFactory = {
@@ -43,7 +35,7 @@ const rollArray: RollArrayFactory = {
diceNum,
};
},
compile(node, scope, context) {
async compile(node, scope, context) {
return {
result: node,
context
@@ -52,7 +44,7 @@ const rollArray: RollArrayFactory = {
toString(node) {
return `${node.diceNum || ''}d${node.diceSize} [${valuesToString(node.values)}]`;
},
reduce(node, scope, context) {
async reduce(node, scope, context) {
const total = node.values.reduce((a, b) => {
if (b.disabled) return a;
return a + b.value;

View File

@@ -1,7 +1,9 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import constant from '/imports/parser/parseTree/constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import ResolveFunction from '/imports/parser/types/ResolveFunction';
import TraverseFunction from '/imports/parser/types/TraverseFunction';
import MapFunction from '/imports/parser/types/MapFunction';
import ToStringFunction from '/imports/parser/types/ToStringFunction';
type UnaryOperatorSymbol = '+' | '-';
@@ -11,17 +13,12 @@ export type UnaryOperatorNode = {
right: ParseNode;
}
interface UnaryOperatorFactory extends NodeFactory {
type UnaryOperatorFactory = {
create(node: Partial<UnaryOperatorNode>): UnaryOperatorNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: UnaryOperatorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: UnaryOperatorNode): string;
traverse(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
resolve: ResolveFunction<UnaryOperatorNode>;
toString: ToStringFunction<UnaryOperatorNode>;
traverse: TraverseFunction<UnaryOperatorNode>;
map: MapFunction<UnaryOperatorNode>;
}
const unaryOperator: UnaryOperatorFactory = {
@@ -32,8 +29,8 @@ const unaryOperator: UnaryOperatorFactory = {
right,
};
},
resolve(fn, node, scope, context) {
const { result: rightNode } = resolve(fn, node.right, scope, context);
async resolve(fn, node, scope, context, inputProvider, resolveOthers) {
const { result: rightNode } = await resolveOthers(fn, node.right, scope, context, inputProvider);
if (
rightNode.parseType !== 'constant'
|| typeof rightNode.value !== 'number'
@@ -59,17 +56,17 @@ const unaryOperator: UnaryOperatorFactory = {
context,
};
},
toString(node) {
return `${node.operator}${toString(node.right)}`;
toString(node, stringOthers) {
return `${node.operator}${stringOthers(node.right)}`;
},
traverse(node, fn) {
traverse(node, fn, traverseOthers) {
fn(node);
traverse(node.right, fn);
traverseOthers(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
async map(node, fn, mapOthers) {
const resultingNode = await fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
node.right = await mapOthers(node.right, fn);
}
return resultingNode;
},

View File

@@ -1,5 +1,6 @@
import { parse } from './parser';
import resolve, { toString } from './resolve';
import { parse } from '/imports/parser/parser';
import resolve from '/imports/parser/resolve';
import toString from './toString';
import { assert } from 'chai';
describe('Parser', function () {
@@ -7,19 +8,19 @@ describe('Parser', function () {
assert.typeOf(parse('1'), 'object');
});
it('parses various operations', function () {
assert.typeOf(parse('1 + 2 * 3 / 4 * 1d8'), 'object');
assert.typeOf(parse('[1,2,3][2] + 1 + 2 * 3 / 4 * 1d8'), 'object');
});
it('simplifies basic addition and multiplication', function () {
it('simplifies basic addition and multiplication', async function () {
let add = parse('1 + 3 + 3 + 4');
({ result: add } = resolve('compile', add));
({ result: add } = await resolve('compile', add));
assert.equal(toString(add), '11');
let mul = parse('2 * 3 * 4');
({ result: mul } = resolve('compile', mul));
({ result: mul } = await resolve('compile', mul));
assert.equal(toString(mul), '24');
});
it('simplifies addition when possible, even if a roll is in the way', function () {
let { result } = resolve('compile', parse('1 + 3 + d12 + 3 + 4'));
it('simplifies addition when possible, even if a roll is in the way', async function () {
let { result } = await resolve('compile', parse('1 + 3 + d12 + 3 + 4'));
assert.equal(toString(result), 'd12 + 11');
});
});

View File

@@ -1,105 +1,57 @@
import nodeTypeIndex from './parseTree/_index';
import '/imports/parser/parseTree/array';
import factories from '/imports/parser/parseTree';
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { ConstantValueType } from '/imports/parser/parseTree/constant';
import rollDice from '/imports/parser/rollDice';
import ResolveLevel from './types/ResolveLevel';
import ResolvedResult from './types/ResolvedResult';
import Context from './types/Context';
import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction';
// Takes a parse node and computes it to a set detail level
// returns {result, context}
export default function resolve(
fn: 'roll' | 'reduce' | 'compile',
export default async function resolve(
fn: ResolveLevel,
node: ParseNode,
scope: Record<string, any>,
context = new Context()
): ResolvedResult {
if (!node) throw 'Node must be supplied';
const factory = nodeTypeIndex[node.parseType];
const handlerFunction = factory[fn];
scope: Record<string, any> = {},
context = new Context(),
inputProvider = computationInputProvider,
): Promise<ResolvedResult> {
if (!node) throw new Error('Node must be supplied');
const factory = factories[node.parseType];
const handlerFunction: ResolveLevelFunction<ParseNode> = factory[fn];
if (!factory) {
throw new Meteor.Error(`Parse node type: ${node.parseType} not implemented`);
}
if (factory.resolve) {
return factory.resolve(fn, node, scope, context);
} else if (handlerFunction) {
return handlerFunction(node, scope, context);
} else if (fn === 'reduce' && factory.roll) {
return factory.roll(node, scope, context)
if ('resolve' in factory) {
return factory.resolve(fn, node as any, scope, context, inputProvider, resolve);
} else if (fn in factory) {
return handlerFunction(node, scope, context, inputProvider, resolve);
} else if (fn === 'reduce' && 'roll' in factory) {
return factory.roll(node as any, scope, context, inputProvider, resolve)
} else if (factory.compile) {
return factory.compile(node, scope, context)
return factory.compile(node as any, scope, context, inputProvider, resolve)
} else {
throw new Meteor.Error('Compile not implemented on ' + node.parseType);
}
}
export function toString(node: ParseNode) {
if (!node) return '';
const type = nodeTypeIndex[node.parseType];
if (!type?.toString) {
throw new Meteor.Error('toString not implemented on ' + node.parseType);
}
return type.toString(node);
}
export function toPrimitiveOrString(node: ParseNode): ConstantValueType {
if (!node) return '';
if (node.parseType === 'constant') return node.value;
if (node.parseType === 'error') return undefined;
return toString(node);
}
export function traverse(node: ParseNode, fn: (ParseNode) => any): ReturnType<typeof fn> {
if (!node) return;
const type = nodeTypeIndex[node.parseType];
if (!type) {
console.error(node);
throw new Meteor.Error('Not valid parse node');
}
if (type.traverse) {
return type.traverse(node, fn);
}
return fn(node);
}
export function map(node: ParseNode, fn: (ParseNode) => any): ReturnType<typeof fn> {
if (!node) return;
const type = nodeTypeIndex[node.parseType];
if (!type) {
console.error(node);
throw new Meteor.Error('Not valid parse node');
}
if (type.map) {
return type.map(node, fn);
}
return fn(node);
}
export type ResolvedResult = {
result: ParseNode,
context: Context
}
export class Context {
errors: (Error | { type: string, message: string })[];
rolls: { number: number, diceSize: number, values: number[] }[];
options: { [key: string]: any };
constructor({ errors = [], rolls = [], options = {} } = {}) {
this.errors = errors;
this.rolls = rolls;
this.options = options;
}
error(e: Error | string) {
if (!e) return;
if (typeof e === 'string') {
this.errors.push({
type: 'error',
message: e,
});
} else {
this.errors.push(e);
const computationInputProvider: InputProvider = {
/**
* By default, just roll the dice as usual
*/
async rollDice(dice) {
return dice.map(d => rollDice(d.number, d.diceSize));
},
/**
* By default just choose the minimum number of options from the front of the list
*/
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) {
chosen.push(choices[i]._id);
}
}
roll(r) {
this.rolls.push(r);
return chosen;
}
}

View File

@@ -0,0 +1,11 @@
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { ConstantValueType } from '/imports/parser/parseTree/constant';
import toString from './toString';
export default function toPrimitiveOrString(node: ParseNode): ConstantValueType {
if (!node) return '';
if (node.parseType === 'constant') return node.value;
if (node.parseType === 'error') return undefined;
return toString(node);
}

View File

@@ -0,0 +1,7 @@
import factories from './parseTree';
import ParseNode from '/imports/parser/parseTree/ParseNode';
export default function toString(node: ParseNode) {
if (!node) return '';
return factories[node.parseType].toString(node as any, toString);
}

View File

@@ -0,0 +1,12 @@
/* eslint-disable no-fallthrough */
import factories from './parseTree';
import ParseNode from '/imports/parser/parseTree/ParseNode';
export default function traverse(node: ParseNode, fn: (ParseNode) => any): ReturnType<typeof fn> {
if (!node) return;
const factory = factories[node.parseType];
if ('traverse' in factory) {
return factory.traverse(node as any, fn, traverse);
}
return fn(node);
}

View File

@@ -0,0 +1,28 @@
export default class Context {
errors: (Error | { type: string; message: string; })[];
rolls: { number: number; diceSize: number; values: number[]; }[];
options: { [key: string]: any; };
constructor({ errors = [], rolls = [], options = {} } = {}) {
this.errors = errors;
this.rolls = rolls;
this.options = options;
}
error(e: Error | string) {
if (!e) return;
if (typeof e === 'string') {
this.errors.push({
type: 'error',
message: e,
});
} else {
this.errors.push(e);
}
}
roll(r) {
this.rolls.push(r);
}
}

View File

@@ -0,0 +1,11 @@
import ParseNode from '/imports/parser/parseTree/ParseNode';
type MapFunction<T extends ParseNode> = {
(node: T, fn: (node: ParseNode) => Promise<ParseNode>, mapOthers: MapOthersFunction): Promise<ParseNode>;
}
export default MapFunction;
type MapOthersFunction = {
(node: ParseNode, fn: (node: ParseNode) => Promise<ParseNode>): Promise<ParseNode>
}

View File

@@ -0,0 +1,18 @@
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import Context from './Context';
import ResolvedResult from './ResolvedResult';
import ResolveLevel from './ResolveLevel';
import ResolveOthersFunction from './ResolveOthersFunction';
type ResolveFunction<T extends ParseNode> = (
fn: ResolveLevel,
node: T,
scope: Record<string, any>,
context: Context,
input: InputProvider,
resolveOthers: ResolveOthersFunction,
) => Promise<ResolvedResult>;
export default ResolveFunction;

View File

@@ -0,0 +1,3 @@
type ResolveLevel = 'compile' | 'roll' | 'reduce';
export default ResolveLevel;

View File

@@ -0,0 +1,15 @@
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import Context from './Context';
import ResolvedResult from './ResolvedResult';
import ResolveOthersFunction from './ResolveOthersFunction';
type ResolveLevelFunction<T extends ParseNode> = (
node: T,
scope: Record<string, any>,
context: Context,
input: InputProvider,
resolveOthers: ResolveOthersFunction,
) => Promise<ResolvedResult>;
export default ResolveLevelFunction;

View File

@@ -0,0 +1,15 @@
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import Context from '/imports/parser/types/Context';
import ResolveLevel from '/imports/parser/types/ResolveLevel';
import ResolvedResult from '/imports/parser/types/ResolvedResult';
type ResolveOthersFunction = (
fn: ResolveLevel,
node: ParseNode,
scope: Record<string, any>,
context: Context,
inputProvider: InputProvider,
) => Promise<ResolvedResult>
export default ResolveOthersFunction;

View File

@@ -0,0 +1,9 @@
import ParseNode from '/imports/parser/parseTree/ParseNode';
import Context from './Context';
type ResolvedResult = {
result: ParseNode;
context: Context;
};
export default ResolvedResult;

View File

@@ -0,0 +1,11 @@
import ParseNode from '/imports/parser/parseTree/ParseNode';
type ToStringFunction<T extends ParseNode> = {
(node: T, stringOthers: ToStringOthersFunction): string;
}
export default ToStringFunction;
type ToStringOthersFunction = {
(node: ParseNode): string;
}

View File

@@ -0,0 +1,15 @@
import ParseNode from '/imports/parser/parseTree/ParseNode';
type TraverseFunction<T extends ParseNode> = {
(
node: T,
fn: (node: ParseNode) => any,
traverseOthers: TraverseOthersFunction
): any;
}
export default TraverseFunction;
type TraverseOthersFunction = {
(node: ParseNode, fn: (node: ParseNode) => any): any
}

View File

@@ -1,7 +1,6 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import Actions from '/imports/api/engine/actions/ActionEngine';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions';
@@ -9,6 +8,7 @@ import computeCreature from '/imports/api/engine/computeCreature';
import VERSION from '/imports/constants/VERSION';
import { loadCreature } from '/imports/api/engine/loadCreatures';
import { rebuildCreatureNestedSets } from '/imports/api/parenting/parentingFunctions';
import EngineActions from '/imports/api/engine/action/EngineActions';
let schema = new SimpleSchema({
creatureId: {
@@ -62,7 +62,7 @@ Meteor.publish('singleCharacter', function (creatureId) {
limit: 20,
sort: { date: -1 },
}),
Actions.find({
EngineActions.find({
creatureId,
}),
// Also publish the owner's username

28
app/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.59",
"version": "2.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1875,14 +1875,17 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"available-typed-arrays": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
"integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"requires": {
"possible-typed-array-names": "^1.0.0"
}
},
"aws-sdk": {
"version": "2.1559.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1559.0.tgz",
"integrity": "sha512-Lz/VgIhMtvDF1kP0xgXckrR5B2w7FD4IwLZfQ//t1Fu5YZVmYz6esHFba2q6X/Tw66q/Aig/rzQeYg0idSn9Kw==",
"version": "2.1561.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1561.0.tgz",
"integrity": "sha512-YbYQOyvy9mfEGRI4JDZjw6J0zW6bjyV7H3WMWeq69qETvZlkq8koy5CTPMCjnL8i7boDjyW9FuhQzICBbeNgLg==",
"requires": {
"buffer": "4.9.2",
"events": "1.1.1",
@@ -2968,9 +2971,9 @@
}
},
"has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
},
"has-symbols": {
"version": "1.0.3",
@@ -4520,6 +4523,11 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
"integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.59",
"version": "2.1.0",
"description": "Unofficial Online Realtime D&D 5e App",
"license": "GPL-3.0",
"repository": {
@@ -25,7 +25,7 @@
"@tozd/vue-observer-utils": "^0.5.0",
"@types/meteor": "^2.9.8",
"alea": "^1.0.1",
"aws-sdk": "^2.1559.0",
"aws-sdk": "^2.1561.0",
"bcrypt": "^5.1.1",
"chroma-js": "^2.4.2",
"css-box-shadow": "^1.0.0-3",

View File

@@ -10,7 +10,6 @@ import '/imports/server/publications/index';
import '/imports/server/cron/deleteSoftRemovedDocuments';
import '/imports/api/parenting/organizeMethods';
import '/imports/api/users/patreon/updatePatreonOnLogin';
import '/imports/api/engine/actions/index';
import '/imports/migrations/server/index';
import '/imports/migrations/methods/index'
import '/imports/constants/MAINTENANCE_MODE';