Added reset task, fixed sheet reset buttons
This commit is contained in:
@@ -10,7 +10,7 @@ export interface EngineAction {
|
||||
_stepThrough?: boolean;
|
||||
_decisions?: any[],
|
||||
creatureId: string;
|
||||
rootPropId: string;
|
||||
rootPropId?: string;
|
||||
targetIds?: string[];
|
||||
results: TaskResult[];
|
||||
taskCount: number;
|
||||
|
||||
@@ -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 },
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
172
app/imports/api/engine/action/tasks/applyResetTask.ts
Normal file
172
app/imports/api/engine/action/tasks/applyResetTask.ts
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user