Began work on new action UI dialog

This commit is contained in:
ThaumRystra
2023-12-06 07:36:02 +02:00
parent 24438d5a92
commit 64a1bfeda5
6 changed files with 145 additions and 64 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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