Added reset task, fixed sheet reset buttons

This commit is contained in:
ThaumRystra
2024-05-02 20:41:04 +02:00
parent 9cd6ca5c6e
commit 7e3a16c96d
7 changed files with 237 additions and 57 deletions

View File

@@ -10,7 +10,7 @@ export interface EngineAction {
_stepThrough?: boolean;
_decisions?: any[],
creatureId: string;
rootPropId: string;
rootPropId?: string;
targetIds?: string[];
results: TaskResult[];
taskCount: number;

View File

@@ -13,6 +13,7 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString';
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';
import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask';
export default async function applyActionProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
@@ -72,7 +73,12 @@ export default async function applyActionProperty(
await applyChildren(action, prop, targetIds, userInput);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(action, prop, result, userInput);
await applyResetTask({
subtaskFn: 'reset',
prop,
eventName: prop.variableName,
targetIds: [action.creatureId],
}, action, result, userInput);
}
// Finish
@@ -244,46 +250,3 @@ function applyCrits(value, scope, resultPushScope) {
}
return { criticalHit, criticalMiss };
}
async function resetProperties(action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider) {
const attributes = getPropertiesOfType(action.creatureId, 'attribute');
for (const att of attributes) {
if (att.removed || att.inactive) continue;
if (att.reset !== prop.variableName) continue;
if (!att.damage) continue;
applyTask(action, {
prop: att,
targetIds: [action.creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(att),
operation: 'increment',
value: -att.damage ?? 0,
targetProp: att,
},
}, userInput)
}
const actions = [
...getPropertiesOfType(action.creatureId, 'action'),
...getPropertiesOfType(action.creatureId, 'spell'),
]
for (const act of actions) {
if (act.removed || act.inactive) continue;
if (act.reset !== prop.variableName) continue;
if (!act.usesUsed) continue;
result.mutations.push({
targetIds: [action.creatureId],
updates: [{
propId: act._id,
set: { usesUsed: 0 },
type: act.type,
}],
contents: [{
name: getPropertyTitle(act),
value: act.usesUsed >= 0 ? `Restored ${act.usesUsed} uses` : `Removed ${-act.usesUsed} uses`,
inline: true,
...prop.silent && { silenced: true },
}]
});
}
}

View File

