Started implementing spells in action engine
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -4,6 +4,9 @@
|
||||
"armor",
|
||||
"autorun",
|
||||
"blackbox",
|
||||
"cantrip",
|
||||
"Cantrips",
|
||||
"Crit",
|
||||
"Crits",
|
||||
"cyrb",
|
||||
"denormalize",
|
||||
@@ -19,6 +22,7 @@
|
||||
"Ruleset",
|
||||
"snackbars",
|
||||
"Spellcasting",
|
||||
"Subheaders",
|
||||
"thumbhash",
|
||||
"uncomputed",
|
||||
"untarget",
|
||||
@@ -26,4 +30,4 @@
|
||||
"Vuex",
|
||||
"walkdown"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||
import TaskResult from '../tasks/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';
|
||||
|
||||
export default async function applyFolderProperty(
|
||||
task: PropTask, 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,
|
||||
slotId: prop?.castWithoutSpellSlots
|
||||
? undefined
|
||||
: getSuggestedSpellSlotId(action.creatureId, prop),
|
||||
ritual: false,
|
||||
});
|
||||
// If the user changed the spell they are casting, use that as the prop
|
||||
prop = getSingleProperty(action.creatureId, castOptions.spellId);
|
||||
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, prop, castOptions, userInput);
|
||||
slotLevel = slot.spellSlotLevel?.value || 0;
|
||||
}
|
||||
// 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(task, 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}`
|
||||
}, targetIds);
|
||||
}
|
||||
}
|
||||
|
||||
function spendSpellSlot(action, prop, castOptions, userInput) {
|
||||
const slot = getSingleProperty(action.creatureId, castOptions.slotId);
|
||||
return applyTask(action, {
|
||||
prop,
|
||||
targetIds: [action.creatureId],
|
||||
subtaskFn: 'damageProp',
|
||||
params: {
|
||||
operation: 'increment',
|
||||
value: 1,
|
||||
targetProp: slot,
|
||||
},
|
||||
}, userInput);
|
||||
}
|
||||
@@ -35,6 +35,10 @@ type InputProvider = {
|
||||
* Get the details of a check or save
|
||||
*/
|
||||
check(suggestedParams: CheckParams): Promise<CheckParams>;
|
||||
/**
|
||||
* Get the details of casting a spell
|
||||
*/
|
||||
castSpell(suggestedParams: CastSpellParams): Promise<CastSpellParams>;
|
||||
}
|
||||
|
||||
export type Advantage = 0 | 1 | -1;
|
||||
@@ -49,4 +53,10 @@ export type CheckParams = {
|
||||
targetAbilityVariableName?: string;
|
||||
}
|
||||
|
||||
export type CastSpellParams = {
|
||||
spellId: string,
|
||||
slotId: string | undefined,
|
||||
ritual: boolean,
|
||||
}
|
||||
|
||||
export default InputProvider;
|
||||
|
||||
@@ -28,6 +28,9 @@ export default function getReplayChoicesInputProvider(actionId: string, decision
|
||||
check() {
|
||||
return Promise.resolve(decisionStack.pop());
|
||||
},
|
||||
castSpell() {
|
||||
return Promise.resolve(decisionStack.pop());
|
||||
},
|
||||
}
|
||||
return replaySavedInput;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ const inputProviderForTests: InputProvider = {
|
||||
},
|
||||
async check(suggestedParams) {
|
||||
return suggestedParams;
|
||||
}
|
||||
},
|
||||
async castSpell(suggestedParams) {
|
||||
return suggestedParams;
|
||||
},
|
||||
}
|
||||
|
||||
export default inputProviderForTests;
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import {
|
||||
getPropertyAncestors, getPropertyDescendants
|
||||
} from '/imports/api/engine/loadCreatures';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
|
||||
// TODO Migrate this to the new action engine
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCastSpell',
|
||||
validate: new SimpleSchema({
|
||||
spellId: SimpleSchema.RegEx.Id,
|
||||
slotId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: 20,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
scope: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
|
||||
console.warn('Do cast spell not implemented');
|
||||
return;
|
||||
// Get action context
|
||||
let spell = CreatureProperties.findOne(spellId);
|
||||
const creatureId = spell.root.id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
});
|
||||
|
||||
const ancestors = getPropertyAncestors(creatureId, spell._id);
|
||||
ancestors.sort((a, b) => a.order - b.order);
|
||||
|
||||
const properties = getPropertyDescendants(creatureId, spell._id);
|
||||
properties.push(spell);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Spend the appropriate slot
|
||||
let slotLevel = spell.level || 0;
|
||||
let slot;
|
||||
|
||||
// If a spell requires a slot, make sure a slot is spent
|
||||
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
|
||||
slot = CreatureProperties.findOne(slotId);
|
||||
if (!slot) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot not found to cast spell');
|
||||
}
|
||||
if (!slot.value) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot depleted');
|
||||
}
|
||||
if (slot.attributeType !== 'spellSlot') {
|
||||
throw new Meteor.Error('Not a slot',
|
||||
'The given property is not a valid spell slot');
|
||||
}
|
||||
if (!slot.spellSlotLevel?.value) {
|
||||
throw new Meteor.Error('No slot level',
|
||||
'Slot does not have a spell slot level');
|
||||
}
|
||||
if (slot.spellSlotLevel.value < spell.level) {
|
||||
throw new Meteor.Error('Slot too small',
|
||||
'Slot is not large enough to cast spell');
|
||||
}
|
||||
slotLevel = slot.spellSlotLevel.value;
|
||||
damagePropertyWork({
|
||||
prop: slot,
|
||||
operation: 'increment',
|
||||
value: 1,
|
||||
actionContext,
|
||||
});
|
||||
}
|
||||
|
||||
// Post the slot level spent to the log
|
||||
if (slot?.spellSlotLevel?.value) {
|
||||
actionContext.addLog({
|
||||
name: `Casting using a level ${slotLevel} spell slot`
|
||||
});
|
||||
} else if (slotLevel) {
|
||||
if (ritual) {
|
||||
actionContext.addLog({
|
||||
name: `Ritual casting at level ${slotLevel}`
|
||||
});
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
actionContext.scope['slotLevel'] = { value: slotLevel };
|
||||
actionContext.scope['~slotLevel'] = { value: slotLevel };
|
||||
|
||||
// Do the action
|
||||
doActionWork({
|
||||
properties, ancestors, actionContext, methodScope: scope,
|
||||
});
|
||||
|
||||
// Force the characters involved to recalculate
|
||||
Creatures.update({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default doAction;
|
||||
@@ -7,10 +7,31 @@ 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: null, //TODO validate this
|
||||
validate: new SimpleSchema({
|
||||
actionId: String,
|
||||
decisions: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'decisions.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
task: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run: async function ({ actionId, decisions = [], task }: { actionId: string, decisions?: any[], task?: Task }) {
|
||||
// Get the action
|
||||
const action = await EngineActions.findOneAsync(actionId);
|
||||
|
||||
@@ -37,7 +37,7 @@ const magicSchools = [
|
||||
'transmutation',
|
||||
];
|
||||
|
||||
let SpellSchema = new SimpleSchema({})
|
||||
const SpellSchema = new SimpleSchema({})
|
||||
.extend(ActionSchema)
|
||||
.extend({
|
||||
name: {
|
||||
@@ -115,10 +115,10 @@ let SpellSchema = new SimpleSchema({})
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlySpellSchema = new SimpleSchema()
|
||||
const ComputedOnlySpellSchema = new SimpleSchema({})
|
||||
.extend(ComputedOnlyActionSchema);
|
||||
|
||||
const ComputedSpellSchema = new SimpleSchema()
|
||||
const ComputedSpellSchema = new SimpleSchema({})
|
||||
.extend(SpellSchema)
|
||||
.extend(ComputedOnlySpellSchema);
|
||||
|
||||
|
||||
@@ -340,20 +340,19 @@ export default {
|
||||
// Cantrips and no-slot spells
|
||||
return slotId && slotId === 'no-slot'
|
||||
} else {
|
||||
// Leveled spells
|
||||
// Levelled spells
|
||||
return slotId !== 'no-slot' && slot && spell && (
|
||||
spell.level <= slot.spellSlotLevel.value
|
||||
);
|
||||
}
|
||||
},
|
||||
cast({ advantage }) {
|
||||
cast() {
|
||||
let selectedSlotId = this.selectedSlotId;
|
||||
const ritual = selectedSlotId === 'ritual';
|
||||
if (selectedSlotId === 'no-slot' || selectedSlotId === 'ritual') selectedSlotId = undefined;
|
||||
this.$store.dispatch('popDialogStack', {
|
||||
spellId: this.selectedSpellId,
|
||||
slotId: selectedSlotId,
|
||||
advantage,
|
||||
ritual,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user