Began work on new action UI dialog
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { create, forEach, get, isEmpty, pick } from 'lodash';
|
||||
import { forEach, get, isEmpty, pick } from 'lodash';
|
||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
||||
import { getPropertyChildren, getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
||||
import { getCreature, getPropertyChildren, getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
||||
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
||||
import recalculateCalculation, { rollAndReduceCalculation } from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
||||
import rollDice from '/imports/parser/rollDice';
|
||||
import { toString } from '/imports/parser/resolve';
|
||||
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
@@ -18,7 +21,6 @@ export interface Action {
|
||||
rootPropId: string;
|
||||
targetIds?: string[];
|
||||
userInputNeeded?: any;
|
||||
stepThrough?: boolean;
|
||||
taskQueue: (Task | DamagePropTask)[];
|
||||
results: TaskResult[];
|
||||
}
|
||||
@@ -38,6 +40,9 @@ interface PropTask extends BaseTask {
|
||||
step?: number,
|
||||
subtaskFn?: undefined,
|
||||
beforeTriggersDone?: undefined | true;
|
||||
taskScope?: {
|
||||
[variableName: string]: { value: number },
|
||||
},
|
||||
}
|
||||
|
||||
interface DamagePropTask extends BaseTask {
|
||||
@@ -139,10 +144,6 @@ const ActionSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
stepThrough: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
// A stack of tasks to apply
|
||||
// Each task has a propId to apply and a targetId list
|
||||
@@ -169,21 +170,6 @@ const ActionSchema = new SimpleSchema({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
|
||||
// Pseudo properties that don't exist on the character, but can be applied by the action
|
||||
// {_id: prop}
|
||||
'taskProperties': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Results that have been partially computed, but require more steps
|
||||
// {_id: partialResult}
|
||||
'deferredResults': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {},
|
||||
},
|
||||
|
||||
// Applied properties
|
||||
results: {
|
||||
type: Array,
|
||||
@@ -279,10 +265,54 @@ Actions.attachSchema(ActionSchema);
|
||||
|
||||
export default Actions;
|
||||
|
||||
export const insertAction: ValidatedMethod = new ValidatedMethod({
|
||||
name: 'actions.insertAction',
|
||||
validate: new SimpleSchema({
|
||||
action: ActionSchema
|
||||
}).validator({ clean: true }),
|
||||
run: async function ({ action }: { action: Action }) {
|
||||
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
|
||||
Actions.removeAsync({ creatureId: action.creatureId });
|
||||
const actionId = await Actions.insertAsync(action);
|
||||
return actionId;
|
||||
},
|
||||
});
|
||||
|
||||
export const runAction = new ValidatedMethod({
|
||||
name: 'actions.runAction',
|
||||
validate: new SimpleSchema({
|
||||
actionId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
userInput: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
stepThrough: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
}
|
||||
}).validator(),
|
||||
run: async function ({ actionId, userInput }) {
|
||||
const action = await Actions.findOneAsync(actionId);
|
||||
if (!action) throw new Meteor.Error('Not found', 'The action does not exist');
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
return await runActionWork(action, userInput);
|
||||
},
|
||||
});
|
||||
|
||||
// Run an already created action
|
||||
export async function runAction(actionId: string, userInput?) {
|
||||
const action = await Actions.findOneAsync(actionId);
|
||||
if (!action) throw new Meteor.Error('Not found', 'The action does not exist');
|
||||
export async function runActionWork(action: string | ActionWithId, stepThrough?: boolean, userInput?) {
|
||||
// If given an actionId, find the action document
|
||||
if (typeof action === 'string') {
|
||||
const foundAction = await Actions.findOneAsync(action);
|
||||
if (!foundAction) throw new Meteor.Error('Not found', 'The action does not exist');
|
||||
action = foundAction;
|
||||
}
|
||||
const originalAction = EJSON.clone(action);
|
||||
let count = 0;
|
||||
do {
|
||||
@@ -293,7 +323,7 @@ export async function runAction(actionId: string, userInput?) {
|
||||
if (count > 100) {
|
||||
break;
|
||||
}
|
||||
} while (!action.userInputNeeded && !action.stepThrough)
|
||||
} while (!action.userInputNeeded && !stepThrough)
|
||||
|
||||
// Persist changes to the action
|
||||
const writePromise = writeChangedAction(originalAction, action);
|
||||
@@ -668,7 +698,35 @@ const applyPropertyByType = {
|
||||
}
|
||||
// Iterate through all the items consumed and push the appropriate subtasks and triggers
|
||||
|
||||
// TODO
|
||||
if (prop.resources?.itemsConsumed?.length) {
|
||||
for (const itemConsumed of prop.resources.itemsConsumed) {
|
||||
recalculateCalculation(itemConsumed.quantity, action, 'reduce');
|
||||
if (!itemConsumed.itemId) {
|
||||
throw 'No ammo was selected';
|
||||
}
|
||||
const item = getSingleProperty(action.creatureId, itemConsumed.itemId);
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
||||
throw 'The prop\'s ammo was not found on the creature';
|
||||
}
|
||||
const quantity = +itemConsumed?.quantity?.value;
|
||||
if (
|
||||
!quantity ||
|
||||
!isFinite(quantity)
|
||||
) continue;
|
||||
tasks.push(
|
||||
// Wrap ammo subtask in the ammo consumed triggers
|
||||
...triggerTasks(action, item, targetIds, 'ammo.before'),
|
||||
{
|
||||
propId: item._id,
|
||||
targetIds,
|
||||
taskScope: {
|
||||
//TODO
|
||||
}
|
||||
},
|
||||
...triggerTasks(action, item, targetIds, 'ammo.after'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Push children tasks
|
||||
tasks.push(...await defaultAfterPropTasks(action, prop, task.targetIds));
|
||||
@@ -1101,5 +1159,5 @@ const applySubtask = {
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,12 +4,19 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import Actions, { Action, Update, LogContent, runAction, propTasks } from '/imports/api/engine/actions/Actions';
|
||||
import Actions, { Action, Update, LogContent, runActionWork, propTasks } from '/imports/api/engine/actions/Actions';
|
||||
import computeCreature from '/imports/api/engine/computeCreature';
|
||||
import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
let creatureId;
|
||||
|
||||
describe('Interrupt action system', function () {
|
||||
let unload: (() => void) | undefined = undefined;
|
||||
const dummySubscription = {
|
||||
onStop(fn) {
|
||||
unload = fn;
|
||||
}
|
||||
};
|
||||
before(async function () {
|
||||
await Promise.all([
|
||||
CreatureProperties.removeAsync({}),
|
||||
@@ -22,8 +29,12 @@ describe('Interrupt action system', function () {
|
||||
dirty: true,
|
||||
});
|
||||
await insertActionTestProps();
|
||||
loadCreature(creatureId, dummySubscription);
|
||||
computeCreature(creatureId);
|
||||
});
|
||||
after(function () {
|
||||
unload?.();
|
||||
});
|
||||
it('writes notes to the log', async function () {
|
||||
const action = await runActionById(note1Id);
|
||||
assert.deepEqual(
|
||||
@@ -98,7 +109,7 @@ function createAction(prop, targetIds?) {
|
||||
const action: Action = {
|
||||
creatureId: prop.ancestors[0].id,
|
||||
rootPropId: prop._id,
|
||||
taskQueue: propTasks(prop, targetIds),
|
||||
taskQueue: [{ propId: prop._id, targetIds }],
|
||||
results: [],
|
||||
};
|
||||
return Actions.insertAsync(action);
|
||||
@@ -107,7 +118,7 @@ function createAction(prop, targetIds?) {
|
||||
async function runActionById(propId) {
|
||||
const prop = await CreatureProperties.findOneAsync(propId);
|
||||
const actionId = await createAction(prop);
|
||||
await runAction(actionId);
|
||||
await runActionWork(actionId);
|
||||
const action = await Actions.findOneAsync(actionId);
|
||||
if (!action) throw 'Action is expected to exist'
|
||||
return action;
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
Action
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
<div>
|
||||
content
|
||||
</div>
|
||||
<pre>
|
||||
<code>
|
||||
{{ actionJson }}
|
||||
</code>
|
||||
</pre>
|
||||
<v-btn
|
||||
slot="actions"
|
||||
text
|
||||
@@ -35,41 +37,39 @@
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import doAction from '/imports/api/engine/actions/doAction';
|
||||
import { provideUserInput } from '/imports/api/engine/actions/getUserInput.js';
|
||||
import Actions, { runAction } from '/imports/api/engine/actions/Actions';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
},
|
||||
props: {
|
||||
propId: {
|
||||
actionId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
invocationId: undefined,
|
||||
answers: {},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
|
||||
action() {
|
||||
return Actions.findOne(this.actionId);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
actionJson() {
|
||||
return JSON.stringify(this.action, null, 2);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
apply(step = false) {
|
||||
if (this.invocationId) {
|
||||
provideUserInput(this.invocationId, 0, { index: 1 });
|
||||
} else {
|
||||
this.invocationId = Random.id();
|
||||
doAction.call({
|
||||
invocationId: this.invocationId,
|
||||
actionId: this.propId,
|
||||
targetIds: [],
|
||||
});
|
||||
}
|
||||
async apply(stepThrough) {
|
||||
await runAction.callAsync({
|
||||
actionId: this.actionId,
|
||||
stepThrough
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.$store.dispatch('popDialogStack');
|
||||
|
||||
18
app/imports/client/ui/creature/actions/doAction.ts
Normal file
18
app/imports/client/ui/creature/actions/doAction.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { insertAction } from '/imports/api/engine/actions/Actions';
|
||||
|
||||
export default async function doAction(prop: any, $store, elementId) {
|
||||
const actionId = await insertAction.call({
|
||||
action: {
|
||||
creatureId: prop.ancestors[0].id,
|
||||
rootPropId: prop._id,
|
||||
taskQueue: [{ propId: prop._id }],
|
||||
}
|
||||
});
|
||||
$store.commit('pushDialogStack', {
|
||||
component: 'action-dialog',
|
||||
elementId,
|
||||
data: {
|
||||
actionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<v-card
|
||||
class="action-card"
|
||||
:class="cardClasses"
|
||||
:data-id="model._id"
|
||||
>
|
||||
<div class="layout align-center px-3">
|
||||
<div class="avatar">
|
||||
@@ -108,7 +109,7 @@
|
||||
<script lang="js">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import numberToSignedString from '../../../../../api/utility/numberToSignedString.js';
|
||||
import doAction from '/imports/api/engine/actions/doAction.js';
|
||||
import doAction from '/imports/client/ui/creature/actions/doAction';
|
||||
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
@@ -221,18 +222,7 @@ export default {
|
||||
doAction({ advantage }) {
|
||||
this.doActionLoading = true;
|
||||
this.shwing();
|
||||
doAction.call({
|
||||
actionId: this.model._id,
|
||||
scope: {
|
||||
'~attackAdvantage': { value: advantage },
|
||||
}
|
||||
}, error => {
|
||||
this.doActionLoading = false;
|
||||
if (error) {
|
||||
console.error(error);
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
}
|
||||
});
|
||||
doAction(this.model, this.$store, this.model._id);
|
||||
},
|
||||
shwing() {
|
||||
this.activated = true;
|
||||
|
||||
@@ -3,6 +3,7 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Actions from '/imports/api/engine/actions/Actions';
|
||||
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
import VERSION from '/imports/constants/VERSION.js';
|
||||
@@ -54,6 +55,9 @@ Meteor.publish('singleCharacter', function (creatureId) {
|
||||
limit: 20,
|
||||
sort: { date: -1 },
|
||||
}),
|
||||
Actions.find({
|
||||
creatureId,
|
||||
}),
|
||||
// Also publish the owner's username
|
||||
Meteor.users.find(permissionCreature.owner, {
|
||||
fields: {
|
||||
|
||||
Reference in New Issue
Block a user