diff --git a/app/imports/api/engine/action/ActionEngine.test.ts b/app/imports/api/engine/action/ActionEngine.test.ts index 973e7492..198691a7 100644 --- a/app/imports/api/engine/action/ActionEngine.test.ts +++ b/app/imports/api/engine/action/ActionEngine.test.ts @@ -266,7 +266,7 @@ function allLogContent(action: EngineAction) { let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId, adjustedStatId, adjustmentIncrementId, adjustmentSetId, rollId, buffId, - removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId, buffAttChildId; + removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId; const propForest = [ // Apply a simple note @@ -363,7 +363,7 @@ const propForest = [ target: 'self', children: [ { - _id: buffAttChildId = Random.id(), + _id: Random.id(), type: 'attribute', attributeType: 'stat', variableName: 'buffStat', diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts index 47a07011..fa3b372e 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -10,7 +10,7 @@ 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 { getConstantValueFromScope, getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; +import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; diff --git a/app/imports/api/engine/action/functions/applyAction.ts b/app/imports/api/engine/action/functions/applyAction.ts index 240f4bcd..4d9bcb45 100644 --- a/app/imports/api/engine/action/functions/applyAction.ts +++ b/app/imports/api/engine/action/functions/applyAction.ts @@ -43,7 +43,7 @@ export default async function applyAction(action: EngineAction, userInput: Input if (!task) { const prop = await getSingleProperty(action.creatureId, action.rootPropId); if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found'); - task = options?.task || { + task = { prop, targetIds: action.targetIds || [], } diff --git a/app/imports/api/engine/action/functions/userInput/InputProvider.ts b/app/imports/api/engine/action/functions/userInput/InputProvider.ts index 90f23f57..51244a25 100644 --- a/app/imports/api/engine/action/functions/userInput/InputProvider.ts +++ b/app/imports/api/engine/action/functions/userInput/InputProvider.ts @@ -30,7 +30,7 @@ type InputProvider = { /** * Get the details of a check or save */ - //check(suggestedParams: CheckParams): Promise; + check(suggestedParams: CheckParams): Promise; } export type Advantage = 0 | 1 | -1; diff --git a/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts index 2690612f..de9fe161 100644 --- a/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts +++ b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts @@ -5,6 +5,7 @@ import getDeterministicDiceRoller from '/imports/api/engine/action/functions/use // Dice rolls are done fresh, no cheating export default function getReplayChoicesInputProvider(actionId: string, decisions: any[]): InputProvider { + const decisionStack = [...decisions].reverse(); const dRoller = getDeterministicDiceRoller(actionId); const replaySavedInput: InputProvider = { nextStep() { @@ -12,15 +13,18 @@ export default function getReplayChoicesInputProvider(actionId: string, decision }, // To roll dice, ignore the user and use the deterministic dice roller again rollDice(dice) { - decisions.pop(); + decisionStack.pop(); return dRoller(dice); }, choose() { - return Promise.resolve(decisions.pop()); + return Promise.resolve(decisionStack.pop()); }, advantage() { - return Promise.resolve(decisions.pop()); - } + return Promise.resolve(decisionStack.pop()); + }, + check() { + return Promise.resolve(decisionStack.pop()); + }, } return replaySavedInput; } diff --git a/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts b/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts index c2a2a682..3067fb27 100644 --- a/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts +++ b/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts @@ -35,6 +35,9 @@ const inputProviderForTests: InputProvider = { */ async advantage(suggestedAdvantage) { return suggestedAdvantage; + }, + async check(suggestedParams) { + return suggestedParams; } } diff --git a/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts index c1e25300..ad1baa45 100644 --- a/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts +++ b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts @@ -16,9 +16,9 @@ export default function saveInputChoices(action: EngineAction, userInput: InputP for (const key in userInput) { const oldFn = userInput[key]; // Make a new function that does the same thing, but saves the result to action._decisions - const newFn = (...args) => { - const result = oldFn(...args); - action._decisions.push(result); + const newFn = async (...args) => { + const result = await oldFn(...args); + action._decisions?.push(result); return result; } newInputProvider[key] = newFn; diff --git a/app/imports/api/engine/action/methods/runAction.ts b/app/imports/api/engine/action/methods/runAction.ts index a5da8323..460c54b1 100644 --- a/app/imports/api/engine/action/methods/runAction.ts +++ b/app/imports/api/engine/action/methods/runAction.ts @@ -6,23 +6,12 @@ import { getCreature } from '/imports/api/engine/loadCreatures'; import applyAction from '/imports/api/engine/action/functions/applyAction'; import writeActionResults from '../functions/writeActionResults'; import getReplayChoicesInputProvider from '/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider'; +import Task from '/imports/api/engine/action/tasks/Task'; export const runAction = new ValidatedMethod({ name: 'actions.runAction', - validate: new SimpleSchema({ - actionId: { - type: String, - }, - decisions: { - type: Array, - optional: true, - }, - 'decisions.$': { - type: Object, - blackbox: true, - }, - }).validator(), - run: async function ({ actionId, decisions = [] }: { actionId: string, decisions?: any[] }) { + validate: null, //TODO validate this + run: async function ({ actionId, decisions = [], task }: { actionId: string, decisions?: any[], task?: Task }) { // Get the action const action = await EngineActions.findOneAsync(actionId); if (!action) throw new Meteor.Error('not-found', 'Action not found'); @@ -34,7 +23,7 @@ export const runAction = new ValidatedMethod({ const userInput = getReplayChoicesInputProvider(actionId, decisions); // Apply the action - applyAction(action, userInput); + await applyAction(action, userInput, { task }); // Persist changes const writePromise = writeActionResults(action); diff --git a/app/imports/api/engine/action/tasks/Task.ts b/app/imports/api/engine/action/tasks/Task.ts index 33233d15..eb920ec5 100644 --- a/app/imports/api/engine/action/tasks/Task.ts +++ b/app/imports/api/engine/action/tasks/Task.ts @@ -1,17 +1,19 @@ -type Task = PropTask | DamagePropTask | ItemAsAmmoTask; +import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; + +type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask; export default Task; -interface BaseTask { +type BaseTask = { prop: { [key: string]: any }; targetIds: string[]; } -export interface PropTask extends BaseTask { +export type PropTask = BaseTask & { subtaskFn?: undefined, } -export interface DamagePropTask extends BaseTask { +export type DamagePropTask = BaseTask & { subtaskFn: 'damageProp'; params: { /** @@ -24,10 +26,14 @@ export interface DamagePropTask extends BaseTask { }; } -export interface ItemAsAmmoTask extends BaseTask { +export type ItemAsAmmoTask = BaseTask & { subtaskFn: 'consumeItemAsAmmo'; params: { value: number; item: any; }; } + +export type CheckTask = BaseTask & CheckParams & { + subtaskFn: 'check'; +} diff --git a/app/imports/api/engine/action/tasks/applyCheckTask.ts b/app/imports/api/engine/action/tasks/applyCheckTask.ts index 16084f8a..11fa54f0 100644 --- a/app/imports/api/engine/action/tasks/applyCheckTask.ts +++ b/app/imports/api/engine/action/tasks/applyCheckTask.ts @@ -1,50 +1,115 @@ -import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; -import { EngineAction } from '/imports/api/engine/action/EngineActions'; -import InputProvider, { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { CheckTask } from '/imports/api/engine/action/tasks/Task'; +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 { applyUnresolvedEffects } from '/imports/api/engine/action/methods/doCheck'; -import { PropTask } from '/imports/api/engine/action/tasks/Task'; -import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; +import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import { getVariables } from '/imports/api/engine/loadCreatures'; +import InputProvider, { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; -import { isFiniteNode } from '/imports/parser/parseTree/constant'; +import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; -// TODO implement this /** * A skill property is applied as a check or a saving throw */ export default async function applyCheckTask( - task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider + task: CheckTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { - - throw new Meteor.Error('Not implemented', 'This function needs to be implemented'); - const prop = task.prop; const targetIds = task.targetIds; - if (targetIds?.length) { - throw new Meteor.Error('too-many-targets', - 'This function is only implemented for zero targets'); + + if (task.contest) { + throw new Meteor.Error('not-implemented', 'This functionality is not implemented yet'); } - let checkParams: CheckParams = { - advantage: 0, - skillVariableName: prop.variableName, - abilityVariableName: prop.ability, - dc: null, + for (const targetId of targetIds) { + let scope; + if (targetId === action.creatureId) { + scope = await getEffectiveActionScope(action); + } else { + scope = getVariables(targetId); + } + // Get the updated parameters from user input + const checkParams = await userInput.check(task); + const advantage = checkParams.advantage; + + const skill = checkParams.skillVariableName && getFromScope(checkParams.skillVariableName, scope) || null; + const skillBonus = (skill?.value || 0) - (skill?.abilityMod || 0); + + const ability = checkParams.abilityVariableName && getFromScope(checkParams.abilityVariableName, scope) || null; + const abilityModifier = ability?.modifier || 0; + + const totalModifier = skillBonus + abilityModifier; + const rollModifierText = numberToSignedString(totalModifier); + + // Get the name of the check + let checkName = 'Check'; + if (ability?.name && skill?.name) { + checkName = `${ability.name} (${skill.name})` + } else if (ability?.name || skill?.name) { + checkName = `${ability?.name || skill?.name}`; + } + + let rollName = 'Roll' + + // Append advantage/disadvantage to the check name + if (advantage === 1) { + rollName += ' (Advantage)' + } else if (advantage === -1) { + rollName += ' (Disadvantage)' + } + + // Print check name and DC if present + const dc = checkParams.dc; + result.appendLog({ + name: checkName, + inline: true, + ...dc !== null && { value: `DC **${dc}**` }, + ...prop?.silent && { silenced: prop.silent } + }, [targetId]); + + // Roll the dice + let rolledValue, resultPrefix; + if (advantage === 1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a >= b) { + rolledValue = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + rolledValue = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else if (advantage === -1) { + const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]); + if (a <= b) { + rolledValue = a; + resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`; + } else { + rolledValue = b; + resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; + } + } else { + [[rolledValue]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]); + resultPrefix = `1d20 [${rolledValue}] ${rollModifierText}` + } + + const totalValue = rolledValue + totalModifier; + + result.appendLog({ + name: rollName, + value: `${resultPrefix}\n**${totalValue}**`, + inline: true, + ...prop?.silent && { silenced: prop.silent } + }, [targetId]); } - checkParams = await inputProvider.check(checkParams); - - const dc = checkParams.dc; - if (!prop.silent && dc !== null) result.appendLog({ - name: prop.name, - value: `DC **${dc}**`, - inline: true, - ...prop.silent && { silenced: prop.silent } - }, targetIds); - const scope = await getEffectiveActionScope(action); - - return applyDefaultAfterPropTasks(action, prop, targetIds, inputProvider); + return applyDefaultAfterPropTasks(action, prop, targetIds, userInput); } + +// TODO set these and potentially read them again if triggers can change them +/* +'~checkAdvantage' +'~checkAdvantage' +'~checkDiceRoll' +'~checkRoll' +'~checkModifier' +*/ diff --git a/app/imports/api/engine/action/tasks/applyTask.ts b/app/imports/api/engine/action/tasks/applyTask.ts index 38030d6e..f8223e12 100644 --- a/app/imports/api/engine/action/tasks/applyTask.ts +++ b/app/imports/api/engine/action/tasks/applyTask.ts @@ -1,11 +1,12 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; -import Task, { DamagePropTask, ItemAsAmmoTask, PropTask } from './Task'; +import Task, { CheckTask, 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'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyProperties from '/imports/api/engine/action/applyProperties'; import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; +import applyCheckTask from '/imports/api/engine/action/tasks/applyCheckTask'; // DamagePropTask promises a number of actual damage done export default async function applyTask( @@ -14,7 +15,7 @@ export default async function applyTask( // Other tasks promise nothing export default async function applyTask( - action: EngineAction, task: PropTask | ItemAsAmmoTask, inputProvider: InputProvider + action: EngineAction, task: PropTask | ItemAsAmmoTask | CheckTask, inputProvider: InputProvider ): Promise export default async function applyTask( @@ -42,6 +43,8 @@ export default async function applyTask( return applyDamagePropTask(task, action, result, inputProvider); case 'consumeItemAsAmmo': return applyItemAsAmmoTask(task, action, result, inputProvider); + case 'check': + return applyCheckTask(task, action, result, inputProvider); } } else { // Get property diff --git a/app/imports/api/users/methods/updateFileStorageUsed.js b/app/imports/api/users/methods/updateFileStorageUsed.js index b72a55e2..656915b2 100644 --- a/app/imports/api/users/methods/updateFileStorageUsed.js +++ b/app/imports/api/users/methods/updateFileStorageUsed.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; -import UserImages from '/imports/api/files/UserImages'; +import UserImages from '/imports/api/files/userImages/UserImages'; const fileCollections = [ArchiveCreatureFiles, UserImages]; const updateFileStorageUsed = new ValidatedMethod({ diff --git a/app/imports/client/ui/components/ImageUploadInput.vue b/app/imports/client/ui/components/ImageUploadInput.vue index ed436bce..1750817d 100644 --- a/app/imports/client/ui/components/ImageUploadInput.vue +++ b/app/imports/client/ui/components/ImageUploadInput.vue @@ -12,7 +12,7 @@ diff --git a/app/imports/client/ui/files/UserImageCard.vue b/app/imports/client/ui/files/UserImageCard.vue index 7a445a57..5307451d 100644 --- a/app/imports/client/ui/files/UserImageCard.vue +++ b/app/imports/client/ui/files/UserImageCard.vue @@ -34,7 +34,7 @@