diff --git a/app/imports/api/creature/actions/Actions.js b/app/imports/api/creature/actions/Actions.js deleted file mode 100644 index 947c779b..00000000 --- a/app/imports/api/creature/actions/Actions.js +++ /dev/null @@ -1,27 +0,0 @@ -import SimpleSchema from 'simpl-schema'; - -// Actions are creature actions that have been partially executed and not yet resolved -// They require some user input to progress -let Actions = new Mongo.Collection('actions'); - -let CreaturePropertySchema = new SimpleSchema({ - _id: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - // Which creature is taking the action - _creatureId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - // The user who began taking the action - user: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - // The property that is about to be applied - property: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, -}); diff --git a/app/imports/api/creature/actions/ActiveActions.js b/app/imports/api/creature/actions/ActiveActions.js new file mode 100644 index 00000000..bbfe4731 --- /dev/null +++ b/app/imports/api/creature/actions/ActiveActions.js @@ -0,0 +1,108 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +// Actions are creature actions that have been partially executed and not yet resolved +// They require some user input to progress +let ActiveActions = new Mongo.Collection('activeActions'); + +let ActiveActionSchema = new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Which creature is taking the action + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // The user who began taking the action + userId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Requests for user input + questions: { + type: Object, + blackbox: true, + optional: true, + }, + // User responses + answers: { + type: Object, + blackbox: true, + optional: true, + }, +}); + +ActiveActions.attachSchema(ActiveActionSchema); + +export default ActiveActions; + +export const answerAction = new ValidatedMethod({ + name: 'activeActions.answer', + validate: null /*new SimpleSchema({ + activeActionId: SimpleSchema.RegEx.Id, + answers: { + type: Object, + blackbox: true, + }, + }).validator()*/, + applyOptions: { + throwStubExceptions: false, + }, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ activeActionId, answers }) { + return ActiveActions.update({}, { + $set: { answers }, + $unset: { questions: 1 }, + }); + const action = ActiveActions.findOne(activeActionId); + // Permissions + if (action.userId !== this.userId) { + throw new Meteor.Error('Permission denied', 'You do not own this action'); + } + return ActiveActions.update(activeActionId, { + $set: { answers }, + $unset: { questions: 1 }, + }); + }, +}); + +export const removeAction = new ValidatedMethod({ + name: 'activeActions.remove', + validate: new SimpleSchema({ + activeActionId: SimpleSchema.RegEx.Id, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ activeActionId }) { + const action = ActiveActions.findOne(activeActionId); + // Permissions + if (action.userId !== this.userId) { + throw new Meteor.Error('Permission denied', 'You do not own this action'); + } + return ActiveActions.remove(activeActionId); + }, +}); + +// TODO remove this +export const removeAllActions = new ValidatedMethod({ + name: 'activeActions.removeAll', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run() { + return ActiveActions.remove({}); + }, +}); diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js index c4e2c862..ae8bd797 100644 --- a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js +++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js @@ -1,5 +1,5 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { getCreature } from '/imports/api/engine/loadCreatures'; -export default function getRootCreatureAncestor(property){ - return Creatures.findOne(property.ancestors[0].id); +export default function getRootCreatureAncestor(property) { + return getCreature(property.ancestors[0].id); } diff --git a/app/imports/api/engine/actions/ActionContext.js b/app/imports/api/engine/actions/ActionContext.js index fb2edaf4..81eb250e 100644 --- a/app/imports/api/engine/actions/ActionContext.js +++ b/app/imports/api/engine/actions/ActionContext.js @@ -5,9 +5,11 @@ import { import { groupBy, remove } from 'lodash'; export default class ActionContext { - constructor(creatureId, targetIds = [], method) { + constructor(creatureId, targetIds = [], method, activeActionId) { // Get the creature this.creature = getCreature(creatureId) + // Store an active action ID for pausing/resuming this action + this.activeActionId = activeActionId if (!this.creature) { throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`) diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index b8e809d8..46b1775a 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -28,8 +28,10 @@ const applyPropertyByType = { toggle, }; -export default function applyProperty(node, actionContext, ...rest) { +export default async function applyProperty(node, actionContext, ...rest) { if (node.node.deactivatedByToggle) return; actionContext.scope[`#${node.node.type}`] = node.node; - applyPropertyByType[node.node.type]?.(node, actionContext, ...rest); + console.log('start apply props by type', node.node.type) + await applyPropertyByType[node.node.type]?.(node, actionContext, ...rest); + console.log('end apply prop by type', node.node.type) } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index b44c1b44..51577b44 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -9,8 +9,8 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; -export default function applyAction(node, actionContext) { - applyNodeTriggers(node, 'before', actionContext); +export default async function applyAction(node, actionContext) { + await applyNodeTriggers(node, 'before', actionContext); const prop = node.node; if (prop.target === 'self') actionContext.targets = [actionContext.creature]; const targets = actionContext.targets; @@ -24,7 +24,7 @@ export default function applyAction(node, actionContext) { if (!prop.silent) actionContext.addLog(content); // Spend the resources - const failed = spendResources(prop, actionContext); + const failed = await spendResources(prop, actionContext); if (failed) return; const attack = prop.attackRoll || prop.attackRollBonus; @@ -32,18 +32,18 @@ export default function applyAction(node, actionContext) { // Attack if there is an attack roll if (attack && attack.calculation) { if (targets.length) { - targets.forEach(target => { - applyAttackToTarget({ attack, target, actionContext }); + for (const target of targets) { + await applyAttackToTarget({ attack, target, actionContext }); // Apply the children, but only to the current target actionContext.targets = [target]; - applyChildren(node, actionContext); - }); + await applyChildren(node, actionContext); + } } else { - applyAttackWithoutTarget({ attack, actionContext }); - applyChildren(node, actionContext); + await applyAttackWithoutTarget({ attack, actionContext }); + await applyChildren(node, actionContext); } } else { - applyChildren(node, actionContext); + await applyChildren(node, actionContext); } if (prop.actionType === 'event' && prop.variableName) { resetProperties(actionContext.creature._id, prop.variableName, actionContext); @@ -189,7 +189,7 @@ function applyCrits(value, scope) { return { criticalHit, criticalMiss }; } -function spendResources(prop, actionContext) { +async function spendResources(prop, actionContext) { // Check Uses if (prop.usesLeft <= 0) { if (!prop.silent) actionContext.addLog({ @@ -297,9 +297,9 @@ function spendResources(prop, actionContext) { }); // Apply the ammo children - ammoToApply.forEach(node => { - applyProperty(node, actionContext); - }); + for (const node of ammoToApply) { + await applyProperty(node, actionContext); + } // Log all the spending if (gainLog.length && !prop.silent) actionContext.addLog({ diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index 8888c461..727097e1 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -3,8 +3,9 @@ import recalculateCalculation from './shared/recalculateCalculation.js'; import rollDice from '/imports/parser/rollDice.js'; import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import getUserInput from '/imports/api/engine/actions/getUserInput'; -export default function applyBranch(node, actionContext) { +export default async function applyBranch(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); const scope = actionContext.scope; const targets = actionContext.targets; @@ -76,5 +77,20 @@ export default function applyBranch(node, actionContext) { applyChildren(node, actionContext); } break; + case 'choice': { + console.log('paused waiting for user input'); + let { index } = await getUserInput({ + index: 'number', + }, actionContext); + console.log('resuming with input ' + index); + if (!isFinite(index) || index < 0) index = 0; + if (index > node.children.length - 1) index = node.children.length - 1; + applyNodeTriggers(node, 'after', actionContext); + console.log('applying child ', index); + console.log(node.children[index]); + applyProperty(node.children[index], actionContext); + applyNodeTriggers(node, 'afterChildren', actionContext); + break; + } } } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js index 332d93dc..b7cb6c13 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js @@ -2,8 +2,8 @@ import recalculateInlineCalculations from './shared/recalculateInlineCalculation import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyNote(node, actionContext) { - applyNodeTriggers(node, 'before', actionContext); +export default async function applyNote(node, actionContext) { + await applyNodeTriggers(node, 'before', actionContext); const prop = node.node; // Log Name, summary @@ -21,5 +21,5 @@ export default function applyNote(node, actionContext) { actionContext.addLog({ value: prop.description.value }); } // Apply children - applyChildren(node, actionContext); + await applyChildren(node, actionContext); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js index 9c5c0cbd..765409be 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js @@ -1,8 +1,10 @@ import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import applyProperty from '/imports/api/engine/actions/applyProperty.js'; -export default function applyChildren(node, actionContext) { +export default async function applyChildren(node, actionContext) { applyNodeTriggers(node, 'after', actionContext); - node.children.forEach(child => applyProperty(child, actionContext)); + for (const child of node.children) { + await applyProperty(child, actionContext); + } applyNodeTriggers(node, 'afterChildren', actionContext); } diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js index de9476f6..e7d842a9 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -6,25 +6,25 @@ 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 function applyNodeTriggers(node, timing, actionContext) { +export async function applyNodeTriggers(node, timing, actionContext) { const prop = node.node; const type = prop.type; const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing]; if (triggers) { - triggers.forEach(trigger => { - applyTrigger(trigger, prop, actionContext); - }); + for (const trigger of triggers) { + await applyTrigger(trigger, prop, actionContext); + } } } -export function applyTriggers(triggers = [], prop, actionContext) { +export async function applyTriggers(triggers = [], prop, actionContext) { // Apply the triggers - triggers.forEach(trigger => { - applyTrigger(trigger, prop, actionContext) - }); + for (const trigger of triggers) { + await applyTrigger(trigger, prop, actionContext) + } } -export function applyTrigger(trigger, prop, actionContext) { +export async function applyTrigger(trigger, prop, actionContext) { // If there is a prop we are applying the trigger from, // don't fire if the tags don't match if (prop && !triggerMatchTags(trigger, prop)) { @@ -71,9 +71,9 @@ export function applyTrigger(trigger, prop, actionContext) { const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); properties.sort((a, b) => a.order - b.order); const propertyForest = nodeArrayToTree(properties); - propertyForest.forEach(node => { - applyProperty(node, actionContext); - }); + for (const node of propertyForest) { + await 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 57c7beac..5cbd3cc4 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -10,6 +10,7 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import applyProperty from './applyProperty.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js'; +import ActiveActions from '/imports/api/creature/actions/ActiveActions'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', @@ -36,11 +37,18 @@ const doAction = new ValidatedMethod({ numRequests: 10, timeInterval: 5000, }, - run({ actionId, targetIds = [], scope }) { + async run({ actionId, targetIds = [], scope }) { // Get action context let action = CreatureProperties.findOne(actionId); const creatureId = action.ancestors[0].id; - const actionContext = new ActionContext(creatureId, targetIds, this); + // TODO remove this + // For testing, remove all other active actions before inserting this one + ActiveActions.remove({}); + const activeActionId = await ActiveActions.insertAsync({ + creatureId, + userId: this.userId, + }); + const actionContext = new ActionContext(creatureId, targetIds, this, activeActionId); // Check permissions assertEditPermission(actionContext.creature, this.userId); @@ -56,7 +64,7 @@ const doAction = new ValidatedMethod({ properties.sort((a, b) => a.order - b.order); // Do the action - doActionWork({ properties, ancestors, actionContext, methodScope: scope }); + await doActionWork({ properties, ancestors, actionContext, methodScope: scope }); // Recompute all involved creatures Creatures.update({ @@ -69,7 +77,7 @@ const doAction = new ValidatedMethod({ export default doAction; -export function doActionWork({ +export async function doActionWork({ properties, ancestors, actionContext, methodScope = {}, }) { // get the docs @@ -84,7 +92,9 @@ export function doActionWork({ // Apply the top level property, it is responsible for applying its children // recursively - applyProperty(propertyForest[0], actionContext); + console.log('start apply properties') + await applyProperty(propertyForest[0], actionContext); + console.log('end apply properties') // Insert the log actionContext.writeLog(); diff --git a/app/imports/api/engine/actions/getUserInput.js b/app/imports/api/engine/actions/getUserInput.js new file mode 100644 index 00000000..0ee4bca5 --- /dev/null +++ b/app/imports/api/engine/actions/getUserInput.js @@ -0,0 +1,28 @@ +import ActiveActions from '/imports/api/creature/actions/ActiveActions'; + +export default async function getUserInput(questions, actionContext) { + const activeActionId = actionContext.activeActionId; + // Set the questions on the active action + ActiveActions.update(activeActionId, { + $set: { questions }, + $unset: { answers: 1 }, + }); + // Wait for answers + return new Promise((resolve, reject) => { + const observerHandle = ActiveActions.find({ + _id: activeActionId + }).observeChanges({ + changed(id, fields) { + // Only watch for answers + if (!fields.answers) return; + // Stop watching + observerHandle.stop(); + // Give answers + resolve(fields.answers); + }, + removed() { + reject('Active action was deleted') + }, + }); + }); +} diff --git a/app/imports/api/properties/Branches.js b/app/imports/api/properties/Branches.js index 73716eb6..1677b87b 100644 --- a/app/imports/api/properties/Branches.js +++ b/app/imports/api/properties/Branches.js @@ -22,7 +22,7 @@ let BranchSchema = createPropertySchema({ 'index', // if it has option children, asks to select one // Otherwise presents its own text with yes/no - //'choice', + 'choice', //'option', ], defaultValue: 'if', diff --git a/app/imports/client/ui/properties/forms/BranchForm.vue b/app/imports/client/ui/properties/forms/BranchForm.vue index 5ea19084..c95720d0 100644 --- a/app/imports/client/ui/properties/forms/BranchForm.vue +++ b/app/imports/client/ui/properties/forms/BranchForm.vue @@ -65,6 +65,7 @@ export default { { value: 'eachTarget', text: 'Apply to each target' }, { value: 'random', text: 'Random' }, { value: 'index', text: 'Calculated index' }, + { value: 'choice', text: 'User choice' }, ], } }, @@ -79,6 +80,7 @@ export default { case 'eachTarget': return 'Applies each child property once per target'; case 'random': return 'Chooses one child property at random and applies it'; case 'index': return 'Chooses one child property to apply based on the given index'; + case 'choice': return 'Pause the action and let the user choose which child to apply'; default: return ''; } } diff --git a/app/imports/client/ui/properties/treeNodeViews/BranchTreeNode.vue b/app/imports/client/ui/properties/treeNodeViews/BranchTreeNode.vue index 3fe77bfc..8215011b 100644 --- a/app/imports/client/ui/properties/treeNodeViews/BranchTreeNode.vue +++ b/app/imports/client/ui/properties/treeNodeViews/BranchTreeNode.vue @@ -29,6 +29,7 @@ export default { case 'eachTarget': return 'Each target'; case 'random': return 'Pick one at random'; case 'index': return 'Pick one by index'; + case 'choice': return 'User choice'; default: return ''; } } diff --git a/app/imports/client/ui/properties/viewers/BranchViewer.vue b/app/imports/client/ui/properties/viewers/BranchViewer.vue index 4ce98e52..badba27c 100644 --- a/app/imports/client/ui/properties/viewers/BranchViewer.vue +++ b/app/imports/client/ui/properties/viewers/BranchViewer.vue @@ -36,6 +36,7 @@ export default { case 'eachTarget': return 'Each target'; case 'random': return 'Pick one at random'; case 'index': return 'Pick one by index'; + case 'choice': return 'User choice'; default: return ''; } } diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index b41c9dcd..b4ff4dc2 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -6,6 +6,7 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import computeCreature from '/imports/api/engine/computeCreature.js'; import VERSION from '/imports/constants/VERSION.js'; +import ActiveActions from '/imports/api/creature/actions/ActiveActions'; import { loadCreature } from '/imports/api/engine/loadCreatures.js'; let schema = new SimpleSchema({ @@ -54,6 +55,11 @@ Meteor.publish('singleCharacter', function (creatureId) { limit: 20, sort: { date: -1 }, }), + ActiveActions.find({ + creatureId, + }, { + limit: 10, + }), // Also publish the owner's username Meteor.users.find(permissionCreature.owner, { fields: {