diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts index f47ac14b..8a5ba16b 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts @@ -1,11 +1,13 @@ import { assert } from 'chai'; import { allMutations, + allUpdates, createTestCreature, randomIds, removeAllCreaturesAndProps, runActionById } from '/imports/api/engine/action/functions/actionEngineTest.testFn'; +import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult'; const [ creatureId, targetCreatureId, targetCreature2Id, @@ -30,7 +32,7 @@ const actionTestCreature = { { _id: attackMissId, type: 'action', - attackRoll: { calculation: '-20' }, + attackRoll: { calculation: '-5' }, }, // Disable crits { @@ -105,48 +107,57 @@ describe('Apply Action Properties', function () { targetCreatureId, targetCreature2Id, ]); - assert.deepEqual(allMutations(action), [{ - contents: [{ - name: 'Action' - }], - targetIds: [ - targetCreatureId, - targetCreature2Id, - ] - }, { - contents: [{ - inline: true, - name: 'Hit!', - value: '1d20 [10] + 10\n**20**', - }], - targetIds: [targetCreatureId], - }, { - contents: [{ - inline: true, - name: 'Hit!', - value: '1d20 [10] + 10\n**20**', - }], - targetIds: [targetCreature2Id], - }]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId, targetCreature2Id] + }, { + contents: [{ + inline: true, + name: 'Hit!', + value: '1d20 [10] + 10\n**20**', + }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Hit!', + value: '1d20 [10] + 10\n**20**', + }], + targetIds: [targetCreature2Id], + }, + ]; + assert.deepEqual(allMutations(action), expectedMutations); + }); + + it('should make attack rolls that use uses', async function () { + const action = await runActionById(usesActionId, [targetCreatureId]); + const expectedUpdates: Update[] = [ + { + propId: usesActionId, + type: 'action', + inc: { usesUsed: 1, usesLeft: -1 }, + } + ] + assert.deepEqual(allUpdates(action), expectedUpdates); }); it('should make attack rolls that miss', async function () { const action = await runActionById(attackMissId, [targetCreatureId]); - assert.deepEqual(allMutations(action), [{ - contents: [{ - name: 'Action' - }], - targetIds: [ - targetCreatureId, - ] - }, { - contents: [{ - inline: true, - name: 'Miss!', - value: '1d20 [10] + 10\n**20**', - }], - targetIds: [targetCreatureId], - }]); + const expectedMutations: Mutation[] = [ + { + contents: [{ name: 'Action' }], + targetIds: [targetCreatureId], + }, { + contents: [{ + inline: true, + name: 'Miss!', + value: '1d20 [10] − 5\n**5**', // DiceCloud uses unicode minus + }], + targetIds: [targetCreatureId], + } + ]; + assert.deepEqual(allMutations(action), expectedMutations); }); }); diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts index 7a3c51a6..93ca5e09 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -10,11 +10,12 @@ import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/ import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; -import rollDice from '/imports/parser/rollDice'; -import { getFromScope, getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; export default async function applyActionProperty( - task: PropTask, action: EngineAction, result: TaskResult, userInput + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { const prop = task.prop; const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; @@ -48,13 +49,13 @@ export default async function applyActionProperty( spendResources(action, prop, targetIds, result, userInput); - const attack = prop.attackRoll || prop.attackRollBonus; + const attack: CalculatedField = prop.attackRoll || prop.attackRollBonus; // Attack if there is an attack roll if (attack && attack.calculation) { if (targetIds.length) { for (const targetId of targetIds) { - await applyAttackToTarget(task, action, attack, targetId, result); + await applyAttackToTarget(task, action, attack, targetId, result, userInput); await applyAfterTriggers(action, prop, [targetId], userInput); await applyChildren(action, prop, [targetId], userInput); } @@ -76,7 +77,8 @@ export default async function applyActionProperty( } async function applyAttackToTarget( - task: PropTask, action: EngineAction, attack, targetId, taskResult: TaskResult + task: PropTask, action: EngineAction, attack: CalculatedField, targetId: string, + taskResult: TaskResult, userInput: InputProvider ) { taskResult.pushScope = { '~attackHit': {}, @@ -86,7 +88,7 @@ async function applyAttackToTarget( '~attackRoll': {}, } - await recalculateCalculation(attack, action, 'reduce'); + await recalculateCalculation(attack, action, 'reduce', userInput); const scope = await getEffectiveActionScope(action); const contents: LogContent[] = []; @@ -95,7 +97,7 @@ async function applyAttackToTarget( result, criticalHit, criticalMiss, - } = await rollAttack(attack, scope, taskResult.pushScope); + } = await rollAttack(attack, scope, taskResult.pushScope, userInput); const targetScope = getVariables(targetId); const targetArmor = getNumberFromScope('armor', targetScope) @@ -143,7 +145,7 @@ async function applyAttackToTarget( } } -async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskResult, userInput) { +async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskResult, userInput: InputProvider) { taskResult.pushScope = { '~attackHit': {}, '~attackMiss': {}, @@ -151,14 +153,14 @@ async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskRe '~criticalMiss': {}, '~attackRoll': {}, } - await recalculateCalculation(attack, action, 'reduce'); + await recalculateCalculation(attack, action, 'reduce', userInput); const scope = await getEffectiveActionScope(action); const { resultPrefix, result, criticalHit, criticalMiss, - } = await rollAttack(attack, scope, taskResult.pushScope); + } = await rollAttack(attack, scope, taskResult.pushScope, userInput); let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit'; if (scope['~attackAdvantage']?.value === 1) { name += ' (Advantage)'; @@ -182,11 +184,11 @@ async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskRe }); } -async function rollAttack(attack, scope, resultPushScope) { +async function rollAttack(attack, scope, resultPushScope, userInput: InputProvider) { const rollModifierText = numberToSignedString(attack.value, true); let value, resultPrefix; if (scope['~attackAdvantage']?.value === 1) { - const [a, b] = await rollDice(2, 20); + const [[a, b]] = await userInput.rollDice(attack, [{ number: 2, diceSize: 20 }]); if (a >= b) { value = a; resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; @@ -195,7 +197,7 @@ async function rollAttack(attack, scope, resultPushScope) { resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; } } else if (scope['~attackAdvantage']?.value === -1) { - const [a, b] = await rollDice(2, 20); + const [[a, b]] = await userInput.rollDice(attack, [{ number: 2, diceSize: 20 }]); if (a <= b) { value = a; resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; @@ -204,7 +206,7 @@ async function rollAttack(attack, scope, resultPushScope) { resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; } } else { - value = await rollDice(1, 20)[0]; + [[value]] = await userInput.rollDice(attack, [{ number: 1, diceSize: 20 }]); resultPrefix = `1d20 [${value}] ${rollModifierText}` } resultPushScope['~attackDiceRoll'] = { value }; @@ -231,7 +233,7 @@ function applyCrits(value, scope, resultPushScope) { return { criticalHit, criticalMiss }; } -async function resetProperties(action: EngineAction, prop: any, result: TaskResult, userInput) { +async function resetProperties(action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider) { const attributes = getPropertiesOfType(action.creatureId, 'attribute'); for (const att of attributes) { if (att.removed || att.inactive) continue; diff --git a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts index c1b3ad20..b3ae201e 100644 --- a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts @@ -1,4 +1,5 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups'; import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation'; import { PropTask } from '/imports/api/engine/action/tasks/Task'; @@ -8,7 +9,7 @@ import { getSingleProperty, getVariables } from '/imports/api/engine/loadCreatur import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; export default async function applyAdjustmentProperty( - task: PropTask, action: EngineAction, result: TaskResult, userInput + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { const prop = task.prop; const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; diff --git a/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts b/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts index 19af353d..d935ce08 100644 --- a/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyBranchProperty.ts @@ -1,4 +1,5 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; import { applyAfterPropTasksForSingleChild, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } 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'; @@ -8,7 +9,7 @@ import { getPropertyChildren } from '/imports/api/engine/loadCreatures'; import rollDice from '/imports/parser/rollDice'; export default async function applyBranchProperty( - task: PropTask, action: EngineAction, result: TaskResult, userInput + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { const prop = task.prop; const targets = task.targetIds; diff --git a/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts b/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts index 28a04efc..6dc5589f 100644 --- a/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyBuffProperty.ts @@ -16,9 +16,10 @@ import recalculateInlineCalculations from '/imports/api/engine/action/functions/ import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX'; import { applyAfterTasksSkipChildren } from '/imports/api/engine/action/functions/applyTaskGroups'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; export default async function applyBuffProperty( - task: PropTask, action: EngineAction, result: TaskResult, userInput + task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider ) { const prop = EJSON.clone(task.prop); const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; diff --git a/app/imports/api/engine/action/functions/InputProvider.ts b/app/imports/api/engine/action/functions/InputProvider.ts new file mode 100644 index 00000000..23c704eb --- /dev/null +++ b/app/imports/api/engine/action/functions/InputProvider.ts @@ -0,0 +1,9 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; + +type InputProvider = { + rollDice( + action: EngineAction, dice: { number: number, diceSize: number }[] + ): Promise; +} + +export default InputProvider; \ No newline at end of file diff --git a/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts b/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts index cb2467a2..6288d629 100644 --- a/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts +++ b/app/imports/api/engine/action/functions/actionEngineTest.testFn.ts @@ -1,4 +1,3 @@ -import { assert } from 'chai'; import '/imports/api/simpleSchemaConfig.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; @@ -9,6 +8,7 @@ import { loadCreature } from '/imports/api/engine/loadCreatures'; import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions'; import { applyAction } from '/imports/api/engine/action/functions/applyAction'; import { LogContent, Mutation, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; /** * Removes all creatures, properties, and creatureVariable documents from the database @@ -59,12 +59,12 @@ export const randomIds = new Array(100).fill(undefined).map(() => Random.id()); * @param userInputFn A function that simulates user input * @returns The Engine Action with mutations resulting from running the action */ -export async function runActionById(propId, targetIds?, userInputFn = () => 0) { +export async function runActionById(propId, targetIds?, userInput = testInputProvider) { const prop = await CreatureProperties.findOneAsync(propId); const actionId = await createAction(prop, targetIds); const action = await EngineActions.findOneAsync(actionId); if (!action) throw 'Action is expected to exist'; - await applyAction(action, userInputFn, { simulate: true }); + await applyAction(action, userInput, { simulate: true }); return action; } @@ -148,3 +148,25 @@ export function allLogContent(action: EngineAction) { }); return contents; } + +const testInputProvider: InputProvider = { + /** + * For testing, randomness is hard to deal with + * rollDice function returns the average roll for every dice rolled + * [5d10, 1d4] => [[6,6,6,6,6], [3]] + */ + async rollDice(action, dice) { + const result: number[][] = []; + for (const diceRoll of dice) { + const averageRoll = Math.round(diceRoll.diceSize / 2); + // Return an array full of averagely rolled dice + result.push( + new Array(diceRoll.number) + .fill(averageRoll) + ) + } + return result; + } +} + +export { testInputProvider } \ No newline at end of file diff --git a/app/imports/api/engine/action/functions/applyAction.ts b/app/imports/api/engine/action/functions/applyAction.ts index ec811e22..1bb11a02 100644 --- a/app/imports/api/engine/action/functions/applyAction.ts +++ b/app/imports/api/engine/action/functions/applyAction.ts @@ -1,7 +1,8 @@ -import { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions'; +import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyTask from '/imports/api/engine/action/tasks/applyTask' import { isEmpty } from 'lodash'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; // TODO create a function to get the effective value of a property, // simulating all the result updates in the action so far @@ -10,12 +11,12 @@ import { isEmpty } from 'lodash'; // This is run once as a simulation on the client awaiting all the various inputs or step through // clicks from the user, then it is run as part of the runAction method, where it is expected to // complete instantly on the client, and sent to the server as a method call -export async function applyAction(action: EngineAction, userInput?: any[] | Function, options?: { +export async function applyAction(action: EngineAction, userInput: InputProvider, options?: { simulate?: boolean, stepThrough?: boolean }) { const { simulate, stepThrough } = options || {}; if (!simulate && stepThrough) throw 'Cannot step through unless simulating'; - if (simulate && typeof userInput !== 'function') throw 'Must provide a function to get user input when simulating'; + if (simulate && !userInput) throw 'Must provide a function to get user input when simulating'; action._stepThrough = stepThrough; action._isSimulation = simulate; @@ -37,6 +38,6 @@ function writeChangedAction(original: EngineAction, changed: EngineAction) { } } if (!isEmpty($set) && original._id) { - return Actions.updateAsync(original._id, { $set }); + return EngineActions.updateAsync(original._id, { $set }); } } diff --git a/app/imports/api/engine/action/functions/recalculateCalculation.ts b/app/imports/api/engine/action/functions/recalculateCalculation.ts index b8fb28ea..02588bcb 100644 --- a/app/imports/api/engine/action/functions/recalculateCalculation.ts +++ b/app/imports/api/engine/action/functions/recalculateCalculation.ts @@ -2,25 +2,33 @@ import { Context, toPrimitiveOrString } from '/imports/parser/resolve'; import { aggregateCalculationEffects, aggregateCalculationProficiencies, - resolveCalculationNode, } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import resolve from '/imports/parser/resolve'; import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; +import { ResolveLevel } from '/imports/parser/parseTree/NodeFactory'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; +import { EngineAction } from '/imports/api/engine/action/EngineActions'; // TODO Redo the work of // imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js // But in the action scope export default async function recalculateCalculation( - calcObj, action, parseLevel = 'reduce', context, scope + calcObj: CalculatedField, + action, + parseLevel: ResolveLevel = 'reduce', + userInput: InputProvider, ) { if (!calcObj?.parseNode) return; - calcObj._parseLevel = parseLevel; - if (!scope) { - scope = await getEffectiveActionScope(action); - } - // Re-resolve the parse node - resolveCalculationNode(calcObj, calcObj.parseNode, scope, context); + const scope = await getEffectiveActionScope(action); + // Re-resolve the parse node before effects and proficiencies + const { + result: unaffectedResult, + context + } = resolve(parseLevel, calcObj.parseNode, scope); + calcObj.valueNode = unaffectedResult; + // store the unaffected value if (calcObj.effectIds || calcObj.proficiencyIds) { calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode); @@ -35,19 +43,28 @@ export default async function recalculateCalculation( id => getSingleProperty(action.creatureId, id), scope['proficiencyBonus']?.value || 0 ); - // Resolve the modified valueNode - resolveCalculationNode(calcObj, calcObj.valueNode, scope, context); - // Store the primitive value - calcObj.value = toPrimitiveOrString(calcObj.valueNode); - // TODO log errors + // Resolve the modified valueNode, use the same context + const { + result: finalResult + } = resolve(parseLevel, calcObj.parseNode, scope, context); + + // Store the errors + calcObj.errors = context.errors; + + // Store the value and its primitive + calcObj.value = toPrimitiveOrString(finalResult); + calcObj.valueNode = finalResult; + } -export async function rollAndReduceCalculation(calcObj, action) { +export async function rollAndReduceCalculation( + calcObj: CalculatedField, action: EngineAction, userInput: InputProvider +) { const context = new Context(); const scope = await getEffectiveActionScope(action); // Compile - recalculateCalculation(calcObj, action, 'compile', context, scope); + recalculateCalculation(calcObj, action, 'compile', userInput); const compiled = calcObj.valueNode; // Roll diff --git a/app/imports/api/engine/action/tasks/applyTask.ts b/app/imports/api/engine/action/tasks/applyTask.ts index 6794f596..796d19e2 100644 --- a/app/imports/api/engine/action/tasks/applyTask.ts +++ b/app/imports/api/engine/action/tasks/applyTask.ts @@ -5,8 +5,11 @@ import applyDamagePropTask from '/imports/api/engine/action/tasks/applyDamagePro import applyItemAsAmmoTask from '/imports/api/engine/action/tasks/applyItemAsAmmoTask'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyProperties from '/imports/api/engine/action/applyProperties'; +import InputProvider from '/imports/api/engine/action/functions/InputProvider'; -export default async function applyTask(action: EngineAction, task: Task, userInput?): Promise { +export default async function applyTask( + action: EngineAction, task: Task, userInput: InputProvider +): Promise { action.taskCount += 1; if (action.taskCount > 100) throw 'Only 100 properties can be applied at once'; diff --git a/app/imports/api/properties/subSchemas/computedField.ts b/app/imports/api/properties/subSchemas/computedField.ts index cba0c0da..515613e1 100644 --- a/app/imports/api/properties/subSchemas/computedField.ts +++ b/app/imports/api/properties/subSchemas/computedField.ts @@ -1,15 +1,20 @@ import SimpleSchema from 'simpl-schema'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import ParseNode from '/imports/parser/parseTree/ParseNode'; +import { ConstantValueType } from '/imports/parser/parseTree/constant'; export interface CalculatedField { - calculation?: string, - value?: string | number, - effectIds?: string[], - parseNode?: any, - parseError?: any, - hash?: number, - errors?: any[], + calculation?: string; + value?: ConstantValueType; + valueNode: ParseNode; + effectIds?: string[]; + proficiencyIds?: string[]; + unaffected?: ConstantValueType; + parseNode?: ParseNode; + parseError?: any; + hash?: number; + errors?: any[]; } // Get schemas that apply fields directly so they can be gracefully extended diff --git a/app/imports/parser/parseTree/constant.ts b/app/imports/parser/parseTree/constant.ts index 23c2db8c..e74a6397 100644 --- a/app/imports/parser/parseTree/constant.ts +++ b/app/imports/parser/parseTree/constant.ts @@ -1,7 +1,7 @@ import NodeFactory from '/imports/parser/parseTree/NodeFactory'; import { Context, ResolvedResult } from '/imports/parser/resolve'; -type ConstantValueType = number | string | boolean | undefined +export type ConstantValueType = number | string | boolean | undefined export type ConstantNode = { parseType: 'constant'; diff --git a/app/imports/parser/resolve.ts b/app/imports/parser/resolve.ts index 7d25bbb7..a97deb72 100644 --- a/app/imports/parser/resolve.ts +++ b/app/imports/parser/resolve.ts @@ -1,5 +1,6 @@ import nodeTypeIndex from './parseTree/_index'; import ParseNode from '/imports/parser/parseTree/ParseNode'; +import { ConstantValueType } from '/imports/parser/parseTree/constant'; // Takes a parse node and computes it to a set detail level // returns {result, context} @@ -37,10 +38,10 @@ export function toString(node: ParseNode) { return type.toString(node); } -export function toPrimitiveOrString(node) { +export function toPrimitiveOrString(node: ParseNode): ConstantValueType { if (!node) return ''; if (node.parseType === 'constant') return node.value; - if (node.parseType === 'error') return null; + if (node.parseType === 'error') return undefined; return toString(node); } diff --git a/app/imports/parser/rollDice.js b/app/imports/parser/rollDice.js deleted file mode 100644 index 4e379ba6..00000000 --- a/app/imports/parser/rollDice.js +++ /dev/null @@ -1,9 +0,0 @@ -export default function rollDice(number, diceSize){ - let values = []; - let randomSrc = DDP.randomStream('diceRoller'); - for (let i = 0; i < number; i++){ - let roll = ~~(randomSrc.fraction() * diceSize) + 1 - values.push(roll); - } - return values; -} diff --git a/app/imports/parser/rollDice.ts b/app/imports/parser/rollDice.ts new file mode 100644 index 00000000..df353c03 --- /dev/null +++ b/app/imports/parser/rollDice.ts @@ -0,0 +1,12 @@ +export default function rollDice(number: number, diceSize: number): number[] { + const values: number[] = []; + const randomSrc = DDP.randomStream('diceRoller'); + if (number > 100) { + throw new Meteor.Error('Too many dice', 'can only roll up to 100 dice at once'); + } + for (let i = 0; i < number; i++) { + const roll = ~~(randomSrc.fraction() * diceSize) + 1 + values.push(roll); + } + return values; +}