refactored action engine into individual files
This commit is contained in:
139
app/imports/api/engine/action/methods/doCastSpell.js
Normal file
139
app/imports/api/engine/action/methods/doCastSpell.js
Normal file
@@ -0,0 +1,139 @@
|
||||
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';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
|
||||
import { doActionWork } from '/imports/api/engine/actions/doAction';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
||||
|
||||
// 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 = {} }) {
|
||||
// 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;
|
||||
140
app/imports/api/engine/action/methods/doCheck.js
Normal file
140
app/imports/api/engine/action/methods/doCheck.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
import rollDice from '/imports/parser/rollDice';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
||||
import recalculateCalculation from '../../actions/applyPropertyByType/shared/recalculateCalculation';
|
||||
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
// TODO Migrate this to the new action engine
|
||||
|
||||
const doCheck = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCheck',
|
||||
validate: new SimpleSchema({
|
||||
propId: SimpleSchema.RegEx.Id,
|
||||
scope: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ propId, scope }) {
|
||||
const prop = CreatureProperties.findOne(propId);
|
||||
if (!prop) throw new Meteor.Error('not-found', 'The property was not found');
|
||||
const creatureId = prop.root.id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
Object.assign(actionContext.scope, scope);
|
||||
actionContext.scope[`#${prop.type}`] = prop;
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Do the check
|
||||
doCheckWork({ prop, actionContext });
|
||||
},
|
||||
});
|
||||
|
||||
export default doCheck;
|
||||
|
||||
export function doCheckWork({ prop, actionContext }) {
|
||||
|
||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||
rollCheck(prop, actionContext);
|
||||
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
|
||||
|
||||
// Insert the log
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
function rollCheck(prop, actionContext) {
|
||||
const scope = actionContext.scope;
|
||||
// get the modifier for the roll
|
||||
let rollModifier;
|
||||
let logName = `${prop.name} check`;
|
||||
if (prop.type === 'skill') {
|
||||
rollModifier = prop.value;
|
||||
if (prop.skillType === 'save') {
|
||||
if (prop.name.match(/save/i)) {
|
||||
logName = prop.name;
|
||||
} else {
|
||||
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
|
||||
}
|
||||
}
|
||||
} else if (prop.type === 'attribute') {
|
||||
if (prop.attributeType === 'ability') {
|
||||
rollModifier = prop.modifier;
|
||||
} else {
|
||||
rollModifier = prop.value;
|
||||
}
|
||||
} else {
|
||||
throw (`${prop.type} not supported for checks`);
|
||||
}
|
||||
|
||||
let rollModifierText = numberToSignedString(rollModifier, true);
|
||||
|
||||
const { effectBonus, effectString } = applyUnresolvedEffects(prop, actionContext)
|
||||
rollModifierText += effectString;
|
||||
rollModifier += effectBonus;
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (scope['~checkAdvantage']?.value === 1) {
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (scope['~checkAdvantage']?.value === -1) {
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else {
|
||||
values = rollDice(1, 20);
|
||||
value = values[0];
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
scope['~checkDiceRoll'] = { value };
|
||||
scope['~checkRoll'] = { value: result };
|
||||
scope['~checkModifier'] = { value: rollModifier };
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO replace this with recalculating and then rolling/reducing the value node
|
||||
export function applyUnresolvedEffects(prop, actionContext) {
|
||||
let effectBonus = 0;
|
||||
let effectString = '';
|
||||
if (!prop.effectIds) {
|
||||
return { effectBonus, effectString };
|
||||
}
|
||||
prop.effectIds.forEach(id => {
|
||||
const effect = getSingleProperty(actionContext.creature._id, id);
|
||||
if (!effect.amount?.parseNode) return;
|
||||
if (effect.operation !== 'add') return;
|
||||
recalculateCalculation(effect.amount, actionContext, undefined, 'reduce');
|
||||
if (typeof effect.amount?.value !== 'number') return;
|
||||
effectBonus += effect.amount.value;
|
||||
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
|
||||
});
|
||||
return { effectBonus, effectString };
|
||||
}
|
||||
21
app/imports/api/engine/action/methods/insertAction.ts
Normal file
21
app/imports/api/engine/action/methods/insertAction.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
export const insertAction: ValidatedMethod = new ValidatedMethod({
|
||||
name: 'actions.insertAction',
|
||||
validate: new SimpleSchema({
|
||||
action: ActionSchema
|
||||
}).validator({ clean: true }),
|
||||
run: async function ({ action }: { action: EngineAction }) {
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
// First remove all other actions on this creature
|
||||
// only do one action at a time, don't wait for this to finish
|
||||
EngineActions.removeAsync({ creatureId: action.creatureId });
|
||||
// Force a random id even if one was provided, we may use it later as the seed for PRNG
|
||||
delete action._id;
|
||||
return await EngineActions.insertAsync(action);
|
||||
},
|
||||
});
|
||||
34
app/imports/api/engine/action/methods/runAction.ts
Normal file
34
app/imports/api/engine/action/methods/runAction.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
export const runAction = new ValidatedMethod({
|
||||
name: 'actions.runAction',
|
||||
validate: new SimpleSchema({
|
||||
action: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
userInput: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
stepThrough: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
}
|
||||
}).validator(),
|
||||
run: async function ({ actionId, userInput }: { actionId: string, userInput?: any }) {
|
||||
const action = await EngineActions.findOneAsync(actionId);
|
||||
if (!action) throw 'Action not found';
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
const originalAction = EJSON.clone(action);
|
||||
applyAction(action, userInput);
|
||||
// Persist changes to the action
|
||||
const writePromise = writeChangedAction(originalAction, action);
|
||||
return writePromise;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user