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;

View File

@@ -0,0 +1,78 @@
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import {
getCreature, getVariables, getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { groupBy, remove } from 'lodash';
export default class ActionContext{
constructor(creatureId, targetIds = [], method) {
// Get the creature
this.creature = getCreature(creatureId)
if (!this.creature) {
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
}
// Create a log
this.log = CreatureLogSchema.clean({
creatureId: creatureId,
creatureName: this.creature.name,
});
// Get the variables of the acting creature
this.creature.variables = getVariables(creatureId);
delete this.creature.variables._id;
delete this.creature.variables._creatureId;
// Alias as scope
this.scope = this.creature.variables;
// Get the targets and their variables
this.targets = [];
targetIds.forEach(targetId => {
let target;
if (targetId === creatureId) {
target = this.creature;
} else {
target = getCreature(targetId);
target.variables = getVariables(targetId);
delete target.variables._id;
delete target.variables._creatureId;
}
this.targets.push(target);
});
// Store a reference to the method for inserting the log
this.method = method;
// Get triggers
this.triggers = getPropertiesOfType(creatureId, 'trigger');
// Remove deleted or inactive triggers
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
// Sort triggers by order
this.triggers.sort((a, b) => a.order - b.order);
// Group the triggers into triggers.<event>.<timing> or
// triggers.doActionProperty.<propertyType>.<timing>
this.triggers = groupBy(this.triggers, 'event');
for (let event in this.triggers) {
if (event === 'doActionProperty') {
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
for (let propertyType in this.triggers[event]) {
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
}
} else {
this.triggers[event] = groupBy(this.triggers[event], 'timing');
}
}
}
addLog(content) {
if (content.name || content.value){
this.log.content.push(content);
}
}
writeLog() {
insertCreatureLogWork({
log: this.log,
creature: this.creature,
method: this.method,
});
}
}

View File

@@ -7,7 +7,7 @@ import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js';
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
const applyPropertyByType = {
action,
@@ -22,9 +22,9 @@ const applyPropertyByType = {
toggle,
};
export default function applyProperty(node, opts, ...rest) {
applyTriggers(node, opts, 'before');
opts.scope[`#${node.node.type}`] = node.node;
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
applyTriggers(node, opts, 'after');
export default function applyProperty(node, actionContext, ...rest) {
applyNodeTriggers(node, actionContext, 'before');
actionContext.scope[`#${node.node.type}`] = node.node;
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
applyNodeTriggers(node, actionContext, 'after');
}

View File

@@ -7,22 +7,21 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default function applyAction(node, {creature, targets, scope, log}){
export default function applyAction(node, actionContext){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
let targets = actionContext.targets;
if (prop.target === 'self') targets = [actionContext.creature];
// Log the name and summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
}
actionContext.addLog(content);
// Spend the resources
const failed = spendResources({prop, log, scope});
const failed = spendResources(prop, actionContext);
if (failed) return;
const attack = prop.attackRoll || prop.attackRollBonus;
@@ -31,28 +30,29 @@ export default function applyAction(node, {creature, targets, scope, log}){
if (attack && attack.calculation){
if (targets.length){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
applyAttackToTarget({attack, target, actionContext});
// Apply the children, but only to the current target
applyChildren(node, {creature, targets: [target], scope, log});
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, scope, log});
applyChildren(node, {creature, targets, scope, log});
applyAttackWithoutTarget({attack, actionContext});
applyChildren(node, actionContext);
}
} else {
applyChildren(node, {creature, targets, scope, log});
applyChildren(node, actionContext);
}
}
function applyAttackWithoutTarget({attack, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
function applyAttackWithoutTarget({attack, actionContext}){
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
delete actionContext.scope['$criticalMiss'];
delete actionContext.scope['$attackRoll'];
recalculateCalculation(attack, actionContext);
const scope = actionContext.scope;
let {
resultPrefix,
result,
@@ -72,14 +72,15 @@ function applyAttackWithoutTarget({attack, scope, log}){
scope['$attackMiss'] = {value: true};
}
log.content.push({
actionContext.addLog({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
}
function applyAttackToTarget({attack, target, scope, log}){
function applyAttackToTarget({attack, target, actionContext}){
const scope = actionContext.scope;
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
@@ -87,7 +88,7 @@ function applyAttackToTarget({attack, target, scope, log}){
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
recalculateCalculation(attack, actionContext);
let {
resultPrefix,
@@ -108,7 +109,7 @@ function applyAttackToTarget({attack, target, scope, log}){
name += ' (Disadvantage)';
}
log.content.push({
actionContext.addLog({
name,
value: `${resultPrefix}\n**${result}**`,
inline: true,
@@ -119,11 +120,11 @@ function applyAttackToTarget({attack, target, scope, log}){
scope['$attackHit'] = {value: true};
}
} else {
log.content.push({
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
});
log.content.push({
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix}\n**${result}**`,
inline: true,
@@ -177,14 +178,14 @@ function applyCrits(value, scope){
return {criticalHit, criticalMiss};
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
function applyChildren(node, actionContext){
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources({prop, log, scope}){
function spendResources(prop, actionContext){
// Check Uses
if (prop.usesLeft <= 0){
log.content.push({
actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
});
@@ -192,7 +193,7 @@ function spendResources({prop, log, scope}){
}
// Resources
if (prop.insufficientResources){
log.content.push({
actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
});
@@ -204,7 +205,7 @@ function spendResources({prop, log, scope}){
let gainLog = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log);
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){
throw 'No ammo was selected for this prop';
}
@@ -235,7 +236,7 @@ function spendResources({prop, log, scope}){
}
});
} catch (e){
log.content.push({
actionContext.addLog({
name: 'Error',
value: e,
});
@@ -253,7 +254,7 @@ function spendResources({prop, log, scope}){
}, {
selector: prop
});
log.content.push({
actionContext.addLog({
name: 'Uses left',
value: prop.usesLeft - 1,
inline: true,
@@ -262,18 +263,19 @@ function spendResources({prop, log, scope}){
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
recalculateCalculation(attConsumed.quantity, scope, log);
recalculateCalculation(attConsumed.quantity, actionContext);
if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName];
let stat = actionContext.scope[attConsumed.variableName];
if (!stat){
spendLog.push(stat.name + ': ' + ' not found');
return;
}
damagePropertyWork({
property: stat,
prop: stat,
operation: 'increment',
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
@@ -283,12 +285,12 @@ function spendResources({prop, log, scope}){
});
// Log all the spending
if (gainLog.length) log.content.push({
if (gainLog.length) actionContext.addLog({
name: 'Gained',
value: gainLog.join('\n'),
inline: true,
});
if (spendLog.length) log.content.push({
if (spendLog.length) actionContext.addLog({
name: 'Spent',
value: spendLog.join('\n'),
inline: true,

View File

@@ -2,40 +2,39 @@ import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function applyAdjustment(node, {
creature, targets, scope, log
}){
export default function applyAdjustment(node, actionContext){
const prop = node.node;
const damageTargets = prop.target === 'self' ? [creature] : targets;
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
if (!prop.amount) {
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
// Evaluate the amount
recalculateCalculation(prop.amount, scope, log);
recalculateCalculation(prop.amount, actionContext);
const value = +prop.amount.value;
if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
if (damageTargets?.length) {
damageTargets.forEach(target => {
let stat = target.variables[prop.stat];
if (!stat?.type) {
log.content.push({
actionContext.addLog({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
damagePropertyWork({
property: stat,
prop: stat,
operation: prop.operation,
value: value,
value,
actionContext,
});
log.content.push({
actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
@@ -43,7 +42,7 @@ export default function applyAdjustment(node, {
});
});
} else {
log.content.push({
actionContext.addLog({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
@@ -51,9 +50,9 @@ export default function applyAdjustment(node, {
});
}
return applyChildren(node, {creature, targets, scope, log});
return applyChildren(node, actionContext);
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
function applyChildren(node, actionContext){
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -2,25 +2,23 @@ import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
export default function applyBranch(node, {
creature, targets, scope, log
}){
export default function applyBranch(node, actionContext){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
node.children.forEach(child => applyProperty(child, actionContext));
};
const scope = actionContext.scope;
const targets = actionContext.targets;
const prop = node.node;
switch(prop.branchType){
case 'if':
recalculateCalculation(prop.condition, scope, log);
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length){
recalculateCalculation(prop.condition, scope, log);
recalculateCalculation(prop.condition, actionContext);
if (!isFinite(prop.condition?.value)) {
log.content.push({
actionContext.addLog({
name: 'Branch Error',
value: 'Index did not resolve into a valid number'
});
@@ -29,49 +27,44 @@ export default function applyBranch(node, {
let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length;
applyProperty(node.children[index - 1], {
creature, targets, scope, log
});
applyProperty(node.children[index - 1], actionContext);
}
break;
case 'hit':
if (scope['$attackHit']?.value){
if (!targets.length) log.content.push({value: '**On hit**'});
if (!targets.length) actionContext.addLog({value: '**On hit**'});
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value){
if (!targets.length) log.content.push({value: '**On miss**'});
if (!targets.length) actionContext.addLog({value: '**On miss**'});
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value){
if (!targets.length) log.content.push({value: '**On failed save**'});
if (!targets.length) actionContext.addLog({value: '**On failed save**'});
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value){
if (!targets.length) log.content.push({value: '**On save**',});
if (!targets.length) actionContext.addLog({value: '**On save**',});
applyChildren();
}
break;
case 'random':
if (node.children.length){
let index = rollDice(1, node.children.length)[0] - 1;
applyProperty(node.children[index], {
creature, targets, scope, log
});
applyProperty(node.children[index], actionContext);
}
break;
case 'eachTarget':
if (targets.length){
targets.forEach(target => {
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
actionContext.targets = [target]
node.children.forEach(child => applyProperty(child, actionContext));
});
} else {
applyChildren();

View File

@@ -13,9 +13,9 @@ import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
export default function applyBuff(node, actionContext){
const prop = node.node;
let buffTargets = prop.target === 'self' ? [creature] : targets;
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Then copy the decendants of the buff to the targets
let propList = [prop];
@@ -26,7 +26,7 @@ export default function applyBuff(node, {creature, targets, scope, log}){
});
}
addChildrenToPropList(node.children);
crystalizeVariables({propList, scope, log});
crystalizeVariables({propList, actionContext});
let oldParent = {
id: prop.parent.id,
@@ -38,9 +38,9 @@ export default function applyBuff(node, {creature, targets, scope, log}){
//Log the buff
if (prop.name || prop.description?.value){
if (target._id === creature._id){
if (target._id === actionContext.creature._id){
// Targeting self
log.content.push({
actionContext.addLog({
name: prop.name,
value: prop.description?.value,
});
@@ -83,7 +83,7 @@ function copyNodeListToTarget(propList, target, oldParent){
* Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total`
*/
function crystalizeVariables({propList, scope, log}){
function crystalizeVariables({propList, actionContext}){
propList.forEach(prop => {
computedSchemas[prop.type].computedFields().forEach( calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
@@ -104,7 +104,7 @@ function crystalizeVariables({propList, scope, log}){
}
} else {
// Can't strip symbols
log.content.push({
actionContext.addLog({
name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property',
});
@@ -112,8 +112,8 @@ function crystalizeVariables({propList, scope, log}){
return node;
} else {
// Resolve all other variables
const {result, context} = resolve('reduce', node, scope);
logErrors(context.errors, log);
const {result, context} = resolve('reduce', node, actionContext.scope);
logErrors(context.errors, actionContext);
return result;
}
});

View File

@@ -1,27 +1,27 @@
import { some, intersection, difference } from 'lodash';
import { some, intersection, difference, remove } from 'lodash';
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import {
getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
export default function applyDamage(node, {
creature, targets, scope, log
}){
export default function applyDamage(node, actionContext){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
node.children.forEach(child => applyProperty(child, actionContext));
};
const prop = node.node;
const scope = actionContext.scope;
// Skip if there is no parse node to work with
if (!prop.amount?.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
@@ -36,19 +36,19 @@ export default function applyDamage(node, {
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
@@ -94,15 +94,17 @@ export default function applyDamage(node, {
logValue
});
actionContext.target = [target];
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
let damageDealt = dealDamage({
target,
damageType: prop.damageType,
amount: damage,
actionContext
});
// Log the damage done
if (target._id === creature._id){
if (target._id === actionContext.creature._id){
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -123,7 +125,7 @@ export default function applyDamage(node, {
// There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`);
}
log.content.push({
actionContext.addLog({
name: logName,
value: logValue.join('\n'),
inline: true,
@@ -178,3 +180,49 @@ function multiplierAppliesTo(damageProp){
return hasRequiredTags && hasNoExcludedTags;
}
}
function dealDamage({target, damageType, amount, actionContext}){
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
// Keep only the healthbars that can take damage/healing
remove(healthBars, (bar) =>
bar.attributeType !== 'healthBar' ||
bar.inactive ||
bar.removed ||
bar.overridden ||
(amount >= 0 && bar.healthBarNoDamage) ||
(amount < 0 && bar.healthBarNoHealing)
);
// Sort healthbars by damage/healing order or tree order as a fallback
healthBars.sort((a, b) => {
let diff;
if (amount >= 0) {
diff = a.healthBarDamageOrder - b.healthBarDamageOrder;
} else {
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
}
if (Number.isFinite(diff)) {
return diff;
} else {
return a.order - b.order;
}
});
// Deal the damage to each healthbar in order until all damage is done
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
prop: healthBar,
operation: 'increment',
value: damageLeft,
actionContext
});
damageLeft -= damageAdded;
});
return totalDamage;
}

View File

@@ -1,25 +1,23 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
export default function applyNote(node, {creature, targets, scope, log}){
export default function applyNote(node, actionContext){
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
actionContext.addLog(content);
}
// Log description
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
log.content.push({value: prop.description.value});
recalculateInlineCalculations(prop.description, actionContext);
actionContext.addLog({value: prop.description.value});
}
// Apply children
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -3,32 +3,30 @@ import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
export default function applyRoll(node, {creature, targets, scope, log}){
export default function applyRoll(node, actionContext){
const prop = node.node;
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
node.children.forEach(child => applyProperty(child, actionContext));
};
if (prop.roll?.calculation){
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, log);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
applyEffectsToCalculationParseNode(prop.roll, actionContext);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
@@ -45,11 +43,11 @@ export default function applyRoll(node, {creature, targets, scope, log}){
}
const value = reduced.value;
scope[prop.variableName] = value;
actionContext.scope[prop.variableName] = value;
logValue.push(`**${value}**`);
if (!prop.silent){
log.content.push({
actionContext.addLog({
name: prop.name,
value: logValue.join('\n'),
inline: true,

View File

@@ -3,37 +3,34 @@ import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default function applySavingThrow(node, {creature, targets, scope, log}){
export default function applySavingThrow(node, actionContext){
const prop = node.node;
let saveTargets = prop.target === 'self' ? [creature] : targets;
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
recalculateCalculation(prop.dc, scope, log);
recalculateCalculation(prop.dc, actionContext);
const dc = (prop.dc?.value);
if (!isFinite(dc)){
log.content.push({
actionContext.addLog({
name: 'Error',
value: 'Saving throw requires a DC',
});
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return node.children.forEach(child => applyProperty(child, actionContext));
}
log.content.push({
actionContext.addLog({
name: prop.name,
value: `DC **${dc}**`,
inline: true,
});
const scope = actionContext.scope;
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length){
scope['$saveFailed'] = {value: true};
scope['$saveSucceeded'] = {value: true};
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return node.children.forEach(child => applyProperty(child, actionContext));
}
// Each target makes the saving throw
@@ -43,16 +40,15 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
delete scope['$saveDiceRoll'];
delete scope['$saveRoll'];
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
const applyChildren = function () {
actionContext.targets = [target]
node.children.forEach(child => applyProperty(child, actionContext));
};
const save = target.variables[prop.stat];
if (!save){
log.content.push({
actionContext.addLog({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
@@ -94,7 +90,7 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
} else {
scope['$saveFailed'] = {value: true};
}
log.content.push({
actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,

View File

@@ -1,14 +1,10 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
export default function applyToggle(node, {
creature, targets, scope, log
}){
export default function applyToggle(node, actionContext){
const prop = node.node;
recalculateCalculation(prop.condition, scope, log);
recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) {
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return node.children.forEach(child => applyProperty(child, actionContext));
}
}

View File

@@ -2,7 +2,7 @@ import operator from '/imports/parser/parseTree/operator.js';
import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, log){
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
if (!calcObj.effects) return;
calcObj.effects.forEach(effect => {
if (effect.operation !== 'add') return;
@@ -18,7 +18,7 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
fn: 'add'
});
} catch (e){
logErrors([e], log)
logErrors([e], actionContext)
}
});
}

View File

@@ -1,7 +1,7 @@
export default function logErrors(errors, log){
export default function logErrors(errors, actionContext){
errors?.forEach(error => {
if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message});
actionContext.addLog({name: 'Error', value: error.message});
}
});
}

View File

@@ -2,10 +2,10 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log, context){
export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, log);
evaluateCalculation(calc, scope, context);
logErrors(calc.errors, log);
applyEffectsToCalculationParseNode(calc, actionContext.log);
evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext.log);
}

View File

@@ -1,12 +1,12 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
import recalculateCalculation from './recalculateCalculation.js'
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
// Skip if there are no calculations
if (!inlineCalcObj?.inlineCalculations?.length) return;
// Recalculate each calculation with the current scope
inlineCalcObj.inlineCalculations.forEach(calc => {
recalculateCalculation(calc, scope, log);
recalculateCalculation(calc, actionContext);
});
// Embed the new calculated values
embedInlineCalculations(inlineCalcObj);

View File

@@ -6,20 +6,28 @@ import applyProperty from '/imports/api/engine/actions/applyProperty.js';
import { difference, intersection } from 'lodash';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default function applyTriggers(node, { creature, targets, scope, log }, timing) {
export function applyNodeTriggers(node, timing, actionContext) {
const prop = node.node;
const type = prop.type;
if (creature.triggers?.[type]?.[timing]) {
creature.triggers[type][timing].forEach(trigger => {
applyTrigger(trigger, { creature, prop, targets, scope, log });
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
if (triggers) {
triggers.forEach(trigger => {
applyTrigger(trigger, prop, actionContext);
});
}
}
export function applyTrigger(trigger, { creature, prop, targets, scope, log }) {
export function applyTriggers(triggers = [], prop, actionContext) {
// Apply the triggers
triggers.forEach(trigger => {
applyTrigger(trigger, prop, actionContext)
});
}
export function applyTrigger(trigger, prop, actionContext) {
// If there is a prop we are applying the trigger from,
// don't fire if the tags don't match
if (!triggerMatchTags(trigger, prop)) {
if (prop && !triggerMatchTags(trigger, prop)) {
return;
}
@@ -30,7 +38,7 @@ export function applyTrigger(trigger, { creature, prop, targets, scope, log }) {
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, scope, log);
recalculateCalculation(trigger.condition, actionContext);
if (!trigger.condition.value) return;
}
@@ -54,22 +62,17 @@ export function applyTrigger(trigger, { creature, prop, targets, scope, log }) {
inline: false,
}
if (trigger.summary?.text){
recalculateInlineCalculations(trigger.summary, scope, log);
recalculateInlineCalculations(trigger.summary, actionContext);
content.value = trigger.summary.value;
}
log.content.push(content);
actionContext.addLog(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(creature._id, trigger._id);
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties);
propertyForest.forEach(node => {
applyProperty(node, {
creature,
targets,
scope,
log,
});
applyProperty(node, actionContext);
});
trigger.firing = false;

View File

@@ -1,16 +1,15 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import {
getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType
getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import applyProperty from './applyProperty.js';
import { groupBy, remove } from 'lodash';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -37,32 +36,16 @@ const doAction = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = [], scope}) {
run({ actionId, targetIds = [], scope }) {
// Get action context
let action = CreatureProperties.findOne(actionId);
// Check permissions
const creatureId = action.ancestors[0].id;
let creature = getCreature(action.ancestors[0].id);
assertEditPermission(creature, this.userId);
// Add the variables to the creature document
const variables = getVariables(creatureId);
delete variables._id;
delete variables._creatureId;
creature.variables = variables;
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = getCreature(targetId);
const actionContext = new ActionContext(creatureId, targetIds, this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
// add the variables to the target documents
const variables = getVariables(creatureId);
delete variables._id;
delete variables._creatureId;
target.variables = variables;
targets.push(target);
});
const ancestors = getProperyAncestors(creatureId, action._id);
@@ -73,11 +56,11 @@ const doAction = new ValidatedMethod({
properties.sort((a, b) => a.order - b.order);
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
doActionWork({properties, ancestors, actionContext, methodScope: scope});
// Recompute all involved creatures
Creatures.update({
_id: { $in: [creature._id, ...targetIds] }
_id: { $in: [creatureId, ...targetIds] }
}, {
$set: {dirty: true},
});
@@ -87,7 +70,7 @@ const doAction = new ValidatedMethod({
export default doAction;
export function doActionWork({
creature, targets, properties, ancestors, method, methodScope = {}, log
properties, ancestors, actionContext, methodScope = {},
}){
// get the docs
const ancestorScope = getAncestorScope(ancestors);
@@ -96,38 +79,15 @@ export function doActionWork({
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
}
// Get the triggers
const triggers = getPropertiesOfType(creature._id, 'trigger');
// Skip triggers that aren't triggered by action props or are inactive
remove(triggers, trigger => trigger.event !== 'doActionProperty' || trigger.inactive);
// Group the triggers into creature.triggers.<propertyType>.<timing>
creature.triggers = groupBy(triggers, 'actionPropertyType');
for (let type in creature.triggers) {
creature.triggers[type] = groupBy(creature.triggers[type], 'timing')
}
// Create the log
if (!log) log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
// Include the ancestry and method scope in the context scope
Object.assign(actionContext.scope, ancestorScope, methodScope);
// Apply the top level property, it is responsible for applying its children
// recursively
const scope = {
...creature.variables,
...ancestorScope,
...methodScope
}
applyProperty(propertyForest[0], {
creature,
targets,
scope,
log,
});
applyProperty(propertyForest[0], actionContext);
// Insert the log
insertCreatureLogWork({log, creature, method});
actionContext.writeLog();
}
// Assumes ancestors are in tree order already

View File

@@ -1,13 +1,16 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doCastSpell',
@@ -39,47 +42,31 @@ const doAction = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({spellId, slotId, targetIds = [], scope = {}}) {
run({ spellId, slotId, targetIds = [], scope = {} }) {
// Get action context
let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
// Check permissions
let creature = getRootCreatureAncestor(spell);
assertEditPermission(creature, this.userId);
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(target, this.userId);
targets.push(target);
});
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
spell.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
const ancestors = getProperyAncestors(creatureId, spell._id);
ancestors.sort((a, b) => a.order - b.order);
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
const properties = getPropertyDecendants(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;
actionContext.scope['slotLevel'] = slotLevel;
if (slotId && !spell.castWithoutSpellSlots){
slot = CreatureProperties.findOne(slotId);
if (!slot){
@@ -104,34 +91,32 @@ const doAction = new ValidatedMethod({
}
slotLevel = slot.spellSlotLevel.value;
damagePropertyWork({
property: slot,
prop: slot,
operation: 'increment',
value: 1,
actionContext,
});
}
scope['slotLevel'] = slotLevel;
// Post the slot level spent to the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
if (slot?.spellSlotLevel?.value){
log.content.push({
actionContext.addLog({
name: `Casting using a level ${slotLevel} spell slot`
});
} else if (slotLevel) {
log.content.push({
actionContext.addLog({
name: `Casting at level ${slotLevel}`
});
}
// Do the action
doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log });
doActionWork({
properties, ancestors, actionContext, methodScope: scope,
});
// Force the characters involved to recalculate
Creatures.update({
_id: { $in: [creature._id, ...targetIds] }
_id: { $in: [creatureId, ...targetIds] }
}, {
$set: { dirty: true },
});

View File

@@ -1,17 +1,12 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {
getPropertiesOfType, getVariables
} from '/imports/api/engine/loadCreatures.js';
import { groupBy, remove } from 'lodash';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck',
@@ -29,62 +24,32 @@ const doCheck = new ValidatedMethod({
},
run({propId, scope}) {
const prop = CreatureProperties.findOne(propId);
const creature = getRootCreatureAncestor(prop);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope);
// Check permissions
assertEditPermission(creature, this.userId);
assertEditPermission(actionContext.creature, this.userId);
// Do the check
doCheckWork({creature, prop, method: this, methodScope: scope});
doCheckWork({prop, actionContext});
},
});
export default doCheck;
export function doCheckWork({
creature, prop, method, methodScope = {}
}){
// Create the log
let log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
export function doCheckWork({prop, actionContext}){
// Add the variables to the creature document
const variables = getVariables(creature._id);
delete variables._id;
delete variables._creatureId;
creature.variables = variables;
const scope = creature.variables;
// Get the triggers
let triggers = getPropertiesOfType(creature._id, 'trigger');
remove(triggers, trigger => trigger.event !== 'check');
triggers = groupBy(triggers, 'timing');
// Set the creature as the target
const targets = [creature];
applyTriggers(triggers, 'before', { creature, prop, targets, scope, log });
rollCheck({prop, log, methodScope});
applyTriggers(triggers, 'after', { creature, prop, targets, scope, log });
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
// Insert the log
insertCreatureLogWork({log, creature, method});
actionContext.writeLog();
}
function applyTriggers(triggers, timing, opts) {
// Get matching triggers
let selectedTriggers = triggers[timing] || [];
// Sort the triggers
selectedTriggers.sort((a, b) => a.order - b.order);
// Apply the triggers
selectedTriggers.forEach(trigger => {
applyTrigger(trigger, opts)
});
}
function rollCheck({prop, log, methodScope}){
function rollCheck(prop, actionContext) {
const scope = actionContext.scope;
// get the modifier for the roll
let rollModifier;
let logName = `${prop.name} check`;
@@ -110,7 +75,7 @@ function rollCheck({prop, log, methodScope}){
const rollModifierText = numberToSignedString(rollModifier, true);
let value, values, resultPrefix;
if (methodScope['$checkAdvantage'] === 1){
if (scope['$checkAdvantage'] === 1){
logName += ' (Advantage)';
const [a, b] = rollDice(2, 20);
if (a >= b) {
@@ -120,7 +85,7 @@ function rollCheck({prop, log, methodScope}){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (methodScope['$checkAdvantage'] === -1){
} else if (scope['$checkAdvantage'] === -1){
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
@@ -136,7 +101,7 @@ function rollCheck({prop, log, methodScope}){
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
log.content.push({
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,
});

View File

@@ -59,6 +59,23 @@ let AttributeSchema = createPropertySchema({
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
// Control how the health bar takes damage or healing
healthBarNoDamage: {
type: Boolean,
optional: true,
},
healthBarNoHealing: {
type: Boolean,
optional: true,
},
healthBarDamageOrder: {
type: SimpleSchema.Integer,
optional: true,
},
healthBarHealingOrder: {
type: SimpleSchema.Integer,
optional: true,
},
// The starting value, before effects
baseValue: {
@@ -79,6 +96,16 @@ let AttributeSchema = createPropertySchema({
decimal: {
type: Boolean,
optional: true,
},
// Can the total after damage be negative
ignoreLowerLimit: {
type: Boolean,
optional: true,
},
// Can the damage value be negative
ignoreUpperLimit: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
reset: {

View File

@@ -7,7 +7,9 @@ const eventOptions = {
// receiveActionProperty: 'Receiving action property',
check: 'Roll check',
// flipToggle: 'Toggle changed',
// adjustProperty: 'Attribute adjusted',
// itemEquipped: 'Item equipped'
// itemUnequipped: 'Item unequipped'
damageProperty: 'Attribute damaged or healed',
anyRest: 'Short or long rest',
longRest: 'Long rest',
shortRest: 'Short rest',

View File

@@ -41,7 +41,7 @@
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
:style="{
backgroundColor: barColor,
transform: `scaleX(${value / maxValue})`,
transform: `scaleX(${fillFraction})`,
}"
/>
<div
@@ -92,87 +92,93 @@ import IncrementMenu from '/imports/ui/components/IncrementMenu.vue';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import chroma from 'chroma-js';
export default {
components: {
IncrementMenu
},
inject: {
theme: {
default: {
isDark: false,
},
export default {
components: {
IncrementMenu
},
inject: {
theme: {
default: {
isDark: false,
},
},
props: {
value: Number,
maxValue: Number,
name: String,
color: {
type: String,
default() {
return this.$vuetify.theme.currentTheme.primary
},
},
props: {
value: Number,
maxValue: Number,
name: String,
color: {
type: String,
default() {
return this.$vuetify.theme.currentTheme.primary
},
midColor: {
type: String,
default: undefined,
},
lowColor: {
type: String,
default: undefined,
},
_id: String,
},
data() {
return {
editing: false,
hover: false,
};
},
computed: {
barColor() {
const fraction = this.value / this.maxValue;
if (!Number.isFinite(fraction)) return this.color;
if (fraction > 0.5){
return this.color;
} else if (this.midColor && this.lowColor) {
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
} else if (this.midColor){
return this.midColor;
}
},
midColor: {
type: String,
default: undefined,
},
lowColor: {
type: String,
default: undefined,
},
_id: String,
},
data() {
return {
editing: false,
hover: false,
};
},
computed: {
fillFraction() {
let fraction = this.value / this.maxValue;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
return fraction;
},
barColor() {
const fraction = this.value / this.maxValue;
if (!Number.isFinite(fraction)) return this.color;
if (fraction > 0.5){
return this.color;
},
barBackgroundColor(){
return chroma(this.barColor)
.darken(1.5)
.desaturate(1.5)
.hex();
},
isTextLight(){
return isDarkColor(this.barBackgroundColor);
/* Change color at the halfway mark
const fraction = this.value / this.maxValue;
if (fraction >= 0.5){
return isDarkColor(this.barColor);
} else {
return isDarkColor(this.barBackgroundColor);
}
*/
} else if (this.midColor && this.lowColor) {
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
} else if (this.midColor){
return this.midColor;
}
return this.color;
},
methods: {
edit() {
this.editing = true;
},
cancelEdit() {
this.editing = false;
},
changeIncrementMenu(e){
this.$emit('change', e);
this.editing = false;
barBackgroundColor(){
return chroma(this.barColor)
.darken(1.5)
.desaturate(1.5)
.hex();
},
isTextLight(){
return isDarkColor(this.barBackgroundColor);
/* Change color at the halfway mark
const fraction = this.value / this.maxValue;
if (fraction >= 0.5){
return isDarkColor(this.barColor);
} else {
return isDarkColor(this.barBackgroundColor);
}
},
};
*/
}
},
methods: {
edit() {
this.editing = true;
},
cancelEdit() {
this.editing = false;
},
changeIncrementMenu(e){
this.$emit('change', e);
this.editing = false;
}
},
};
</script>
<style>

View File

@@ -8,7 +8,7 @@
<v-btn
icon
small
:disabled="model.value >= model.total || context.editPermission === false"
:disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
@click="increment(1)"
>
<v-icon>mdi-chevron-up</v-icon>
@@ -16,7 +16,7 @@
<v-btn
icon
small
:disabled="model.value <= 0 || context.editPermission === false"
:disabled="(model.value <= 0 && !model.ignoreLowerLimit) || context.editPermission === false"
@click="increment(-1)"
>
<v-icon>mdi-chevron-down</v-icon>
@@ -28,7 +28,10 @@
<div class="text-h4">
{{ model.value }}
</div>
<div class="text-h6 ml-2 max-value">
<div
v-if="model.total !== 0"
class="text-h6 ml-2 max-value"
>
/{{ model.total }}
</div>
</div>

View File

@@ -78,7 +78,6 @@
<form-section
v-if="model.attributeType === 'healthBar'"
name="Health Bar Settings"
standalone
>
<color-picker
:value="model.healthBarColorMid"
@@ -90,6 +89,47 @@
label="Empty color"
@input="value => $emit('change', {path: ['healthBarColorLow'], value})"
/>
<v-layout
wrap
class="mt-4"
>
<text-field
label="Damage order"
type="number"
style="max-width: 300px;"
hint="Lower ordered health bars will take damage before higher ordered ones"
class="mr-4"
:disabled="model.healthBarNoDamage"
:value="model.healthBarDamageOrder"
:error-messages="errors.healthBarDamageOrder"
@change="change('healthBarDamageOrder', ...arguments)"
/>
<smart-switch
label="Ignore damage"
:value="model.healthBarNoDamage"
:error-messages="errors.healthBarNoDamage"
@change="change('healthBarNoDamage', ...arguments)"
/>
</v-layout>
<v-layout wrap>
<text-field
label="Healing order"
type="number"
style="max-width: 300px;"
hint="Lower ordered health bars will take healing before higher ordered ones"
class="mr-4"
:disabled="model.healthBarNoHealing"
:value="model.healthBarHealingOrder"
:error-messages="errors.healthBarHealingOrder"
@change="change('healthBarHealingOrder', ...arguments)"
/>
<smart-switch
label="Ignore healing"
:value="model.healthBarNoHealing"
:error-messages="errors.healthBarNoHealing"
@change="change('healthBarNoHealing', ...arguments)"
/>
</v-layout>
</form-section>
</v-expand-transition>
@@ -121,6 +161,18 @@
:error-messages="errors.decimal"
@change="change('decimal', ...arguments)"
/>
<smart-switch
label="Can be damaged into negative values"
:value="model.ignoreLowerLimit"
:error-messages="errors.ignoreLowerLimit"
@change="change('ignoreLowerLimit', ...arguments)"
/>
<smart-switch
label="Can be incremented above total"
:value="model.ignoreUpperLimit"
:error-messages="errors.ignoreUpperLimit"
@change="change('ignoreUpperLimit', ...arguments)"
/>
<div
class="layout justify-center"
style="align-self: stretch;"