Began migrating action engine to async

To suspending actions to await user input
This commit is contained in:
ThaumRystra
2023-11-13 00:24:51 +02:00
parent 800ef3328c
commit 5a2df36e8b
17 changed files with 222 additions and 71 deletions

View File

@@ -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,
},
});

View 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({});
},
});

View File

@@ -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);
}

View File

@@ -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}`)

View File

@@ -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)
}

View File

@@ -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({

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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();

View 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')
},
});
});
}

View File

@@ -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',

View File

@@ -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 '';
}
}

View File

@@ -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 '';
}
}

View File

@@ -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 '';
}
}

View File

@@ -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: {