Refactored damage props into a subtask
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { forEach, get, isEmpty, pick } from 'lodash';
|
import { create, forEach, get, isEmpty, pick } from 'lodash';
|
||||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
||||||
import { getPropertyChildren, getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
import { getPropertyChildren, getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
||||||
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
||||||
import recalculateCalculation, { rollAndReduceCalculation } from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
import recalculateCalculation, { rollAndReduceCalculation } from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
||||||
import rollDice from '/imports/parser/rollDice';
|
import rollDice from '/imports/parser/rollDice';
|
||||||
import { toString } from '/imports/parser/resolve';
|
import { toString } from '/imports/parser/resolve';
|
||||||
|
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
|
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export interface Action {
|
|||||||
targetIds?: string[];
|
targetIds?: string[];
|
||||||
userInputNeeded?: any;
|
userInputNeeded?: any;
|
||||||
stepThrough?: boolean;
|
stepThrough?: boolean;
|
||||||
taskQueue: Task[];
|
taskQueue: (Task | DamagePropTask)[];
|
||||||
results: TaskResult[];
|
results: TaskResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,10 +27,31 @@ interface ActionWithId extends Action {
|
|||||||
_id: string;
|
_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Task = {
|
type Task = PropTask | DamagePropTask;
|
||||||
|
|
||||||
|
interface BaseTask {
|
||||||
propId: string;
|
propId: string;
|
||||||
targetIds: string[];
|
targetIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropTask extends BaseTask {
|
||||||
step?: number,
|
step?: number,
|
||||||
|
type?: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DamagePropTask extends BaseTask {
|
||||||
|
subtaskFn: 'damageProp';
|
||||||
|
type: 'subtask';
|
||||||
|
params: {
|
||||||
|
/**
|
||||||
|
* Use getPropertyTitle(prop) to set the title
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
operation: 'increment' | 'set';
|
||||||
|
value: number;
|
||||||
|
stat: string;
|
||||||
|
silent?: true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskResult = {
|
type TaskResult = {
|
||||||
@@ -288,19 +311,25 @@ async function applyNextTask(action: Action, userInput?) {
|
|||||||
const task = action.taskQueue.shift();
|
const task = action.taskQueue.shift();
|
||||||
if (!task) throw 'Next task does not exist';
|
if (!task) throw 'Next task does not exist';
|
||||||
|
|
||||||
// Get property
|
let result: PartialTaskResult;
|
||||||
const prop = await getSingleProperty(action.creatureId, task.propId);
|
|
||||||
|
|
||||||
// Ensure the prop exists
|
if (task.type === 'subtask') {
|
||||||
if (!prop) throw new Meteor.Error('Not found', 'Property could not be found');
|
result = await applySubtask[task.subtaskFn](task, action);
|
||||||
if (prop.deactivatedByToggle) return;
|
} else {
|
||||||
|
// Get property
|
||||||
|
const prop = await getSingleProperty(action.creatureId, task.propId);
|
||||||
|
|
||||||
// Apply the property
|
// Ensure the prop exists
|
||||||
const result: PartialTaskResult = await applyPropertyByType[prop.type]?.(prop, task, action, userInput);
|
if (!prop) throw new Meteor.Error('Not found', 'Property could not be found');
|
||||||
// store the task's details and save the result
|
if (prop.deactivatedByToggle) return;
|
||||||
// Because we recomputed the property in the action context, store the whole thing,
|
|
||||||
// rather than just a reference to it
|
// Apply the property
|
||||||
result.scope[`#${prop.type}`] = prop;
|
result = await applyPropertyByType[prop.type]?.(prop, task, action, userInput);
|
||||||
|
// store the task's details and save the result
|
||||||
|
// Because we recomputed the property in the action context, store the whole thing,
|
||||||
|
// rather than just a reference to it
|
||||||
|
result.scope[`#${prop.type}`] = prop;
|
||||||
|
}
|
||||||
action.results.push({
|
action.results.push({
|
||||||
...result,
|
...result,
|
||||||
propId: task.propId,
|
propId: task.propId,
|
||||||
@@ -320,6 +349,11 @@ function writeChangedAction(original: ActionWithId, changed: ActionWithId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPropertyTitle(prop) {
|
||||||
|
if (prop.name) return prop.name;
|
||||||
|
return getPropertyName(prop.type);
|
||||||
|
}
|
||||||
|
|
||||||
// When doing a thing, you can only change the queue by pushing an array of ordered props
|
// When doing a thing, you can only change the queue by pushing an array of ordered props
|
||||||
// to the front of it
|
// to the front of it
|
||||||
// This makes sure that if the queue is full, you finish doing all subtasks in order
|
// This makes sure that if the queue is full, you finish doing all subtasks in order
|
||||||
@@ -430,7 +464,7 @@ export function propTasks(prop, targetIds?) {
|
|||||||
* @param targetIds
|
* @param targetIds
|
||||||
* @returns Copies of the task, but with a single target each
|
* @returns Copies of the task, but with a single target each
|
||||||
*/
|
*/
|
||||||
function perTargetTasks(task: Task, targetIds: string[] = task.targetIds) {
|
function perTargetTasks(task: PropTask, targetIds: string[] = task.targetIds) {
|
||||||
const tasks: Task[] = [];
|
const tasks: Task[] = [];
|
||||||
// Keep propId
|
// Keep propId
|
||||||
const propId = task.propId;
|
const propId = task.propId;
|
||||||
@@ -528,7 +562,7 @@ function doNextStep(action, task) {
|
|||||||
|
|
||||||
const applyPropertyByType = {
|
const applyPropertyByType = {
|
||||||
|
|
||||||
async action(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
async action(prop, task: PropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
||||||
|
|
||||||
@@ -562,7 +596,7 @@ const applyPropertyByType = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async adjustment(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
async adjustment(prop, task: PropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
|
|
||||||
const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
||||||
@@ -571,7 +605,6 @@ const applyPropertyByType = {
|
|||||||
doNext(action, await childAndTriggerTasks(action, prop, damageTargetIds));
|
doNext(action, await childAndTriggerTasks(action, prop, damageTargetIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Step 0, split the task
|
// Step 0, split the task
|
||||||
if (!task.step) {
|
if (!task.step) {
|
||||||
doNext(action, perTargetTasks(task, damageTargetIds));
|
doNext(action, perTargetTasks(task, damageTargetIds));
|
||||||
@@ -630,67 +663,36 @@ const applyPropertyByType = {
|
|||||||
} else {
|
} else {
|
||||||
result.pushScope['~attributeDamaged'] = stat;
|
result.pushScope['~attributeDamaged'] = stat;
|
||||||
}
|
}
|
||||||
// Wrap next step in the damage property triggers
|
|
||||||
doNext(action, [
|
doNext(action, [
|
||||||
|
// Wrap damage prop subtask in the damage property triggers
|
||||||
|
// Then run the children after that
|
||||||
...triggerTasks(action, stat, damageTargetIds, 'damageProperty.before'),
|
...triggerTasks(action, stat, damageTargetIds, 'damageProperty.before'),
|
||||||
{
|
{
|
||||||
propId: task.propId,
|
propId: task.propId,
|
||||||
targetIds: damageTargetIds,
|
targetIds: damageTargetIds,
|
||||||
step: 2,
|
type: 'subtask',
|
||||||
|
subtaskFn: 'damageProp',
|
||||||
|
params: {
|
||||||
|
title: getPropertyTitle(prop),
|
||||||
|
operation: prop.operation,
|
||||||
|
value,
|
||||||
|
stat: prop.stat,
|
||||||
|
silent: prop.silent,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...triggerTasks(action, stat, damageTargetIds, 'damageProperty.after'),
|
...triggerTasks(action, stat, damageTargetIds, 'damageProperty.after'),
|
||||||
|
...await childAndTriggerTasks(action, prop, damageTargetIds)
|
||||||
]);
|
]);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2, Apply the damage and Log the results
|
// Handle incorrect steps
|
||||||
else if (task.step === 2) {
|
else {
|
||||||
const scope = getEffectiveActionScope(action);
|
throw `Step ${task.step} is not valid for this task`
|
||||||
result.popScope = {
|
|
||||||
'~damage': 1, '~healing': 1, '~set': 1, '~attributeDamaged': 1,
|
|
||||||
};
|
|
||||||
let value = +prop.amount.value;
|
|
||||||
if (prop.operation === 'increment') {
|
|
||||||
if (value >= 0) {
|
|
||||||
value = scope['~damage']?.value;
|
|
||||||
} else {
|
|
||||||
value = -scope['~healing']?.value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = scope['~set']?.value;
|
|
||||||
}
|
|
||||||
const targetPropId = scope['~attributeDamaged']?._propId;
|
|
||||||
if (damageTargetIds?.length) {
|
|
||||||
if (damageTargetIds.length !== 1) {
|
|
||||||
throw 'At this step, only a single target is supported'
|
|
||||||
}
|
|
||||||
const targetId = damageTargetIds[0];
|
|
||||||
await damageProp(action, { value, operation: prop.operation, targetPropId }, targetId, result)
|
|
||||||
result.appendLog({
|
|
||||||
name: 'Attribute damage',
|
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
||||||
` ${value}`,
|
|
||||||
inline: true,
|
|
||||||
silenced: prop.silent,
|
|
||||||
}, [targetId]);
|
|
||||||
await queueChildren();
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
result.appendLog({
|
|
||||||
name: 'Attribute damage',
|
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
||||||
` ${value}`,
|
|
||||||
inline: true,
|
|
||||||
silenced: prop.silent,
|
|
||||||
}, damageTargetIds);
|
|
||||||
await queueChildren();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async branch(prop, task: Task, action: Action, userInput): Promise<PartialTaskResult> {
|
async branch(prop, task: PropTask, action: Action, userInput): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
const targets = task.targetIds;
|
const targets = task.targetIds;
|
||||||
|
|
||||||
@@ -829,13 +831,13 @@ const applyPropertyByType = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async folder(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
async folder(prop, task: PropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
doNext(action, await childAndTriggerTasks(action, prop, task.targetIds));
|
doNext(action, await childAndTriggerTasks(action, prop, task.targetIds));
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async note(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
async note(prop, task: PropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
|
|
||||||
let contents: LogContent[] | undefined = undefined;
|
let contents: LogContent[] | undefined = undefined;
|
||||||
@@ -865,7 +867,7 @@ const applyPropertyByType = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async roll(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
async roll(prop, task: PropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
const result = createResult();
|
const result = createResult();
|
||||||
|
|
||||||
// If there isn't a calculation, just apply the children instead
|
// If there isn't a calculation, just apply the children instead
|
||||||
@@ -918,59 +920,107 @@ const applyPropertyByType = {
|
|||||||
doNext(action, await childAndTriggerTasks(action, prop, task.targetIds));
|
doNext(action, await childAndTriggerTasks(action, prop, task.targetIds));
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DamageProp = {
|
const applySubtask = {
|
||||||
operation: 'increment' | 'set';
|
async damageProp(task: DamagePropTask, action: Action): Promise<PartialTaskResult> {
|
||||||
value: number;
|
const result = createResult();
|
||||||
targetPropId: string;
|
let { value } = task.params;
|
||||||
}
|
const { operation, silent, stat } = task.params;
|
||||||
|
|
||||||
async function damageProp(action, prop: DamageProp, targetId, result): Promise<void> {
|
// Get the user-mutable state from scope
|
||||||
// fetch the value from the scope after the before triggers, in case they changed them
|
const scope = getEffectiveActionScope(action);
|
||||||
const operation = prop.operation;
|
result.popScope = {
|
||||||
const value = prop.value;
|
'~damage': 1, '~healing': 1, '~set': 1, '~attributeDamaged': 1,
|
||||||
let damage, newValue, increment;
|
};
|
||||||
const targetProp = await getSingleProperty(targetId, prop.targetPropId);
|
value = +value;
|
||||||
if (!targetProp) return;
|
if (operation === 'increment') {
|
||||||
if (operation === 'set') {
|
if (value >= 0) {
|
||||||
const total = targetProp.total || 0;
|
value = scope['~damage']?.value;
|
||||||
// Set represents what we want the value to be after damage
|
} else {
|
||||||
// So we need the actual damage to get to that value
|
value = -scope['~healing']?.value;
|
||||||
damage = total - value;
|
}
|
||||||
// Damage can't exceed total value
|
} else {
|
||||||
if (damage > total && !targetProp.ignoreLowerLimit) damage = total;
|
value = scope['~set']?.value;
|
||||||
// Damage must be positive
|
}
|
||||||
if (damage < 0 && !targetProp.ignoreUpperLimit) damage = 0;
|
const targetPropId = scope['~attributeDamaged']?._propId;
|
||||||
newValue = targetProp.total - damage;
|
|
||||||
// Write the results
|
// If there are no targets, just log the result that would apply and end
|
||||||
result.mutations.push({
|
if (!task.targetIds?.length) {
|
||||||
targetIds: [targetId],
|
// Get the locally equivalent stat with the same variable name
|
||||||
updates: [{
|
const localStat = getFromScope(stat, scope);
|
||||||
propId: targetProp._id,
|
const statName = localStat ? getPropertyTitle(localStat) : stat;
|
||||||
set: { damage, value: newValue },
|
result.appendLog({
|
||||||
type: targetProp.type,
|
name: 'Attribute damage',
|
||||||
}],
|
value: `${statName}${operation === 'set' ? ' set to' : ''}` +
|
||||||
});
|
` ${value}`,
|
||||||
} else if (operation === 'increment') {
|
inline: true,
|
||||||
const currentValue = targetProp.value || 0;
|
silenced: silent,
|
||||||
const currentDamage = targetProp.damage || 0;
|
}, task.targetIds);
|
||||||
increment = value;
|
return result;
|
||||||
// Can't increase damage above the remaining value
|
}
|
||||||
if (increment > currentValue && !targetProp.ignoreLowerLimit) increment = currentValue;
|
|
||||||
// Can't decrease damage below zero
|
if (task.targetIds.length !== 1) {
|
||||||
if (-increment > currentDamage && !targetProp.ignoreUpperLimit) increment = -currentDamage;
|
throw 'This subtask can only be called on a single target';
|
||||||
damage = currentDamage + increment;
|
}
|
||||||
newValue = targetProp.total - damage;
|
const targetId = task.targetIds[0];
|
||||||
// Write the results
|
|
||||||
result.mutations.push({
|
let damage, newValue, increment;
|
||||||
targetIds: [targetId],
|
const targetProp = await getSingleProperty(targetId, targetPropId);
|
||||||
updates: [{
|
|
||||||
propId: targetProp._id,
|
if (!targetProp) return result;
|
||||||
inc: { damage: increment, value: -increment },
|
|
||||||
type: targetProp.type,
|
if (operation === 'set') {
|
||||||
}],
|
const total = targetProp.total || 0;
|
||||||
});
|
// Set represents what we want the value to be after damage
|
||||||
|
// So we need the actual damage to get to that value
|
||||||
|
damage = total - value;
|
||||||
|
// Damage can't exceed total value
|
||||||
|
if (damage > total && !targetProp.ignoreLowerLimit) damage = total;
|
||||||
|
// Damage must be positive
|
||||||
|
if (damage < 0 && !targetProp.ignoreUpperLimit) damage = 0;
|
||||||
|
newValue = targetProp.total - damage;
|
||||||
|
// Write the results
|
||||||
|
result.mutations.push({
|
||||||
|
targetIds: [targetId],
|
||||||
|
updates: [{
|
||||||
|
propId: targetProp._id,
|
||||||
|
set: { damage, value: newValue },
|
||||||
|
type: targetProp.type,
|
||||||
|
}],
|
||||||
|
contents: [{
|
||||||
|
name: 'Attribute damage',
|
||||||
|
value: `${getPropertyTitle(targetProp)} set to ${value}`,
|
||||||
|
inline: true,
|
||||||
|
silenced: task.params.silent,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else if (operation === 'increment') {
|
||||||
|
const currentValue = targetProp.value || 0;
|
||||||
|
const currentDamage = targetProp.damage || 0;
|
||||||
|
increment = value;
|
||||||
|
// Can't increase damage above the remaining value
|
||||||
|
if (increment > currentValue && !targetProp.ignoreLowerLimit) increment = currentValue;
|
||||||
|
// Can't decrease damage below zero
|
||||||
|
if (-increment > currentDamage && !targetProp.ignoreUpperLimit) increment = -currentDamage;
|
||||||
|
damage = currentDamage + increment;
|
||||||
|
newValue = targetProp.total - damage;
|
||||||
|
// Write the results
|
||||||
|
result.mutations.push({
|
||||||
|
targetIds: [targetId],
|
||||||
|
updates: [{
|
||||||
|
propId: targetProp._id,
|
||||||
|
inc: { damage: increment, value: -increment },
|
||||||
|
type: targetProp.type,
|
||||||
|
}],
|
||||||
|
contents: [{
|
||||||
|
name: 'Attribute damage',
|
||||||
|
value: `${getPropertyTitle(targetProp)} ${value}`,
|
||||||
|
inline: true,
|
||||||
|
silenced: silent,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ let creatureId;
|
|||||||
|
|
||||||
describe('Interrupt action system', function () {
|
describe('Interrupt action system', function () {
|
||||||
before(async function () {
|
before(async function () {
|
||||||
CreatureProperties.remove({});
|
await Promise.all([
|
||||||
Creatures.remove({});
|
CreatureProperties.removeAsync({}),
|
||||||
CreatureVariables.remove({});
|
Creatures.removeAsync({}),
|
||||||
|
CreatureVariables.removeAsync({}),
|
||||||
|
]);
|
||||||
creatureId = await Creatures.insertAsync({
|
creatureId = await Creatures.insertAsync({
|
||||||
name: 'action test creature',
|
name: 'action test creature',
|
||||||
owner: Random.id(),
|
owner: Random.id(),
|
||||||
|
|||||||
Reference in New Issue
Block a user