diff --git a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js deleted file mode 100644 index 6428eeb3..00000000 --- a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js +++ /dev/null @@ -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; diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js index 315b6f2c..d9af78f5 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -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; diff --git a/app/imports/api/creature/creatureProperties/methods/dealDamage.js b/app/imports/api/creature/creatureProperties/methods/dealDamage.js deleted file mode 100644 index ddb3dcc1..00000000 --- a/app/imports/api/creature/creatureProperties/methods/dealDamage.js +++ /dev/null @@ -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; diff --git a/app/imports/api/creature/creatureProperties/methods/index.js b/app/imports/api/creature/creatureProperties/methods/index.js index 45a0e323..b71515e9 100644 --- a/app/imports/api/creature/creatureProperties/methods/index.js +++ b/app/imports/api/creature/creatureProperties/methods/index.js @@ -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'; diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index 6eafbbcc..49d228aa 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -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}, @@ -132,7 +107,7 @@ function doRestWork(creature, restType) { fields: { hitDiceSize: 1, damage: 1, - value: 1, + total: 1, } }).fetch(); // Use a collator to do sorting in natural order @@ -143,8 +118,8 @@ function doRestWork(creature, restType) { let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) 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 totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0); + 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; diff --git a/app/imports/api/engine/actions/ActionContext.js b/app/imports/api/engine/actions/ActionContext.js new file mode 100644 index 00000000..9c5e2e50 --- /dev/null +++ b/app/imports/api/engine/actions/ActionContext.js @@ -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.. or + // triggers.doActionProperty.. + 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, + }); + } +} \ No newline at end of file diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 5930a93d..ec9c28d9 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -7,7 +7,6 @@ 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'; const applyPropertyByType = { action, @@ -22,9 +21,7 @@ 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) { + actionContext.scope[`#${node.node.type}`] = node.node; + applyPropertyByType[node.node.type]?.(node, actionContext, ...rest); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 0366bf49..f666bad9 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -6,23 +6,24 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyAction(node, {creature, targets, scope, log}){ +export default function applyAction(node, actionContext) { + applyNodeTriggers(node, 'before', 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 +32,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 +74,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 +90,7 @@ function applyAttackToTarget({attack, target, scope, log}){ delete scope['$attackDiceRoll']; delete scope['$attackRoll']; - recalculateCalculation(attack, scope, log); + recalculateCalculation(attack, actionContext); let { resultPrefix, @@ -108,7 +111,7 @@ function applyAttackToTarget({attack, target, scope, log}){ name += ' (Disadvantage)'; } - log.content.push({ + actionContext.addLog({ name, value: `${resultPrefix}\n**${result}**`, inline: true, @@ -119,11 +122,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 +180,15 @@ function applyCrits(value, scope){ return {criticalHit, criticalMiss}; } -function applyChildren(node, args){ - node.children.forEach(child => applyProperty(child, args)); +function applyChildren(node, actionContext) { + applyNodeTriggers(node, 'after', 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 +196,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 +208,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 +239,7 @@ function spendResources({prop, log, scope}){ } }); } catch (e){ - log.content.push({ + actionContext.addLog({ name: 'Error', value: e, }); @@ -253,7 +257,7 @@ function spendResources({prop, log, scope}){ }, { selector: prop }); - log.content.push({ + actionContext.addLog({ name: 'Uses left', value: prop.usesLeft - 1, inline: true, @@ -262,18 +266,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 +288,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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js index b920283b..cc63f53a 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js @@ -1,41 +1,42 @@ import applyProperty from '../applyProperty.js'; import recalculateCalculation from './shared/recalculateCalculation.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyAdjustment(node, { - creature, targets, scope, log -}){ +export default function applyAdjustment(node, actionContext){ + applyNodeTriggers(node, 'before', 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 +44,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 +52,10 @@ 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){ + applyNodeTriggers(node, 'after', actionContext); + node.children.forEach(child => applyProperty(child, actionContext)); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index 6a46e07c..3106b88b 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -1,26 +1,27 @@ import applyProperty from '../applyProperty.js'; import recalculateCalculation from './shared/recalculateCalculation.js'; import rollDice from '/imports/parser/rollDice.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyBranch(node, { - creature, targets, scope, log -}){ +export default function applyBranch(node, actionContext){ + applyNodeTriggers(node, 'before', actionContext); const applyChildren = function(){ - node.children.forEach(child => applyProperty(child, { - creature, targets, scope, log - })); + applyNodeTriggers(node, 'after', actionContext); + 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 +30,47 @@ 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 - }); + applyNodeTriggers(node, 'after', actionContext); + 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 - }); + applyNodeTriggers(node, 'after', actionContext); + applyProperty(node.children[index], actionContext); } break; case 'eachTarget': - if (targets.length){ + if (targets.length) { targets.forEach(target => { - node.children.forEach(child => applyProperty(child, { - creature, targets: [target], scope, log - })); + applyNodeTriggers(node, 'after', actionContext); + actionContext.targets = [target] + node.children.forEach(child => applyProperty(child, actionContext)); }); } else { applyChildren(); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 66857f6c..b6e7d682 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -12,10 +12,12 @@ import symbol from '/imports/parser/parseTree/symbol.js'; 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'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyBuff(node, {creature, targets, scope, log}){ +export default function applyBuff(node, actionContext){ + applyNodeTriggers(node, 'before', 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 +28,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 +40,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, }); @@ -58,6 +60,7 @@ export default function applyBuff(node, {creature, targets, scope, log}){ } } }); + applyNodeTriggers(node, 'after', actionContext); // Don't apply the children of the buff, they get copied to the target instead } @@ -83,7 +86,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 +107,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 +115,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; } }); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index f8b6eb8b..0ee3bbd7 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -1,27 +1,30 @@ -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'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyDamage(node, { - creature, targets, scope, log -}){ +export default function applyDamage(node, actionContext){ + applyNodeTriggers(node, 'before', actionContext); const applyChildren = function(){ - node.children.forEach(child => applyProperty(child, { - creature, targets, scope, log - })); + applyNodeTriggers(node, 'after', actionContext); + 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 +39,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 +97,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 +128,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 +183,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; +} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js index 2d460ee2..0d5f9e84 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js @@ -1,25 +1,27 @@ import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; import applyProperty from '../applyProperty.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyNote(node, {creature, targets, scope, log}){ +export default function applyNote(node, actionContext){ + applyNodeTriggers(node, 'before', 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 triggers + applyNodeTriggers(node, 'after', actionContext); // Apply children - node.children.forEach(child => applyProperty(child, { - creature, targets, scope, log - })); + node.children.forEach(child => applyProperty(child, actionContext)); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index 8d923e4e..7d860948 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -2,33 +2,34 @@ import applyProperty from '../applyProperty.js'; 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'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyRoll(node, {creature, targets, scope, log}){ +export default function applyRoll(node, actionContext){ + applyNodeTriggers(node, 'before', actionContext); const prop = node.node; const applyChildren = function(){ - node.children.forEach(child => applyProperty(child, { - creature, targets, scope, log - })); + applyNodeTriggers(node, 'after', actionContext); + 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 +46,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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js index 503442ab..6064e3bd 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js @@ -2,38 +2,38 @@ import rollDice from '/imports/parser/rollDice.js'; import recalculateCalculation from './shared/recalculateCalculation.js'; import applyProperty from '../applyProperty.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applySavingThrow(node, {creature, targets, scope, log}){ +export default function applySavingThrow(node, actionContext){ + applyNodeTriggers(node, 'before', 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 - })); + scope['$saveSucceeded'] = { value: true }; + applyNodeTriggers(node, 'after', actionContext); + return node.children.forEach(child => applyProperty(child, actionContext)); } // Each target makes the saving throw @@ -43,16 +43,16 @@ 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 () { + applyNodeTriggers(node, 'after', actionContext); + 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 +94,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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js index 5162c41f..be80b012 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js @@ -1,14 +1,13 @@ import applyProperty from '../applyProperty.js'; import recalculateCalculation from './shared/recalculateCalculation.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyToggle(node, { - creature, targets, scope, log -}){ +export default function applyToggle(node, actionContext){ + applyNodeTriggers(node, 'before', 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 - })); + applyNodeTriggers(node, 'after', actionContext); + return node.children.forEach(child => applyProperty(child, actionContext)); } } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js index be625eea..0ada4ea1 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js @@ -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) } }); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js b/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js index 219fddd5..9ea760e6 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/logErrors.js @@ -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}); } }); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index a10340be..53e98e2d 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -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); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js index e4b49be0..1a2b1cb7 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js @@ -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); diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js index 22507780..9343e1ac 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -6,18 +6,78 @@ 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 => { - if (triggerMatchTags(trigger, prop)) { - applyTrigger(trigger, { creature, targets, scope, log }); - } + const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing]; + if (triggers) { + triggers.forEach(trigger => { + applyTrigger(trigger, prop, actionContext); }); } } +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 (prop && !triggerMatchTags(trigger, prop)) { + return; + } + + // Prevent trigger from firing if it's inactive + if (trigger.inactive) { + return; + } + + // Prevent triggers from firing if their condition is false + if (trigger.condition?.parseNode) { + recalculateCalculation(trigger.condition, actionContext); + if (!trigger.condition.value) return; + } + + // Prevent triggers from firing themselves in a loop + if (trigger.firing) { + /* + log.content.push({ + name: trigger.name || 'Trigger', + value: 'Trigger can\'t fire itself', + inline: true, + }); + */ + return; + } + trigger.firing = true; + + // Fire the trigger + const content = { + name: trigger.name || 'Trigger', + value: trigger.description, + inline: false, + } + if (trigger.description?.text){ + recalculateInlineCalculations(trigger.description, actionContext); + content.value = trigger.description.value; + } + actionContext.addLog(content); + + // Get all the trigger's properties and apply them + 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, actionContext); + }); + + trigger.firing = false; +} + function triggerMatchTags(trigger, prop) { let matched = false; const propTags = getEffectivePropTags(prop); @@ -49,51 +109,3 @@ function triggerMatchTags(trigger, prop) { }); return matched; } - -export function applyTrigger(trigger, { creature, targets, scope, log }) { - // Prevent triggers from firing if their condition is false - if (trigger.condition?.parseNode) { - recalculateCalculation(trigger.condition, scope, log); - if (!trigger.condition.value) return; - } - - // Prevent triggers from firing themselves in a loop - if (trigger.firing) { - /* - log.content.push({ - name: trigger.name || 'Trigger', - value: 'Trigger can\'t fire itself', - inline: true, - }); - */ - return; - } - trigger.firing = true; - - // Fire the trigger - const content = { - name: trigger.name || 'Trigger', - value: trigger.summary, - inline: false, - } - if (trigger.summary?.text){ - recalculateInlineCalculations(trigger.summary, scope, log); - content.value = trigger.summary.value; - } - log.content.push(content); - - // Get all the trigger's properties and apply them - const properties = getPropertyDecendants(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, - }); - }); - - trigger.firing = false; -} diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index 75a4112c..51934483 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -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,36 +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'); - remove(triggers, trigger => trigger.event !== 'doActionProperty'); - 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 diff --git a/app/imports/api/engine/actions/doAction.test.js b/app/imports/api/engine/actions/doAction.test.js index 19560704..56c365cb 100644 --- a/app/imports/api/engine/actions/doAction.test.js +++ b/app/imports/api/engine/actions/doAction.test.js @@ -1,11 +1,53 @@ import '/imports/api/simpleSchemaConfig.js'; //import testTypes from './testTypes/index.js'; import { doActionWork } from './doAction.js'; -import createAction from './tests/createAction.testFn.js'; +import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; + +function cleanProp(prop){ + let schema = CreatureProperties.simpleSchema(prop); + return schema.clean(prop); +} + +function cleanCreature(creature){ + let schema = Creatures.simpleSchema(creature); + return schema.clean(creature); +} + +// Fake ActionContext to test actions with +const creatureId = 'actionTestCreatureId'; +const creatureName = 'Action Test Creature'; +const testActionContext = { + creature: cleanCreature({ + _id: creatureId, + }), + log: CreatureLogSchema.clean({ + creatureId: creatureId, + creatureName: creatureName, + }), + scope: {}, + addLog(content) { + if (content.name || content.value){ + this.log.content.push(content); + } + }, + writeLog: () => { }, +} + +const action = cleanProp({ + type: 'action', +}); +const actionAncestors = []; describe('Do Action', function(){ it('Does an empty action', function(){ - doActionWork(createAction({properties: [{type: 'action'}]})); + doActionWork({ + properties: [action], + ancestors: actionAncestors, + actionContext: testActionContext, + methodScope: {}, + }); }); //testTypes.forEach(test => it(test.text, test.fn)); }); diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index e403e271..95169e90 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -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 }, }); diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index 9f139f2c..42e98f61 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -1,12 +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 { 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 { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import ActionContext from '/imports/api/engine/actions/ActionContext.js'; const doCheck = new ValidatedMethod({ name: 'creatureProperties.doCheck', @@ -24,34 +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}){ - rollCheck({prop, log, methodScope}); + 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 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`; @@ -77,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) { @@ -87,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) { @@ -103,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}**`, }); diff --git a/app/imports/api/engine/actions/tests/createAction.testFn.js b/app/imports/api/engine/actions/tests/createAction.testFn.js deleted file mode 100644 index 84ffa907..00000000 --- a/app/imports/api/engine/actions/tests/createAction.testFn.js +++ /dev/null @@ -1,26 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; - -export default function createAction({ - creature = {_id: 'creatureId'}, - targets = [], - properties = [], - ancestors = [], - method -} = {}){ - properties = properties.map(cleanProp); - ancestors = ancestors.map(cleanProp); - creature = cleanCreature(creature); - ancestors = ancestors.map(cleanCreature); - return {creature, targets, properties, ancestors, method}; -} - -function cleanProp(prop){ - let schema = CreatureProperties.simpleSchema(prop); - return schema.clean(prop); -} - -function cleanCreature(creature){ - let schema = Creatures.simpleSchema(creature); - return schema.clean(creature); -} diff --git a/app/imports/api/engine/actions/tests/testTypes/applyAction.testFn.js b/app/imports/api/engine/actions/tests/testTypes/applyAction.testFn.js deleted file mode 100644 index e69de29b..00000000 diff --git a/app/imports/api/engine/actions/tests/testTypes/index.testFn.js b/app/imports/api/engine/actions/tests/testTypes/index.testFn.js deleted file mode 100644 index e8a4b486..00000000 --- a/app/imports/api/engine/actions/tests/testTypes/index.testFn.js +++ /dev/null @@ -1,6 +0,0 @@ -import applyAction from './applyAction.testFn.js'; - -export default [{ - text: 'Applies actions', - fn: applyAction, -},]; diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 5e907acc..ad566b58 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -178,8 +178,8 @@ function getTargetListFromTags(tags, computation){ const targetTagIdLists = []; if (!tags) return []; tags.forEach(tag => { - const idList = computation.propsWithTag[tag]; - if (idList) targetTagIdLists.push(idList); + const idList = computation.propsWithTag[tag] || []; + targetTagIdLists.push(idList); }); const targets = intersection(...targetTagIdLists); return targets; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 828059c0..37b6afdd 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -86,7 +86,7 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){ // to a skill from its ability if (link.data === 'effect'){ if (![ - 'advantage', 'disadvantage', 'passiveAdd', 'fail' + 'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional' ].includes(linkedNode.data.operation)){ return; } diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 4b4537c0..38721928 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -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: { diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js index 3b9b0d13..f8f6c4a3 100644 --- a/app/imports/api/properties/Triggers.js +++ b/app/imports/api/properties/Triggers.js @@ -5,8 +5,11 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; const eventOptions = { doActionProperty: 'Do action', // 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', @@ -26,6 +29,7 @@ const actionPropertyTypeOptions = { note: 'Note', roll: 'Roll', savingThrow: 'Saving throw', + spell: 'Spell', toggle: 'Toggle', } @@ -40,10 +44,6 @@ let TriggerSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, - summary: { - type: 'inlineCalculationFieldToCompute', - optional: true, - }, description: { type: 'inlineCalculationFieldToCompute', optional: true, diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 0f0ef37b..0d7ad114 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -28,6 +28,7 @@ function computedOnlyField(field){ [`${field}.effects`]: { type: Array, optional: true, + removeBeforeCompute: true, }, [`${field}.effects.$`]: { type: Object, diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index 2d789993..edd1d44a 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -110,8 +110,8 @@ export function getUserTier(user){ export function assertUserHasPaidBenefits(user){ let tier = getUserTier(user); if (!tier.paidBenefits){ - throw new Meteor.Error('Creatures.methods.insert.denied', - `The ${tier.name} tier does not allow you to insert a creature`); + throw new Meteor.Error('no paid benefits', + `The ${tier.name} tier does not have the required benefits`); } } diff --git a/app/imports/migrations/server/dbv1/dbv1.test.js b/app/imports/migrations/server/dbv1/dbv1.test.js index 9a0887ac..efbd4fdd 100644 --- a/app/imports/migrations/server/dbv1/dbv1.test.js +++ b/app/imports/migrations/server/dbv1/dbv1.test.js @@ -124,6 +124,7 @@ const expectedMigratedAttribute = { damage: 3, value: 17, constitutionMod: 2, + dirty: true, } const exampleAttack = { @@ -221,6 +222,7 @@ describe('migrateProperty', function() { prop: newAction, reversed: true, }); + delete reversedAction.dirty; assert.deepEqual(action, exampleAction, 'action should not be bashed'); assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible'); }); diff --git a/app/imports/ui/components/MarkdownText.vue b/app/imports/ui/components/MarkdownText.vue index 681878d2..ab9d35b2 100644 --- a/app/imports/ui/components/MarkdownText.vue +++ b/app/imports/ui/components/MarkdownText.vue @@ -24,10 +24,3 @@ }, } - - diff --git a/app/imports/ui/components/tree/TreeSearchInput.vue b/app/imports/ui/components/tree/TreeSearchInput.vue index 9c843b9a..5abac7e5 100644 --- a/app/imports/ui/components/tree/TreeSearchInput.vue +++ b/app/imports/ui/components/tree/TreeSearchInput.vue @@ -13,6 +13,16 @@