Action interruption progress
This commit is contained in:
@@ -1,4 +1,10 @@
|
|||||||
|
import Task from '/imports/api/engine/action/tasks/Task';
|
||||||
|
|
||||||
type InputProvider = {
|
type InputProvider = {
|
||||||
|
/**
|
||||||
|
* Show the user the next property or task to apply and wait for input to continue
|
||||||
|
*/
|
||||||
|
nextStep?(task: Task): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Roll dice
|
* Roll dice
|
||||||
* @param dice How many dice
|
* @param dice How many dice
|
||||||
@@ -20,7 +26,23 @@ type InputProvider = {
|
|||||||
/**
|
/**
|
||||||
* Get advantage, natural, or disadvantage for a d20 roll
|
* Get advantage, natural, or disadvantage for a d20 roll
|
||||||
*/
|
*/
|
||||||
advantage(suggestedAdvantage: 0 | 1 | -1): Promise<0 | 1 | -1>;
|
advantage(suggestedAdvantage: Advantage): Promise<Advantage>;
|
||||||
|
/**
|
||||||
|
* Get the details of a check or save
|
||||||
|
*/
|
||||||
|
check(suggestedParams: CheckParams): Promise<CheckParams>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Advantage = 0 | 1 | -1;
|
||||||
|
|
||||||
|
export type CheckParams = {
|
||||||
|
advantage: Advantage;
|
||||||
|
skillVariableName: string;
|
||||||
|
abilityVariableName: string;
|
||||||
|
dc: number | null;
|
||||||
|
contest?: true;
|
||||||
|
targetSkillVariableName?: string;
|
||||||
|
targetAbilityVariableName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InputProvider;
|
export default InputProvider;
|
||||||
@@ -20,6 +20,7 @@ export default async function applyAction(action: EngineAction, userInput: Input
|
|||||||
action._stepThrough = stepThrough;
|
action._stepThrough = stepThrough;
|
||||||
action._isSimulation = simulate;
|
action._isSimulation = simulate;
|
||||||
action.taskCount = 0;
|
action.taskCount = 0;
|
||||||
|
console.log(JSON.stringify(action, null, 2));
|
||||||
const prop = await getSingleProperty(action.creatureId, action.rootPropId);
|
const prop = await getSingleProperty(action.creatureId, action.rootPropId);
|
||||||
if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found');
|
if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found');
|
||||||
await applyTask(action, {
|
await applyTask(action, {
|
||||||
|
|||||||
3
app/imports/api/engine/action/methods/index.ts
Normal file
3
app/imports/api/engine/action/methods/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import './insertAction';
|
||||||
|
import './runAction';
|
||||||
|
import './updateAction';
|
||||||
@@ -4,7 +4,7 @@ import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/a
|
|||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||||
import { getCreature } from '/imports/api/engine/loadCreatures';
|
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
export const insertAction: ValidatedMethod = new ValidatedMethod({
|
export const insertAction = new ValidatedMethod({
|
||||||
name: 'actions.insertAction',
|
name: 'actions.insertAction',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
action: ActionSchema
|
action: ActionSchema
|
||||||
@@ -16,6 +16,6 @@ export const insertAction: ValidatedMethod = new ValidatedMethod({
|
|||||||
EngineActions.removeAsync({ creatureId: action.creatureId });
|
EngineActions.removeAsync({ creatureId: action.creatureId });
|
||||||
// Force a random id even if one was provided, we may use it later as the seed for PRNG
|
// Force a random id even if one was provided, we may use it later as the seed for PRNG
|
||||||
delete action._id;
|
delete action._id;
|
||||||
return await EngineActions.insertAsync(action);
|
return EngineActions.insertAsync(action);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
22
app/imports/api/engine/action/methods/updateAction.ts
Normal file
22
app/imports/api/engine/action/methods/updateAction.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||||
|
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
export const updateAction = new ValidatedMethod({
|
||||||
|
name: 'actions.updateAction',
|
||||||
|
validate({ _id, path, value }) {
|
||||||
|
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||||
|
// We cannot change these fields with a simple update
|
||||||
|
if (path !== 'targetIds') throw new Meteor.Error('Can only update target ids');
|
||||||
|
if (!Array.isArray(value)) throw new Meteor.Error('TargetIds must be an array');
|
||||||
|
},
|
||||||
|
run: async function ({ _id, path, value }: { _id: string, path: 'targetIds', value: string[] }) {
|
||||||
|
const action = await EngineActions.findOneAsync(_id);
|
||||||
|
if (!action) {
|
||||||
|
throw new Meteor.Error('not found', 'The given action was not found');
|
||||||
|
}
|
||||||
|
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||||
|
return EngineActions.updateAsync(_id, { $set: { [path]: value } });
|
||||||
|
},
|
||||||
|
});
|
||||||
50
app/imports/api/engine/action/tasks/applyCheckTask.ts
Normal file
50
app/imports/api/engine/action/tasks/applyCheckTask.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import InputProvider, { CheckParams } from '/imports/api/engine/action/functions/InputProvider';
|
||||||
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
|
||||||
|
import { applyUnresolvedEffects } from '/imports/api/engine/action/methods/doCheck';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { getVariables } from '/imports/api/engine/loadCreatures';
|
||||||
|
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||||
|
import { isFiniteNode } from '/imports/parser/parseTree/constant';
|
||||||
|
|
||||||
|
// TODO implement this
|
||||||
|
/**
|
||||||
|
* A skill property is applied as a check or a saving throw
|
||||||
|
*/
|
||||||
|
export default async function applyCheckTask(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
throw new Meteor.Error('Not implemented', 'This function needs to be implemented');
|
||||||
|
|
||||||
|
const prop = task.prop;
|
||||||
|
const targetIds = task.targetIds;
|
||||||
|
if (targetIds?.length) {
|
||||||
|
throw new Meteor.Error('too-many-targets',
|
||||||
|
'This function is only implemented for zero targets');
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkParams: CheckParams = {
|
||||||
|
advantage: 0,
|
||||||
|
skillVariableName: prop.variableName,
|
||||||
|
abilityVariableName: prop.ability,
|
||||||
|
dc: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkParams = await inputProvider.check(checkParams);
|
||||||
|
|
||||||
|
const dc = checkParams.dc;
|
||||||
|
if (!prop.silent && dc !== null) result.appendLog({
|
||||||
|
name: prop.name,
|
||||||
|
value: `DC **${dc}**`,
|
||||||
|
inline: true,
|
||||||
|
...prop.silent && { silenced: prop.silent }
|
||||||
|
}, targetIds);
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targetIds, inputProvider);
|
||||||
|
}
|
||||||
@@ -20,6 +20,15 @@ export default async function applyTask(
|
|||||||
export default async function applyTask(
|
export default async function applyTask(
|
||||||
action: EngineAction, task: Task, inputProvider: InputProvider
|
action: EngineAction, task: Task, inputProvider: InputProvider
|
||||||
): Promise<void | number> {
|
): 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no more than 100 tasks are performed by a single action
|
||||||
action.taskCount += 1;
|
action.taskCount += 1;
|
||||||
if (action.taskCount > 100) throw 'Only 100 properties can be applied at once';
|
if (action.taskCount > 100) throw 'Only 100 properties can be applied at once';
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
{{ actionJson }}
|
{{ actionJson }}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<tree-node-view
|
<pre>
|
||||||
v-for="prop in taskProps"
|
<code>
|
||||||
:key="prop._id"
|
{{ resultJson }}
|
||||||
:model="prop"
|
</code>
|
||||||
/>
|
</pre>
|
||||||
<v-btn
|
<v-btn
|
||||||
slot="actions"
|
slot="actions"
|
||||||
text
|
text
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
slot="actions"
|
slot="actions"
|
||||||
text
|
text
|
||||||
@click="apply(true)"
|
:disabled="!ackNextStep"
|
||||||
|
@click="step"
|
||||||
>
|
>
|
||||||
Step
|
Step
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -42,13 +43,12 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||||
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
|
import applyAction from '/imports/api/engine/action/functions/applyAction';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
DialogBase,
|
DialogBase,
|
||||||
TreeNodeView,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
actionId: {
|
actionId: {
|
||||||
@@ -59,27 +59,66 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
}
|
actionResult: undefined,
|
||||||
},
|
ackNextStep: undefined,
|
||||||
meteor: {
|
};
|
||||||
action() {
|
|
||||||
return Actions.findOne(this.actionId);
|
|
||||||
},
|
|
||||||
taskProps() {
|
|
||||||
if (!this.action) return;
|
|
||||||
return this.action.taskQueue.map(task => {
|
|
||||||
return CreatureProperties.findOne(task.propId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
actionJson() {
|
actionJson() {
|
||||||
return JSON.stringify(this.action, null, 2);
|
return JSON.stringify(this.action, null, 2);
|
||||||
},
|
},
|
||||||
|
resultJson() {
|
||||||
|
return JSON.stringify(this.actionResult, null, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const vueInstance = this;
|
||||||
|
this.inputProvider = {
|
||||||
|
ackNextStep: undefined,
|
||||||
|
async nextStep() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
console.log('ackNexStep set')
|
||||||
|
vueInstance.ackNextStep = () => {
|
||||||
|
vueInstance.ackNextStep = undefined;
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async advantage() {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
async rollDice(dice) {
|
||||||
|
const results = [];
|
||||||
|
for (const die of dice) {
|
||||||
|
const rolls = [];
|
||||||
|
for (let i = 0; i < die.number; i++){
|
||||||
|
rolls.push(1);
|
||||||
|
}
|
||||||
|
results.push(rolls);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meteor: {
|
||||||
|
action() {
|
||||||
|
return EngineActions.findOne(this.actionId);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async apply(stepThrough) {
|
async apply() {
|
||||||
throw new Error('Not implemented')
|
this.actionResult = {
|
||||||
|
...this.action,
|
||||||
|
_stepThrough: undefined,
|
||||||
|
_isSimulation: undefined,
|
||||||
|
taskCount: undefined,
|
||||||
|
};
|
||||||
|
applyAction(
|
||||||
|
this.actionResult, this.inputProvider, { simulate: true, stepThrough: true }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
step() {
|
||||||
|
this.ackNextStep();
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$store.dispatch('popDialogStack');
|
this.$store.dispatch('popDialogStack');
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { insertAction } from '/imports/api/engine/actions/ActionEngine';
|
import { insertAction } from '/imports/api/engine/action/methods/insertAction';
|
||||||
|
|
||||||
export default async function doAction(prop: any, $store, elementId) {
|
export default async function doAction(prop: any, $store, elementId) {
|
||||||
const actionId = await insertAction.call({
|
const actionId = await insertAction.call({
|
||||||
action: {
|
action: {
|
||||||
creatureId: prop.ancestors[0].id,
|
creatureId: prop.root.id,
|
||||||
rootPropId: prop._id,
|
rootPropId: prop._id,
|
||||||
taskQueue: [{ propId: prop._id }],
|
results: [],
|
||||||
|
taskCount: 0,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$store.commit('pushDialogStack', {
|
$store.commit('pushDialogStack', {
|
||||||
|
|||||||
@@ -116,15 +116,14 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||||
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
|
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||||
//TODO import doAction from '/imports/api/engine/actions/doAction';
|
import doAction from '/imports/client/ui/creature/actions/doAction';
|
||||||
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
||||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||||
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
|
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
|
||||||
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
||||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
|
||||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||||
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
|
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
|
||||||
import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
|
import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-btn
|
<v-btn
|
||||||
:loading="doActionLoading"
|
|
||||||
:disabled="context.editPermission === false"
|
:disabled="context.editPermission === false"
|
||||||
|
:data-id="`event-btn-${model._id}`"
|
||||||
outlined
|
outlined
|
||||||
class="event-button"
|
class="event-button"
|
||||||
style="min-width: 160px; max-width: 100%;"
|
style="min-width: 160px; max-width: 100%;"
|
||||||
@@ -21,9 +21,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
//TODO import doAction from '/imports/api/engine/actions/doAction';
|
import doAction from '/imports/client/ui/creature/actions/doAction';
|
||||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -39,36 +38,12 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
activated: undefined,
|
|
||||||
doActionLoading: false,
|
|
||||||
hovering: false,
|
hovering: false,
|
||||||
}},
|
}},
|
||||||
methods: {
|
methods: {
|
||||||
click(e) {
|
doAction() {
|
||||||
this.$emit('click', e);
|
doAction(this.model, this.$store, `event-btn-${this.model._id}`);
|
||||||
},
|
},
|
||||||
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 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
shwing() {
|
|
||||||
this.activated = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.activated = undefined;
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ import '/imports/constants/MAINTENANCE_MODE';
|
|||||||
import '/imports/api/creature/creatureProperties/methods/index';
|
import '/imports/api/creature/creatureProperties/methods/index';
|
||||||
import '/imports/api/creature/archive/methods/index';
|
import '/imports/api/creature/archive/methods/index';
|
||||||
import '/imports/api/creature/creatures/methods/index';
|
import '/imports/api/creature/creatures/methods/index';
|
||||||
|
import '/imports/api/engine/action/methods/index';
|
||||||
|
|||||||
Reference in New Issue
Block a user