Implemented checks at least back to 2.0 functionality in new action engine

This commit is contained in:
Thaum Rystra
2024-04-17 19:37:38 +02:00
parent 24d7f3074a
commit a40163b9cf
21 changed files with 242 additions and 123 deletions

View File

@@ -266,7 +266,7 @@ function allLogContent(action: EngineAction) {
let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId,
adjustedStatId, adjustmentIncrementId, adjustmentSetId, rollId, buffId,
removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId, buffAttChildId;
removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId;
const propForest = [
// Apply a simple note
@@ -363,7 +363,7 @@ const propForest = [
target: 'self',
children: [
{
_id: buffAttChildId = Random.id(),
_id: Random.id(),
type: 'attribute',
attributeType: 'stat',
variableName: 'buffStat',

View File

@@ -10,7 +10,7 @@ import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { getConstantValueFromScope, getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';

View File

@@ -43,7 +43,7 @@ export default async function applyAction(action: EngineAction, userInput: Input
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 || {
task = {
prop,
targetIds: action.targetIds || [],
}

View File

@@ -30,7 +30,7 @@ type InputProvider = {
/**
* Get the details of a check or save
*/
//check(suggestedParams: CheckParams): Promise<CheckParams>;
check(suggestedParams: CheckParams): Promise<CheckParams>;
}
export type Advantage = 0 | 1 | -1;

View File

@@ -5,6 +5,7 @@ import getDeterministicDiceRoller from '/imports/api/engine/action/functions/use
// Dice rolls are done fresh, no cheating
export default function getReplayChoicesInputProvider(actionId: string, decisions: any[]):
InputProvider {
const decisionStack = [...decisions].reverse();
const dRoller = getDeterministicDiceRoller(actionId);
const replaySavedInput: InputProvider = {
nextStep() {
@@ -12,15 +13,18 @@ export default function getReplayChoicesInputProvider(actionId: string, decision
},
// To roll dice, ignore the user and use the deterministic dice roller again
rollDice(dice) {
decisions.pop();
decisionStack.pop();
return dRoller(dice);
},
choose() {
return Promise.resolve(decisions.pop());
return Promise.resolve(decisionStack.pop());
},
advantage() {
return Promise.resolve(decisions.pop());
}
return Promise.resolve(decisionStack.pop());
},
check() {
return Promise.resolve(decisionStack.pop());
},
}
return replaySavedInput;
}

View File

@@ -35,6 +35,9 @@ const inputProviderForTests: InputProvider = {
*/
async advantage(suggestedAdvantage) {
return suggestedAdvantage;
},
async check(suggestedParams) {
return suggestedParams;
}
}

View File

@@ -16,9 +16,9 @@ export default function saveInputChoices(action: EngineAction, userInput: InputP
for (const key in userInput) {
const oldFn = userInput[key];
// Make a new function that does the same thing, but saves the result to action._decisions
const newFn = (...args) => {
const result = oldFn(...args);
action._decisions.push(result);
const newFn = async (...args) => {
const result = await oldFn(...args);
action._decisions?.push(result);
return result;
}
newInputProvider[key] = newFn;

View File

@@ -6,23 +6,12 @@ import { getCreature } from '/imports/api/engine/loadCreatures';
import applyAction from '/imports/api/engine/action/functions/applyAction';
import writeActionResults from '../functions/writeActionResults';
import getReplayChoicesInputProvider from '/imports/api/engine/action/functions/userInput/getReplayChoicesInputProvider';
import Task from '/imports/api/engine/action/tasks/Task';
export const runAction = new ValidatedMethod({
name: 'actions.runAction',
validate: new SimpleSchema({
actionId: {
type: String,
},
decisions: {
type: Array,
optional: true,
},
'decisions.$': {
type: Object,
blackbox: true,
},
}).validator(),
run: async function ({ actionId, decisions = [] }: { actionId: string, decisions?: any[] }) {
validate: null, //TODO validate this
run: async function ({ actionId, decisions = [], task }: { actionId: string, decisions?: any[], task?: Task }) {
// Get the action
const action = await EngineActions.findOneAsync(actionId);
if (!action) throw new Meteor.Error('not-found', 'Action not found');
@@ -34,7 +23,7 @@ export const runAction = new ValidatedMethod({
const userInput = getReplayChoicesInputProvider(actionId, decisions);
// Apply the action
applyAction(action, userInput);
await applyAction(action, userInput, { task });
// Persist changes
const writePromise = writeActionResults(action);

View File

@@ -1,17 +1,19 @@
type Task = PropTask | DamagePropTask | ItemAsAmmoTask;
import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider';
type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask;
export default Task;
interface BaseTask {
type BaseTask = {
prop: { [key: string]: any };
targetIds: string[];
}
export interface PropTask extends BaseTask {
export type PropTask = BaseTask & {
subtaskFn?: undefined,
}
export interface DamagePropTask extends BaseTask {
export type DamagePropTask = BaseTask & {
subtaskFn: 'damageProp';
params: {
/**
@@ -24,10 +26,14 @@ export interface DamagePropTask extends BaseTask {
};
}
export interface ItemAsAmmoTask extends BaseTask {
export type ItemAsAmmoTask = BaseTask & {
subtaskFn: 'consumeItemAsAmmo';
params: {
value: number;
item: any;
};
}
export type CheckTask = BaseTask & CheckParams & {
subtaskFn: 'check';
}

View File

@@ -1,50 +1,115 @@
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/userInput/InputProvider';
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { CheckTask } from '/imports/api/engine/action/tasks/Task';
import { EngineAction } from '/imports/api/engine/action/EngineActions';
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 { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import { getVariables } from '/imports/api/engine/loadCreatures';
import InputProvider, { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { isFiniteNode } from '/imports/parser/parseTree/constant';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
// 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
task: CheckTask, action: EngineAction, result: TaskResult, userInput: 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');
if (task.contest) {
throw new Meteor.Error('not-implemented', 'This functionality is not implemented yet');
}
let checkParams: CheckParams = {
advantage: 0,
skillVariableName: prop.variableName,
abilityVariableName: prop.ability,
dc: null,
for (const targetId of targetIds) {
let scope;
if (targetId === action.creatureId) {
scope = await getEffectiveActionScope(action);
} else {
scope = getVariables(targetId);
}
// Get the updated parameters from user input
const checkParams = await userInput.check(task);
const advantage = checkParams.advantage;
const skill = checkParams.skillVariableName && getFromScope(checkParams.skillVariableName, scope) || null;
const skillBonus = (skill?.value || 0) - (skill?.abilityMod || 0);
const ability = checkParams.abilityVariableName && getFromScope(checkParams.abilityVariableName, scope) || null;
const abilityModifier = ability?.modifier || 0;
const totalModifier = skillBonus + abilityModifier;
const rollModifierText = numberToSignedString(totalModifier);
// Get the name of the check
let checkName = 'Check';
if (ability?.name && skill?.name) {
checkName = `${ability.name} (${skill.name})`
} else if (ability?.name || skill?.name) {
checkName = `${ability?.name || skill?.name}`;
}
checkParams = await inputProvider.check(checkParams);
let rollName = 'Roll'
// Append advantage/disadvantage to the check name
if (advantage === 1) {
rollName += ' (Advantage)'
} else if (advantage === -1) {
rollName += ' (Disadvantage)'
}
// Print check name and DC if present
const dc = checkParams.dc;
if (!prop.silent && dc !== null) result.appendLog({
name: prop.name,
value: `DC **${dc}**`,
result.appendLog({
name: checkName,
inline: true,
...prop.silent && { silenced: prop.silent }
}, targetIds);
const scope = await getEffectiveActionScope(action);
...dc !== null && { value: `DC **${dc}**` },
...prop?.silent && { silenced: prop.silent }
}, [targetId]);
return applyDefaultAfterPropTasks(action, prop, targetIds, inputProvider);
// Roll the dice
let rolledValue, resultPrefix;
if (advantage === 1) {
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a >= b) {
rolledValue = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
rolledValue = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (advantage === -1) {
const [[a, b]] = await userInput.rollDice([{ number: 2, diceSize: 20 }]);
if (a <= b) {
rolledValue = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
rolledValue = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
[[rolledValue]] = await userInput.rollDice([{ number: 1, diceSize: 20 }]);
resultPrefix = `1d20 [${rolledValue}] ${rollModifierText}`
}
const totalValue = rolledValue + totalModifier;
result.appendLog({
name: rollName,
value: `${resultPrefix}\n**${totalValue}**`,
inline: true,
...prop?.silent && { silenced: prop.silent }
}, [targetId]);
}
return applyDefaultAfterPropTasks(action, prop, targetIds, userInput);
}
// TODO set these and potentially read them again if triggers can change them
/*
'~checkAdvantage'
'~checkAdvantage'
'~checkDiceRoll'
'~checkRoll'
'~checkModifier'
*/

View File

@@ -1,11 +1,12 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import Task, { DamagePropTask, ItemAsAmmoTask, PropTask } from './Task';
import Task, { CheckTask, DamagePropTask, ItemAsAmmoTask, PropTask } from './Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyDamagePropTask from '/imports/api/engine/action/tasks/applyDamagePropTask';
import applyItemAsAmmoTask from '/imports/api/engine/action/tasks/applyItemAsAmmoTask';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import applyProperties from '/imports/api/engine/action/applyProperties';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import applyCheckTask from '/imports/api/engine/action/tasks/applyCheckTask';
// DamagePropTask promises a number of actual damage done
export default async function applyTask(
@@ -14,7 +15,7 @@ export default async function applyTask(
// Other tasks promise nothing
export default async function applyTask(
action: EngineAction, task: PropTask | ItemAsAmmoTask, inputProvider: InputProvider
action: EngineAction, task: PropTask | ItemAsAmmoTask | CheckTask, inputProvider: InputProvider
): Promise<void>
export default async function applyTask(
@@ -42,6 +43,8 @@ export default async function applyTask(
return applyDamagePropTask(task, action, result, inputProvider);
case 'consumeItemAsAmmo':
return applyItemAsAmmoTask(task, action, result, inputProvider);
case 'check':
return applyCheckTask(task, action, result, inputProvider);
}
} else {
// Get property

View File

@@ -1,7 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles';
import UserImages from '/imports/api/files/UserImages';
import UserImages from '/imports/api/files/userImages/UserImages';
const fileCollections = [ArchiveCreatureFiles, UserImages];
const updateFileStorageUsed = new ValidatedMethod({

View File

@@ -12,7 +12,7 @@
</template>
<script lang="js">
import UserImages from '/imports/api/files/UserImages';
import UserImages from '/imports/api/files/userImages/UserImages';
export default {
data(){return {

View File

@@ -5,6 +5,7 @@ const globalState = Vue.observable({queue: []});
let lastSnackbarId = 0;
function snackbar(data) {
console.log(data);
globalState.queue.push({
data, //{text OR content, callback, callbackName} // content is logContent
id: ++lastSnackbarId,

View File

@@ -56,6 +56,7 @@ import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import EngineActions from '/imports/api/engine/action/EngineActions';
import applyAction from '/imports/api/engine/action/functions/applyAction';
import AdvantageInput from '/imports/client/ui/creature/actions/input/AdvantageInput.vue';
import CheckInput from '/imports/client/ui/creature/actions/input/CheckInput.vue';
import RollInput from '/imports/client/ui/creature/actions/input/RollInput.vue';
import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller';
import LogContent from '/imports/client/ui/log/LogContent.vue';
@@ -64,6 +65,7 @@ export default {
components: {
DialogBase,
AdvantageInput,
CheckInput,
RollInput,
LogContent,
},
@@ -72,6 +74,10 @@ export default {
type: String,
default: undefined,
},
task: {
type: Object,
default: undefined,
},
},
data() {
return {
@@ -111,11 +117,6 @@ export default {
return EngineActions.findOne(this.actionId);
},
},
watch: {
resultAction(val) {
console.log(val);
}
},
mounted() {
this.deterministicDiceRoller = getDeterministicDiceRoller(this.actionId);
this.startAction({ stepThrough: false });
@@ -130,9 +131,8 @@ export default {
taskCount: undefined,
};
applyAction(
this.actionResult, this, { simulate: true, stepThrough }
this.actionResult, this, { simulate: true, stepThrough, task: this.task}
).then(() => {
console.log('action is done');
this.actionDone = true
});
},
@@ -172,7 +172,6 @@ export default {
},
// inputProvider methods
async rollDice(dice) {
console.log('Waiting for dice roll');
this.activeInputParams = {
deterministicDiceRoller: this.deterministicDiceRoller,
dice
@@ -181,25 +180,20 @@ export default {
return this.promiseInput();
},
async nextStep(task) {
console.log('waiting for next step');
console.log({ task });
return this.promiseInput();
},
async choose(choices, quantity) {
console.log('Waiting for choice');
console.log({choices, quantity});
return this.promiseInput();
},
async advantage(suggestedAdvantage) {
console.log('Waiting for advantage');
this.userInput = suggestedAdvantage;
this.activeInput = 'advantage-input';
this.userInputReady = true;
return this.promiseInput();
},
async check(suggestedParams) {
console.log('Waiting for check');
console.log({ suggestedParams })
this.userInput = suggestedParams;
this.activeInput = 'check-input';
return this.promiseInput();
},
}

View File

@@ -46,7 +46,7 @@ export default async function doAction(
const finishedAction = await applyAction(
action, errorOnInputRequest, { simulate: true, task }
);
return callActionMethod(finishedAction);
return callActionMethod(finishedAction, task);
} catch (e) {
if (e !== 'input-requested') throw e;
return new Promise(resolve => {
@@ -55,18 +55,21 @@ export default async function doAction(
elementId,
data: {
actionId,
task,
},
async callback(action: EngineAction) {
resolve(await callActionMethod(action));
callback(action: EngineAction) {
resolve(callActionMethod(action, task));
return elementId;
},
});
})
}
}
const callActionMethod = (action: EngineAction) => {
const callActionMethod = (action: EngineAction, task?: Task) => {
if (!action._id) throw new Meteor.Error('type-error', 'Action must have and _id');
return runAction.callAsync({ actionId: action._id, decisions: action._decisions });
//@ts-expect-error callAsync not defined in types
return runAction.callAsync({ actionId: action._id, decisions: action._decisions, task });
}
const throwInputRequestedError = () => {
@@ -78,4 +81,5 @@ const errorOnInputRequest: InputProvider = {
rollDice: throwInputRequestedError,
choose: throwInputRequestedError,
advantage: throwInputRequestedError,
check: throwInputRequestedError,
}

View File

@@ -0,0 +1,59 @@
<template>
<div class="d-flex flex-column justify-center align-center">
<v-btn-toggle
:value="value.advantage"
color="accent"
@change="changeAdvantage"
>
<v-btn :value="-1">
Disadvantage
</v-btn>
<v-btn :value="1">
Advantage
</v-btn>
</v-btn-toggle>
<div style="position: relative;">
<v-scale-transition
origin="center center"
>
<vertical-hex
v-if="value.advantage"
id="extra-hex"
style="position:absolute; transition: margin-left 0.3s ease;"
:style="{marginLeft: value.advantage == 1 ? '24px' : '-24px'}"
disable-hover
/>
</v-scale-transition>
<vertical-hex
id="roll-hex"
@click="$emit('continue')"
>
<div>
Roll
</div>
</vertical-hex>
</div>
</div>
</template>
<script lang="js">
import VerticalHex from '/imports/client/ui/components/VerticalHex.vue';
export default {
components: {
VerticalHex
},
props: {
value: {
type: Object,
required: true,
}
},
methods: {
changeAdvantage(e) {
const newValue = { ...this.value, advantage: e };
this.$emit('input', newValue)
},
}
};
</script>

View File

@@ -34,7 +34,7 @@
</template>
<script lang="js">
import removeUserImage from '/imports/api/files/UserImages/methods/removeUserImage';
import removeUserImage from '/imports/api/files/userImages/UserImages/methods/removeUserImage';
export default {
props: {

View File

@@ -100,7 +100,7 @@
<script lang="js">
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles';
import UserImages from '/imports/api/files/UserImages';
import UserImages from '/imports/api/files/userImages/UserImages';
import prettyBytes from 'pretty-bytes';
import ArchiveFileCard from '/imports/client/ui/files/ArchiveFileCard.vue';
import FileStorageStats from '/imports/client/ui/files/FileStorageStats.vue';

View File

@@ -6,17 +6,14 @@
>
<v-list-item-content class="py-0">
<v-list-item-title class="d-flex align-center">
<roll-popup
<v-btn
v-if="!hideModifier"
class="prof-mod mr-1 flex-shrink-0"
button-class="pl-3 pr-2"
text
:roll-text="displayedModifier"
:name="model.name"
:advantage="model.advantage"
:loading="checkLoading"
tile
:disabled="!context.editPermission"
@roll="check"
:data-id="`check-btn-${model._id}`"
class="pl-3 pr-2 prof-mod mr-1 flex-shrink-0"
@click.stop="check"
>
<proficiency-icon
:value="model.proficiency"
@@ -37,7 +34,7 @@
>
mdi-chevron-double-down
</v-icon>
</roll-popup>
</v-btn>
<proficiency-icon
v-else
:value="model.proficiency"
@@ -59,15 +56,12 @@
<script lang="js">
import ProficiencyIcon from '/imports/client/ui/properties/shared/ProficiencyIcon.vue';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import doCheck from '/imports/api/engine/action/methods/doCheck';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
components: {
ProficiencyIcon,
RollPopup,
},
inject: {
context: {
@@ -106,20 +100,18 @@ export default {
click(e) {
this.$emit('click', e);
},
check({ advantage }) {
this.checkLoading = true;
doCheck.call({
propId: this.model._id,
scope: {
'~checkAdvantage': { value: advantage },
},
}, error => {
this.checkLoading = false;
if (error) {
console.error(error);
snackbar({ text: error.reason });
}
});
check() {
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
}).catch(e => {
console.error(e);
})
},
}
}
@@ -138,4 +130,3 @@ export default {
color: rgba(0, 0, 0, 0.54) !important;
}
</style>
../../../../../api/engine/action/methods/doCheck

View File

@@ -1,4 +1,4 @@
import UserImages from '/imports/api/files/UserImages';
import UserImages from '/imports/api/files/userImages/UserImages';
Meteor.publish('userImages', function () {
return UserImages.find({