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..7af25d1e 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}, @@ -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; 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..2a8b3add 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -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'); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 0366bf49..19481501 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js index b920283b..525f980c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js @@ -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)); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index 6a46e07c..6e0ddd06 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -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(); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 66857f6c..f36d5ae5 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -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; } }); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index f8b6eb8b..4d9145b3 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -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; +} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js index 2d460ee2..b1b5e37a 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js @@ -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)); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index 8d923e4e..194c52a2 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js index 503442ab..0fe34329 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js @@ -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, diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js index 5162c41f..16a97412 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js @@ -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)); } } 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 d0863ed4..f8346163 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -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; diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index 271e2ba8..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,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.. - 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/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 0a5fef41..42e98f61 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -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}**`, }); 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 da99fddc..ce886d3d 100644 --- a/app/imports/api/properties/Triggers.js +++ b/app/imports/api/properties/Triggers.js @@ -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', diff --git a/app/imports/ui/properties/components/attributes/HealthBar.vue b/app/imports/ui/properties/components/attributes/HealthBar.vue index f71f8d89..ae9b3bce 100644 --- a/app/imports/ui/properties/components/attributes/HealthBar.vue +++ b/app/imports/ui/properties/components/attributes/HealthBar.vue @@ -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})`, }" />
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; + } + }, +};