Refactored entire action engine

Triggers needed action context to function outside of the action engine
proper, so now it's been abstracted into its own class
This commit is contained in:
Stefan Zermatten
2022-08-13 00:22:32 +02:00
parent 13fc0c0b12
commit ee0e764294
29 changed files with 626 additions and 617 deletions

View File

@@ -1,53 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
variableName: {
type: String,
},
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, variableName, operation, value}) {
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
variables: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
removed: {$ne: false},
inactive: {$ne: true},
}).forEach(property => {
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property, operation, value});
});
}
});
export default damagePropertiesByName;

View File

@@ -2,8 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
@@ -20,58 +21,105 @@ const damageProperty = new ValidatedMethod({
numRequests: 20,
timeInterval: 5000,
},
run({_id, operation, value}) {
// Check permissions
let property = CreatureProperties.findOne(_id);
if (!property) throw new Meteor.Error(
run({ _id, operation, value }) {
// Get action context
const prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist'
);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
let schema = CreatureProperties.simpleSchema(prop);
if (!schema.allowsKey('damage')){
throw new Meteor.Error(
'Damage property failed',
`Property of type "${property.type}" can't be damaged`
`Property of type "${prop.type}" can't be damaged`
);
}
let result = damagePropertyWork({ property, operation, value });
}
const result = damagePropertyWork({ prop, operation, value, actionContext });
// Insert the log
actionContext.writeLog();
return result;
},
});
export function damagePropertyWork({property, operation, value}){
let damage, newValue;
export function damagePropertyWork({ prop, operation, value, actionContext }) {
// Save the value to the scope before applying the before triggers
if (operation === 'increment') {
if (value >= 0) {
actionContext.scope['$damage'] = value;
} else {
actionContext.scope['$healing'] = -value;
}
} else {
actionContext.scope['$set'] = value;
}
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
// fetch the value from the scope after the before triggers, in case they changed them
if (operation === 'increment') {
if (value >= 0) {
value = actionContext.scope['$damage'];
} else {
value = -actionContext.scope['$healing'];
}
} else {
value = actionContext.scope['$set'];
}
let damage, newValue, increment;
if (operation === 'set'){
const total = property.total || 0;
const total = prop.total || 0;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
damage = total - value;
// Damage can't exceed total value
if (damage > total) damage = total;
if (damage > total && !prop.ignoreLowerLimit) damage = total;
// Damage must be positive
if (damage < 0) damage = 0;
newValue = property.total - damage;
if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$set: { damage, value: newValue, dirty: true }
}, {
selector: prop
});
} else if (operation === 'increment'){
let currentValue = property.value || 0;
let currentDamage = property.damage || 0;
let increment = value;
let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0;
increment = value;
// Can't increase damage above the remaining value
if (increment > currentValue) increment = currentValue;
if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage;
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
damage = currentDamage + increment;
newValue = property.total - damage;
newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$inc: { damage: increment, value: -increment },
$set: { dirty: true },
}, {
selector: prop
});
}
// Write the results
CreatureProperties.update(property._id, {
$set: {damage, value: newValue, dirty: true}
}, {
selector: property
});
return damage;
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
if (operation === 'set') {
return damage;
} else if (operation === 'increment') {
return increment;
}
}
export default damageProperty;

View File

@@ -1,70 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
damageType: {
type: String,
},
amount: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, damageType, amount}) {
// permissions
let creature = Creatures.findOne(creatureId, {
fields: {
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
const totalDamage = dealDamageWork({creature, damageType, amount})
return totalDamage;
},
});
export function dealDamageWork({creature, damageType, amount}){
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creature._id,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
//let multiplier = creature.damageMultipliers[damageType];
//if (multiplier === undefined) multiplier = 1;
//let totalDamage = Math.floor(amount * multiplier);
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
});
return totalDamage;
}
export default dealDamage;

View File

@@ -1,7 +1,5 @@
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';

View File

@@ -3,12 +3,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { groupBy, remove, union } from 'lodash';
import {
getCreature, getVariables, getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js';
import { union } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.rest',
@@ -27,59 +24,37 @@ const restCreature = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, restType}) {
run({ creatureId, restType }) {
// Get action context
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
let creature = getCreature(creatureId);
assertEditPermission(creature, this.userId);
assertEditPermission(actionContext.creature, this.userId);
// Add the variables to the creature document
const variables = getVariables(creatureId);
delete variables._id;
delete variables._creatureId;
creature.variables = variables;
const scope = creature.variables;
// Join, sort, and apply before triggers
const beforeTriggers = union(
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
).sort((a, b) => a.order - b.order);
applyTriggers(beforeTriggers, null, actionContext);
// Get the triggers
let triggers = getPropertiesOfType(creatureId, 'trigger');
remove(triggers, trigger =>
trigger.event !== 'anyRest' &&
trigger.event !== 'longRest' &&
trigger.event !== 'shortRest'
);
triggers = groupBy(triggers, 'event');
for (let type in triggers) {
triggers[type] = groupBy(triggers[type], 'timing')
}
// Create the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
// Rest
actionContext.addLog({
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
});
doRestWork(restType, actionContext);
const targets = [creature];
// Join, sort, and apply after triggers
const afterTriggers = union(
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
).sort((a, b) => a.order - b.order);
applyTriggers(afterTriggers, null, actionContext);
applyTriggers(triggers, restType, 'before', { creature, targets, scope, log });
doRestWork(creature, restType);
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
insertCreatureLogWork({log, creature, method: this});
// Insert log
actionContext.writeLog();
},
});
function applyTriggers(triggers, restType, timing, opts) {
// Get matching triggers
let selectedTriggers = triggers[restType]?.[timing] || [];
// Get any rest triggers as well
selectedTriggers = union(selectedTriggers, triggers['anyRest']?.[timing]);
selectedTriggers.sort((a, b) => a.order - b.order);
// Apply the triggers
selectedTriggers.forEach(trigger => {
applyTrigger(trigger, opts)
});
}
function doRestWork(creature, restType) {
function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
@@ -89,7 +64,7 @@ function doRestWork(creature, restType) {
}
// Only apply to active properties
let filter = {
'ancestors.id': creature._id,
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
@@ -123,7 +98,7 @@ function doRestWork(creature, restType) {
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = CreatureProperties.find({
'ancestors.id': creature._id,
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
@@ -144,7 +119,7 @@ function doRestWork(creature, restType) {
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0);
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;