Implementing persisting action result mutations
This commit is contained in:
@@ -9,7 +9,6 @@ import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||
import resolve from '/imports/parser/resolve';
|
||||
import toString from '/imports/parser/toString';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||
import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
|
||||
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
|
||||
@@ -39,17 +38,20 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
// The acting creature initiating the logged events
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
// creatures targeted by any of the logged events
|
||||
targetIds: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
index: 1,
|
||||
},
|
||||
creatureName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -137,7 +139,7 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
export function insertCreatureLogWork({ log, creature, tabletopId, method }) {
|
||||
export function insertCreatureLogWork({ log, creature, method }) {
|
||||
// Build the new log
|
||||
if (typeof log === 'string') {
|
||||
log = { content: [{ value: log }] };
|
||||
@@ -151,7 +153,6 @@ export function insertCreatureLogWork({ log, creature, tabletopId, method }) {
|
||||
}
|
||||
});
|
||||
log.date = new Date();
|
||||
if (tabletopId) log.tabletopId = tabletopId;
|
||||
if (creature && creature.tabletop) log.tabletopId = creature.tabletop;
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
@@ -186,20 +187,15 @@ const logRoll = new ValidatedMethod({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
async run({ roll, tabletopId, creatureId }) {
|
||||
if (!creatureId && !tabletopId) throw new Meteor.Error('no-id',
|
||||
'A creature id or tabletop id must be given'
|
||||
async run({ roll, creatureId }) {
|
||||
if (!creatureId) throw new Meteor.Error('no-id',
|
||||
'A creature id must be given'
|
||||
);
|
||||
let creature;
|
||||
if (creatureId) {
|
||||
@@ -215,9 +211,6 @@ const logRoll = new ValidatedMethod({
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
}
|
||||
if (tabletopId) {
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
}
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId }) || {};
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
@@ -259,7 +252,7 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({ log, creature, tabletopId, method: this });
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
@@ -62,6 +62,13 @@ let LogContentSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
}
|
||||
});
|
||||
|
||||
export default LogContentSchema;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import TaskResult from './tasks/TaskResult';
|
||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
const EngineActions = new Mongo.Collection<EngineAction>('actions');
|
||||
|
||||
@@ -34,11 +33,6 @@ const ActionSchema = new SimpleSchema({
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
userInputNeeded: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
|
||||
// Applied properties
|
||||
results: {
|
||||
|
||||
@@ -3,16 +3,24 @@ import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||
import applyTask from '/imports/api/engine/action/tasks/applyTask'
|
||||
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
|
||||
import saveInputChoices from './userInput/saveInputChoices';
|
||||
import Task from '/imports/api/engine/action/tasks/Task';
|
||||
|
||||
// TODO create a function to get the effective value of a property,
|
||||
// simulating all the result updates in the action so far
|
||||
|
||||
// Apply an action
|
||||
// This is run once as a simulation on the client awaiting all the various inputs or step through
|
||||
// clicks from the user, then it is run as part of the runAction method, where it is expected to
|
||||
// complete instantly on the client, and sent to the server as a method call
|
||||
/**
|
||||
* Apply an action
|
||||
* This is run once as a simulation on the client awaiting all the various inputs or step through
|
||||
* clicks from the user, then it is run as part of the runAction method, where it is expected to
|
||||
* complete instantly on the client, and sent to the server as a method call
|
||||
* @param action The action to apply
|
||||
* @param userInput The input provider
|
||||
* @param { Object } options
|
||||
* @param { Task } options.task If provided, the action will start with this task instead of
|
||||
* applying the root property of the action
|
||||
*/
|
||||
export default async function applyAction(action: EngineAction, userInput: InputProvider, options?: {
|
||||
simulate?: boolean, stepThrough?: boolean
|
||||
simulate?: boolean, stepThrough?: boolean, task?: Task,
|
||||
}) {
|
||||
const { simulate, stepThrough } = options || {};
|
||||
if (!simulate && stepThrough) throw 'Cannot step through unless simulating';
|
||||
@@ -31,12 +39,15 @@ export default async function applyAction(action: EngineAction, userInput: Input
|
||||
action._stepThrough = stepThrough;
|
||||
action._isSimulation = simulate;
|
||||
action.taskCount = 0;
|
||||
console.log(JSON.stringify(action, null, 2));
|
||||
const prop = await getSingleProperty(action.creatureId, action.rootPropId);
|
||||
if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found');
|
||||
await applyTask(action, {
|
||||
prop,
|
||||
targetIds: action.targetIds || [],
|
||||
}, userInput);
|
||||
return { action, userInput };
|
||||
let task = options?.task;
|
||||
if (!task) {
|
||||
const prop = await getSingleProperty(action.creatureId, action.rootPropId);
|
||||
if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found');
|
||||
task = options?.task || {
|
||||
prop,
|
||||
targetIds: action.targetIds || [],
|
||||
}
|
||||
}
|
||||
await applyTask(action, task, userInput);
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Mutation } from '/imports/api/engine/action/tasks/TaskResult';
|
||||
|
||||
export default function mutationToLogUpdates(mutation: Mutation) {
|
||||
if (!mutation.contents) return [];
|
||||
const contents: any[] = [];
|
||||
for (const content of mutation.contents) {
|
||||
contents.push({
|
||||
...content,
|
||||
targetIds: mutation.targetIds,
|
||||
});
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Mutation } from '/imports/api/engine/action/tasks/TaskResult';
|
||||
import { newOperation } from '/imports/api/engine/shared/bulkWrite';
|
||||
|
||||
export default function mutationToPropUpdates(mutation: Mutation) {
|
||||
const bulkWriteOps: any[] = [];
|
||||
// Updates to creature properties
|
||||
if (mutation.updates) {
|
||||
const propUpdatesById: Record<string, any> = {};
|
||||
for (const update of mutation.updates) {
|
||||
if (!propUpdatesById[update.propId]) {
|
||||
propUpdatesById[update.propId] = newOperation(update.propId);
|
||||
}
|
||||
if (update.set) {
|
||||
propUpdatesById[update.propId].updateOne.update.$set = {
|
||||
...propUpdatesById[update.propId].updateOne.update.$set,
|
||||
...update.set,
|
||||
};
|
||||
}
|
||||
if (update.inc) {
|
||||
propUpdatesById[update.propId].updateOne.update.$inc = {
|
||||
...propUpdatesById[update.propId].updateOne.update.$inc,
|
||||
...update.inc,
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const id in propUpdatesById) {
|
||||
bulkWriteOps.push(propUpdatesById[id]);
|
||||
}
|
||||
}
|
||||
// Insert creature properties
|
||||
if (mutation.inserts) for (const insertOne of mutation.inserts) {
|
||||
bulkWriteOps.push({
|
||||
insertOne
|
||||
});
|
||||
}
|
||||
// Remove creature properties
|
||||
if (mutation.removals) for (const removeOne of mutation.removals) {
|
||||
bulkWriteOps.push({
|
||||
removeOne,
|
||||
});
|
||||
}
|
||||
return bulkWriteOps;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import mutationToPropUpdates from './mutationToPropUpdates';
|
||||
import mutationToLogUpdates from '/imports/api/engine/action/functions/mutationToLogUpdates';
|
||||
import { union } from 'lodash';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
|
||||
import bulkWrite from '/imports/api/engine/shared/bulkWrite';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
|
||||
export default async function writeActionResults(action: EngineAction) {
|
||||
if (!action._id) throw new Meteor.Error('type-error', 'Action does not have an _id');
|
||||
EngineActions.remove(action._id);
|
||||
const creaturePropUpdates: any[] = [];
|
||||
const logContents: any[] = [];
|
||||
// Collect all the updates and log content
|
||||
action.results.forEach(result => {
|
||||
result.mutations.forEach(mutation => {
|
||||
creaturePropUpdates.push(...mutationToPropUpdates(mutation));
|
||||
logContents.push(...mutationToLogUpdates(mutation));
|
||||
});
|
||||
});
|
||||
const allTargetIds = union(...logContents.map(c => c.targetIds));
|
||||
|
||||
// Write the log
|
||||
const logPromise = CreatureLogs.insertAsync({
|
||||
content: logContents,
|
||||
creatureId: action.creatureId,
|
||||
targetIds: allTargetIds,
|
||||
});
|
||||
|
||||
// Write the bulk updates
|
||||
const bulkWritePromise = bulkWrite(creaturePropUpdates, CreatureProperties);
|
||||
|
||||
return Promise.all([logPromise, bulkWritePromise]);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions';
|
||||
|
||||
export default async function writeChangedAction(original: EngineAction, changed: EngineAction) {
|
||||
const $set = {};
|
||||
for (const key of ActionSchema.objectKeys()) {
|
||||
if (!EJSON.equals(original[key], changed[key])) {
|
||||
$set[key] = changed[key];
|
||||
}
|
||||
}
|
||||
if (!isEmpty($set) && original._id) {
|
||||
return EngineActions.updateAsync(original._id, { $set });
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ export const insertAction = new ValidatedMethod({
|
||||
validate: new SimpleSchema({
|
||||
action: ActionSchema
|
||||
}).validator({ clean: true }),
|
||||
run: async function ({ action }: { action: EngineAction }) {
|
||||
run: function ({ action }: { action: EngineAction }) {
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
// First remove all other actions on this creature
|
||||
// only do one action at a time, don't wait for this to finish
|
||||
EngineActions.removeAsync({ creatureId: action.creatureId });
|
||||
EngineActions.remove({ creatureId: action.creatureId });
|
||||
// Force a random id even if one was provided, we may use it later as the seed for PRNG
|
||||
delete action._id;
|
||||
return EngineActions.insertAsync(action);
|
||||
return EngineActions.insert(action);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||
import applyAction from '/imports/api/engine/action/functions/applyAction';
|
||||
import writeChangedAction from '../functions/writeChangedAction';
|
||||
import writeActionResults from '../functions/writeActionResults';
|
||||
import getReplayChoicesInputProvider from '/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider';
|
||||
|
||||
export const runAction = new ValidatedMethod({
|
||||
@@ -22,7 +22,7 @@ export const runAction = new ValidatedMethod({
|
||||
blackbox: true,
|
||||
},
|
||||
}).validator(),
|
||||
run: async function ({ actionId, decisions }: { actionId: string, decisions: any[] }) {
|
||||
run: async function ({ actionId, decisions = [] }: { actionId: string, decisions?: any[] }) {
|
||||
// Get the action
|
||||
const action = await EngineActions.findOneAsync(actionId);
|
||||
if (!action) throw new Meteor.Error('not-found', 'Action not found');
|
||||
@@ -30,9 +30,6 @@ export const runAction = new ValidatedMethod({
|
||||
// Permissions
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
|
||||
// Keep a copy of the original so that a diff can be done later to store what changed
|
||||
const originalAction = EJSON.clone(action);
|
||||
|
||||
// Replay the user's decisions as user input
|
||||
const userInput = getReplayChoicesInputProvider(actionId, decisions);
|
||||
|
||||
@@ -40,7 +37,7 @@ export const runAction = new ValidatedMethod({
|
||||
applyAction(action, userInput);
|
||||
|
||||
// Persist changes
|
||||
const writePromise = writeChangedAction(originalAction, action);
|
||||
const writePromise = writeActionResults(action);
|
||||
return writePromise;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,14 +17,16 @@ export default async function applyTask(
|
||||
action: EngineAction, task: PropTask | ItemAsAmmoTask, inputProvider: InputProvider
|
||||
): Promise<void>
|
||||
|
||||
export default async function applyTask(
|
||||
action: EngineAction, task: Task, inputProvider: InputProvider
|
||||
): Promise<void | number>
|
||||
|
||||
export default async function applyTask(
|
||||
action: EngineAction, task: Task, inputProvider: InputProvider
|
||||
): Promise<void | number> {
|
||||
|
||||
// Pause and wait for the user if the action is being stepped through
|
||||
console.log('applying task', action, inputProvider)
|
||||
if (action._isSimulation && action._stepThrough && inputProvider.nextStep) {
|
||||
console.log('waiting for next step resolution', task)
|
||||
await inputProvider.nextStep(task);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Meteor } from 'meteor/meteor'
|
||||
import { EJSON } from 'meteor/ejson';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex';
|
||||
import bulkWrite, { addSetOp, addUnsetOp, newOperation } from '/imports/api/engine/shared/bulkWrite';
|
||||
|
||||
export default function writeAlteredProperties(computation) {
|
||||
let bulkWriteOperations = [];
|
||||
@@ -33,7 +32,7 @@ export default function writeAlteredProperties(computation) {
|
||||
bulkWriteOperations.push(op);
|
||||
}
|
||||
});
|
||||
bulkWriteProperties(bulkWriteOperations);
|
||||
bulkWrite(bulkWriteOperations, CreatureProperties);
|
||||
//if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`);
|
||||
}
|
||||
|
||||
@@ -42,7 +41,7 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
||||
// and compile an operation that sets all those keys
|
||||
for (let key of keys) {
|
||||
if (!EJSON.equals(original[key], changed[key])) {
|
||||
if (!op) op = newOperation(original._id, changed.type);
|
||||
if (!op) op = newOperation(original._id);
|
||||
let value = changed[key];
|
||||
if (value === undefined) {
|
||||
// Unset values that become undefined
|
||||
@@ -55,70 +54,3 @@ function addChangedKeysToOp(op, keys, original, changed) {
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
function newOperation(_id, type) {
|
||||
let newOp = {
|
||||
updateOne: {
|
||||
filter: { _id },
|
||||
update: {},
|
||||
}
|
||||
};
|
||||
if (Meteor.isClient) {
|
||||
newOp.type = type;
|
||||
}
|
||||
return newOp;
|
||||
}
|
||||
|
||||
function addSetOp(op, key, value) {
|
||||
if (op.updateOne.update.$set) {
|
||||
op.updateOne.update.$set[key] = value;
|
||||
} else {
|
||||
op.updateOne.update.$set = { [key]: value };
|
||||
}
|
||||
}
|
||||
|
||||
function addUnsetOp(op, key) {
|
||||
if (op.updateOne.update.$unset) {
|
||||
op.updateOne.update.$unset[key] = 1;
|
||||
} else {
|
||||
op.updateOne.update.$unset = { [key]: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// If we re-enable client-side sheet recalculation, this needs to be run on
|
||||
// both client and server to preserve latency compensation. Bulkwrite breaks
|
||||
// latency compensation and causes flickering
|
||||
function writePropertiesSequentially(bulkWriteOps) {
|
||||
bulkWriteOps.forEach(op => {
|
||||
let updateOneOrMany = op.updateOne || op.updateMany;
|
||||
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
|
||||
// The bulk code is bypassing validation, so do the same here
|
||||
// selector: {type: op.type} // include this if bypass is off
|
||||
bypassCollection2: true,
|
||||
});
|
||||
});
|
||||
//if (bulkWriteOps.length) console.log(`Wrote ${bulkWriteOps.length} props`);
|
||||
}
|
||||
|
||||
// This is more efficient on the database, but significantly less efficient
|
||||
// in the UI because of incompatibility with latency compensation. If the
|
||||
// duplicate redraws can be fixed, this is a strictly better way of processing
|
||||
// writes
|
||||
function bulkWriteProperties(bulkWriteOps) {
|
||||
if (!bulkWriteOps.length) return;
|
||||
// bulkWrite is only available on the server
|
||||
if (Meteor.isServer) {
|
||||
CreatureProperties.rawCollection().bulkWrite(
|
||||
bulkWriteOps,
|
||||
{ ordered: false },
|
||||
function (e) {
|
||||
if (e) {
|
||||
console.error('Bulk write failed: ');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
writePropertiesSequentially(bulkWriteOps);
|
||||
}
|
||||
}
|
||||
|
||||
55
app/imports/api/engine/shared/bulkWrite.ts
Normal file
55
app/imports/api/engine/shared/bulkWrite.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// This is more efficient on the database, but significantly less efficient
|
||||
// in the UI because of incompatibility with latency compensation. If the
|
||||
// duplicate redraws can be fixed, this is a strictly better way of processing
|
||||
// writes
|
||||
export default function bulkWrite(bulkWriteOps, collection): void | Promise<any> {
|
||||
if (!bulkWriteOps.length) return;
|
||||
// bulkWrite is only available on the server
|
||||
if (!Meteor.isServer) {
|
||||
return writePropertiesSequentially(bulkWriteOps, collection);
|
||||
}
|
||||
return collection.rawCollection().bulkWrite(
|
||||
bulkWriteOps,
|
||||
{ ordered: false }
|
||||
);
|
||||
}
|
||||
|
||||
// If we re-enable client-side sheet recalculation, this needs to be run on
|
||||
// both client and server to preserve latency compensation. Bulkwrite breaks
|
||||
// latency compensation and causes flickering
|
||||
function writePropertiesSequentially(bulkWriteOps: any[], collection: Mongo.Collection<any>) {
|
||||
bulkWriteOps.forEach(op => {
|
||||
const updateOneOrMany = op.updateOne || op.updateMany;
|
||||
collection.update(updateOneOrMany.filter, updateOneOrMany.update, {
|
||||
// The bulk code is bypassing validation, so do the same here
|
||||
// @ts-expect-error Collection 2 has no typescript support
|
||||
bypassCollection2: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function newOperation(_id) {
|
||||
const newOp = {
|
||||
updateOne: {
|
||||
filter: { _id },
|
||||
update: {},
|
||||
}
|
||||
};
|
||||
return newOp;
|
||||
}
|
||||
|
||||
export function addSetOp(op, key, value) {
|
||||
if (op.updateOne.update.$set) {
|
||||
op.updateOne.update.$set[key] = value;
|
||||
} else {
|
||||
op.updateOne.update.$set = { [key]: value };
|
||||
}
|
||||
}
|
||||
|
||||
export function addUnsetOp(op, key) {
|
||||
if (op.updateOne.update.$unset) {
|
||||
op.updateOne.update.$unset[key] = 1;
|
||||
} else {
|
||||
op.updateOne.update.$unset = { [key]: 1 };
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,31 @@
|
||||
import { Store } from 'vuex';
|
||||
import { insertAction } from '/imports/api/engine/action/methods/insertAction';
|
||||
import Task from '/imports/api/engine/action/tasks/Task';
|
||||
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
|
||||
import applyAction from '/imports/api/engine/action/functions/applyAction';
|
||||
import { runAction } from '/imports/api/engine/action/methods/runAction';
|
||||
|
||||
export default async function doAction(prop: any, $store, elementId) {
|
||||
const actionId = await insertAction.call({
|
||||
/**
|
||||
* Apply an action on the client that first creates the action on both the client and server, then
|
||||
* simulates the action, opening the action dialog if necessary to get input from the user, saving
|
||||
* the decisions the user makes, then applying the action as a method call to the server with the
|
||||
* saved decisions, which will persist the action results.
|
||||
*
|
||||
* @param prop The property initializing the action, if no task is applied the property will be
|
||||
* applied as the starting point of the action
|
||||
* @param $store The Vuex store instance that has the dialog stack
|
||||
* @param elementId The element to animate the dialog from if a dialog needs to open
|
||||
* @param task The task to apply instead of applying the property itself
|
||||
*/
|
||||
export default async function doAction(
|
||||
prop: { _id: string, root: { id: string } },
|
||||
$store: Store<any>,
|
||||
elementId: string,
|
||||
task?: Task,
|
||||
) {
|
||||
// Create the action
|
||||
const actionId = insertAction.call({
|
||||
action: {
|
||||
creatureId: prop.root.id,
|
||||
rootPropId: prop._id,
|
||||
@@ -9,11 +33,49 @@ export default async function doAction(prop: any, $store, elementId) {
|
||||
taskCount: 0,
|
||||
}
|
||||
});
|
||||
$store.commit('pushDialogStack', {
|
||||
component: 'action-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
actionId,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the inserted and cleaned action instance
|
||||
const action = EngineActions.findOne(actionId);
|
||||
|
||||
if (!action) throw new Meteor.Error('not-found', 'The action could not be found');
|
||||
|
||||
// Applying the action is deterministic, so we apply it, if it asks for user input, we escape and
|
||||
// create a dialog that will re-apply the action, but with the ability to actually get input
|
||||
// Either way, call the action method afterwards
|
||||
try {
|
||||
const finishedAction = await applyAction(
|
||||
action, errorOnInputRequest, { simulate: true, task }
|
||||
);
|
||||
return callActionMethod(finishedAction);
|
||||
} catch (e) {
|
||||
if (e !== 'input-requested') throw e;
|
||||
return new Promise(resolve => {
|
||||
$store.commit('pushDialogStack', {
|
||||
component: 'action-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
actionId,
|
||||
},
|
||||
async callback(action, decisions) {
|
||||
resolve(await callActionMethod(action, decisions));
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const callActionMethod = (action: EngineAction, decisions?: any[]) => {
|
||||
if (!action._id) throw new Meteor.Error('type-error', 'Action must have and _id');
|
||||
return runAction.call({ actionId: action._id, decisions });
|
||||
}
|
||||
|
||||
const throwInputRequestedError = () => {
|
||||
throw 'input-requested';
|
||||
}
|
||||
|
||||
const errorOnInputRequest: InputProvider = {
|
||||
nextStep: throwInputRequestedError,
|
||||
rollDice: throwInputRequestedError,
|
||||
choose: throwInputRequestedError,
|
||||
advantage: throwInputRequestedError,
|
||||
}
|
||||
|
||||
12
app/imports/client/ui/creature/actions/doClientAction.ts
Normal file
12
app/imports/client/ui/creature/actions/doClientAction.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Apply an action on the client that first creates the action on both the client and server, then
|
||||
* simulates the action, opening the action dialog if necessary to get input from the user, saving
|
||||
* the decisions the user makes, then applying the action as a method call to the server with the
|
||||
* saved decisions, which will persist the action results.
|
||||
*/
|
||||
|
||||
import Task from '/imports/api/engine/action/tasks/Task';
|
||||
|
||||
export default function doClientAction(propIdOrTask: string | Task) {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user