From 4993506ec98256195bd4162f8c1b439b28d96394 Mon Sep 17 00:00:00 2001 From: ThaumRystra Date: Sat, 25 Jan 2025 14:29:26 +0200 Subject: [PATCH] Fixed spell casting --- .../applyActionProperty.test.ts | 4 +- .../applyProperties/applyActionProperty.ts | 18 +- .../applyAdjustmentProperty.test.ts | 6 +- .../applyDamageProperty.test.ts | 10 +- .../engine/action/functions/applyAction.ts | 1 + .../functions/userInput/InputProvider.ts | 10 - .../getReplayChoicesInputProvider.ts | 3 - .../userInput/inputProviderForTests.testFn.ts | 5 +- .../functions/userInput/saveInputChoices.ts | 4 +- app/imports/api/engine/action/tasks/Task.ts | 19 +- .../api/engine/action/tasks/TaskResult.ts | 4 +- .../engine/action/tasks/applyCastSpellTask.ts | 119 ++--- .../action/tasks/applyDamagePropTask.ts | 4 +- .../ui/creature/actions/ActionDialog.vue | 4 +- .../client/ui/creature/actions/doAction.ts | 10 +- .../ui/dialogStack/DialogComponentIndex.js | 2 + .../client/ui/dialogStack/DialogStack.vue | 31 +- .../client/ui/dialogStack/dialogStackStore.js | 13 +- .../client/ui/log/TabletopLogContent.vue | 2 +- .../components/attributes/SpellSlotCard.vue | 22 +- .../attributes/SpellSlotListTile.vue | 33 +- .../spells/CastSpellWithSlotDialog.vue | 445 ++++++++++++++++++ .../ui/properties/viewers/ActionViewer.vue | 10 + .../ui/tabletop/TabletopLogStreamEntry.vue | 2 +- .../SelectedCreatureBar.vue | 15 +- 25 files changed, 628 insertions(+), 168 deletions(-) create mode 100644 app/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts index 7f03a562..70c25760 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.test.ts @@ -377,7 +377,7 @@ describe('Apply Action Properties', function () { { contents: [{ inline: true, - name: 'Attribute damaged', + name: 'Stat damaged', value: '−2 Resource Name', }], targetIds: [creatureId], @@ -435,7 +435,7 @@ describe('Apply Action Properties', function () { contents: [ { inline: true, - name: 'Attribute restored', + name: 'Stat restored', value: '+13 Attribute Reset By testEvent Event', }, ], diff --git a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts index d38dbddb..40fa8c28 100644 --- a/app/imports/api/engine/action/applyProperties/applyActionProperty.ts +++ b/app/imports/api/engine/action/applyProperties/applyActionProperty.ts @@ -18,8 +18,22 @@ export default async function applyActionProperty( task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider ): Promise { const prop = task.prop; + if (prop.type !== 'action' && prop.type !== 'spell') { + throw new Meteor.Error('wrong-property', `Expected an action or a spell, got ${prop.type} instead`); + } const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds; + // If the action is a a spell, make sure we have spell slot defined + if (prop.type === 'spell') { + const scope = await getEffectiveActionScope(action); + if (!('slotLevel' in scope)) { + result.pushScope = { + '~slotLevel': { value: prop.level }, + 'slotLevel': { value: prop.level }, + }; + } + } + //Log the name and summary, check that the property has enough resources to fire if (prop.summary?.text) { await recalculateInlineCalculations(prop.summary, action, 'reduce', userInput); @@ -31,7 +45,7 @@ export default async function applyActionProperty( }, targetIds); // Check Uses - if (prop.usesLeft <= 0) { + if (prop.usesLeft !== undefined && prop.usesLeft <= 0) { result.appendLog({ name: 'Error', value: `${getPropertyTitle(prop)} does not have enough uses left`, @@ -52,7 +66,7 @@ export default async function applyActionProperty( await spendResources(action, prop, targetIds, result, userInput); - const attack: CalculatedField = prop.attackRoll || prop.attackRollBonus; + const attack = prop.attackRoll; // Attack if there is an attack roll if (attack && attack.calculation) { diff --git a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts index 6d841889..9690e7d2 100644 --- a/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts +++ b/app/imports/api/engine/action/applyProperties/applyAdjustmentProperty.test.ts @@ -108,7 +108,7 @@ describe('Apply Adjustment Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Ability damaged', value: '−2 Attribute', } ], @@ -132,7 +132,7 @@ describe('Apply Adjustment Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Ability damaged', value: '−2 Attribute', } ], @@ -148,7 +148,7 @@ describe('Apply Adjustment Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Ability damaged', value: '−2 Attribute', } ], diff --git a/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts b/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts index 97f46eef..31ab341c 100644 --- a/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts +++ b/app/imports/api/engine/action/applyProperties/applyDamageProperty.test.ts @@ -115,7 +115,7 @@ describe('Apply Damage Properties', function () { }, { contents: [{ inline: true, - name: 'Attribute damaged', + name: 'Health bar damaged', value: '−13 Hit Points', }], updates: [ @@ -145,7 +145,7 @@ describe('Apply Damage Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Health bar damaged', value: '−14 Hit Points', } ], @@ -181,7 +181,7 @@ describe('Apply Damage Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Health bar damaged', value: '−14 Hit Points', } ], @@ -197,7 +197,7 @@ describe('Apply Damage Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Health bar damaged', value: '−14 Hit Points', } ], @@ -228,7 +228,7 @@ describe('Apply Damage Properties', function () { contents: [ { inline: true, - name: 'Attribute damaged', + name: 'Health bar damaged', value: '−22 Hit Points', } ], diff --git a/app/imports/api/engine/action/functions/applyAction.ts b/app/imports/api/engine/action/functions/applyAction.ts index 60b31b15..de30fa9c 100644 --- a/app/imports/api/engine/action/functions/applyAction.ts +++ b/app/imports/api/engine/action/functions/applyAction.ts @@ -40,6 +40,7 @@ export default async function applyAction(action: EngineAction, userInput: Input !action.task.targetIds?.length && action.tabletopId && 'prop' in action.task + && 'target' in action.task.prop && ( action.task.prop?.target === 'singleTarget' || action.task.prop?.target === 'multipleTargets' diff --git a/app/imports/api/engine/action/functions/userInput/InputProvider.ts b/app/imports/api/engine/action/functions/userInput/InputProvider.ts index 4da07d59..9d564b7e 100644 --- a/app/imports/api/engine/action/functions/userInput/InputProvider.ts +++ b/app/imports/api/engine/action/functions/userInput/InputProvider.ts @@ -35,10 +35,6 @@ type InputProvider = { * Get the details of a check or save */ check(suggestedParams: CheckParams): Promise; - /** - * Get the details of casting a spell - */ - castSpell(suggestedParams: Partial): Promise; } export type Advantage = 0 | 1 | -1; @@ -53,10 +49,4 @@ export type CheckParams = { targetAbilityVariableName?: string; } -export type CastSpellParams = { - spellId: string, - slotId: string | undefined, - ritual: boolean, -} - export default InputProvider; diff --git a/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts index f67de791..ae6a706b 100644 --- a/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts +++ b/app/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider.ts @@ -28,9 +28,6 @@ export default function getReplayChoicesInputProvider(actionId: string, decision check() { return Promise.resolve(decisionStack.pop()); }, - castSpell() { - 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 e91b159f..3a5d58ef 100644 --- a/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts +++ b/app/imports/api/engine/action/functions/userInput/inputProviderForTests.testFn.ts @@ -1,4 +1,4 @@ -import InputProvider, { CastSpellParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; const inputProviderForTests: InputProvider = { async targetIds(target, currentTargetIds = []) { @@ -42,9 +42,6 @@ const inputProviderForTests: InputProvider = { async check(suggestedParams) { return suggestedParams; }, - async castSpell(suggestedParams) { - return suggestedParams as CastSpellParams; - }, } export const critInputProvider: InputProvider = { diff --git a/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts index ad1baa45..5c328cfa 100644 --- a/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts +++ b/app/imports/api/engine/action/functions/userInput/saveInputChoices.ts @@ -13,8 +13,10 @@ export default function saveInputChoices(action: EngineAction, userInput: InputP } // For every function in the given input provider - for (const key in userInput) { + let key: keyof InputProvider; + for (key in userInput) { const oldFn = userInput[key]; + if (!oldFn) continue; // Make a new function that does the same thing, but saves the result to action._decisions const newFn = async (...args) => { const result = await oldFn(...args); diff --git a/app/imports/api/engine/action/tasks/Task.ts b/app/imports/api/engine/action/tasks/Task.ts index 0b008815..e3a1072f 100644 --- a/app/imports/api/engine/action/tasks/Task.ts +++ b/app/imports/api/engine/action/tasks/Task.ts @@ -1,3 +1,4 @@ +import { CreatureProperty, CreaturePropertyTypes } from '/imports/api/creature/creatureProperties/CreatureProperties'; import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask | CastSpellTask; @@ -9,14 +10,8 @@ type BaseTask = { silent?: boolean | undefined; } -type Prop = { - _id: string; - type: string; - [key: string]: any, -} - export type PropTask = BaseTask & { - prop: Prop; + prop: CreatureProperty; subtaskFn?: undefined; silent?: undefined; } @@ -30,13 +25,13 @@ export type DamagePropTask = BaseTask & { title?: string; operation: 'increment' | 'set'; value: number; - targetProp: Prop; + targetProp: CreatureProperty; }; } export type ItemAsAmmoTask = BaseTask & { subtaskFn: 'consumeItemAsAmmo'; - prop: Prop; + prop: CreatureProperty; silent?: undefined; params: { value: number; @@ -57,10 +52,12 @@ export type ResetTask = BaseTask & { } export type CastSpellTask = BaseTask & { - prop?: Prop | undefined; + prop: CreaturePropertyTypes['spell']; silent?: undefined; subtaskFn: 'castSpell'; params: { - spellId: string | undefined; + slotId: string | undefined; + ritual: boolean | undefined; + withoutSpellSlot: boolean | undefined; }; } diff --git a/app/imports/api/engine/action/tasks/TaskResult.ts b/app/imports/api/engine/action/tasks/TaskResult.ts index a9426fbe..dd614aa3 100644 --- a/app/imports/api/engine/action/tasks/TaskResult.ts +++ b/app/imports/api/engine/action/tasks/TaskResult.ts @@ -26,7 +26,7 @@ export default class TaskResult { this.scope = {}; } // Appends the log content to the latest mutation - appendLog(content: LogContent & { silenced: boolean }, targetIds: string[]) { + appendLog(content: LogContent & { silenced: boolean | undefined }, targetIds: string[]) { // Create a shallow copy of the content const logContent: LogContent = { ...content }; // remove false silenced properties @@ -42,7 +42,7 @@ export default class TaskResult { } latestMutation.contents.push(logContent); } - appendParserContextErrors(context: Context, targetIds) { + appendParserContextErrors(context: Context, targetIds: string[]) { if (!context.errors?.length) return; if (!this.mutations.length) { this.mutations.push({ targetIds, contents: [] }); diff --git a/app/imports/api/engine/action/tasks/applyCastSpellTask.ts b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts index 0907cd12..5f9c598d 100644 --- a/app/imports/api/engine/action/tasks/applyCastSpellTask.ts +++ b/app/imports/api/engine/action/tasks/applyCastSpellTask.ts @@ -2,32 +2,14 @@ import { EngineAction } from '/imports/api/engine/action/EngineActions'; 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 { getSingleProperty } from '/imports/api/engine/loadCreatures'; import applyTask from '/imports/api/engine/action/tasks/applyTask'; import applyActionProperty from '../applyProperties/applyActionProperty'; 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: 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', - silenced: false, - }, [action.creatureId]); - return; - } - // If the user changed the spell they are casting, use that as the prop - prop = getSingleProperty(action.creatureId, castOptions.spellId); + const prop = task.prop; if (!prop) { result.appendLog({ @@ -38,65 +20,62 @@ export default async function applySpellProperty( return; } let slotLevel = prop.level || 0; - // Get the slot being cast with - const slot = castOptions.slotId && getSingleProperty(action.creatureId, castOptions.slotId); - // Log casting method - 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, castOptions, userInput); - slotLevel = slot.spellSlotLevel?.value || 0; + let message = ''; + + if (task.params.withoutSpellSlot) { + message = `Casting at level ${slotLevel}` + } else if (task.params.ritual) { + message = `Ritual casting at level ${slotLevel}` + } else { + // Get the slot being cast with + const spellSlot = task.params.slotId && getSingleProperty(action.creatureId, task.params.slotId) || undefined; + // Ensure the slot exists + if (!spellSlot) { + result.appendLog({ + name: 'Error casting spell', + value: 'The chosen spell requires a spell slot to cast', + silenced: false, + }, [action.creatureId]); + return; + } + // And is the right type + if (spellSlot.type !== 'attribute' || spellSlot.attributeType !== 'spellSlot') { + result.appendLog({ + name: 'Error casting spell', + value: 'The chosen slot was not actually a spell slot', + silenced: false, + }, [action.creatureId]); + return; + } + // Spend the slot + await applyTask(action, { + targetIds: [action.creatureId], + subtaskFn: 'damageProp', + params: { + operation: 'increment', + value: 1, + targetProp: spellSlot, + }, + }, userInput); + slotLevel = spellSlot ? Number(spellSlot.spellSlotLevel?.value) || 0 : slotLevel; + message = `Casting using a level ${slotLevel} spell slot`; } + + // Log casting method + result.appendLog({ + name: message, + silenced: prop.silent, + }, task.targetIds); + // Add the slot level to the scope result.pushScope = { '~slotLevel': { value: slotLevel }, 'slotLevel': { value: slotLevel }, }; + // Run the rest of the spell as if it were an action return applyActionProperty({ prop, targetIds: task.targetIds, }, action, result, userInput); } - -function getSuggestedSpellSlotId(creatureId, prop) { - if (!prop) return; - const slots = getPropertiesOfType(creatureId, 'spellSlot') - .sort((a, b) => a.spellSlotLevel?.value - b.spellSlotLevel?.value) - .filter(slot => slot.spellSlotLevel.value > prop.level); - return slots[0]?._id; -} - -function logCastingMessage(slotLevel: number, castOptions, result: TaskResult, prop, targetIds: string[]) { - let message = ''; - // Determine which message to post - if (slotLevel) { - message = `Casting using a level ${slotLevel} spell slot` - } else if (prop.level) { - if (castOptions.ritual) { - message = `Ritual casting at level ${slotLevel}` - } else { - message = `Casting at level ${slotLevel}` - } - } - // Post the message - if (message) { - result.appendLog({ - name: `Casting at level ${slotLevel}`, - silenced: prop.silent, - }, targetIds); - } -} - -function spendSpellSlot(action, castOptions, userInput) { - const slot = getSingleProperty(action.creatureId, castOptions.slotId); - return applyTask(action, { - targetIds: [action.creatureId], - subtaskFn: 'damageProp', - params: { - operation: 'increment', - value: 1, - targetProp: slot, - }, - }, userInput); -} diff --git a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts index 039e9a5e..2930ce96 100644 --- a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts +++ b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts @@ -6,6 +6,7 @@ import { getEffectiveActionScope } from '/imports/api/engine/action/functions/ge import getPropertyTitle from '/imports/api/utility/getPropertyTitle'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import { lowerCase, upperFirst } from 'lodash'; export default async function applyDamagePropTask( task: DamagePropTask, action: EngineAction, result: TaskResult, userInput @@ -120,6 +121,7 @@ export default async function applyDamagePropTask( if (increment !== 0) { damage = currentDamage + increment; newValue = targetProp.total - damage; + const attributeTypeName = upperFirst(lowerCase(targetProp.attributeType)); // Write the results result.mutations.push({ targetIds: [targetId], @@ -129,7 +131,7 @@ export default async function applyDamagePropTask( type: targetProp.type, }], contents: [{ - name: increment >= 0 ? 'Attribute damaged' : 'Attribute restored', + name: increment >= 0 ? `${attributeTypeName} damaged` : `${attributeTypeName} restored`, value: `${numberToSignedString(-increment)} ${getPropertyTitle(targetProp)}`, inline: true, ...task.silent && { silenced: true }, diff --git a/app/imports/client/ui/creature/actions/ActionDialog.vue b/app/imports/client/ui/creature/actions/ActionDialog.vue index 56d6a4e1..d2a1a6c1 100644 --- a/app/imports/client/ui/creature/actions/ActionDialog.vue +++ b/app/imports/client/ui/creature/actions/ActionDialog.vue @@ -238,8 +238,8 @@ export default { diff --git a/app/imports/client/ui/properties/viewers/ActionViewer.vue b/app/imports/client/ui/properties/viewers/ActionViewer.vue index 40706223..2c9127ba 100644 --- a/app/imports/client/ui/properties/viewers/ActionViewer.vue +++ b/app/imports/client/ui/properties/viewers/ActionViewer.vue @@ -187,6 +187,16 @@ export default { }, methods: { doAction() { + if (this.model.type === 'spell') { + return this.$store.commit('pushDialogStack', { + component: 'cast-spell-with-slot-dialog', + elementId: 'cast-spell', + data: { + creatureId: this.model.root.id, + spellId: this.model._id, + }, + }); + } this.doActionLoading = true; doAction({ creatureId: this.model.root.id, diff --git a/app/imports/client/ui/tabletop/TabletopLogStreamEntry.vue b/app/imports/client/ui/tabletop/TabletopLogStreamEntry.vue index cae729ba..a07cb96d 100644 --- a/app/imports/client/ui/tabletop/TabletopLogStreamEntry.vue +++ b/app/imports/client/ui/tabletop/TabletopLogStreamEntry.vue @@ -1,6 +1,6 @@