Fixed spell casting
This commit is contained in:
@@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -18,8 +18,22 @@ export default async function applyActionProperty(
|
||||
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
|
||||
): Promise<void> {
|
||||
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) {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -35,10 +35,6 @@ type InputProvider = {
|
||||
* Get the details of a check or save
|
||||
*/
|
||||
check(suggestedParams: CheckParams): Promise<CheckParams>;
|
||||
/**
|
||||
* Get the details of casting a spell
|
||||
*/
|
||||
castSpell(suggestedParams: Partial<CastSpellParams>): Promise<CastSpellParams>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -238,8 +238,8 @@ export default {
|
||||
|
||||
<style lang="css" scoped>
|
||||
.action-dialog {
|
||||
max-height: min(100%, 800px);
|
||||
max-width: min(100%, 1000px);
|
||||
max-height: min(100vh, 800px);
|
||||
max-width: min(100vh, 1000px);
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ type BaseDoActionParams = {
|
||||
$store: Store<any>;
|
||||
elementId: string;
|
||||
callback?: (action: EngineAction) => void;
|
||||
replaceDialog?: boolean;
|
||||
}
|
||||
|
||||
type DoTaskParams = BaseDoActionParams & {
|
||||
@@ -33,7 +34,9 @@ type DoActionParams = BaseDoActionParams & {
|
||||
* 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.
|
||||
*/
|
||||
export default async function doAction({ propId, creatureId, $store, elementId, task, targetIds, callback }: DoActionParams | DoTaskParams): Promise<any | void> {
|
||||
export default async function doAction({
|
||||
propId, creatureId, $store, elementId, task, targetIds, callback, replaceDialog
|
||||
}: DoActionParams | DoTaskParams): Promise<any | void> {
|
||||
if (!task) {
|
||||
targetIds ??= [];
|
||||
if (!propId) throw new Meteor.Error('no-prop-id', 'Either propId or task must be provided');
|
||||
@@ -73,7 +76,7 @@ export default async function doAction({ propId, creatureId, $store, elementId,
|
||||
} catch (e) {
|
||||
if (e !== 'input-requested') throw e;
|
||||
return new Promise<void>((resolve) => {
|
||||
$store.commit('pushDialogStack', {
|
||||
$store.commit(replaceDialog ? 'replaceDialog' : 'pushDialogStack', {
|
||||
component: 'action-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
@@ -81,7 +84,7 @@ export default async function doAction({ propId, creatureId, $store, elementId,
|
||||
task,
|
||||
actionFinishedCallback: resolve,
|
||||
},
|
||||
callback(action) {
|
||||
callback(action: any) {
|
||||
resolve();
|
||||
return callback?.(action);
|
||||
},
|
||||
@@ -107,7 +110,6 @@ function getErrorOnInputRequestProvider(actionId: string) {
|
||||
choose: throwInputRequestedError,
|
||||
advantage: throwInputRequestedError,
|
||||
check: throwInputRequestedError,
|
||||
castSpell: throwInputRequestedError,
|
||||
}
|
||||
return errorOnInputRequest;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import TransferOwnershipDialog from '/imports/client/ui/sharing/TransferOwnershi
|
||||
|
||||
// Lazily load less common dialogs
|
||||
const ArchiveDialog = () => import('/imports/client/ui/creature/archive/ArchiveDialog.vue');
|
||||
const CastSpellWithSlotDialog = () => import('/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue');
|
||||
const CharacterImportDialog = () => import('/imports/client/ui/creature/character/CharacterImportDialog.vue');
|
||||
const CreatureFromLibraryDialog = () => import('/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue');
|
||||
const DeleteUserAccountDialog = () => import('/imports/client/ui/user/DeleteUserAccountDialog.vue');
|
||||
@@ -41,6 +42,7 @@ const UsernameDialog = () => import('/imports/client/ui/user/UsernameDialog.vue'
|
||||
export default {
|
||||
ActionDialog,
|
||||
ArchiveDialog,
|
||||
CastSpellWithSlotDialog,
|
||||
CharacterCreationDialog,
|
||||
CharacterImportDialog,
|
||||
CharacterSheetDialog,
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
v-bind="dialog.data"
|
||||
class="unsized-dialog dialog-component"
|
||||
:data-element-id="dialog.elementId"
|
||||
:data-id="dialog._id"
|
||||
:data-index="index"
|
||||
:style="getDialogStyle(index)"
|
||||
:elevation="6"
|
||||
@@ -37,6 +38,7 @@
|
||||
:ref="index"
|
||||
class="dialog"
|
||||
:data-element-id="dialog.elementId"
|
||||
:data-id="dialog._id"
|
||||
:data-index="index"
|
||||
:style="getDialogStyle(index)"
|
||||
:elevation="6"
|
||||
@@ -74,6 +76,7 @@
|
||||
data(){return {
|
||||
hiddenElements: [],
|
||||
shake: false,
|
||||
leavingPromise: undefined,
|
||||
}},
|
||||
computed: {
|
||||
dialogs(){
|
||||
@@ -135,15 +138,16 @@
|
||||
let stackLength = this.$store.state.dialogStack.dialogs.length - offset;
|
||||
if (stackLength){
|
||||
let topDialog = this.$refs[stackLength - 1][0];
|
||||
// First look in the active window, then look elsewhere
|
||||
return topDialog.$el.querySelector(`.v-window-item--active [data-id='${elementId}']`) ??
|
||||
topDialog.$el.querySelector(`[data-id='${elementId}']`);
|
||||
return topDialog.$el.querySelector(`.v-window-item--active [data-id='${elementId}']`)
|
||||
?? topDialog.$el.querySelector(`[data-id='${elementId}']`)
|
||||
?? document.querySelector(`.v-window-item--active [data-id='${elementId}']`)
|
||||
?? document.querySelector(`[data-id='${elementId}']`);
|
||||
} else {
|
||||
return document.querySelector(`.v-window-item--active [data-id='${elementId}']`) ??
|
||||
document.querySelector(`[data-id='${elementId}']`);
|
||||
return document.querySelector(`.v-window-item--active [data-id='${elementId}']`)
|
||||
?? document.querySelector(`[data-id='${elementId}']`);
|
||||
}
|
||||
},
|
||||
async enter(target, done){
|
||||
async enter(target, done) {
|
||||
if (!target || !target.attributes['data-element-id']){
|
||||
done();
|
||||
return;
|
||||
@@ -194,11 +198,9 @@
|
||||
source.style.transition = originalStyle.sourceTransition;
|
||||
setTimeout(done, 300 / animationSpeed);
|
||||
},
|
||||
leave(target, done){
|
||||
async leave(target, done) {
|
||||
// Give minimongo time to update documents we might need to animate to
|
||||
setTimeout(() => this.doLeave(target, done));
|
||||
},
|
||||
async doLeave(target, done){
|
||||
await new Promise(requestAnimationFrame);
|
||||
let elementId;
|
||||
let hiddenElement = this.hiddenElements.pop();
|
||||
let returnElementId = await this.$store.state.dialogStack.currentReturnElement;
|
||||
@@ -211,11 +213,14 @@
|
||||
}
|
||||
elementId = target.attributes['data-element-id'].value;
|
||||
}
|
||||
const replacing = this.$store.state.dialogStack.replacingDialog === target.attributes['data-id'].value;
|
||||
let source = this.getTopElementByDataId(elementId);
|
||||
if (!source){
|
||||
console.warn(`Can't find source for ${elementId}`);
|
||||
if (!source || replacing){
|
||||
if (hiddenElement) hiddenElement.style.opacity = '';
|
||||
else console.warn('No hidden element to reveal', hiddenElement);
|
||||
// Just fade out gracefully
|
||||
target.style.transition = 'all 0.3s ease';
|
||||
target.style.opacity = '0';
|
||||
await timeout(300 / animationSpeed);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const dialogStackStore = {
|
||||
dialogs: [],
|
||||
currentResult: null,
|
||||
currentReturnElement: null,
|
||||
replacingDialog: null,
|
||||
},
|
||||
mutations: {
|
||||
pushDialogStack(state, { component, data, elementId, callback }) {
|
||||
@@ -23,17 +24,21 @@ const dialogStackStore = {
|
||||
});
|
||||
updateHistory();
|
||||
},
|
||||
replaceDialog(state, { component, data }) {
|
||||
replaceDialog(state, { component, data, elementId, callback }) {
|
||||
if (!state.dialogs.length) {
|
||||
throw new Meteor.Error('can\'t replace dialog if no dialogs are open');
|
||||
}
|
||||
let currentDialog = state.dialogs[state.dialogs.length - 1]
|
||||
state.replacingDialog = currentDialog._id;
|
||||
Vue.set(state.dialogs, state.dialogs.length - 1, {
|
||||
_id: currentDialog._id,
|
||||
_id: Random.id(),
|
||||
component,
|
||||
data,
|
||||
elementId: currentDialog.elementId,
|
||||
callback: currentDialog.callback,
|
||||
elementId: elementId || currentDialog._id,
|
||||
callback: (...args) => {
|
||||
callback?.(...args);
|
||||
return currentDialog.callback?.(...args);
|
||||
},
|
||||
});
|
||||
},
|
||||
popDialogStackMutation(state, result) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
v-for="(contentGroup, index) in contentByTargetId"
|
||||
:key="index"
|
||||
class="d-flex justify-space-between"
|
||||
class="d-flex justify-space-between mb-2"
|
||||
>
|
||||
<div class="d-flex flex-wrap">
|
||||
<div
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
|
||||
<script lang="js">
|
||||
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||
import doAction from '/imports/client/ui/creature/actions/doAction';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -56,23 +54,19 @@ export default {
|
||||
}},
|
||||
methods: {
|
||||
castSpell() {
|
||||
this.castSpellLoading = true;
|
||||
doAction({
|
||||
creatureId: this.model.root.id,
|
||||
propId: this.model._id,
|
||||
$store: this.$store,
|
||||
elementId: `spell-slot-card-${this.model._id}`
|
||||
}).catch(error => {
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
console.error(error);
|
||||
}).finally(() => {
|
||||
this.castSpellLoading = false;
|
||||
// push spell cast dialog
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'cast-spell-with-slot-dialog',
|
||||
elementId: 'spell-slot-card',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
});
|
||||
},
|
||||
clickProperty({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: 'cast-spell-btn',
|
||||
elementId: `spell-slot-card-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<v-list-item
|
||||
:key="model._id"
|
||||
:data-id="`spell-slot-list-tile-${model._id}`"
|
||||
:disabled="disabled"
|
||||
class="spell-slot-list-tile"
|
||||
v-bind="$attrs"
|
||||
v-on="hasClickListener ? {click} : {}"
|
||||
@@ -27,13 +28,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-else-if="canEdit"
|
||||
class="layout align-center slot-bubbles"
|
||||
>
|
||||
<smart-btn
|
||||
v-for="i in model.total"
|
||||
:key="i"
|
||||
:disabled="disabled(i)"
|
||||
icon
|
||||
single-click
|
||||
@click="ack => damageProperty({
|
||||
@@ -51,6 +51,23 @@
|
||||
</v-icon>
|
||||
</smart-btn>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="layout align-center slot-bubbles view-only"
|
||||
:class="{'disabled-icon': disabled}"
|
||||
>
|
||||
<v-icon
|
||||
v-for="i in model.total"
|
||||
:key="i"
|
||||
class="ma-1"
|
||||
>
|
||||
{{
|
||||
i > model.value ?
|
||||
'mdi-radiobox-blank' :
|
||||
'mdi-radiobox-marked'
|
||||
}}
|
||||
</v-icon>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title v-else>
|
||||
<code>
|
||||
@@ -80,20 +97,22 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
dark: Boolean,
|
||||
viewOnly: Boolean,
|
||||
disabled: Boolean,
|
||||
},
|
||||
computed: {
|
||||
hasClickListener() {
|
||||
return this.$listeners && !!this.$listeners.click;
|
||||
},
|
||||
canEdit() {
|
||||
return this.context.editPermission && !this.viewOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
signed: numberToSignedString,
|
||||
click(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
disabled(i) {
|
||||
return !this.context.editPermission;
|
||||
},
|
||||
damageProperty({ type, value, ack }) {
|
||||
const model = this.model;
|
||||
doAction({
|
||||
@@ -143,6 +162,10 @@ export default {
|
||||
background: #515151 !important;
|
||||
}
|
||||
|
||||
.disabled-icon {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Cast a Spell
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
hide-details
|
||||
:value="searchValue"
|
||||
:error-messages="searchError"
|
||||
:debounce="200"
|
||||
@change="searchChanged"
|
||||
/>
|
||||
<v-menu
|
||||
v-model="filterMenuOpen"
|
||||
left
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
:class="{'primary--text': filtersApplied}"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-filter</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="filter in booleanFilters"
|
||||
:key="filter.name"
|
||||
style="height: 52px;"
|
||||
>
|
||||
<v-checkbox
|
||||
v-model="filter.enabled"
|
||||
style="flex-grow: 0; margin-right: 8px;"
|
||||
/>
|
||||
<v-switch
|
||||
v-model="filter.value"
|
||||
:disabled="!filter.enabled"
|
||||
:label="filter.name"
|
||||
/>
|
||||
</v-list-item>
|
||||
<div class="layout">
|
||||
<v-btn
|
||||
text
|
||||
@click="clearBooleanFilters"
|
||||
>
|
||||
Clear
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
class="primary--text"
|
||||
@click="filterMenuOpen = false"
|
||||
>
|
||||
Done
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
<split-list-layout>
|
||||
<template slot="left">
|
||||
<div
|
||||
key="slot-title"
|
||||
class="text-h6 my-3"
|
||||
>
|
||||
Slot
|
||||
</div>
|
||||
<v-list-item-group
|
||||
key="slot-list"
|
||||
v-model="selectedSlotId"
|
||||
>
|
||||
<v-list-item
|
||||
key="cantrip-dummy-slot"
|
||||
class="spell-slot-list-tile"
|
||||
:class="{ 'primary--text': selectedSlotId === 'no-slot' }"
|
||||
value="no-slot"
|
||||
:disabled="!canCastSpellWithSlot(selectedSpell, 'no-slot')"
|
||||
@click="selectedSlotId = 'no-slot'"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Cast without spell slot
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
key="ritual-dummy-slot"
|
||||
class="spell-slot-list-tile"
|
||||
:class="{ 'primary--text': selectedSlotId === 'ritual' }"
|
||||
value="ritual"
|
||||
:disabled="!canCastSpellWithSlot(selectedSpell, 'ritual')"
|
||||
@click="selectedSlotId = 'ritual'"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Cast as ritual
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<spell-slot-list-tile
|
||||
v-for="spellSlot in spellSlots"
|
||||
:key="spellSlot._id"
|
||||
:model="spellSlot"
|
||||
:class="{ 'primary--text': selectedSlotId === spellSlot._id }"
|
||||
:value="spellSlot._id"
|
||||
:disabled="!canCastSpellWithSlot(selectedSpell, spellSlot._id, spellSlot)"
|
||||
view-only
|
||||
@click="selectedSlotId = spellSlot._id"
|
||||
/>
|
||||
</v-list-item-group>
|
||||
</template>
|
||||
<template slot="right">
|
||||
<div
|
||||
key="spell-title-right"
|
||||
class="text-h6 my-3"
|
||||
>
|
||||
Spell
|
||||
</div>
|
||||
<v-list-item-group
|
||||
key="slot-list-right"
|
||||
v-model="selectedSpellId"
|
||||
>
|
||||
<template v-for="spell in computedSpells">
|
||||
<v-subheader
|
||||
v-if="spell.isSubheader"
|
||||
:key="`${spell.level}-header`"
|
||||
class="item"
|
||||
>
|
||||
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
||||
</v-subheader>
|
||||
<spell-list-tile
|
||||
v-else
|
||||
:key="spell._id"
|
||||
hide-handle
|
||||
show-info-button
|
||||
:model="spell"
|
||||
:value="spell._id"
|
||||
:class="{ 'primary--text': selectedSpellId === spell._id }"
|
||||
:disabled="!canCastSpellWithSlot(spell, selectedSlotId, selectedSlot)"
|
||||
@show-info="spellDialog(spell._id)"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item-group>
|
||||
</template>
|
||||
</split-list-layout>
|
||||
<template slot="actions">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
:disabled="!canCast"
|
||||
class="mx-2 px-4"
|
||||
color="primary"
|
||||
data-id="cast-spell-dialog-btn"
|
||||
@click="cast"
|
||||
>
|
||||
Cast
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import SplitListLayout from '/imports/client/ui/properties/components/attributes/SplitListLayout.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import spellsWithSubheaders from '/imports/client/ui/properties/components/spells/spellsWithSubheaders';
|
||||
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||
import SpellListTile from '/imports/client/ui/properties/components/spells/SpellListTile.vue';
|
||||
import { find } from 'lodash';
|
||||
import doAction from '/imports/client/ui/creature/actions/doAction';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
|
||||
const slotFilter = {
|
||||
type: 'attribute',
|
||||
attributeType: 'spellSlot',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
overridden: { $ne: true },
|
||||
'spellSlotLevel.value': { $gte: 1 },
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
SplitListLayout,
|
||||
SpellSlotListTile,
|
||||
SpellListTile,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slotId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
spellId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchString: undefined,
|
||||
selectedSlotId: this.slotId,
|
||||
selectedSpellId: this.spellId,
|
||||
selectedSlot: undefined,
|
||||
selectedSpell: undefined,
|
||||
searchValue: undefined,
|
||||
searchError: undefined,
|
||||
filterMenuOpen: false,
|
||||
booleanFilters: {
|
||||
verbal: { name: 'Verbal', enabled: false, value: true },
|
||||
somatic: { name: 'Somatic', enabled: false, value: true },
|
||||
material: { name: 'Material', enabled: false, value: true },
|
||||
concentration: { name: 'Concentration', enabled: false, value: true },
|
||||
ritual: { name: 'Ritual', enabled: false, value: true },
|
||||
},
|
||||
}
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['editPermission'],
|
||||
},
|
||||
computed: {
|
||||
computedSpells() {
|
||||
return spellsWithSubheaders(this.spells);
|
||||
},
|
||||
canCast() {
|
||||
if (!this.selectedSpell || !this.selectedSlotId) return false;
|
||||
return this.canCastSpellWithSlot(
|
||||
this.selectedSpell, this.selectedSlotId, this.selectedSlot
|
||||
);
|
||||
},
|
||||
filtersApplied() {
|
||||
for (let key in this.booleanFilters) {
|
||||
if (this.booleanFilters[key].enabled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedSpellId: {
|
||||
handler(spellId) {
|
||||
this.selectedSpell = CreatureProperties.findOne(spellId)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectedSpell: {
|
||||
handler(spell) {
|
||||
if (!spell) return;
|
||||
if (this.selectedSlotId && this.canCastSpellWithSlot(
|
||||
spell, this.selectedSlotId, this.selectedSlot
|
||||
)) return;
|
||||
if (
|
||||
(spell.level === 0 || spell.castWithoutSpellSlots)
|
||||
) {
|
||||
this.selectedSlotId = 'no-slot';
|
||||
} else {
|
||||
const newSlot = find(
|
||||
CreatureProperties.find({
|
||||
'root.id': this.creatureId,
|
||||
...slotFilter
|
||||
}, {
|
||||
sort: { 'spellSlotLevel.value': 1, order: 1 },
|
||||
}).fetch(),
|
||||
slot => {
|
||||
return this.canCastSpellWithSlot(spell, slot._id, slot)
|
||||
}
|
||||
);
|
||||
if (newSlot) {
|
||||
this.selectedSlotId = newSlot._id;
|
||||
} else if (spell.ritual) {
|
||||
this.selectedSlotId = 'ritual';
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
selectedSlotId: {
|
||||
handler(slotId) {
|
||||
this.selectedSlot = CreatureProperties.findOne(slotId);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectedSlot: {
|
||||
handler(slot) {
|
||||
if (!slot) return;
|
||||
if (!this.selectedSpell) return;
|
||||
if (this.selectedSpell.level > slot.spellSlotLevel.value) {
|
||||
this.selectedSpellId = undefined;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.selectedSpellId) {
|
||||
this.$vuetify.goTo('.spell.v-list-item--active', { container: '.right' });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearBooleanFilters() {
|
||||
for (let key in this.booleanFilters) {
|
||||
this.booleanFilters[key].enabled = false;
|
||||
}
|
||||
},
|
||||
spellDialog(_id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `spell-info-btn-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
searchChanged(val, ack) {
|
||||
this.searchValue = val;
|
||||
setTimeout(ack, 200);
|
||||
},
|
||||
canCastSpellWithSlot(spell, slotId, slot) {
|
||||
if (slot && !slot.value) return false;
|
||||
if (!spell) return true;
|
||||
if (!slotId) return true;
|
||||
if (
|
||||
spell.castWithoutSpellSlots &&
|
||||
spell.insufficientResources
|
||||
) return false;
|
||||
if (spell.ritual && slotId === 'ritual') return true;
|
||||
if (!spell.level || spell.castWithoutSpellSlots) {
|
||||
// Cantrips and no-slot spells
|
||||
return slotId && slotId === 'no-slot'
|
||||
} else {
|
||||
// Levelled spells
|
||||
return slotId !== 'no-slot' && slot && spell && (
|
||||
spell.level <= slot.spellSlotLevel.value
|
||||
);
|
||||
}
|
||||
},
|
||||
cast() {
|
||||
const spellId = this.selectedSpellId;
|
||||
let slotId = this.selectedSlotId;
|
||||
const ritual = slotId === 'ritual';
|
||||
const withoutSpellSlot = slotId === 'no-slot';
|
||||
if (ritual || withoutSpellSlot) slotId = undefined;
|
||||
const $store = this.$store;
|
||||
doAction({
|
||||
creatureId: this.creatureId,
|
||||
$store,
|
||||
task: {
|
||||
subtaskFn: 'castSpell',
|
||||
prop: CreatureProperties.findOne(spellId),
|
||||
params: {
|
||||
slotId,
|
||||
ritual,
|
||||
withoutSpellSlot,
|
||||
},
|
||||
},
|
||||
elementId: 'cast-spell-dialog-btn',
|
||||
replaceDialog: true,
|
||||
}).catch(error => {
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.creatureId, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
spells() {
|
||||
let filter = {
|
||||
'root.id': this.creatureId,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
$or: [
|
||||
{ prepared: true },
|
||||
{ alwaysPrepared: true },
|
||||
],
|
||||
};
|
||||
|
||||
// Apply the filters from the filter menu
|
||||
for (let key in this.booleanFilters) {
|
||||
if (this.booleanFilters[key].enabled) {
|
||||
let value = this.booleanFilters[key].value;
|
||||
if (key === 'material') {
|
||||
filter[key] = { $exists: this.booleanFilters[key].value };
|
||||
} else {
|
||||
filter[key] = value ? true : { $ne: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply the search string to the name field
|
||||
if (this.searchValue) {
|
||||
filter.name = {
|
||||
$regex: this.searchValue.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
$options: 'i'
|
||||
};
|
||||
}
|
||||
return CreatureProperties.find(filter, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
spellSlots() {
|
||||
return CreatureProperties.find({
|
||||
'root.id': this.creatureId,
|
||||
...slotFilter
|
||||
}, {
|
||||
sort: { 'spellSlotLevel.value': 1, order: 1 },
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.v-list {
|
||||
flex-basis: 200px;
|
||||
}
|
||||
|
||||
.v-list.spells {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
class="pa-2 pt-0 my-1 rounded-sm"
|
||||
class="px-2 my-1 rounded-sm"
|
||||
:data-id="model.actionId"
|
||||
>
|
||||
<v-list-item
|
||||
|
||||
@@ -287,16 +287,11 @@ export default {
|
||||
openStandardAction(standardId) {
|
||||
this.menuOpen = false;
|
||||
if (standardId === 'cast-spell') {
|
||||
doAction({
|
||||
creatureId: this.creatureId,
|
||||
$store: this.$store,
|
||||
elementId: standardId,
|
||||
task: {
|
||||
subtaskFn: 'castSpell',
|
||||
targetIds: [],
|
||||
params: {
|
||||
spellId: undefined,
|
||||
},
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'cast-spell-with-slot-dialog',
|
||||
elementId: 'cast-spell',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user