748 lines
22 KiB
TypeScript
748 lines
22 KiB
TypeScript
import SimpleSchema from 'simpl-schema';
|
|
import { forEach, isEmpty, pick } from 'lodash';
|
|
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
|
import { getPropertyChildren, getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
|
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
|
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
|
import rollDice from '/imports/parser/rollDice';
|
|
|
|
const Actions = new Mongo.Collection<ActionWithId>('actions');
|
|
|
|
export interface Action {
|
|
creatureId: string;
|
|
rootPropId: string;
|
|
targetIds?: string[];
|
|
userInputNeeded?: any;
|
|
stepThrough?: boolean;
|
|
taskQueue: Task[];
|
|
taskProperties: any;
|
|
deferredResults: { [id: string]: PartialTaskResult };
|
|
results: TaskResult[];
|
|
}
|
|
|
|
interface ActionWithId extends Action {
|
|
_id: string;
|
|
}
|
|
|
|
type Task = {
|
|
propId: string;
|
|
targetIds: string[];
|
|
step?: number,
|
|
}
|
|
|
|
type TaskResult = {
|
|
propId: string;
|
|
targetIds: string[];
|
|
scope: any;
|
|
mutations: Mutation[];
|
|
}
|
|
|
|
class PartialTaskResult {
|
|
scope: any;
|
|
mutations: Mutation[];
|
|
constructor() {
|
|
this.scope = {};
|
|
this.mutations = [];
|
|
}
|
|
// Appends the log content to the latest mutation
|
|
appendLog(content: LogContent, targetIds: string[]) {
|
|
if (!this.mutations.length) {
|
|
this.mutations.push({ targetIds, contents: [] });
|
|
}
|
|
const latestMutation = this.mutations[this.mutations.length - 1]
|
|
if (!latestMutation.contents) {
|
|
latestMutation.contents = [];
|
|
}
|
|
latestMutation.contents.push(content);
|
|
}
|
|
}
|
|
|
|
type Mutation = {
|
|
// Which creatures the mutation is applied to
|
|
targetIds: string[];
|
|
// What changes in the database
|
|
updates?: Update[];
|
|
// Logged when this is applied
|
|
contents?: LogContent[];
|
|
}
|
|
|
|
export type Update = {
|
|
propId: string;
|
|
type: string,
|
|
set?: any;
|
|
inc?: any;
|
|
}
|
|
|
|
export type LogContent = {
|
|
name?: string;
|
|
value?: string;
|
|
inline?: boolean;
|
|
context?: any;
|
|
silenced?: boolean;
|
|
}
|
|
|
|
const ActionSchema = new SimpleSchema({
|
|
creatureId: {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
rootPropId: {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
targetIds: {
|
|
type: Array,
|
|
defaultValue: [],
|
|
},
|
|
'targetIds.$': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
userInputNeeded: {
|
|
type: Object,
|
|
optional: true,
|
|
blackbox: true,
|
|
},
|
|
stepThrough: {
|
|
type: Boolean,
|
|
defaultValue: false,
|
|
},
|
|
|
|
// A stack of tasks to apply
|
|
// Each task has a propId to apply and a targetId list
|
|
taskQueue: {
|
|
type: Array,
|
|
},
|
|
'taskQueue.$': {
|
|
type: Object,
|
|
},
|
|
'taskQueue.$.propId': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
'taskQueue.$.step': {
|
|
type: Number,
|
|
optional: true,
|
|
},
|
|
'taskQueue.$.targetIds': {
|
|
type: Array,
|
|
defaultValue: [],
|
|
},
|
|
'taskQueue.$.targetIds.$': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
|
|
// Pseudo properties that don't exist on the character, but can be applied by the action
|
|
// {_id: prop}
|
|
'taskProperties': {
|
|
type: Object,
|
|
blackbox: true,
|
|
defaultValue: {},
|
|
},
|
|
// Results that have been partially computed, but require more steps
|
|
// {_id: partialResult}
|
|
'deferredResults': {
|
|
type: Object,
|
|
blackbox: true,
|
|
defaultValue: {},
|
|
},
|
|
|
|
// Applied properties
|
|
results: {
|
|
type: Array,
|
|
defaultValue: [],
|
|
},
|
|
'results.$': {
|
|
type: Object,
|
|
},
|
|
// The property and target ids popped off the task stack
|
|
// Pushing these to the top of the stack and deleting the results from this point onwards
|
|
// Should re-run the action identically from this point
|
|
'results.$.propId': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
'results.$.targetIds': {
|
|
type: Array,
|
|
defaultValue: [],
|
|
},
|
|
'results.$.targetIds.$': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
// Changes in local scope made by this result
|
|
'results.$.scope': {
|
|
type: Object,
|
|
optional: true,
|
|
blackbox: true,
|
|
},
|
|
// database changes
|
|
'results.$.mutations': {
|
|
type: Array,
|
|
optional: true,
|
|
},
|
|
'results.$.mutations.$': {
|
|
type: Object,
|
|
},
|
|
'results.$.mutations.$.targetIds': {
|
|
type: Array,
|
|
},
|
|
'results.$.mutations.$.targetIds.$': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
'results.$.mutations.$.updates': {
|
|
type: Array,
|
|
optional: true,
|
|
},
|
|
'results.$.mutations.$.updates.$': {
|
|
type: Object,
|
|
},
|
|
'results.$.mutations.$.updates.$.propId': {
|
|
type: String,
|
|
regEx: SimpleSchema.RegEx.Id,
|
|
},
|
|
// Required, because CreatureProperties.update requires a selector of { type }
|
|
'results.$.mutations.$.updates.$.type': {
|
|
type: String,
|
|
},
|
|
'results.$.mutations.$.updates.$.set': {
|
|
type: Object,
|
|
optional: true,
|
|
blackbox: true,
|
|
},
|
|
'results.$.mutations.$.updates.$.inc': {
|
|
type: Object,
|
|
optional: true,
|
|
blackbox: true,
|
|
},
|
|
'results.$.mutations.$.contents': {
|
|
type: Array,
|
|
optional: true,
|
|
},
|
|
'results.$.mutations.$.contents.$': {
|
|
type: LogContentSchema,
|
|
},
|
|
});
|
|
|
|
// @ts-expect-error Collections2 lacks TypeScript support
|
|
Actions.attachSchema(ActionSchema);
|
|
|
|
export default Actions;
|
|
|
|
/**
|
|
* Create a new action ready to be run starting at the given property (or its 'before' triggers)
|
|
* @param prop
|
|
*/
|
|
export function createAction(prop) {
|
|
const action: Action = {
|
|
creatureId: prop.ancestors[0].id,
|
|
rootPropId: prop._id,
|
|
taskQueue: [],
|
|
taskProperties: {},
|
|
deferredResults: {},
|
|
results: [],
|
|
};
|
|
pushPropAndTriggers(action, prop);
|
|
return Actions.insertAsync(action);
|
|
}
|
|
|
|
// Run an already created action
|
|
export async function runAction(actionId: string, userInput?) {
|
|
const action = await Actions.findOneAsync(actionId);
|
|
if (!action) throw new Meteor.Error('Not found', 'The action does not exist');
|
|
const originalAction = EJSON.clone(action);
|
|
let count = 0;
|
|
do {
|
|
// If there isn't a next task, stop
|
|
if (!action.taskQueue.length) break;
|
|
await applyNextTask(action, userInput);
|
|
count += 1;
|
|
if (count > 100) {
|
|
break;
|
|
}
|
|
} while (!action.userInputNeeded && !action.stepThrough)
|
|
|
|
// Persist changes to the action
|
|
const writePromise = writeChangedAction(originalAction, action);
|
|
if (count > 100) {
|
|
throw new Meteor.Error('Too many properties', 'Only 100 properties may fire at a time');
|
|
}
|
|
return writePromise;
|
|
}
|
|
|
|
// TODO create a function to get the effective value of a property,
|
|
// simulating all the result updates in the action so far
|
|
|
|
async function applyNextTask(action: Action, userInput?) {
|
|
// Get the next task
|
|
const task = action.taskQueue.shift();
|
|
if (!task) throw 'Next task does not exist';
|
|
// Get the property from the action's task properties or the creature's properties
|
|
let prop;
|
|
const taskProp = action.taskProperties[task.propId];
|
|
if (taskProp) {
|
|
prop = taskProp;
|
|
} else {
|
|
prop = await getSingleProperty(action.creatureId, task.propId);
|
|
}
|
|
// Ensure the prop exists
|
|
if (!prop) throw new Meteor.Error('Not found', 'Property could not be found');
|
|
if (prop.deactivatedByToggle) return;
|
|
|
|
// Apply the property
|
|
const result: PartialTaskResult = await applyPropertyByType[prop.type]?.(prop, task, action, userInput);
|
|
// store the task's details and save the result
|
|
result.scope[`#${prop.type}`] = prop;
|
|
action.results.push({
|
|
propId: task.propId,
|
|
targetIds: task.targetIds,
|
|
scope: result.scope,
|
|
mutations: result.mutations,
|
|
});
|
|
}
|
|
|
|
function writeChangedAction(original: ActionWithId, changed: ActionWithId) {
|
|
const $set = {};
|
|
for (const key of ActionSchema.objectKeys()) {
|
|
if (!EJSON.equals(original[key], changed[key])) {
|
|
$set[key] = changed[key];
|
|
}
|
|
}
|
|
if (!isEmpty($set)) {
|
|
return Actions.updateAsync(original._id, { $set });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push a prop and its before/after triggers to the task stack
|
|
* Triggers will share the same targetIds as the prop task
|
|
* @param action The action to add the task to
|
|
* @param prop The property to make a task of
|
|
* @param targetIds The targetIds the prop and triggers will apply to
|
|
*/
|
|
function pushPropAndTriggers(action: Action, prop, targetIds?) {
|
|
// Push the before triggers to the queue
|
|
forEach(prop.triggerIds?.before, triggerId => {
|
|
action.taskQueue.push({ propId: triggerId, targetIds });
|
|
});
|
|
|
|
// Push the prop task to the queue
|
|
action.taskQueue.push({ propId: prop._id, targetIds });
|
|
|
|
// Push the after triggers to the queue
|
|
forEach(prop.triggerIds?.after, triggerId => {
|
|
action.taskQueue.push({ propId: triggerId, targetIds });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Push all the children of a prop and all trigger of those children to the task queue
|
|
* @param action The action to add the task to
|
|
* @param prop The property to make a task of
|
|
* @param targetIds The targetIds the prop and triggers will apply to
|
|
*/
|
|
async function pushChildren(action: Action, prop, targetIds) {
|
|
const children = await getPropertyChildren(action.creatureId, prop._id);
|
|
// Push the child tasks and related triggers to the stack
|
|
forEach(children, childProp => {
|
|
pushPropAndTriggers(action, childProp, targetIds);
|
|
});
|
|
}
|
|
|
|
function pushAfterChildrenTriggers(action: Action, prop, targetIds) {
|
|
forEach(prop.triggerIds?.afterChildren, triggerId => {
|
|
action.taskQueue.push({ propId: triggerId, targetIds });
|
|
});
|
|
}
|
|
|
|
function createResult(): PartialTaskResult {
|
|
// Add the property to the action's local scope
|
|
return new PartialTaskResult();
|
|
}
|
|
|
|
// Combine all the action results into the scope at present
|
|
export function getEffectiveActionScope(action: Action) {
|
|
const scope = getVariables(action.creatureId);
|
|
// First combine the applied results
|
|
for (const result of action.results) {
|
|
Object.assign(scope, result.scope);
|
|
}
|
|
// Then the deferred results
|
|
// Warning: order is not guaranteed here
|
|
for (const id in action.deferredResults) {
|
|
const result = action.deferredResults[id];
|
|
Object.assign(scope, result.scope);
|
|
}
|
|
return scope;
|
|
}
|
|
|
|
type DamageProp = {
|
|
_id?: string;
|
|
operation: 'increment' | 'set';
|
|
type: 'damageProp';
|
|
value: number;
|
|
targetPropId: string;
|
|
}
|
|
|
|
function pushDamagePropertyTasks(action, damageProp: DamageProp, targetProp, targetIds, result: PartialTaskResult) {
|
|
// Save the values to the scope
|
|
if (damageProp.operation === 'increment') {
|
|
if (damageProp.value >= 0) {
|
|
result.scope['~damage'] = { value: damageProp.value };
|
|
} else {
|
|
result.scope['~healing'] = { value: -damageProp.value };
|
|
}
|
|
} else {
|
|
result.scope['~set'] = { value: damageProp.value };
|
|
}
|
|
// Push before triggers
|
|
if (targetProp.triggers?.damageProperty?.before) {
|
|
for (const triggerId of targetProp.triggers.damageProperty.before) {
|
|
action.taskQueue.push({ propId: triggerId, targetIds });
|
|
}
|
|
}
|
|
|
|
// Push damage pseudo prop
|
|
if (!damageProp._id) damageProp._id = Random.id();
|
|
action.taskProperties[damageProp._id] = damageProp;
|
|
action.taskQueue.push({ propId: damageProp._id, targetIds });
|
|
|
|
// Push after triggers
|
|
if (targetProp.triggers?.damageProperty?.after) {
|
|
for (const triggerId of targetProp.triggers.damageProperty.after) {
|
|
action.taskQueue.push({ propId: triggerId, targetIds });
|
|
}
|
|
}
|
|
}
|
|
|
|
const applyPropertyByType = {
|
|
async note(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
|
const result = createResult();
|
|
|
|
let contents: LogContent[] | undefined = undefined;
|
|
const logContent = { name: prop.name, value: undefined };
|
|
if (prop.summary?.text) {
|
|
recalculateInlineCalculations(prop.summary, action);
|
|
logContent.value = prop.summary.value;
|
|
}
|
|
|
|
if (logContent.name || logContent.value) {
|
|
contents = [logContent];
|
|
}
|
|
// Log description
|
|
if (prop.description?.text) {
|
|
recalculateInlineCalculations(prop.description, action);
|
|
if (!contents) contents = [];
|
|
contents.push({ value: prop.description.value });
|
|
}
|
|
if (contents) {
|
|
result.mutations.push({
|
|
contents,
|
|
targetIds: task.targetIds,
|
|
});
|
|
}
|
|
|
|
await pushChildren(action, prop, task.targetIds);
|
|
await pushAfterChildrenTriggers(action, prop, task.targetIds);
|
|
|
|
return result;
|
|
},
|
|
|
|
async branch(prop, task: Task, action: Action, userInput): Promise<PartialTaskResult> {
|
|
// const scope = getEffectiveActionScope(action);
|
|
const result = createResult();
|
|
const targets = task.targetIds;
|
|
|
|
switch (prop.branchType) {
|
|
case 'if': {
|
|
recalculateCalculation(prop.condition, action, 'reduce');
|
|
if (prop.condition?.value) {
|
|
await pushChildren(action, prop, targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'index': {
|
|
const children = await getPropertyChildren(action.creatureId, prop._id);
|
|
if (children.length) {
|
|
recalculateCalculation(prop.condition, action, 'reduce');
|
|
if (!isFinite(prop.condition?.value)) {
|
|
result.appendLog({
|
|
name: 'Branch Error',
|
|
value: 'Index did not resolve into a valid number'
|
|
}, targets);
|
|
break;
|
|
}
|
|
let index = Math.floor(prop.condition?.value);
|
|
if (index < 1) index = 1;
|
|
if (index > children.length) index = children.length;
|
|
pushPropAndTriggers(action, children[index - 1], targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'hit': {
|
|
const scope = getEffectiveActionScope(action);
|
|
if (scope['~attackHit']?.value) {
|
|
if (!targets.length && !prop.silent) {
|
|
result.appendLog({
|
|
value: '**On hit**'
|
|
}, targets);
|
|
}
|
|
await pushChildren(action, prop, targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'miss': {
|
|
const scope = getEffectiveActionScope(action);
|
|
if (scope['~attackMiss']?.value) {
|
|
if (!targets.length && !prop.silent) {
|
|
result.appendLog({
|
|
value: '**On miss**'
|
|
}, targets);
|
|
}
|
|
await pushChildren(action, prop, targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'failedSave': {
|
|
const scope = getEffectiveActionScope(action);
|
|
if (scope['~saveFailed']?.value) {
|
|
if (!targets.length && !prop.silent) {
|
|
result.appendLog({
|
|
value: '**On failed save**'
|
|
}, targets);
|
|
}
|
|
await pushChildren(action, prop, targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'successfulSave': {
|
|
const scope = getEffectiveActionScope(action);
|
|
if (scope['~saveSucceeded']?.value) {
|
|
if (!targets.length && !prop.silent) {
|
|
result.appendLog({
|
|
value: '**On save**'
|
|
}, targets);
|
|
}
|
|
await pushChildren(action, prop, targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'random': {
|
|
const children = await getPropertyChildren(action.creatureId, prop._id);
|
|
if (children.length) {
|
|
const index = rollDice(1, children.length)[0] - 1;
|
|
pushPropAndTriggers(action, children[index], targets);
|
|
}
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
break;
|
|
}
|
|
case 'eachTarget':
|
|
if (targets.length) {
|
|
for (const targetId in targets) {
|
|
await pushChildren(action, prop, [targetId]);
|
|
pushAfterChildrenTriggers(action, prop, [targetId]);
|
|
}
|
|
} else {
|
|
await pushChildren(action, prop, targets);
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
}
|
|
break;
|
|
case 'choice': {
|
|
// Step 0, halt the action to get user input
|
|
if (!task.step) {
|
|
// Mark the action as needing user input so that it halts
|
|
action.userInputNeeded = pick(prop, ['_id', 'type', 'branchType']);
|
|
// Put this task back in the queue, but at step 1
|
|
action.taskQueue.push({
|
|
...task,
|
|
step: 1,
|
|
});
|
|
return result;
|
|
}
|
|
// Step 1 consume the user input
|
|
else if (task.step === 1) {
|
|
if (!userInput) {
|
|
throw 'User input was required for this step'
|
|
}
|
|
const children = await getPropertyChildren(action.creatureId, prop._id);
|
|
let index = userInput.choice;
|
|
if (!isFinite(index) || index < 0) index = 0;
|
|
if (index > children.length - 1) index = children.length - 1;
|
|
pushPropAndTriggers(action, children[index], targets);
|
|
pushAfterChildrenTriggers(action, prop, targets);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
async adjustment(prop, task: Task, action: Action): Promise<PartialTaskResult> {
|
|
const result = createResult();
|
|
|
|
const queueChildren = async function (targetIds) {
|
|
await pushChildren(action, prop, targetIds);
|
|
await pushAfterChildrenTriggers(action, prop, targetIds);
|
|
}
|
|
|
|
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
|
task.targetIds = damageTargets;
|
|
|
|
// Step 0, get the operation and value and push the damage pseudo prop to the queue
|
|
if (!task.step) {
|
|
|
|
if (!prop.amount) {
|
|
queueChildren(task.targetIds);
|
|
return result;
|
|
}
|
|
|
|
// Evaluate the amount
|
|
recalculateCalculation(prop.amount, action, 'reduce');
|
|
const value = +prop.amount.value;
|
|
if (!isFinite(value)) {
|
|
queueChildren(task.targetIds);
|
|
return result;
|
|
}
|
|
|
|
if (damageTargets?.length) {
|
|
for (const targetId of damageTargets) {
|
|
const statId = getVariables(targetId)?.[prop.stat]?._propId;
|
|
if (!statId) continue;
|
|
|
|
const stat = getSingleProperty(targetId, statId);
|
|
if (!stat) continue;
|
|
|
|
if (!stat?.type) {
|
|
if (!prop.silent) result.appendLog({
|
|
name: 'Error',
|
|
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
|
}, [targetId]);
|
|
continue;
|
|
}
|
|
// Do the damage
|
|
pushDamagePropertyTasks(action, {
|
|
type: 'damageProp',
|
|
value,
|
|
operation: prop.operation,
|
|
targetPropId: stat._id,
|
|
}, stat, [targetId], result);
|
|
// Do the next step of this property
|
|
action.taskQueue.push({
|
|
...task,
|
|
step: 1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Step 1, Log the results
|
|
else if (task.step === 1) {
|
|
const scope = getEffectiveActionScope(action);
|
|
let value;
|
|
if (prop.operation === 'increment') {
|
|
if (prop.value >= 0) {
|
|
value = scope['~damage']?.value;
|
|
} else {
|
|
value = -scope['~healing']?.value;
|
|
}
|
|
} else {
|
|
value = scope['~set']?.value;
|
|
}
|
|
if (damageTargets?.length) {
|
|
for (const targetId of damageTargets) {
|
|
await queueChildren([targetId]);
|
|
result.appendLog({
|
|
name: 'Attribute damage',
|
|
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
` ${value}`,
|
|
inline: true,
|
|
silenced: prop.silent,
|
|
}, [targetId]);
|
|
}
|
|
} else {
|
|
await queueChildren(task.targetIds);
|
|
result.appendLog({
|
|
name: 'Attribute damage',
|
|
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
` ${value}`,
|
|
inline: true,
|
|
silenced: prop.silent,
|
|
}, task.targetIds);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
async damageProp(prop: DamageProp, task: Task, action: Action): Promise<PartialTaskResult> {
|
|
// fetch the value from the scope after the before triggers, in case they changed them
|
|
const result = createResult();
|
|
const scope = getEffectiveActionScope(action);
|
|
const operation = prop.operation;
|
|
let value;
|
|
if (prop.operation === 'increment') {
|
|
if (prop.value >= 0) {
|
|
value = scope['~damage']?.value;
|
|
} else {
|
|
value = -scope['~healing']?.value;
|
|
}
|
|
} else {
|
|
value = scope['~set']?.value;
|
|
}
|
|
let damage, newValue, increment;
|
|
if (task.targetIds.length) {
|
|
for (const targetId of task.targetIds) {
|
|
const targetProp = getSingleProperty(targetId, prop.targetPropId);
|
|
if (!targetProp) continue;
|
|
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,
|
|
}],
|
|
});
|
|
} 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,
|
|
}],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|