@@ -1,6 +1,6 @@
import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider';
type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask;
type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask;
export default Task;
@@ -38,3 +38,10 @@ export type ItemAsAmmoTask = BaseTask & {
export type CheckTask = BaseTask & CheckParams & {
subtaskFn: 'check';
}
export type ResetTask = BaseTask & {
subtaskFn: 'reset',
eventName: string;
// One and only one target
targetIds: [string];
}

View File

@@ -0,0 +1,172 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { ResetTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import { getCreature, getPropertiesByFilter, getPropertiesOfType } from '/imports/api/engine/loadCreatures';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
export default async function applyResetTask(
task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider
): Promise<void> {
// Event name must be defined
if (!task.eventName) return;
// This task can only be applied to a single target
if (task.targetIds.length !== 1) {
throw new Meteor.Error('wrong-number-of-targets', `Must reset the properties of a single creature at a time, ${task.targetIds.length} targets were provided`)
}
// Print a title for the event
let name: string;
switch (task.eventName) {
case 'shortRest':
name = 'Short Rest';
break;
case 'longRest':
name = 'Long Rest';
break;
default:
name = task.eventName;
break;
}
result.appendLog({ name }, task.targetIds);
// Reset the properties by this event name
await resetProperties(task, action, result, userInput);
// Reset hit dice on a long rest, starting with the highest dice
if (task.eventName === 'longRest') {
await resetHitDice(task, action, result, userInput);
}
}
export async function resetProperties(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) {
const creatureId = task.targetIds[0];
// Long rests reset short rest properties as well
let mongoFilter: Mongo.Selector<object>
if (task.eventName === 'longRest') {
mongoFilter = { reset: { $in: ['shortRest', 'longRest'] } }
} else {
mongoFilter = { reset: task.eventName };
}
const filterFn = (prop) => {
if (task.eventName === 'longRest') {
if (prop.reset !== 'longRest' && prop.reset !== 'shortRest') return false;
} else {
if (prop.reset !== task.eventName) return false;
}
return true;
}
// Attributes
const attributeFilter: Mongo.Selector<object> = {
...mongoFilter,
type: 'attribute',
damage: { $nin: [0, undefined] },
}
const attributeFilterFunction = (att) => {
if (att.type !== 'attribute') return false;
if (!filterFn(att)) return false;
if (att.damage === 0 || att.damage === undefined) return false;
return true;
}
const attributes = getPropertiesByFilter(creatureId, attributeFilterFunction, attributeFilter);
for (const prop of attributes) {
await applyTask(action, {
prop: task.prop || prop,
targetIds: [action.creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(prop),
operation: 'increment',
value: -prop.damage ?? 0,
targetProp: prop,
},
}, userInput);
}
// Action-like properties
const actionFilter = {
...mongoFilter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $nin: [0, undefined] },
};
const actionFilterFunction = (prop) => {
if (prop.type !== 'action' && prop.type !== 'spell') return false;
if (!filterFn(prop)) return false;
if (prop.usesUsed === 0 || prop.usesUsed === undefined) return false;
return true;
}
const actionProps = getPropertiesByFilter(creatureId, actionFilterFunction, actionFilter);
for (const prop of actionProps) {
result.mutations.push({
targetIds: [creatureId],
updates: [{
propId: prop._id,
type: prop.type,
set: { usesUsed: 0 },
}],
contents: [{
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
}],
});
}
}
async function resetHitDice(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) {
const creatureId = task.targetIds[0];
const hitDice = getPropertiesOfType(creatureId, 'hitDice');
// Use a collator to do sorting in natural order
const collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
const compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
const totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
const creature = getCreature(creatureId);
const resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover;
for (const hd of hitDice) {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
// Apply the damage prop task
await applyTask(action, {
prop: task.prop || hd,
targetIds: [creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(hd),
operation: 'increment',
value: -amountToRecover,
targetProp: hd,
},
}, userInput);
}
}

View File

@@ -7,6 +7,7 @@ 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';
import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask';
// DamagePropTask promises a number of actual damage done
export default async function applyTask(
@@ -45,6 +46,10 @@ export default async function applyTask(
return applyItemAsAmmoTask(task, action, result, inputProvider);
case 'check':
return applyCheckTask(task, action, result, inputProvider);
case 'reset':
return applyResetTask(task, action, result, inputProvider);
default:
throw 'No case defined for the given subtaskFn';
}
} else {
// Get property

View File

@@ -94,6 +94,36 @@ export function getPropertiesOfType(creatureId, propType) {
return props;
}
/**
* Get the properties of a creature that matches the filters given
* @param creatureId The id of the creature
* @param filterFn A function that returns true if the given prop matches the filter
* @param mongoFilter A mongo selector that is exactly equal to the above function
*/
export function getPropertiesByFilter(creatureId, filterFn: (any) => boolean, mongoFilter: Mongo.Selector<object>) {
const creature = loadedCreatures.get(creatureId);
if (creature) {
const props: CreatureProperty[] = []
for (const prop of creature.properties.values()) {
if (filterFn(prop)) {
props.push(prop);
}
}
props.sort((a, b) => a.left - b.left);
return EJSON.clone(props);
}
// console.time(`Cache miss on creature properties: ${creatureId}`)
const props = CreatureProperties.find({
'root.id': creatureId,
'removed': { $ne: true },
...mongoFilter
}, {
sort: { left: 1 },
}).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props;
}
export function getCreature(creatureId: string) {
const loadedCreature = loadedCreatures.get(creatureId);
const loadedCreatureDoc = loadedCreature?.creature;

View File

@@ -3,6 +3,7 @@
:loading="loading"
:disabled="context.editPermission === false"
outlined
:data-id="`rest-btn-${type}`"
style="width: 160px;"
@click="rest"
>
@@ -14,7 +15,7 @@
</template>
<script lang="js">
import restCreature from '/imports/api/creature/creatures/methods/restCreature';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
inject: {
@@ -36,19 +37,21 @@ export default {
methods: {
rest(){
this.loading = true;
restCreature.call({
creatureId: this.creatureId,
restType: this.type,
}, error => {
const emptyProp = {
_id: this.creatureId,
root: { id: this.creatureId },
};
doAction(emptyProp, this.$store, `rest-btn-${this.type}`, {
subtaskFn: 'reset',
prop: emptyProp,
targetIds: [this.creatureId],
eventName: this.type,
}).catch(e => {
console.error(e);
}).finally(() => {
this.loading = false;
if (error){
console.error(error);
}
});
}
}
}
</script>
<style lang="css" scoped>
</style>