diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js deleted file mode 100644 index e1a61f33..00000000 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ /dev/null @@ -1,139 +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'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; - -const damageProperty = new ValidatedMethod({ - name: 'creatureProperties.damage', - validate: new SimpleSchema({ - _id: SimpleSchema.RegEx.Id, - operation: { - type: String, - allowedValues: ['set', 'increment'] - }, - value: Number, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 20, - timeInterval: 5000, - }, - run({ _id, operation, value }) { - - // Get action context - let prop = CreatureProperties.findOne(_id); - if (!prop) throw new Meteor.Error( - 'Damage property failed', 'Property doesn\'t exist' - ); - const creatureId = prop.root.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(prop); - if (!schema.allowsKey('damage')) { - throw new Meteor.Error( - 'Damage property failed', - `Property of type "${prop.type}" can't be damaged` - ); - } - - // Replace the prop by its actionContext counterpart if possible - if (prop.variableName) { - const actionContextProp = actionContext.scope[prop.variableName]; - if (actionContextProp?._id === prop._id) { - prop = actionContextProp; - } - } - - const result = damagePropertyWork({ prop, operation, value, actionContext }); - - // Insert the log - actionContext.writeLog(); - return result; - }, -}); - -export function damagePropertyWork({ prop, operation, value, actionContext, logFunction = undefined }) { - - // 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: -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']?.value; - } else { - value = -actionContext.scope['~healing']?.value; - } - } else { - value = actionContext.scope['~set']?.value; - } - - let damage, newValue, increment; - if (operation === 'set') { - 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 && !prop.ignoreLowerLimit) damage = total; - // Damage must be positive - 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 - }); - // Also write it straight to the prop so that it is updated in the actionContext - prop.damage = damage; - prop.value = newValue; - logFunction?.(newValue); - } else if (operation === 'increment') { - let currentValue = prop.value || 0; - let currentDamage = prop.damage || 0; - increment = value; - // Can't increase damage above the remaining value - if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue; - // Can't decrease damage below zero - if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage; - damage = currentDamage + increment; - newValue = prop.total - damage; - // Write the results - CreatureProperties.update(prop._id, { - $inc: { damage: increment, value: -increment }, - $set: { dirty: true }, - }, { - selector: prop - }); - // Also write it straight to the prop so that it is updated in the actionContext - prop.damage += increment; - prop.value -= increment; - logFunction?.(increment); - } - - 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/engine/action/functions/applyTaskGroups.ts b/app/imports/api/engine/action/functions/applyTaskGroups.ts index ca0d3d1e..098afd0b 100644 --- a/app/imports/api/engine/action/functions/applyTaskGroups.ts +++ b/app/imports/api/engine/action/functions/applyTaskGroups.ts @@ -131,7 +131,7 @@ export async function applyAfterPropTasksForSomeChildren( export async function applyTriggers( action: EngineAction, prop, targetIds: string[], triggerPath: string, inputProvider: InputProvider ) { - const triggerIds = get(prop?.triggerIds, triggerPath); + const triggerIds = get(prop, triggerPath); if (!triggerIds) return; for (const triggerId of triggerIds) { const trigger = await getSingleProperty(action.creatureId, triggerId); diff --git a/app/imports/api/engine/action/methods/doCastSpell.js b/app/imports/api/engine/action/methods/doCastSpell.js index c2e1f957..f15696cb 100644 --- a/app/imports/api/engine/action/methods/doCastSpell.js +++ b/app/imports/api/engine/action/methods/doCastSpell.js @@ -7,7 +7,6 @@ import { } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; // TODO Migrate this to the new action engine diff --git a/app/imports/api/engine/action/tasks/applyCheckTask.ts b/app/imports/api/engine/action/tasks/applyCheckTask.ts index 11fa54f0..b8f32e39 100644 --- a/app/imports/api/engine/action/tasks/applyCheckTask.ts +++ b/app/imports/api/engine/action/tasks/applyCheckTask.ts @@ -1,10 +1,10 @@ -import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups'; +import { applyTriggers } from '/imports/api/engine/action/functions/applyTaskGroups'; import { CheckTask } from '/imports/api/engine/action/tasks/Task'; import { EngineAction } from '/imports/api/engine/action/EngineActions'; import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope'; import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables'; import { getVariables } from '/imports/api/engine/loadCreatures'; -import InputProvider, { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider'; +import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider'; import numberToSignedString from '/imports/api/utility/numberToSignedString'; import TaskResult from '/imports/api/engine/action/tasks/TaskResult'; @@ -38,6 +38,17 @@ export default async function applyCheckTask( const ability = checkParams.abilityVariableName && getFromScope(checkParams.abilityVariableName, scope) || null; const abilityModifier = ability?.modifier || 0; + + // Run the before triggers which may change scope properties + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.before', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.before', userInput); + + if (skill || ability) { + // Create a new result after before triggers have run + result = new TaskResult(task.prop._id, task.targetIds); + action.results.push(result); + } + const totalModifier = skillBonus + abilityModifier; const rollModifierText = numberToSignedString(totalModifier); @@ -100,9 +111,15 @@ export default async function applyCheckTask( inline: true, ...prop?.silent && { silenced: prop.silent } }, [targetId]); - } - return applyDefaultAfterPropTasks(action, prop, targetIds, userInput); + // After check triggers + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.after', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.after', userInput); + + // After children check triggers + if (skill) await applyTriggers(action, skill, [targetId], 'checkTriggerIds.afterChildren', userInput); + if (ability) await applyTriggers(action, ability, [targetId], 'checkTriggerIds.afterChildren', userInput); + } } // TODO set these and potentially read them again if triggers can change them diff --git a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts index 15012c7a..e7dada77 100644 --- a/app/imports/api/engine/action/tasks/applyDamagePropTask.ts +++ b/app/imports/api/engine/action/tasks/applyDamagePropTask.ts @@ -42,7 +42,7 @@ export default async function applyDamagePropTask( } // Run the before triggers which may change scope properties - await applyTriggers(action, targetProp, [action.creatureId], 'before', userInput); + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.before', userInput); // Create a new result after triggers have run result = new TaskResult(task.prop._id, task.targetIds); @@ -136,8 +136,8 @@ export default async function applyDamagePropTask( }); setScope(result, targetProp, newValue, damage); } - await applyTriggers(action, targetProp, [action.creatureId], 'after', userInput); - await applyTriggers(action, targetProp, [action.creatureId], 'afterChildren', userInput); + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.after', userInput); + await applyTriggers(action, targetProp, [targetId], 'damageTriggerIds.afterChildren', userInput); return increment; } diff --git a/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts index 092b6321..fd57645b 100644 --- a/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts +++ b/app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts @@ -22,7 +22,11 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: result.pushScope = { ['~ammoConsumed']: { value } }; // Apply the before triggers - await applyTriggers(action, item, [action.creatureId], 'ammo.before', userInput); + await applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.before', userInput); + + // Create a new result after before triggers have run + result = new TaskResult(task.prop._id, task.targetIds); + action.results.push(result); // Refetch the scope properties const scope = await getEffectiveActionScope(action); @@ -52,11 +56,12 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: }, }); - await applyTriggers(action, item, [action.creatureId], 'ammo.after', userInput); + await applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.after', userInput); if (task.params.skipChildren) { - return applyAfterTasksSkipChildren(action, item, task.targetIds, userInput); + await applyAfterTasksSkipChildren(action, item, task.targetIds, userInput); } else { - return applyDefaultAfterPropTasks(action, item, task.targetIds, userInput); + await applyDefaultAfterPropTasks(action, item, task.targetIds, userInput); } + return applyTriggers(action, item, task.targetIds, 'ammoTriggerIds.afterChildren', userInput); } \ No newline at end of file diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js index 268de44a..f80e6f13 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeTrigger.js @@ -9,31 +9,40 @@ export default function computeTrigger(computation, node) { // Link triggers to all the properties that would fire them when applied const tagTargets = getEffectTagTargets(prop, computation); - switch (prop.event) { - case 'doActionProperty': - tagTargets.forEach(targetId => { - const targetProp = computation.propsById[targetId]; + for (const targetId of tagTargets) { + const targetProp = computation.propsById[targetId]; + switch (prop.event) { + case 'doActionProperty': // Only apply if the trigger matches this property type - if (targetProp.type !== prop.actionPropertyType) return; - setTrigger(prop, targetProp); - }); - break; - case 'damageProperty': - tagTargets.forEach(targetId => { - const targetProp = computation.propsById[targetId]; + if (targetProp.type === prop.actionPropertyType) { + setTrigger(prop, targetProp, 'triggerIds'); + } + // Or on an item used as ammo + else if (prop.actionPropertyType === 'ammo' && targetProp.type === 'item') { + setTrigger(prop, targetProp, 'ammoTriggerIds'); + } + break; + case 'damageProperty': // Only apply to attributes - if (targetProp.type !== 'attribute') return; - setTrigger(prop, targetProp); - }); - break; + if (targetProp.type === 'attribute') { + setTrigger(prop, targetProp, 'damageTriggerIds'); + } + break; + case 'check': + // Only apply to attributes and skills + if (targetProp.type === 'attribute' || targetProp.type === 'skill') { + setTrigger(prop, targetProp, 'checkTriggerIds'); + } + break; + } } } -function setTrigger(prop, targetProp) { - let triggerIdArray = get(targetProp, `triggerIds.${prop.timing}`); +function setTrigger(prop, targetProp, field = 'triggerIds') { + let triggerIdArray = get(targetProp, `${field}.${prop.timing}`); if (!triggerIdArray) { triggerIdArray = []; - set(targetProp, `triggerIds.${prop.timing}`, triggerIdArray); + set(targetProp, `${field}.${prop.timing}`, triggerIdArray); } triggerIdArray.push(prop._id); } \ No newline at end of file diff --git a/app/imports/api/properties/Attributes.ts b/app/imports/api/properties/Attributes.ts index 9cb99fa7..88d0fcae 100644 --- a/app/imports/api/properties/Attributes.ts +++ b/app/imports/api/properties/Attributes.ts @@ -280,9 +280,69 @@ const ComputedOnlyAttributeSchema = createPropertySchema({ type: Number, optional: true, }, + // Triggers that fire when this property is damaged + 'damageTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'damageTriggerIds.before': { + type: Array, + optional: true, + }, + 'damageTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'damageTriggerIds.after': { + type: Array, + optional: true, + }, + 'damageTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'damageTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'damageTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Triggers that fire when this property is used to make a check + 'checkTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'checkTriggerIds.before': { + type: Array, + optional: true, + }, + 'checkTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'checkTriggerIds.after': { + type: Array, + optional: true, + }, + 'checkTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'checkTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'checkTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, }); -const ComputedAttributeSchema = new SimpleSchema() +const ComputedAttributeSchema = new SimpleSchema({}) .extend(ComputedOnlyAttributeSchema) .extend(AttributeSchema); diff --git a/app/imports/api/properties/Items.ts b/app/imports/api/properties/Items.ts index 02e0e0c9..d513b690 100644 --- a/app/imports/api/properties/Items.ts +++ b/app/imports/api/properties/Items.ts @@ -68,16 +68,46 @@ const ItemSchema = createPropertySchema({ type: Boolean, optional: true, }, + // Triggers that fire when this property is used as ammo + 'ammoTriggerIds': { + type: Object, + optional: true, + removeBeforeCompute: true, + }, + 'ammoTriggerIds.before': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.before.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'ammoTriggerIds.after': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.after.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'ammoTriggerIds.afterChildren': { + type: Array, + optional: true, + }, + 'ammoTriggerIds.afterChildren.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, }); -let ComputedOnlyItemSchema = createPropertySchema({ +const ComputedOnlyItemSchema = createPropertySchema({ description: { type: 'computedOnlyInlineCalculationField', optional: true, }, }); -const ComputedItemSchema = new SimpleSchema() +const ComputedItemSchema = new SimpleSchema({}) .extend(ItemSchema) .extend(ComputedOnlyItemSchema);