diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index 0115db52..2f630a5b 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -1,9 +1,14 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; +import { groupBy, remove, rest, 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'; const restCreature = new ValidatedMethod({ name: 'creature.methods.rest', @@ -23,101 +28,142 @@ const restCreature = new ValidatedMethod({ timeInterval: 5000, }, run({creatureId, restType}) { - let creature = Creatures.findOne(creatureId, { - fields: { - owner: 1, - writers: 1, - settings: 1, - } - }) ; - // Need edit permissions + // Check permissions + let creature = getCreature(creatureId); assertEditPermission(creature, this.userId); - // Long rests reset short rest properties as well - let resetFilter; - if (restType === 'shortRest'){ - resetFilter = 'shortRest' - } else { - resetFilter = {$in: ['shortRest', 'longRest']} + // Add the variables to the creature document + const variables = getVariables(creatureId); + delete variables._id; + delete variables._creatureId; + creature.variables = variables; + const scope = creature.variables; + + // 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') } - // Only apply to active properties - let filter = { - 'ancestors.id': creatureId, - reset: resetFilter, - removed: { $ne: true }, - inactive: { $ne: true }, - }; - // update all attribute's damage - filter.type = 'attribute'; - CreatureProperties.update(filter, { - $set: { - damage: 0, - dirty: true, - } - }, { - selector: {type: 'attribute'}, - multi: true, + + // Create the log + const log = CreatureLogSchema.clean({ + creatureId: creature._id, + creatureName: creature.name, }); - // Update all action-like properties' usesUsed - filter.type = {$in: [ - 'action', - 'attack', - 'spell' - ]}; - CreatureProperties.update(filter, { - $set: { - usesUsed: 0, - dirty: true, - } - }, { - selector: {type: 'action'}, - multi: true, - }); - // Reset half hit dice on a long rest, starting with the highest dice - if (restType === 'longRest'){ - let hitDice = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'attribute', - attributeType: 'hitDice', - removed: {$ne: true}, - inactive: {$ne: true}, - }, { - fields: { - hitDiceSize: 1, - damage: 1, - value: 1, - } - }).fetch(); - // Use a collator to do sorting in natural order - let collator = new Intl.Collator('en', { - numeric: true, sensitivity: 'base' - }); - // Get the hit dice in decending order of hitDiceSize - 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 recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); - // recover each hit dice in turn until the recoverable amount is used up - let amountToRecover, resultingDamage; - hitDice.forEach(hd => { - if (!recoverableHd) return; - amountToRecover = Math.min(recoverableHd, hd.damage || 0); - if (!amountToRecover) return; - recoverableHd -= amountToRecover; - resultingDamage = hd.damage - amountToRecover; - CreatureProperties.update(hd._id, { - $set: { - damage: resultingDamage, - dirty: true, - } - }, { - selector: {type: 'attribute'}, - }); - }); - } + + const targets = [creature]; + + applyTriggers(triggers, restType, 'before', { creature, targets, scope, log }); + doRestWork(creature, restType); + applyTriggers(triggers, restType, 'after', { creature, targets, scope, log }); + + insertCreatureLogWork({log, creature, method: this}); }, }); +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) { + // Long rests reset short rest properties as well + let resetFilter; + if (restType === 'shortRest'){ + resetFilter = 'shortRest' + } else { + resetFilter = {$in: ['shortRest', 'longRest']} + } + // Only apply to active properties + let filter = { + 'ancestors.id': creature._id, + reset: resetFilter, + removed: { $ne: true }, + inactive: { $ne: true }, + }; + // update all attribute's damage + filter.type = 'attribute'; + CreatureProperties.update(filter, { + $set: { + damage: 0, + dirty: true, + } + }, { + selector: {type: 'attribute'}, + multi: true, + }); + // Update all action-like properties' usesUsed + filter.type = {$in: [ + 'action', + 'attack', + 'spell' + ]}; + CreatureProperties.update(filter, { + $set: { + usesUsed: 0, + dirty: true, + } + }, { + selector: {type: 'action'}, + multi: true, + }); + // Reset half hit dice on a long rest, starting with the highest dice + if (restType === 'longRest'){ + let hitDice = CreatureProperties.find({ + 'ancestors.id': creature._id, + type: 'attribute', + attributeType: 'hitDice', + removed: {$ne: true}, + inactive: {$ne: true}, + }, { + fields: { + hitDiceSize: 1, + damage: 1, + value: 1, + } + }).fetch(); + // Use a collator to do sorting in natural order + let collator = new Intl.Collator('en', { + numeric: true, sensitivity: 'base' + }); + // Get the hit dice in decending order of hitDiceSize + 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 recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); + // recover each hit dice in turn until the recoverable amount is used up + let amountToRecover, resultingDamage; + hitDice.forEach(hd => { + if (!recoverableHd) return; + amountToRecover = Math.min(recoverableHd, hd.damage || 0); + if (!amountToRecover) return; + recoverableHd -= amountToRecover; + resultingDamage = hd.damage - amountToRecover; + CreatureProperties.update(hd._id, { + $set: { + damage: resultingDamage, + dirty: true, + } + }, { + selector: {type: 'attribute'}, + }); + }); + } +} + export default restCreature; diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 32542390..5930a93d 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -7,6 +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'; const applyPropertyByType = { action, @@ -21,7 +22,9 @@ const applyPropertyByType = { toggle, }; -export default function applyProperty(node, opts, ...rest){ +export default function applyProperty(node, opts, ...rest) { + applyTriggers(node, opts, 'before'); opts.scope[`#${node.node.type}`] = node.node; - return applyPropertyByType[node.node.type]?.(node, opts, ...rest); + applyPropertyByType[node.node.type]?.(node, opts, ...rest); + applyTriggers(node, opts, 'after'); } diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js new file mode 100644 index 00000000..3844ae15 --- /dev/null +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -0,0 +1,96 @@ +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; +import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js'; +import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import applyProperty from '/imports/api/engine/actions/applyProperty.js'; +import { difference, intersection } from 'lodash'; + +export default function applyTriggers(node, { creature, targets, scope, log }, timing) { + const prop = node.node; + const type = prop.type; + if (creature.triggers?.[type]?.[timing]) { + creature.triggers[type][timing].forEach(trigger => { + // Tags + if (!triggerMatchTags(trigger, prop)) return; + // Condition + if (trigger.condition?.parseNode) { + recalculateCalculation(trigger.condition, scope, log); + if (!trigger.condition.value) return; + } + // Apply + applyTrigger(trigger, { creature, targets, scope, log }); + }); + } +} + +function triggerMatchTags(trigger, prop) { + let matched = false; + // Check the target tags + if ( + !trigger.targetTags?.length || + difference(trigger.targetTags, prop.tags).length === 0 + ) { + matched = true; + } + // Check the extra tags + trigger.extraTags?.forEach(extra => { + if (extra.operation === 'OR') { + if (matched) return; + if ( + !extra.tags.length || + difference(extra.tags, prop.tags).length === 0 + ) { + matched = true; + } + } else if (extra.operation === 'NOT') { + if ( + extra.tags.length && + intersection(extra.tags, prop.tags) + ) { + return false; + } + } + }); + return matched; +} + +export function applyTrigger(trigger, { creature, targets, scope, log }) { + 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 c22deff2..75a4112c 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -1,14 +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 CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.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 { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import { + getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType +} 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'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', @@ -38,52 +40,37 @@ const doAction = new ValidatedMethod({ run({actionId, targetIds = [], scope}) { let action = CreatureProperties.findOne(actionId); // Check permissions - let creature = getRootCreatureAncestor(action); - const variables = CreatureVariables.findOne({ - _creatureId: creature._id - }, { - fields: {_id: 0, _creatureId: 0}, - }); - creature.variables = variables; - + 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 = Creatures.findOne(targetId); + let target = getCreature(targetId); assertEditPermission(target, this.userId); - const variables = CreatureVariables.findOne({ - _creatureId: targetId - }, { - fields: {_id: 0, _creatureId: 0}, - }); + + // add the variables to the target documents + const variables = getVariables(creatureId); + delete variables._id; + delete variables._creatureId; target.variables = variables; + targets.push(target); }); - // Fetch all the action's ancestor creatureProperties - const ancestorIds = []; - action.ancestors.forEach(ref => { - if (ref.collection === 'creatureProperties') { - ancestorIds.push(ref.id); - } - }); + const ancestors = getProperyAncestors(creatureId, action._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: action._id}, {'ancestors.id': action._id}], - removed: {$ne: true}, - }, { - sort: {order: 1}, - }); + const properties = getPropertyDecendants(creatureId, action._id); + properties.push(action); + properties.sort((a, b) => a.order - b.order); // Do the action doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope}); @@ -109,6 +96,14 @@ 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, diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index 83cadace..d99d9290 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -1,10 +1,7 @@ import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import CreatureProperties, - { DenormalisedOnlyCreaturePropertySchema as denormSchema } +import { DenormalisedOnlyCreaturePropertySchema as denormSchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { loadedCreatures } from '../loadCreatures.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; +import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js'; import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; import linkInventory from './buildComputation/linkInventory.js'; @@ -40,53 +37,6 @@ export default function buildCreatureComputation(creatureId){ return computation; } -function getProperties(creatureId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = Array.from(creature.properties.values()); - const cloneProps = EJSON.clone(props); - return cloneProps - } - // console.time(`Cache miss on creature properties: ${creatureId}`) - const props = CreatureProperties.find({ - 'ancestors.id': creatureId, - 'removed': {$ne: true}, - }, { - sort: { order: 1 }, - fields: { icon: 0 }, - }).fetch(); - // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); - return props; -} - -function getCreature(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const creature = loadedCreature.creature; - if (creature) return creature; - } - // console.time(`Cache miss on Creature: ${creatureId}`); - const creature = Creatures.findOne(creatureId, { - denormalizedStats: 1, - variables: 1, - dirty: 1, - }); - // console.timeEnd(`Cache miss on Creature: ${creatureId}`); - return creature; -} - -function getVariables(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const variables = loadedCreature.variables; - if (variables) return variables; - } - // console.time(`Cache miss on variables: ${creatureId}`); - const variables = CreatureVariables.findOne({_creatureId: creatureId}); - // console.timeEnd(`Cache miss on variables: ${creatureId}`); - return variables; -} - export function buildComputationFromProps(properties, creature, variables){ const computation = new CreatureComputation(properties, creature, variables); diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.js index d7a07154..9823f892 100644 --- a/app/imports/api/engine/loadCreatures.js +++ b/app/imports/api/engine/loadCreatures.js @@ -32,6 +32,153 @@ function unloadCreature(creatureId, subscription) { } } +export function getSingleProperty(creatureId, propertyId) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const property = creature.properties.get(propertyId); + const cloneProp = EJSON.clone(property); + return cloneProp; + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const prop = CreatureProperties.findOne({ + _id: propertyId, + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return prop; +} + +export function getProperties(creatureId) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = Array.from(creature.properties.values()); + const cloneProps = EJSON.clone(props); + return cloneProps + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getPropertiesOfType(creatureId, propType) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = [] + for (const prop of creature.properties.values()){ + if (prop.type === propType) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props); + return cloneProps + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': { $ne: true }, + 'type': propType, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getCreature(creatureId) { + if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + const creature = loadedCreature.creature; + if (creature) return creature; + } + // console.time(`Cache miss on Creature: ${creatureId}`); + const creature = Creatures.findOne(creatureId, { + denormalizedStats: 1, + variables: 1, + dirty: 1, + }); + // console.timeEnd(`Cache miss on Creature: ${creatureId}`); + return creature; +} + +export function getVariables(creatureId) { + if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + const variables = loadedCreature.variables; + if (variables) return variables; + } + // console.time(`Cache miss on variables: ${creatureId}`); + const variables = CreatureVariables.findOne({_creatureId: creatureId}); + // console.timeEnd(`Cache miss on variables: ${creatureId}`); + return variables; +} + +export function getProperyAncestors(creatureId, propertyId) { + const prop = getSingleProperty(creatureId, propertyId); + if (!prop) return []; + const ancestorIds = []; + prop.ancestors.forEach(ref => { + if (ref.collection === 'creatureProperties') { + ancestorIds.push(ref.id); + } + }); + if (loadedCreatures.has(creatureId)) { + // Get the ancestor properties from the cache + const creature = loadedCreatures.get(creatureId); + const props = []; + ancestorIds.forEach(id => { + const prop = creature.properties.get(id); + if (prop) { + props.push(prop); + } + }); + const cloneProps = EJSON.clone(props); + return cloneProps + } else { + // Fetch from database + return CreatureProperties.find({ + _id: { $in: ancestorIds }, + }, { + sort: { order: 1 }, + }).fetch(); + } +} + +export function getPropertyDecendants(creatureId, propertyId) { + const property = getSingleProperty(creatureId, propertyId); + if (!property) return []; + // This prop will always appear at the same position in the ancestor array + // of its decendants, so only check there + const expectedAncestorPostition = property.ancestors.length; + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = []; + for(const prop of creature.properties.values()){ + if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props); + return cloneProps + } else { + return CreatureProperties.find({ + 'ancestors.id': propertyId, + removed: { $ne: true }, + }).fetch(); + } +} + class LoadedCreature { constructor(sub, creatureId) { // This may be called from a subscription, but we don't want the observers diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index ccda82d1..53e57afa 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -5,9 +5,6 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; /* * Actions are things a character can do - * Any rolls that are children of actions will be rolled when taking the action - * Any actions that are children of this action will be considered alternatives - * to this action */ let ActionSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js new file mode 100644 index 00000000..3b9b0d13 --- /dev/null +++ b/app/imports/api/properties/Triggers.js @@ -0,0 +1,136 @@ +import SimpleSchema from 'simpl-schema'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const eventOptions = { + doActionProperty: 'Do action', + // receiveActionProperty: 'Receiving action property', + // flipToggle: 'Toggle changed', + // adjustProperty: 'Attribute adjusted', + anyRest: 'Short or long rest', + longRest: 'Long rest', + shortRest: 'Short rest', +} + +const timingOptions = { + before: 'Before', + after: 'After', +} + +const actionPropertyTypeOptions = { + action: 'Action', + adjustment: 'Attribute damage', + branch: 'Branch', + buff: 'Buff', + damage: 'Damage', + note: 'Note', + roll: 'Roll', + savingThrow: 'Saving throw', + toggle: 'Toggle', +} + +/* + * Triggers are like actions that fire themselves when certain things happen on + * the sheet. Either during another action or as its own action after a sheet + * event. The same trigger can't fire twice in the same action step. + */ +let TriggerSchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + summary: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + event: { + type: String, + allowedValues: Object.keys(eventOptions), + defaultValue: 'doActionProperty', + }, + // Action type + actionPropertyType: { + type: String, + allowedValues: Object.keys(actionPropertyTypeOptions), + optional: true, + }, + timing: { + type: String, + allowedValues: Object.keys(timingOptions), + defaultValue: 'after', + }, + condition: { + type: 'fieldToCompute', + optional: true, + parseLevel: 'compile', + }, + // Which tags the trigger is applied to + targetTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.tagCount, + }, + 'targetTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.extraTagsCount, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + defaultValue: 'OR', + }, + 'extraTags.$.tags': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'extraTags.$.tags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, +}); + +const ComputedOnlyTriggerSchema = createPropertySchema({ + summary: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, + condition: { + type: 'computedOnlyField', + optional: true, + parseLevel: 'compile', + }, +}); + +const ComputedTriggerSchema = new SimpleSchema() + .extend(TriggerSchema) + .extend(ComputedOnlyTriggerSchema); + +export { + TriggerSchema, ComputedOnlyTriggerSchema, ComputedTriggerSchema, + eventOptions, timingOptions, actionPropertyTypeOptions +}; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index d47d560d..d490e4e3 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -25,6 +25,7 @@ import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFiller import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js'; import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js'; import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js'; +import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js'; const propertySchemasIndex = { action: ComputedOnlyActionSchema, @@ -53,6 +54,7 @@ const propertySchemasIndex = { spellList: ComputedOnlySpellListSchema, spell: ComputedOnlySpellSchema, toggle: ComputedOnlyToggleSchema, + trigger: ComputedOnlyTriggerSchema, any: new SimpleSchema({}), }; diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 1fa8c9d8..9084e3b7 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -25,6 +25,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js'; import { ComputedSpellSchema } from '/imports/api/properties/Spells.js'; import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js'; import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js'; +import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js'; const propertySchemasIndex = { action: ComputedActionSchema, @@ -51,6 +52,7 @@ const propertySchemasIndex = { spellList: ComputedSpellListSchema, spell: ComputedSpellSchema, toggle: ComputedToggleSchema, + trigger: ComputedTriggerSchema, container: ComputedContainerSchema, item: ComputedItemSchema, any: new SimpleSchema({}), diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 0cba80a0..cc4b77ee 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -23,6 +23,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js'; import { SpellListSchema } from '/imports/api/properties/SpellLists.js'; import { SpellSchema } from '/imports/api/properties/Spells.js'; import { ToggleSchema } from '/imports/api/properties/Toggles.js'; +import { TriggerSchema } from '/imports/api/properties/Triggers.js'; import { ContainerSchema } from '/imports/api/properties/Containers.js'; import { ItemSchema } from '/imports/api/properties/Items.js'; @@ -51,6 +52,7 @@ const propertySchemasIndex = { spellList: SpellListSchema, spell: SpellSchema, toggle: ToggleSchema, + trigger: TriggerSchema, container: ContainerSchema, item: ItemSchema, any: new SimpleSchema({}), diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index 503f893e..c2aa3be1 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -159,6 +159,12 @@ const PROPERTIES = Object.freeze({ helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.', suggestedParents: [], }, + trigger: { + icon: 'mdi-electric-switch', + name: 'Trigger', + helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage', + suggestedParents: [], + }, }); export default PROPERTIES; diff --git a/app/imports/ui/properties/forms/TriggerForm.vue b/app/imports/ui/properties/forms/TriggerForm.vue new file mode 100644 index 00000000..59e7f479 --- /dev/null +++ b/app/imports/ui/properties/forms/TriggerForm.vue @@ -0,0 +1,206 @@ + + + diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js index 1687a2cb..1aa8f42e 100644 --- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js +++ b/app/imports/ui/properties/forms/shared/propertyFormIndex.js @@ -24,6 +24,7 @@ const SlotFillerForm = () => import('/imports/ui/properties/forms/SlotFillerForm const SpellListForm = () => import('/imports/ui/properties/forms/SpellListForm.vue'); const SpellForm = () => import('/imports/ui/properties/forms/SpellForm.vue'); const ToggleForm = () => import('/imports/ui/properties/forms/ToggleForm.vue'); +const TriggerForm = () => import('/imports/ui/properties/forms/TriggerForm.vue'); export default { action: ActionForm, @@ -52,4 +53,5 @@ export default { spellList: SpellListForm, spell: SpellForm, toggle: ToggleForm, + trigger: TriggerForm, }; diff --git a/app/imports/ui/properties/viewers/TriggerViewer.vue b/app/imports/ui/properties/viewers/TriggerViewer.vue new file mode 100644 index 00000000..dbecd9c3 --- /dev/null +++ b/app/imports/ui/properties/viewers/TriggerViewer.vue @@ -0,0 +1,74 @@ + + + diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js index 1df889ee..572bfad0 100644 --- a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js +++ b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js @@ -24,6 +24,7 @@ const SlotFillerViewer = () => import ('/imports/ui/properties/viewers/SlotFille const SpellListViewer = () => import ('/imports/ui/properties/viewers/SpellListViewer.vue'); const SpellViewer = () => import ('/imports/ui/properties/viewers/SpellViewer.vue'); const ToggleViewer = () => import ('/imports/ui/properties/viewers/ToggleViewer.vue'); +const TriggerViewer = () => import ('/imports/ui/properties/viewers/TriggerViewer.vue'); export default { action: ActionViewer, @@ -52,4 +53,5 @@ export default { spellList: SpellListViewer, spell: SpellViewer, toggle: ToggleViewer, + trigger: TriggerViewer, };