Began migrating action engine to async
To suspending actions to await user input
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
108
app/imports/api/creature/actions/ActiveActions.js
Normal file
108
app/imports/api/creature/actions/ActiveActions.js
Normal file
@@ -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({});
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
28
app/imports/api/engine/actions/getUserInput.js
Normal file
28
app/imports/api/engine/actions/getUserInput.js
Normal file
@@ -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')
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user