From 8f8c9c28aa8e0ac2cddafa5dce416e863cfca4fd Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:28:36 +0200 Subject: [PATCH] Refactored actions, 'cast a spell' task now works --- .../api/engine/action/EngineActions.ts | 18 +- .../applyProperties/applyActionProperty.ts | 1 - .../applyAdjustmentProperty.ts | 1 - .../applyProperties/applyDamageProperty.ts | 1 - .../engine/action/functions/applyAction.ts | 40 ++-- .../functions/userInput/InputProvider.ts | 2 +- .../api/engine/action/methods/insertAction.ts | 2 +- .../api/engine/action/methods/runAction.ts | 22 +-- .../api/engine/action/methods/updateAction.ts | 6 + app/imports/api/engine/action/tasks/Task.ts | 29 ++- .../api/engine/action/tasks/TaskResult.ts | 4 +- .../applyCastSpellTask.ts} | 37 +++- .../api/engine/action/tasks/applyCheckTask.ts | 7 +- .../action/tasks/applyDamagePropTask.ts | 12 +- .../action/tasks/applyItemAsAmmoTask.ts | 6 +- .../api/engine/action/tasks/applyResetTask.ts | 4 +- .../api/engine/action/tasks/applyTask.ts | 9 +- app/imports/client/ui/creature/RestButton.vue | 18 +- .../ui/creature/actions/ActionDialog.vue | 47 ++--- .../client/ui/creature/actions/doAction.ts | 54 ++++-- .../ui/creature/actions/doClientAction.ts | 12 -- .../actions/input/CastSpellInput.vue} | 174 ++++++++---------- .../character/characterSheetTabs/StatsTab.vue | 24 ++- .../CreaturePropertyDialog.vue | 25 ++- .../ui/dialogStack/DialogComponentIndex.js | 2 - .../components/actions/ActionCard.vue | 8 +- .../components/actions/EventButton.vue | 17 +- .../components/attributes/AbilityListTile.vue | 21 ++- .../attributes/AttributeCardContent.vue | 21 ++- .../components/attributes/SpellSlotCard.vue | 42 ++--- .../attributes/SpellSlotListTile.vue | 16 +- .../AttributeGroupComponent.vue | 27 +-- .../components/skills/SkillListTile.vue | 23 ++- .../ui/properties/viewers/ActionViewer.vue | 9 +- .../client/ui/tabletop/TabletopActionCard.vue | 8 +- .../client/ui/tabletop/TabletopBuffCard.vue | 11 +- .../client/ui/tabletop/TabletopComponent.vue | 4 +- .../SelectedCreatureBar.vue | 50 +++-- app/tsconfig.json | 8 +- 39 files changed, 423 insertions(+), 399 deletions(-) rename app/imports/api/engine/action/{applyProperties/applySpellProperty.ts => tasks/applyCastSpellTask.ts} (73%) delete mode 100644 app/imports/client/ui/creature/actions/doClientAction.ts rename app/imports/client/ui/{properties/components/spells/CastSpellWithSlotDialog.vue => creature/actions/input/CastSpellInput.vue} (79%) diff --git a/app/imports/api/engine/action/EngineActions.ts b/app/imports/api/engine/action/EngineActions.ts index c6af46d5..966e5a87 100644 --- a/app/imports/api/engine/action/EngineActions.ts +++ b/app/imports/api/engine/action/EngineActions.ts @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import TaskResult from './tasks/TaskResult'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema'; +import Task from './tasks/Task'; const EngineActions = new Mongo.Collection('actions'); @@ -9,10 +10,9 @@ export interface EngineAction { _isSimulation?: boolean; _stepThrough?: boolean; _decisions?: any[], + task: Task; creatureId: string; - rootPropId?: string; tabletopId?: string; - targetIds?: string[]; results: TaskResult[]; taskCount: number; } @@ -21,27 +21,25 @@ const ActionSchema = new SimpleSchema({ creatureId: { type: String, regEx: SimpleSchema.RegEx.Id, + // @ts-expect-error index not defined index: 1, }, rootPropId: { type: String, regEx: SimpleSchema.RegEx.Id, + optional: true, }, tabletopId: { type: String, regEx: SimpleSchema.RegEx.Id, optional: true, + // @ts-expect-error index not defined index: 1, }, - targetIds: { - type: Array, - optional: true, + task: { + type: Object, + blackbox: true, }, - 'targetIds.$': { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - // Applied properties results: { type: Array, diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts index d4c7c480..7a56ca34 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -74,7 +74,6 @@ export default async function applyActionProperty( if (prop.actionType === 'event' && prop.variableName) { await applyResetTask({ subtaskFn: 'reset', - prop, eventName: prop.variableName, targetIds: [action.creatureId], }, action, result, userInput); diff --git a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts index 80e97f15..aee14885 100644 --- a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.ts @@ -49,7 +49,6 @@ export default async function applyAdjustmentProperty( return; } await applyTask(action, { - prop, targetIds: damageTargetIds, subtaskFn: 'damageProp', params: { diff --git a/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts b/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts index ebd68256..2bbe6999 100644 --- a/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyDamageProperty.ts @@ -298,7 +298,6 @@ async function dealDamage( if (damageLeft === 0) return; // Do the damage const damageAdded = await applyTask(action, { - prop, targetIds: [targetId], subtaskFn: 'damageProp', params: { diff --git a/app/imports/api/engine/action/functions/applyAction.ts b/app/imports/api/engine/action/functions/applyAction.ts index 39e9efbd..60b31b15 100644 --- a/app/imports/api/engine/action/functions/applyAction.ts +++ b/app/imports/api/engine/action/functions/applyAction.ts @@ -1,9 +1,7 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; -import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyTask from '/imports/api/engine/action/tasks/applyTask' import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; import saveInputChoices from './userInput/saveInputChoices'; -import Task from '/imports/api/engine/action/tasks/Task'; // TODO create a function to get the effective value of a property, // simulating all the result updates in the action so far @@ -16,11 +14,9 @@ import Task from '/imports/api/engine/action/tasks/Task'; * @param action The action to apply * @param userInput The input provider * @param { Object } options - * @param { Task } options.task If provided, the action will start with this task instead of - * applying the root property of the action */ export default async function applyAction(action: EngineAction, userInput: InputProvider, options?: { - simulate?: boolean, stepThrough?: boolean, task?: Task, + simulate?: boolean, stepThrough?: boolean, }) { const { simulate, stepThrough } = options || {}; if (!simulate && stepThrough) throw 'Cannot step through unless simulating'; @@ -39,29 +35,19 @@ export default async function applyAction(action: EngineAction, userInput: Input action._stepThrough = stepThrough; action._isSimulation = simulate; action.taskCount = 0; - let task = options?.task; - 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'); - - // If the target ids weren't already set, get them from the user - if ( - !action.targetIds - && action.tabletopId - && ( - prop.target === 'singleTarget' || - prop.target === 'multipleTargets' - ) - ) { - action.targetIds = await (userInput.targetIds(prop.target)); - } - - task = { - prop, - targetIds: action.targetIds || [], - } + // Get the target Ids from the user input if they are expected and not found + if ( + !action.task.targetIds?.length + && action.tabletopId + && 'prop' in action.task + && ( + action.task.prop?.target === 'singleTarget' || + action.task.prop?.target === 'multipleTargets' + ) + ) { + action.task.targetIds = await (userInput.targetIds(action.task.prop.target)); } - await applyTask(action, task, userInput); + await applyTask(action, action.task, userInput); return action; } diff --git a/app/imports/api/engine/action/functions/userInput/InputProvider.ts b/app/imports/api/engine/action/functions/userInput/InputProvider.ts index 8a5366fc..4da07d59 100644 --- a/app/imports/api/engine/action/functions/userInput/InputProvider.ts +++ b/app/imports/api/engine/action/functions/userInput/InputProvider.ts @@ -38,7 +38,7 @@ type InputProvider = { /** * Get the details of casting a spell */ - castSpell(suggestedParams: CastSpellParams): Promise; + castSpell(suggestedParams: Partial): Promise; } export type Advantage = 0 | 1 | -1; diff --git a/app/imports/api/engine/action/methods/insertAction.ts b/app/imports/api/engine/action/methods/insertAction.ts index 7b4c0327..ffc973a4 100644 --- a/app/imports/api/engine/action/methods/insertAction.ts +++ b/app/imports/api/engine/action/methods/insertAction.ts @@ -22,7 +22,7 @@ export const insertAction = new ValidatedMethod({ action.tabletopId = creature.tabletopId; // Ensure that all the targeted creatures exist and share a tabletop - if (action.targetIds) for (const targetId of action.targetIds) { + if (action.task.targetIds) for (const targetId of action.task.targetIds) { const target = getCreature(targetId); if (!target) { throw new Meteor.Error('not-found', 'Target creature does not exist'); diff --git a/app/imports/api/engine/action/methods/runAction.ts b/app/imports/api/engine/action/methods/runAction.ts index eb4be184..6c415de8 100644 --- a/app/imports/api/engine/action/methods/runAction.ts +++ b/app/imports/api/engine/action/methods/runAction.ts @@ -6,33 +6,17 @@ 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'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; export const runAction = new ValidatedMethod({ name: 'actions.runAction', - validate: new SimpleSchema({ - actionId: String, - decisions: { - type: Array, - optional: true, - }, - 'decisions.$': { - type: Object, - blackbox: true, - }, - task: { - type: Object, - optional: true, - blackbox: true, - }, - }).validator(), + validate: null, mixins: [RateLimiterMixin], rateLimit: { numRequests: 10, timeInterval: 5000, }, - run: async function ({ actionId, decisions = [], task }: { actionId: string, decisions?: any[], task?: Task }) { + run: async function ({ actionId, decisions = [] }: { actionId: string, decisions?: any[] }) { // Get the action const action = await EngineActions.findOneAsync(actionId); if (!action) throw new Meteor.Error('not-found', 'Action not found'); @@ -44,7 +28,7 @@ export const runAction = new ValidatedMethod({ const userInput = getReplayChoicesInputProvider(actionId, decisions); // Apply the action - await applyAction(action, userInput, { task }); + await applyAction(action, userInput); // Persist changes const writePromise = writeActionResults(action); diff --git a/app/imports/api/engine/action/methods/updateAction.ts b/app/imports/api/engine/action/methods/updateAction.ts index cdb4998d..e4101f00 100644 --- a/app/imports/api/engine/action/methods/updateAction.ts +++ b/app/imports/api/engine/action/methods/updateAction.ts @@ -2,6 +2,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import EngineActions from '/imports/api/engine/action/EngineActions'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { getCreature } from '/imports/api/engine/loadCreatures'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; export const updateAction = new ValidatedMethod({ name: 'actions.updateAction', @@ -11,6 +12,11 @@ export const updateAction = new ValidatedMethod({ if (path !== 'targetIds') throw new Meteor.Error('Can only update target ids'); if (!Array.isArray(value)) throw new Meteor.Error('TargetIds must be an array'); }, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, run: async function ({ _id, path, value }: { _id: string, path: 'targetIds', value: string[] }) { const action = await EngineActions.findOneAsync(_id); if (!action) { diff --git a/app/imports/api/engine/action/tasks/Task.ts b/app/imports/api/engine/action/tasks/Task.ts index 676d972d..0b008815 100644 --- a/app/imports/api/engine/action/tasks/Task.ts +++ b/app/imports/api/engine/action/tasks/Task.ts @@ -1,16 +1,24 @@ import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; -type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask; +type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask | CastSpellTask; export default Task; type BaseTask = { - prop: { type: string, [key: string]: any }; targetIds: string[]; + silent?: boolean | undefined; +} + +type Prop = { + _id: string; + type: string; + [key: string]: any, } export type PropTask = BaseTask & { - subtaskFn?: undefined, + prop: Prop; + subtaskFn?: undefined; + silent?: undefined; } export type DamagePropTask = BaseTask & { @@ -22,12 +30,14 @@ export type DamagePropTask = BaseTask & { title?: string; operation: 'increment' | 'set'; value: number; - targetProp: any; + targetProp: Prop; }; } export type ItemAsAmmoTask = BaseTask & { subtaskFn: 'consumeItemAsAmmo'; + prop: Prop; + silent?: undefined; params: { value: number; item: any; @@ -40,8 +50,17 @@ export type CheckTask = BaseTask & CheckParams & { } export type ResetTask = BaseTask & { - subtaskFn: 'reset', + subtaskFn: 'reset'; eventName: string; // One and only one target targetIds: [string]; } + +export type CastSpellTask = BaseTask & { + prop?: Prop | undefined; + silent?: undefined; + subtaskFn: 'castSpell'; + params: { + spellId: string | undefined; + }; +} diff --git a/app/imports/api/engine/action/tasks/TaskResult.ts b/app/imports/api/engine/action/tasks/TaskResult.ts index ca52a11b..10ca4232 100644 --- a/app/imports/api/engine/action/tasks/TaskResult.ts +++ b/app/imports/api/engine/action/tasks/TaskResult.ts @@ -6,7 +6,6 @@ import Context from '../../../../parser/types/Context'; * Each mutation may apply to a different subset of targets */ export default class TaskResult { - propId: string; // The targets of the original task targetIds: string[]; scope: any; @@ -21,8 +20,7 @@ export default class TaskResult { // properties can be found on variable.previous pushScope?: any; mutations: Mutation[]; - constructor(propId: string, targetIds: string[]) { - this.propId = propId; + constructor(targetIds: string[]) { this.targetIds = targetIds; this.mutations = []; this.scope = {}; diff --git a/app/imports/api/engine/action/applyProperties/applySpellProperty.ts b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts similarity index 73% rename from app/imports/api/engine/action/applyProperties/applySpellProperty.ts rename to app/imports/api/engine/action/tasks/applyCastSpellTask.ts index 2d467d55..8f900a97 100644 --- a/app/imports/api/engine/action/applyProperties/applySpellProperty.ts +++ b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts @@ -1,25 +1,40 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; -import { PropTask } from '/imports/api/engine/action/tasks/Task'; -import TaskResult from '../tasks/TaskResult'; +import { CastSpellTask } from '/imports/api/engine/action/tasks/Task'; +import TaskResult from './TaskResult'; import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; import { getPropertiesOfType, getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyTask from '/imports/api/engine/action/tasks/applyTask'; -import applyActionProperty from './applyActionProperty'; +import applyActionProperty from '../applyProperties/applyActionProperty'; -export default async function applyFolderProperty( - task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider +export default async function applySpellProperty( + task: CastSpellTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { let prop = task.prop; // Ask the user how this spell is being cast const castOptions = await userInput.castSpell({ - spellId: prop?._id, + spellId: task.params.spellId, slotId: prop?.castWithoutSpellSlots ? undefined : getSuggestedSpellSlotId(action.creatureId, prop), ritual: false, }); + if (!castOptions.spellId) { + result.appendLog({ + name: 'Error casting spell', + value: 'No spell was selected', + }, [action.creatureId]); + return; + } // If the user changed the spell they are casting, use that as the prop prop = getSingleProperty(action.creatureId, castOptions.spellId); + + if (!prop) { + result.appendLog({ + name: 'Error casting spell', + value: 'The chosen spell was not found', + }, [action.creatureId]); + return; + } let slotLevel = prop.level || 0; // Get the slot being cast with const slot = castOptions.slotId && getSingleProperty(action.creatureId, castOptions.slotId); @@ -27,7 +42,7 @@ export default async function applyFolderProperty( logCastingMessage(slot?.spellSlotLevel?.value, castOptions, result, prop, task.targetIds); // Spend the spell slot and change the spell's casting level if a slot is used if (slot) { - await spendSpellSlot(action, prop, castOptions, userInput); + await spendSpellSlot(action, castOptions, userInput); slotLevel = slot.spellSlotLevel?.value || 0; } // Add the slot level to the scope @@ -36,7 +51,10 @@ export default async function applyFolderProperty( 'slotLevel': { value: slotLevel }, }; // Run the rest of the spell as if it were an action - return applyActionProperty(task, action, result, userInput); + return applyActionProperty({ + prop, + targetIds: task.targetIds, + }, action, result, userInput); } function getSuggestedSpellSlotId(creatureId, prop) { @@ -67,10 +85,9 @@ function logCastingMessage(slotLevel: number, castOptions, result: TaskResult, p } } -function spendSpellSlot(action, prop, castOptions, userInput) { +function spendSpellSlot(action, castOptions, userInput) { const slot = getSingleProperty(action.creatureId, castOptions.slotId); return applyTask(action, { - prop, targetIds: [action.creatureId], subtaskFn: 'damageProp', params: { diff --git a/app/imports/api/engine/action/tasks/applyCheckTask.ts b/app/imports/api/engine/action/tasks/applyCheckTask.ts index b8f32e39..fe350d8b 100644 --- a/app/imports/api/engine/action/tasks/applyCheckTask.ts +++ b/app/imports/api/engine/action/tasks/applyCheckTask.ts @@ -14,7 +14,6 @@ import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; export default async function applyCheckTask( task: CheckTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { - const prop = task.prop; const targetIds = task.targetIds; if (task.contest) { @@ -45,7 +44,7 @@ export default async function applyCheckTask( if (skill || ability) { // Create a new result after before triggers have run - result = new TaskResult(task.prop._id, task.targetIds); + result = new TaskResult(task.targetIds); action.results.push(result); } @@ -75,7 +74,7 @@ export default async function applyCheckTask( name: checkName, inline: true, ...dc !== null && { value: `DC **${dc}**` }, - ...prop?.silent && { silenced: prop.silent } + ...task?.silent && { silenced: task.silent } }, [targetId]); // Roll the dice @@ -109,7 +108,7 @@ export default async function applyCheckTask( name: rollName, value: `${resultPrefix}\n**${totalValue}**`, inline: true, - ...prop?.silent && { silenced: prop.silent } + ...task?.silent && { silenced: task.silent } }, [targetId]); // After check triggers diff --git a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts index aeeec912..43d7afee 100644 --- a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts +++ b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts @@ -10,8 +10,6 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString'; export default async function applyDamagePropTask( task: DamagePropTask, action: EngineAction, result: TaskResult, userInput ): Promise { - const prop = task.prop; - if (task.targetIds.length > 1) { throw 'This subtask can only be called on a single target'; } @@ -45,7 +43,7 @@ export default async function applyDamagePropTask( await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.before', userInput); // Create a new result after triggers have run - result = new TaskResult(task.prop._id, task.targetIds); + result = new TaskResult(task.targetIds); action.results.push(result); // Refetch the scope properties @@ -75,7 +73,7 @@ export default async function applyDamagePropTask( value: `${statName}${operation === 'set' ? ' set to' : ''}` + ` ${value}`, inline: true, - ...prop.silent && { silenced: true }, + ...task.silent && { silenced: true }, }, task.targetIds); } @@ -106,7 +104,7 @@ export default async function applyDamagePropTask( name: title, value: `${getPropertyTitle(targetProp)} set from ${targetProp.value} to ${value}`, inline: true, - ...prop.silent && { silenced: true }, + ...task.silent && { silenced: true }, }] }); if (targetId === action.creatureId) setScope(result, targetProp, newValue, damage); @@ -132,7 +130,7 @@ export default async function applyDamagePropTask( name: increment >= 0 ? 'Attribute damaged' : 'Attribute restored', value: `${numberToSignedString(-increment)} ${getPropertyTitle(targetProp)}`, inline: true, - ...prop.silent && { silenced: true }, + ...task.silent && { silenced: true }, }] }); if (targetId === action.creatureId) setScope(result, targetProp, newValue, damage); @@ -159,4 +157,4 @@ function setScope(result, targetProp, newValue, damage) { value: newValue, damage, }; -} \ No newline at end of file +} diff --git a/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts index 51d69405..6b6dde63 100644 --- a/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts +++ b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts @@ -25,7 +25,7 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: await applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.before', userInput); // Create a new result after before triggers have run - result = new TaskResult(task.prop._id, task.targetIds); + result = new TaskResult(task.targetIds); action.results.push(result); // Refetch the scope properties @@ -51,7 +51,7 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: contents: [{ name: getPropertyTitle(item) || 'Ammo', inline: false, - silenced: prop.silent, + ...prop?.silent && { silenced: true }, }] }, }); @@ -64,4 +64,4 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: await applyDefaultAfterPropTasks(action, item, task.targetIds, userInput); } return applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.afterChildren', userInput); -} \ No newline at end of file +} diff --git a/app/imports/api/engine/action/tasks/applyResetTask.ts b/app/imports/api/engine/action/tasks/applyResetTask.ts index f3bdea79..26dd24f0 100644 --- a/app/imports/api/engine/action/tasks/applyResetTask.ts +++ b/app/imports/api/engine/action/tasks/applyResetTask.ts @@ -75,13 +75,12 @@ export async function resetProperties(task: ResetTask, action: EngineAction, res for (const prop of attributes) { await applyTask(action, { - prop: task.prop || prop, targetIds: [action.creatureId], subtaskFn: 'damageProp', params: { title: getPropertyTitle(prop), operation: 'increment', - value: -prop.damage ?? 0, + value: -prop.damage || 0, targetProp: prop, }, }, userInput); @@ -152,7 +151,6 @@ async function resetHitDice(task: ResetTask, action: EngineAction, result: TaskR // Apply the damage prop task await applyTask(action, { - prop: task.prop || hd, targetIds: [creatureId], subtaskFn: 'damageProp', params: { diff --git a/app/imports/api/engine/action/tasks/applyTask.ts b/app/imports/api/engine/action/tasks/applyTask.ts index b3a809ff..494c1bf9 100644 --- a/app/imports/api/engine/action/tasks/applyTask.ts +++ b/app/imports/api/engine/action/tasks/applyTask.ts @@ -8,6 +8,7 @@ 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'; import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask'; +import applyCastSpellTask from '/imports/api/engine/action/tasks/applyCastSpellTask'; // DamagePropTask promises a number of actual damage done export default async function applyTask( @@ -37,7 +38,7 @@ export default async function applyTask( if (action.taskCount > 100) throw 'Only 100 properties can be applied at once'; if (task.subtaskFn) { - const result = new TaskResult(task.prop._id, task.targetIds); + const result = new TaskResult(task.targetIds); action.results.push(result); switch (task.subtaskFn) { case 'damageProp': @@ -48,6 +49,8 @@ export default async function applyTask( return applyCheckTask(task, action, result, inputProvider); case 'reset': return applyResetTask(task, action, result, inputProvider); + case 'castSpell': + return applyCastSpellTask(task, action, result, inputProvider); default: throw 'No case defined for the given subtaskFn'; } @@ -71,11 +74,11 @@ export default async function applyTask( } // Create a result an push it to the action results, pass it to the apply function to modify - const result = new TaskResult(task.prop._id, task.targetIds); + const result = new TaskResult(task.targetIds); result.scope[`#${prop.type}`] = { _propId: prop._id }; action.results.push(result); // Apply the property return applyProperties[prop.type]?.(task, action, result, inputProvider); } -} \ No newline at end of file +} diff --git a/app/imports/client/ui/creature/RestButton.vue b/app/imports/client/ui/creature/RestButton.vue index a977899f..ff6b3804 100644 --- a/app/imports/client/ui/creature/RestButton.vue +++ b/app/imports/client/ui/creature/RestButton.vue @@ -37,15 +37,15 @@ export default { methods: { rest(){ this.loading = true; - const emptyProp = { - _id: this.creatureId, - root: { id: this.creatureId }, - }; - doAction(emptyProp, this.$store, `rest-btn-${this.type}`, { - subtaskFn: 'reset', - prop: emptyProp, - targetIds: [this.creatureId], - eventName: this.type, + doAction({ + creatureId: this.creatureId, + $store: this.$store, + elementId: `rest-btn-${this.type}`, + task: { + subtaskFn: 'reset', + targetIds: [this.creatureId], + eventName: this.type, + }, }).catch(e => { console.error(e); }).finally(() => { diff --git a/app/imports/client/ui/creature/actions/ActionDialog.vue b/app/imports/client/ui/creature/actions/ActionDialog.vue index b911d253..7380f51f 100644 --- a/app/imports/client/ui/creature/actions/ActionDialog.vue +++ b/app/imports/client/ui/creature/actions/ActionDialog.vue @@ -41,38 +41,6 @@ - - - Cancel - - - - Step - - - {{ 'Apply Results' }} - - - {{ 'Start' }} - - @@ -88,6 +56,7 @@ import EngineActions from '/imports/api/engine/action/EngineActions'; import LogContent from '/imports/client/ui/log/LogContent.vue'; //import RollInput from '/imports/client/ui/creature/actions/input/RollInput.vue'; import TargetsInput from '/imports/client/ui/creature/actions/input/TargetsInput.vue'; +import CastSpellInput from '/imports/client/ui/creature/actions/input/CastSpellInput.vue'; export default { components: { @@ -98,6 +67,7 @@ export default { LogContent, //RollInput, TargetsInput, + CastSpellInput, }, props: { actionId: { @@ -161,7 +131,7 @@ export default { taskCount: undefined, }; applyAction( - this.actionResult, this, { simulate: true, stepThrough, task: this.task} + this.actionResult, this, { simulate: true, stepThrough} ).then(() => { this.actionDone = true; // If we aren't stepping through close the dialog and apply the action @@ -194,6 +164,7 @@ export default { this.activeInput = undefined; this.activeInputParams = {}; this.userInputReady = false; + console.log({savedInput}) resolve(savedInput); } }); @@ -248,6 +219,16 @@ export default { this.activeInput = 'check-input'; return this.promiseInput(); }, + async castSpell(suggestedParams) { + this.userInput = suggestedParams; + console.log(this.action); + console.log(this.action.root); + this.activeInputParams = { + creatureId: this.action.creatureId, + }; + this.activeInput = 'cast-spell-input'; + return this.promiseInput(); + }, } }; diff --git a/app/imports/client/ui/creature/actions/doAction.ts b/app/imports/client/ui/creature/actions/doAction.ts index de4e2768..293a78c1 100644 --- a/app/imports/client/ui/creature/actions/doAction.ts +++ b/app/imports/client/ui/creature/actions/doAction.ts @@ -6,32 +6,46 @@ import InputProvider from '/imports/api/engine/action/functions/userInput/InputP import applyAction from '/imports/api/engine/action/functions/applyAction'; import { runAction } from '/imports/api/engine/action/methods/runAction'; import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller'; +import { getSingleProperty } from '../../../../api/engine/loadCreatures'; + +type BaseDoActionParams = { + creatureId: string; + $store: Store; + elementId: string; +} + +type DoTaskParams = BaseDoActionParams & { + task: Task; + propId?: undefined; +} + +type DoActionParams = BaseDoActionParams & { + propId: string; + task?: undefined; +} /** * Apply an action on the client that first creates the action on both the client and server, then * simulates the action, opening the action dialog if necessary to get input from the user, saving * the decisions the user makes, then applying the action as a method call to the server with the * saved decisions, which will persist the action results. - * - * @param prop The property initializing the action, if no task is applied the property will be - * applied as the starting point of the action - * @param $store The Vuex store instance that has the dialog stack - * @param elementId The element to animate the dialog from if a dialog needs to open - * @param task The task to apply instead of applying the property itself */ -export default async function doAction( - prop: { _id: string, root: { id: string } }, - $store: Store, - elementId: string, - task?: Task, -) { +export default async function doAction({ propId, creatureId, $store, elementId, task }: DoActionParams | DoTaskParams) { + if (!task) { + if (!propId) throw new Meteor.Error('no-prop-id', 'Either propId or task must be provided'); + task = { + prop: getSingleProperty(creatureId, propId), + targetIds: [], + }; + } // Create the action const actionId = insertAction.call({ action: { - creatureId: prop.root.id, - rootPropId: prop._id, + creatureId, + task, results: [], taskCount: 0, + _decisions: [], } }); @@ -45,9 +59,9 @@ export default async function doAction( // Either way, call the action method afterwards try { const finishedAction = await applyAction( - action, getErrorOnInputRequestProvider(action._id), { simulate: true, task } + action, getErrorOnInputRequestProvider(action._id), { simulate: true } ); - return callActionMethod(finishedAction, task); + return callActionMethod(finishedAction); } catch (e) { if (e !== 'input-requested') throw e; return new Promise(resolve => { @@ -60,7 +74,7 @@ export default async function doAction( }, callback(action: EngineAction) { if (!action) return; - resolve(callActionMethod(action, task)); + resolve(callActionMethod(action)); return elementId; }, }); @@ -68,10 +82,9 @@ export default async function doAction( } } -const callActionMethod = (action: EngineAction, task?: Task) => { +const callActionMethod = (action: EngineAction) => { if (!action._id) throw new Meteor.Error('type-error', 'Action must have and _id'); - //@ts-expect-error callAsync not defined in types - return runAction.callAsync({ actionId: action._id, decisions: action._decisions, task }); + return runAction.callAsync({ actionId: action._id, decisions: action._decisions }); } const throwInputRequestedError = () => { @@ -86,6 +99,7 @@ function getErrorOnInputRequestProvider(actionId) { choose: throwInputRequestedError, advantage: throwInputRequestedError, check: throwInputRequestedError, + castSpell: throwInputRequestedError, } return errorOnInputRequest; } diff --git a/app/imports/client/ui/creature/actions/doClientAction.ts b/app/imports/client/ui/creature/actions/doClientAction.ts deleted file mode 100644 index 4b00d1fa..00000000 --- a/app/imports/client/ui/creature/actions/doClientAction.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Apply an action on the client that first creates the action on both the client and server, then - * simulates the action, opening the action dialog if necessary to get input from the user, saving - * the decisions the user makes, then applying the action as a method call to the server with the - * saved decisions, which will persist the action results. - */ - -import Task from '/imports/api/engine/action/tasks/Task'; - -export default function doClientAction(propIdOrTask: string | Task) { - -} diff --git a/app/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue b/app/imports/client/ui/creature/actions/input/CastSpellInput.vue similarity index 79% rename from app/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue rename to app/imports/client/ui/creature/actions/input/CastSpellInput.vue index 2fa78ad3..f94d4190 100644 --- a/app/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue +++ b/app/imports/client/ui/creature/actions/input/CastSpellInput.vue @@ -1,70 +1,64 @@ ../../../../../api/engine/action/methods/doCastSpell \ No newline at end of file + diff --git a/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue b/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue index 90dea6f6..af69866c 100644 --- a/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue +++ b/app/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue @@ -1,6 +1,7 @@