Refactored actions, 'cast a spell' task now works

This commit is contained in:
Thaum Rystra
2024-10-28 12:28:36 +02:00
parent 804c5f3aee
commit 8f8c9c28aa
39 changed files with 423 additions and 399 deletions

View File

@@ -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<EngineAction>('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,

View File

@@ -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);

View File

@@ -49,7 +49,6 @@ export default async function applyAdjustmentProperty(
return;
}
await applyTask(action, {
prop,
targetIds: damageTargetIds,
subtaskFn: 'damageProp',
params: {

View File

@@ -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: {

View File

@@ -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;
}

View File

@@ -38,7 +38,7 @@ type InputProvider = {
/**
* Get the details of casting a spell
*/
castSpell(suggestedParams: CastSpellParams): Promise<CastSpellParams>;
castSpell(suggestedParams: Partial<CastSpellParams>): Promise<CastSpellParams>;
}
export type Advantage = 0 | 1 | -1;

View File

@@ -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');

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;
};
}

View File

@@ -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 = {};

View File

@@ -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<void> {
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: {

View File

@@ -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<void> {
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

View File

@@ -10,8 +10,6 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString';
export default async function applyDamagePropTask(
task: DamagePropTask, action: EngineAction, result: TaskResult, userInput
): Promise<number> {
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,
};
}
}

View File

@@ -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);
}
}

View File

@@ -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: {

View File

@@ -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);
}
}
}