refactored action engine into individual files
This commit is contained in:
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"autorun",
|
||||||
|
"cyrb",
|
||||||
|
"EJSON",
|
||||||
|
"uncomputed"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -172,7 +172,7 @@ function assertSourceLibraryCopyPermission(props, method) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanProps(props) {
|
export function cleanProps(props) {
|
||||||
return props.map(prop => {
|
return props.map(prop => {
|
||||||
let schema = LibraryNodes.simpleSchema(prop);
|
let schema = LibraryNodes.simpleSchema(prop);
|
||||||
return schema.clean(prop);
|
return schema.clean(prop);
|
||||||
|
|||||||
@@ -226,5 +226,3 @@ Creatures.attachSchema(CreatureSchema);
|
|||||||
|
|
||||||
export default Creatures;
|
export default Creatures;
|
||||||
export { CreatureSchema };
|
export { CreatureSchema };
|
||||||
|
|
||||||
import '/imports/api/engine/actions/doAction';
|
|
||||||
|
|||||||
@@ -4,20 +4,18 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn';
|
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
import Actions, { Action, Update, LogContent, applyAction } from '/imports/api/engine/actions/ActionEngine';
|
|
||||||
import computeCreature from '/imports/api/engine/computeCreature';
|
import computeCreature from '/imports/api/engine/computeCreature';
|
||||||
import { loadCreature } from '/imports/api/engine/loadCreatures';
|
import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyAction } from '/imports/api/engine/action/functions/applyAction';
|
||||||
|
import { LogContent, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
|
||||||
const creatureId = Random.id();
|
const creatureId = Random.id();
|
||||||
const targetId = Random.id();
|
const targetId = Random.id();
|
||||||
|
|
||||||
describe('Interrupt action system', function () {
|
describe('Interrupt action system', function () {
|
||||||
let unload: (() => void) | undefined = undefined;
|
const dummySubscription = Tracker.autorun(() => undefined)
|
||||||
const dummySubscription = {
|
this.timeout(8000);
|
||||||
onStop(fn) {
|
|
||||||
unload = fn;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
// Remove old data
|
// Remove old data
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -48,7 +46,7 @@ describe('Interrupt action system', function () {
|
|||||||
loadCreature(creatureId, dummySubscription);
|
loadCreature(creatureId, dummySubscription);
|
||||||
});
|
});
|
||||||
after(function () {
|
after(function () {
|
||||||
unload?.();
|
dummySubscription.stop();
|
||||||
});
|
});
|
||||||
it('writes notes to the log', async function () {
|
it('writes notes to the log', async function () {
|
||||||
const action = await runActionById(note1Id);
|
const action = await runActionById(note1Id);
|
||||||
@@ -86,7 +84,7 @@ describe('Interrupt action system', function () {
|
|||||||
it('Halts execution of choice branches', async function () {
|
it('Halts execution of choice branches', async function () {
|
||||||
let userInputRequested = false;
|
let userInputRequested = false;
|
||||||
const requestUserInput = () => { userInputRequested = true; return 0 };
|
const requestUserInput = () => { userInputRequested = true; return 0 };
|
||||||
const action = await runActionById(choiceBranchId, requestUserInput);
|
await runActionById(choiceBranchId, requestUserInput);
|
||||||
assert.isTrue(userInputRequested, 'User input should be requested when a choice branch is applied');
|
assert.isTrue(userInputRequested, 'User input should be requested when a choice branch is applied');
|
||||||
});
|
});
|
||||||
it('Applies adjustments', async function () {
|
it('Applies adjustments', async function () {
|
||||||
@@ -124,29 +122,102 @@ describe('Interrupt action system', function () {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('Applies buffs', async function () {
|
||||||
|
const action = await runActionById(buffId);
|
||||||
|
const inserts = allInserts(action);
|
||||||
|
const newIds = inserts.map(p => p._id);
|
||||||
|
assert.notEqual(buffId, newIds[0]);
|
||||||
|
assert.deepEqual(inserts, [
|
||||||
|
{
|
||||||
|
_id: newIds[0],
|
||||||
|
left: 43,
|
||||||
|
parentId: null,
|
||||||
|
right: 48,
|
||||||
|
root: {
|
||||||
|
collection: 'creatures',
|
||||||
|
id: creatureId,
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
target: 'self',
|
||||||
|
type: 'buff',
|
||||||
|
}, {
|
||||||
|
_id: newIds[1],
|
||||||
|
attributeType: 'stat',
|
||||||
|
baseValue: {
|
||||||
|
calculation: '13 + buffSourceStat + 7',
|
||||||
|
},
|
||||||
|
left: 44,
|
||||||
|
parentId: newIds[0],
|
||||||
|
right: 45,
|
||||||
|
root: {
|
||||||
|
collection: 'creatures',
|
||||||
|
id: creatureId,
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
type: 'attribute',
|
||||||
|
variableName: 'buffStat',
|
||||||
|
}, {
|
||||||
|
_id: newIds[2],
|
||||||
|
left: 46,
|
||||||
|
parentId: newIds[0],
|
||||||
|
removeAll: true,
|
||||||
|
right: 47,
|
||||||
|
root: {
|
||||||
|
collection: 'creatures',
|
||||||
|
id: creatureId,
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
target: 'self',
|
||||||
|
targetParentBuff: true,
|
||||||
|
type: 'buffRemover',
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('Removes parent buffs', async function () {
|
||||||
|
const action = await runActionById(removeParentBuffId);
|
||||||
|
console.log(allLogContent(action));
|
||||||
|
assert.deepEqual(allRemovals(action), [
|
||||||
|
{ propId: buffId }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('Removes all buffs by tag', async function () {
|
||||||
|
const action = await runActionById(removeTaggedBuffsId);
|
||||||
|
console.log(allLogContent(action));
|
||||||
|
assert.deepEqual(allRemovals(action), [
|
||||||
|
{ propId: taggedBuffId },
|
||||||
|
{ propId: secondTaggedBuffId },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('Removes a single buff by tag', async function () {
|
||||||
|
const action = await runActionById(removeOneTaggedBuffId);
|
||||||
|
console.log(allLogContent(action));
|
||||||
|
assert.deepEqual(allRemovals(action), [
|
||||||
|
{ propId: taggedBuffId },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createAction(prop, targetIds?) {
|
function createAction(prop, targetIds?) {
|
||||||
const action: Action = {
|
const action: EngineAction = {
|
||||||
creatureId: prop.root.id,
|
creatureId: prop.root.id,
|
||||||
rootPropId: prop._id,
|
rootPropId: prop._id,
|
||||||
results: [],
|
results: [],
|
||||||
taskCount: 0,
|
taskCount: 0,
|
||||||
targetIds,
|
targetIds,
|
||||||
};
|
};
|
||||||
return Actions.insertAsync(action);
|
return EngineActions.insertAsync(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runActionById(propId, userInputFn = () => 0) {
|
async function runActionById(propId, userInputFn = () => 0) {
|
||||||
const prop = await CreatureProperties.findOneAsync(propId);
|
const prop = await CreatureProperties.findOneAsync(propId);
|
||||||
const actionId = await createAction(prop);
|
const actionId = await createAction(prop);
|
||||||
const action = await Actions.findOneAsync(actionId);
|
const action = await EngineActions.findOneAsync(actionId);
|
||||||
if (!action) throw 'Action is expected to exist';
|
if (!action) throw 'Action is expected to exist';
|
||||||
await applyAction(action, userInputFn, { simulate: true });
|
await applyAction(action, userInputFn, { simulate: true });
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
function allUpdates(action: Action) {
|
function allUpdates(action: EngineAction) {
|
||||||
const updates: Update[] = [];
|
const updates: Update[] = [];
|
||||||
action.results.forEach(result => {
|
action.results.forEach(result => {
|
||||||
result.mutations.forEach(mutation => {
|
result.mutations.forEach(mutation => {
|
||||||
@@ -158,7 +229,31 @@ function allUpdates(action: Action) {
|
|||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function allLogContent(action: Action) {
|
function allInserts(action: EngineAction) {
|
||||||
|
const inserts: any[] = [];
|
||||||
|
action.results.forEach(result => {
|
||||||
|
result.mutations.forEach(mutation => {
|
||||||
|
mutation.inserts?.forEach(update => {
|
||||||
|
inserts.push(update);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return inserts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allRemovals(action: EngineAction) {
|
||||||
|
const removals: Removal[] = [];
|
||||||
|
action.results.forEach(result => {
|
||||||
|
result.mutations.forEach(mutation => {
|
||||||
|
mutation.removals?.forEach(update => {
|
||||||
|
removals.push(update);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return removals
|
||||||
|
}
|
||||||
|
|
||||||
|
function allLogContent(action: EngineAction) {
|
||||||
const contents: LogContent[] = [];
|
const contents: LogContent[] = [];
|
||||||
action.results.forEach(result => {
|
action.results.forEach(result => {
|
||||||
result.mutations.forEach(mutation => {
|
result.mutations.forEach(mutation => {
|
||||||
@@ -170,8 +265,9 @@ function allLogContent(action: Action) {
|
|||||||
return contents;
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId, adjustedStatId,
|
let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId,
|
||||||
adjustmentIncrementId, adjustmentSetId, rollId;
|
adjustedStatId, adjustmentIncrementId, adjustmentSetId, rollId, buffId,
|
||||||
|
removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId, buffAttChildId;
|
||||||
|
|
||||||
const propForest = [
|
const propForest = [
|
||||||
// Apply a simple note
|
// Apply a simple note
|
||||||
@@ -255,6 +351,63 @@ const propForest = [
|
|||||||
{ type: 'note', summary: { text: 'adjustment increment applied' } },
|
{ type: 'note', summary: { text: 'adjustment increment applied' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Apply buffs
|
||||||
|
{
|
||||||
|
_id: Random.id(),
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'stat',
|
||||||
|
variableName: 'buffSourceStat',
|
||||||
|
baseValue: { calculation: '13' },
|
||||||
|
}, {
|
||||||
|
_id: buffId = Random.id(),
|
||||||
|
type: 'buff',
|
||||||
|
target: 'self',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
_id: buffAttChildId = Random.id(),
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'stat',
|
||||||
|
variableName: 'buffStat',
|
||||||
|
baseValue: { calculation: 'buffSourceStat + ~target.buffSourceStat + 7' },
|
||||||
|
}, {
|
||||||
|
_id: removeParentBuffId = Random.id(),
|
||||||
|
type: 'buffRemover',
|
||||||
|
target: 'self',
|
||||||
|
targetParentBuff: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Extra buffs with and without tags
|
||||||
|
{
|
||||||
|
_id: taggedBuffId = Random.id(),
|
||||||
|
name: 'Tagged Buff',
|
||||||
|
type: 'buff',
|
||||||
|
tags: ['buff tag', 'other tag']
|
||||||
|
}, {
|
||||||
|
_id: secondTaggedBuffId = Random.id(),
|
||||||
|
name: 'Tagged buff 2',
|
||||||
|
type: 'buff',
|
||||||
|
tags: ['buff tag', 'yet another tag']
|
||||||
|
}, {
|
||||||
|
_id: Random.id(),
|
||||||
|
name: 'Untagged buff',
|
||||||
|
type: 'buff',
|
||||||
|
tags: ['other tag']
|
||||||
|
},
|
||||||
|
// Remove buffs by tag
|
||||||
|
{
|
||||||
|
_id: removeTaggedBuffsId = Random.id(),
|
||||||
|
type: 'buffRemover',
|
||||||
|
target: 'self',
|
||||||
|
removeAll: true,
|
||||||
|
targetTags: 'buff tag',
|
||||||
|
}, {
|
||||||
|
_id: removeOneTaggedBuffId = Random.id(),
|
||||||
|
type: 'buffRemover',
|
||||||
|
target: 'self',
|
||||||
|
removeAll: false,
|
||||||
|
targetTags: 'buff tag',
|
||||||
|
},
|
||||||
// Apply rolls
|
// Apply rolls
|
||||||
{
|
{
|
||||||
_id: rollId = Random.id(),
|
_id: rollId = Random.id(),
|
||||||
135
app/imports/api/engine/action/EngineActions.ts
Normal file
135
app/imports/api/engine/action/EngineActions.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import TaskResult from './tasks/TaskResult';
|
||||||
|
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
||||||
|
|
||||||
|
const EngineActions = new Mongo.Collection<EngineAction>('actions');
|
||||||
|
|
||||||
|
export interface EngineAction {
|
||||||
|
_id?: string;
|
||||||
|
_isSimulation?: boolean;
|
||||||
|
_stepThrough?: boolean;
|
||||||
|
creatureId: string;
|
||||||
|
rootPropId: string;
|
||||||
|
targetIds?: string[];
|
||||||
|
results: TaskResult[];
|
||||||
|
taskCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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 that override the local scope
|
||||||
|
'results.$.scope': {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
// Changes that consume pushed values from the local scope
|
||||||
|
'results.$.popScope': {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
// Changes that push values to the local scope
|
||||||
|
'results.$.pushScope': {
|
||||||
|
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
|
||||||
|
EngineActions.attachSchema(ActionSchema);
|
||||||
|
|
||||||
|
export default EngineActions;
|
||||||
|
export { ActionSchema }
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { PropTask } from '../tasks/Task';
|
||||||
|
import TaskResult, { LogContent } from '../tasks/TaskResult';
|
||||||
|
import { getPropertiesOfType } from '/imports/api/engine/loadCreatures';
|
||||||
|
import applyTask from '/imports/api/engine/action/tasks/applyTask';
|
||||||
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
|
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
|
||||||
|
import spendResources from '/imports/api/engine/action/functions/spendResources';
|
||||||
|
import { applyAfterChildrenTriggers, applyAfterTriggers, applyChildren } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
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 rollDice from '/imports/parser/rollDice';
|
||||||
|
|
||||||
|
export default async function applyActionProperty(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
||||||
|
|
||||||
|
//Log the name and summary, check that the property has enough resources to fire
|
||||||
|
const content: LogContent = { name: prop.name };
|
||||||
|
if (prop.summary?.text) {
|
||||||
|
await recalculateInlineCalculations(prop.summary, action);
|
||||||
|
content.value = prop.summary.value;
|
||||||
|
}
|
||||||
|
if (prop.silent) content.silenced = true;
|
||||||
|
result.appendLog(content, targetIds);
|
||||||
|
|
||||||
|
// Check Uses
|
||||||
|
if (prop.usesLeft <= 0) {
|
||||||
|
if (!prop.silent) result.appendLog({
|
||||||
|
name: 'Error',
|
||||||
|
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||||
|
}, targetIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Resources
|
||||||
|
if (prop.insufficientResources) {
|
||||||
|
if (!prop.silent) result.appendLog({
|
||||||
|
name: 'Error',
|
||||||
|
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||||
|
}, targetIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spendResources(action, prop, targetIds, result, userInput);
|
||||||
|
|
||||||
|
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||||
|
|
||||||
|
// Attack if there is an attack roll
|
||||||
|
if (attack && attack.calculation) {
|
||||||
|
if (targetIds.length) {
|
||||||
|
for (const target of targetIds) {
|
||||||
|
await applyAttackToTarget(action, prop, attack, targetIds, result, userInput);
|
||||||
|
await applyAfterTriggers(action, prop, [target], userInput);
|
||||||
|
await applyChildren(action, prop, [target], userInput);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await applyAttackWithoutTarget(action, prop, attack, result, userInput);
|
||||||
|
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||||
|
await applyChildren(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||||
|
await applyChildren(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
if (prop.actionType === 'event' && prop.variableName) {
|
||||||
|
resetProperties(action, prop, result, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish
|
||||||
|
return await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyAttackToTarget(action, prop, attack, target, taskResult: TaskResult, userInput) {
|
||||||
|
taskResult.pushScope = {
|
||||||
|
'~attackHit': {},
|
||||||
|
'~attackMiss': {},
|
||||||
|
'~criticalHit': {},
|
||||||
|
'~criticalMiss': {},
|
||||||
|
'~attackRoll': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
await recalculateCalculation(attack, action, 'reduce');
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
const contents: LogContent[] = [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
resultPrefix,
|
||||||
|
result,
|
||||||
|
criticalHit,
|
||||||
|
criticalMiss,
|
||||||
|
} = await rollAttack(attack, scope, taskResult.pushScope);
|
||||||
|
|
||||||
|
if (target.variables.armor) {
|
||||||
|
const armor = target.variables.armor.value;
|
||||||
|
|
||||||
|
let name = criticalHit ? 'Critical Hit!' :
|
||||||
|
criticalMiss ? 'Critical Miss!' :
|
||||||
|
result > armor ? 'Hit!' : 'Miss!';
|
||||||
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
|
name += ' (Advantage)';
|
||||||
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
|
name += ' (Disadvantage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.push({
|
||||||
|
name,
|
||||||
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (criticalMiss || result < armor) {
|
||||||
|
scope['~attackMiss'] = { value: true };
|
||||||
|
} else {
|
||||||
|
scope['~attackHit'] = { value: true };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contents.push({
|
||||||
|
name: 'Error',
|
||||||
|
value: 'Target has no `armor`',
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}, {
|
||||||
|
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||||
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (contents.length) {
|
||||||
|
taskResult.mutations.push({
|
||||||
|
contents,
|
||||||
|
targetIds: [target],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyAttackWithoutTarget(action, prop, attack, taskResult: TaskResult, userInput) {
|
||||||
|
taskResult.pushScope = {
|
||||||
|
'~attackHit': {},
|
||||||
|
'~attackMiss': {},
|
||||||
|
'~criticalHit': {},
|
||||||
|
'~criticalMiss': {},
|
||||||
|
'~attackRoll': {},
|
||||||
|
}
|
||||||
|
await recalculateCalculation(attack, action, 'reduce');
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
const {
|
||||||
|
resultPrefix,
|
||||||
|
result,
|
||||||
|
criticalHit,
|
||||||
|
criticalMiss,
|
||||||
|
} = await rollAttack(attack, scope, taskResult.pushScope);
|
||||||
|
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||||
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
|
name += ' (Advantage)';
|
||||||
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
|
name += ' (Disadvantage)';
|
||||||
|
}
|
||||||
|
if (!criticalMiss) {
|
||||||
|
scope['~attackHit'] = { value: true }
|
||||||
|
}
|
||||||
|
if (!criticalHit) {
|
||||||
|
scope['~attackMiss'] = { value: true };
|
||||||
|
}
|
||||||
|
taskResult.mutations.push({
|
||||||
|
contents: [{
|
||||||
|
name,
|
||||||
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}],
|
||||||
|
targetIds: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollAttack(attack, scope, resultPushScope) {
|
||||||
|
const rollModifierText = numberToSignedString(attack.value, true);
|
||||||
|
let value, resultPrefix;
|
||||||
|
if (scope['~attackAdvantage']?.value === 1) {
|
||||||
|
const [a, b] = await rollDice(2, 20);
|
||||||
|
if (a >= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
|
}
|
||||||
|
} else if (scope['~attackAdvantage']?.value === -1) {
|
||||||
|
const [a, b] = await rollDice(2, 20);
|
||||||
|
if (a <= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = await rollDice(1, 20)[0];
|
||||||
|
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||||
|
}
|
||||||
|
resultPushScope['~attackDiceRoll'] = { value };
|
||||||
|
const result = value + attack.value;
|
||||||
|
resultPushScope['~attackRoll'] = { value: result };
|
||||||
|
const { criticalHit, criticalMiss } = applyCrits(value, scope, resultPushScope);
|
||||||
|
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCrits(value, scope, resultPushScope) {
|
||||||
|
let scopeCrit = scope['~criticalHitTarget']?.value;
|
||||||
|
if (scopeCrit?.parseType === 'constant') {
|
||||||
|
scopeCrit = scopeCrit.value;
|
||||||
|
}
|
||||||
|
const criticalHitTarget = scopeCrit || 20;
|
||||||
|
const criticalHit = value >= criticalHitTarget;
|
||||||
|
let criticalMiss;
|
||||||
|
if (criticalHit) {
|
||||||
|
resultPushScope['~criticalHit'] = { value: true };
|
||||||
|
} else {
|
||||||
|
criticalMiss = value === 1;
|
||||||
|
if (criticalMiss) {
|
||||||
|
resultPushScope['~criticalMiss'] = { value: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { criticalHit, criticalMiss };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetProperties(action: EngineAction, prop: any, result: TaskResult, userInput) {
|
||||||
|
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,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
|
||||||
|
import { PropTask } 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 { getSingleProperty, getVariables } from '/imports/api/engine/loadCreatures';
|
||||||
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
|
|
||||||
|
export default async function applyAdjustmentProperty(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
const damageTargetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
||||||
|
|
||||||
|
if (damageTargetIds.length > 1) {
|
||||||
|
return await applyTaskToEachTarget(action, task, damageTargetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the operation and value and push the damage hooks to the queue
|
||||||
|
if (!prop.amount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the amount
|
||||||
|
await recalculateCalculation(prop.amount, action, 'reduce');
|
||||||
|
const value = +prop.amount.value;
|
||||||
|
if (!isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!damageTargetIds?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damageTargetIds.length !== 1) {
|
||||||
|
throw 'At this step, only a single target is supported'
|
||||||
|
}
|
||||||
|
const targetId = damageTargetIds[0];
|
||||||
|
const statId = getVariables(targetId)?.[prop.stat]?._propId;
|
||||||
|
const stat = statId && getSingleProperty(targetId, statId);
|
||||||
|
if (!stat?.type) {
|
||||||
|
result.appendLog({
|
||||||
|
name: 'Error',
|
||||||
|
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}, damageTargetIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTask(action, {
|
||||||
|
prop,
|
||||||
|
targetIds: damageTargetIds,
|
||||||
|
subtaskFn: 'damageProp',
|
||||||
|
params: {
|
||||||
|
title: getPropertyTitle(prop),
|
||||||
|
operation: prop.operation,
|
||||||
|
value,
|
||||||
|
targetProp: stat,
|
||||||
|
},
|
||||||
|
}, userInput);
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, damageTargetIds, userInput);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyAfterPropTasksForSingleChild, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { getPropertyChildren } from '/imports/api/engine/loadCreatures';
|
||||||
|
import rollDice from '/imports/parser/rollDice';
|
||||||
|
|
||||||
|
export default async function applyBranchProperty(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
const targets = task.targetIds;
|
||||||
|
|
||||||
|
switch (prop.branchType) {
|
||||||
|
case 'if': {
|
||||||
|
await recalculateCalculation(prop.condition, action, 'reduce');
|
||||||
|
if (prop.condition?.value) {
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'index': {
|
||||||
|
const children = await getPropertyChildren(action.creatureId, prop);
|
||||||
|
if (!children.length) {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
await 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);
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
let index = Math.floor(prop.condition?.value);
|
||||||
|
if (index < 1) index = 1;
|
||||||
|
if (index > children.length) index = children.length;
|
||||||
|
const child = children[index - 1];
|
||||||
|
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
|
||||||
|
}
|
||||||
|
case 'hit': {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
if (scope['~attackHit']?.value) {
|
||||||
|
if (!targets.length && !prop.silent) {
|
||||||
|
result.appendLog({
|
||||||
|
value: '**On hit**'
|
||||||
|
}, targets);
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'miss': {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
if (scope['~attackMiss']?.value) {
|
||||||
|
if (!targets.length && !prop.silent) {
|
||||||
|
result.appendLog({
|
||||||
|
value: '**On miss**'
|
||||||
|
}, targets);
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'failedSave': {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
if (scope['~saveFailed']?.value) {
|
||||||
|
if (!targets.length && !prop.silent) {
|
||||||
|
result.appendLog({
|
||||||
|
value: '**On failed save**'
|
||||||
|
}, targets);
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'successfulSave': {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
if (scope['~saveSucceeded']?.value) {
|
||||||
|
if (!targets.length && !prop.silent) {
|
||||||
|
result.appendLog({
|
||||||
|
value: '**On save**'
|
||||||
|
}, targets);
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'random': {
|
||||||
|
const children = await getPropertyChildren(action.creatureId, prop);
|
||||||
|
if (children.length) {
|
||||||
|
const index = rollDice(1, children.length)[0];
|
||||||
|
const child = children[index - 1];
|
||||||
|
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
|
||||||
|
} else {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'eachTarget':
|
||||||
|
if (targets.length > 1) {
|
||||||
|
return applyTaskToEachTarget(action, task, targets, userInput);
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||||
|
case 'choice': {
|
||||||
|
let index;
|
||||||
|
if (action._isSimulation) {
|
||||||
|
index = await userInput(prop);
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
throw 'Reading stored user input not implemented'
|
||||||
|
}
|
||||||
|
const children = await getPropertyChildren(action.creatureId, prop);
|
||||||
|
if (!children.length) {
|
||||||
|
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||||
|
}
|
||||||
|
if (!isFinite(index) || index < 0) index = 0;
|
||||||
|
if (index > children.length - 1) index = children.length - 1;
|
||||||
|
const child = children[index];
|
||||||
|
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
|
||||||
|
import resolve, { toString, map } from '/imports/parser/resolve';
|
||||||
|
import computedSchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||||
|
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey';
|
||||||
|
import accessor from '/imports/parser/parseTree/accessor';
|
||||||
|
import TaskResult, { Mutation } from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53';
|
||||||
|
import { renewDocIds } from '/imports/api/parenting/parentingFunctions';
|
||||||
|
import { cleanProps } from '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary';
|
||||||
|
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
|
||||||
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
|
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX';
|
||||||
|
import { applyAfterTasksSkipChildren } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
|
||||||
|
export default async function applyBuffProperty(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
) {
|
||||||
|
const prop = EJSON.clone(task.prop);
|
||||||
|
const targetIds = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
||||||
|
|
||||||
|
// Get the buff and its descendants
|
||||||
|
const propList = [
|
||||||
|
EJSON.clone(prop),
|
||||||
|
...getPropertyDescendants(action.creatureId, prop._id),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Crystalize the variables
|
||||||
|
if (!prop.skipCrystalization) {
|
||||||
|
await crystalizeVariables(action, propList, task, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIds.forEach(target => {
|
||||||
|
// Create a per-target mutation
|
||||||
|
const mutation: Mutation = { targetIds: [target], contents: [] };
|
||||||
|
|
||||||
|
// Create a per-target copy of the propList
|
||||||
|
let targetPropList = EJSON.clone(propList);
|
||||||
|
|
||||||
|
// Give the properties new IDs as descendants of the target
|
||||||
|
renewDocIds({
|
||||||
|
docArray: targetPropList,
|
||||||
|
idMap: {
|
||||||
|
[prop.parentId]: null,
|
||||||
|
[prop.root.id]: target,
|
||||||
|
},
|
||||||
|
collectionMap: { [prop.root.collection]: 'creatures' }
|
||||||
|
});
|
||||||
|
|
||||||
|
//Log the buff
|
||||||
|
let logValue = prop.description?.value
|
||||||
|
if (prop.description?.text) {
|
||||||
|
recalculateInlineCalculations(prop.description, action);
|
||||||
|
logValue = prop.description?.value;
|
||||||
|
}
|
||||||
|
result.appendLog({
|
||||||
|
name: getPropertyTitle(prop),
|
||||||
|
value: logValue
|
||||||
|
}, [target]);
|
||||||
|
|
||||||
|
// remove all the computed fields
|
||||||
|
targetPropList = cleanProps(targetPropList);
|
||||||
|
|
||||||
|
// Insert the props in the mutation
|
||||||
|
mutation.inserts = targetPropList;
|
||||||
|
|
||||||
|
// Add the mutation to the results
|
||||||
|
result.mutations.push(mutation);
|
||||||
|
});
|
||||||
|
applyAfterTasksSkipChildren(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all variables with their resolved values
|
||||||
|
* except variables of the form `~target.thing.total` become `thing.total`
|
||||||
|
*/
|
||||||
|
async function crystalizeVariables(
|
||||||
|
action: EngineAction, propList: any[], task: PropTask, result: TaskResult
|
||||||
|
) {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
propList.forEach(prop => {
|
||||||
|
if (prop._skipCrystalize) {
|
||||||
|
delete prop._skipCrystalize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Iterate through all the calculations and crystalize them
|
||||||
|
computedSchemas[prop.type].computedFields().forEach(calcKey => {
|
||||||
|
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||||
|
const calcObj = get(prop, key);
|
||||||
|
if (!calcObj?.parseNode) return;
|
||||||
|
calcObj.parseNode = map(calcObj.parseNode, node => {
|
||||||
|
// Skip nodes that aren't symbols or accessors
|
||||||
|
if (
|
||||||
|
node.parseType !== 'accessor'
|
||||||
|
) return node;
|
||||||
|
// Handle variables
|
||||||
|
if (node.parseType === 'accessor' && node.name === '~target') {
|
||||||
|
// strip ~target
|
||||||
|
if (node.path?.length > 0) {
|
||||||
|
const name = node.path.shift();
|
||||||
|
return accessor.create({
|
||||||
|
name,
|
||||||
|
path: node.path?.length ? node.path : undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Can't strip if there isn't anything in the path after ~target
|
||||||
|
result.appendLog({
|
||||||
|
name: 'Error',
|
||||||
|
value: 'Variable `~target` should not be used without a property: ~target.property',
|
||||||
|
}, task.targetIds);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
} else {
|
||||||
|
// Resolve all other variables
|
||||||
|
const { result, context } = resolve('reduce', node, scope);
|
||||||
|
context.errors?.forEach(error => {
|
||||||
|
result.appendLog({
|
||||||
|
name: 'Error',
|
||||||
|
value: error,
|
||||||
|
}, task.targetIds);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calcObj.calculation = toString(calcObj.parseNode);
|
||||||
|
calcObj.hash = cyrb53(calcObj.calculation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// For each key in the schema
|
||||||
|
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
|
||||||
|
// That ends in .inlineCalculations
|
||||||
|
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||||
|
const inlineCalcObj = get(prop, key);
|
||||||
|
if (!inlineCalcObj) return;
|
||||||
|
|
||||||
|
// If there is no text, skip
|
||||||
|
if (!inlineCalcObj.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace all the existing calculations
|
||||||
|
let index = -1;
|
||||||
|
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
|
||||||
|
index += 1;
|
||||||
|
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the value to the uncomputed string
|
||||||
|
inlineCalcObj.value = inlineCalcObj.text;
|
||||||
|
|
||||||
|
// Write a new hash
|
||||||
|
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||||
|
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||||
|
// Skip if nothing changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inlineCalcObj.hash = inlineCalcHash;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,39 +1,40 @@
|
|||||||
import { findLast, difference, intersection, filter } from 'lodash';
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
import applyProperty from '../applyProperty';
|
import TaskResult from 'imports/api/engine/action/tasks/TaskResult';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
import { getPropertyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures';
|
import { findLast, filter, difference, intersection } from 'lodash';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
import { getPropertiesOfType, getPropertyAncestors } from '/imports/api/engine/loadCreatures';
|
||||||
import { softRemove } from '/imports/api/parenting/softRemove';
|
|
||||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
|
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
|
||||||
export default function applyBuffRemover(node, actionContext) {
|
export default function applyBuffRemoverProperty(
|
||||||
// Apply triggers
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
) {
|
||||||
|
const prop = task.prop;
|
||||||
|
|
||||||
const prop = node.doc
|
|
||||||
|
|
||||||
// Log Name
|
|
||||||
if (prop.name && !prop.silent) {
|
if (prop.name && !prop.silent) {
|
||||||
actionContext.addLog({ name: prop.name });
|
// Log Name
|
||||||
|
result.appendLog({
|
||||||
|
name: getPropertyTitle(prop),
|
||||||
|
}, task.targetIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove buffs
|
// Remove buffs
|
||||||
if (prop.targetParentBuff) {
|
if (prop.targetParentBuff) {
|
||||||
// Remove nearest ancestor buff
|
// Remove nearest ancestor buff
|
||||||
const ancestors = getPropertyAncestors(actionContext.creature._id, prop._id);
|
const ancestors = getPropertyAncestors(action.creatureId, prop._id);
|
||||||
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
|
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
|
||||||
if (!nearestBuff) {
|
if (!nearestBuff) {
|
||||||
actionContext.addLog({
|
result.appendLog({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: 'Buff remover does not have a parent buff to remove',
|
value: 'Buff remover does not have a parent buff to remove',
|
||||||
});
|
}, task.targetIds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeBuff(nearestBuff, actionContext, prop);
|
removeBuff(nearestBuff, prop, result);
|
||||||
} else {
|
} else {
|
||||||
// Get all the buffs targeted by tags
|
// Get all the buffs targeted by tags
|
||||||
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
|
const allBuffs = getPropertiesOfType(action.creatureId, 'buff');
|
||||||
const targetedBuffs = filter(allBuffs, buff => {
|
const targetedBuffs = filter(allBuffs, buff => {
|
||||||
if (buff.inactive) return false;
|
if (buff.inactive) return false;
|
||||||
if (buffRemoverMatchTags(prop, buff)) return true;
|
if (buffRemoverMatchTags(prop, buff)) return true;
|
||||||
@@ -42,7 +43,7 @@ export default function applyBuffRemover(node, actionContext) {
|
|||||||
if (prop.removeAll) {
|
if (prop.removeAll) {
|
||||||
// Remove all matching buffs
|
// Remove all matching buffs
|
||||||
targetedBuffs.forEach(buff => {
|
targetedBuffs.forEach(buff => {
|
||||||
removeBuff(buff, actionContext, prop);
|
removeBuff(buff, prop, result);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Sort in reverse order
|
// Sort in reverse order
|
||||||
@@ -50,19 +51,23 @@ export default function applyBuffRemover(node, actionContext) {
|
|||||||
// Remove the one with the highest order
|
// Remove the one with the highest order
|
||||||
const buff = targetedBuffs[0];
|
const buff = targetedBuffs[0];
|
||||||
if (buff) {
|
if (buff) {
|
||||||
removeBuff(buff, actionContext, prop);
|
removeBuff(buff, prop, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyChildren(node, actionContext);
|
applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBuff(buff, actionContext, prop) {
|
function removeBuff(buff: any, prop, result: TaskResult) {
|
||||||
if (!prop.silent) actionContext.addLog({
|
result.mutations.push({
|
||||||
|
targetIds: result.targetIds,
|
||||||
|
removals: [{ propId: buff._id }],
|
||||||
|
contents: [{
|
||||||
name: 'Removed',
|
name: 'Removed',
|
||||||
value: `${buff.name || 'Buff'}`
|
value: `${buff.name || 'Buff'}`,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
softRemove({ _id: buff._id, collection: CreatureProperties });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buffRemoverMatchTags(buffRemover, prop) {
|
function buffRemoverMatchTags(buffRemover, prop) {
|
||||||
@@ -1,16 +1,4 @@
|
|||||||
import { some, intersection, difference, remove, includes } from 'lodash';
|
// TODO
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs';
|
|
||||||
import resolve, { Context, toString } from '/imports/parser/resolve';
|
|
||||||
import logErrors from './shared/logErrors';
|
|
||||||
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'
|
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
|
|
||||||
import {
|
|
||||||
getPropertiesOfType
|
|
||||||
} from '/imports/api/engine/loadCreatures';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
|
|
||||||
import applySavingThrow from '/imports/api/engine/actions/applyPropertyByType/applySavingThrow';
|
|
||||||
|
|
||||||
export default function applyDamage(node, actionContext) {
|
export default function applyDamage(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
|
||||||
|
|
||||||
|
export default async function applyFolderProperty(
|
||||||
|
task: PropTask, action: EngineAction, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult, { LogContent } from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
|
||||||
|
export default async function applyNoteProperty(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
let contents: LogContent[] | undefined = undefined;
|
||||||
|
const logContent: LogContent = {};
|
||||||
|
if (prop.name) logContent.name = prop.name;
|
||||||
|
if (prop.summary?.text) {
|
||||||
|
await recalculateInlineCalculations(prop.summary, action);
|
||||||
|
logContent.value = prop.summary.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logContent.name || logContent.value) {
|
||||||
|
contents = [logContent];
|
||||||
|
}
|
||||||
|
// Log description
|
||||||
|
if (prop.description?.text) {
|
||||||
|
await recalculateInlineCalculations(prop.description, action);
|
||||||
|
if (!contents) contents = [];
|
||||||
|
contents.push({ value: prop.description.value });
|
||||||
|
}
|
||||||
|
if (contents) {
|
||||||
|
result.mutations.push({
|
||||||
|
contents,
|
||||||
|
targetIds: task.targetIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { rollAndReduceCalculation } from '/imports/api/engine/action/functions/recalculateCalculation';
|
||||||
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { toString } from '/imports/parser/resolve';
|
||||||
|
|
||||||
|
export default async function roll(
|
||||||
|
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
// If there isn't a calculation, just apply the children instead
|
||||||
|
if (!prop.roll?.calculation) {
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logValue: string[] = [];
|
||||||
|
|
||||||
|
// roll the dice only and store that string
|
||||||
|
const {
|
||||||
|
rolled, reduced, errors
|
||||||
|
} = await rollAndReduceCalculation(prop.roll, action);
|
||||||
|
|
||||||
|
if (rolled.parseType !== 'constant') {
|
||||||
|
logValue.push(toString(rolled));
|
||||||
|
}
|
||||||
|
errors?.forEach(error => {
|
||||||
|
result.appendLog({ name: 'Error', value: error.message }, task.targetIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the result
|
||||||
|
if (reduced.parseType === 'constant') {
|
||||||
|
prop.roll.value = reduced.value;
|
||||||
|
} else if (reduced.parseType === 'error') {
|
||||||
|
prop.roll.value = null;
|
||||||
|
} else {
|
||||||
|
prop.roll.value = toString(reduced);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't end up with a constant or a number of finite value, give up
|
||||||
|
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
|
}
|
||||||
|
const value = reduced.value;
|
||||||
|
|
||||||
|
result.scope[prop.variableName] = { value };
|
||||||
|
logValue.push(`**${value}**`);
|
||||||
|
|
||||||
|
result.appendLog({
|
||||||
|
name: prop.name,
|
||||||
|
value: logValue.join('\n'),
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}, task.targetIds);
|
||||||
|
|
||||||
|
// Apply children
|
||||||
|
return applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import rollDice from '/imports/parser/rollDice';
|
// TODO
|
||||||
import recalculateCalculation from './shared/recalculateCalculation';
|
|
||||||
import applyProperty from '../applyProperty';
|
|
||||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck';
|
|
||||||
|
|
||||||
export default function applySavingThrow(node, actionContext) {
|
export default function applySavingThrow(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import recalculateCalculation from './shared/recalculateCalculation';
|
// TODO
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
|
|
||||||
export default function applyToggle(node, actionContext) {
|
export default function applyToggle(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
26
app/imports/api/engine/action/applyProperties/index.ts
Normal file
26
app/imports/api/engine/action/applyProperties/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import action from './applyActionProperty';
|
||||||
|
import adjustment from './applyAdjustmentProperty';
|
||||||
|
import branch from './applyBranchProperty';
|
||||||
|
import buff from './applyBuffProperty';
|
||||||
|
import buffRemover from './applyBuffRemoverProperty';
|
||||||
|
import damage from './applyDamageProperty';
|
||||||
|
import folder from './applyFolderProperty';
|
||||||
|
import note from './applyNoteProperty';
|
||||||
|
import roll from './applyRollProperty';
|
||||||
|
import savingThrow from './applySavingThrowProperty';
|
||||||
|
import toggle from './applyToggleProperty';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
action,
|
||||||
|
adjustment,
|
||||||
|
branch,
|
||||||
|
buff,
|
||||||
|
buffRemover,
|
||||||
|
damage,
|
||||||
|
folder,
|
||||||
|
note,
|
||||||
|
roll,
|
||||||
|
savingThrow,
|
||||||
|
propertySlot: folder,
|
||||||
|
toggle,
|
||||||
|
}
|
||||||
42
app/imports/api/engine/action/functions/applyAction.ts
Normal file
42
app/imports/api/engine/action/functions/applyAction.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
|
import applyTask from '/imports/api/engine/action/tasks/applyTask'
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
// TODO create a function to get the effective value of a property,
|
||||||
|
// simulating all the result updates in the action so far
|
||||||
|
|
||||||
|
// Apply an action
|
||||||
|
// This is run once as a simulation on the client awaiting all the various inputs or step through
|
||||||
|
// clicks from the user, then it is run as part of the runAction method, where it is expected to
|
||||||
|
// complete instantly on the client, and sent to the server as a method call
|
||||||
|
export async function applyAction(action: EngineAction, userInput?: any[] | Function, options?: {
|
||||||
|
simulate?: boolean, stepThrough?: boolean
|
||||||
|
}) {
|
||||||
|
const { simulate, stepThrough } = options || {};
|
||||||
|
if (!simulate && stepThrough) throw 'Cannot step through unless simulating';
|
||||||
|
if (simulate && typeof userInput !== 'function') throw 'Must provide a function to get user input when simulating';
|
||||||
|
|
||||||
|
action._stepThrough = stepThrough;
|
||||||
|
action._isSimulation = simulate;
|
||||||
|
action.taskCount = 0;
|
||||||
|
const prop = await getSingleProperty(action.creatureId, action.rootPropId);
|
||||||
|
if (!prop) throw new Meteor.Error('Not found', 'Root action property could not be found');
|
||||||
|
await applyTask(action, {
|
||||||
|
prop,
|
||||||
|
targetIds: action.targetIds || [],
|
||||||
|
}, userInput);
|
||||||
|
return { action, userInput };
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeChangedAction(original: EngineAction, changed: EngineAction) {
|
||||||
|
const $set = {};
|
||||||
|
for (const key of ActionSchema.objectKeys()) {
|
||||||
|
if (!EJSON.equals(original[key], changed[key])) {
|
||||||
|
$set[key] = changed[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isEmpty($set) && original._id) {
|
||||||
|
return Actions.updateAsync(original._id, { $set });
|
||||||
|
}
|
||||||
|
}
|
||||||
141
app/imports/api/engine/action/functions/applyTaskGroups.ts
Normal file
141
app/imports/api/engine/action/functions/applyTaskGroups.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
import { getPropertyChildren, getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import applyTask from '../tasks/applyTask';
|
||||||
|
import { PropTask } from '../tasks/Task';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the child tasks of a given property
|
||||||
|
* @param action
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyChildren(
|
||||||
|
action: EngineAction, prop, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
const children = await getPropertyChildren(action.creatureId, prop);
|
||||||
|
// Push the child tasks and related triggers to the stack
|
||||||
|
for (const childProp of children) {
|
||||||
|
await applyTask(action, { prop: childProp, targetIds }, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the afterChildren triggers for a given property
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyAfterChildrenTriggers(
|
||||||
|
action: EngineAction, prop, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
if (!prop.triggerIds?.afterChildren) return;
|
||||||
|
for (const triggerId of prop.triggerIds.afterChildren) {
|
||||||
|
const trigger = await getSingleProperty(action.creatureId, triggerId);
|
||||||
|
if (!trigger) continue;
|
||||||
|
await applyTask(action, { prop: trigger, targetIds }, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyAfterTriggers(
|
||||||
|
action: EngineAction, prop, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
if (!prop.triggerIds?.after) return;
|
||||||
|
for (const triggerId of prop.triggerIds.after) {
|
||||||
|
const trigger = await getSingleProperty(action.creatureId, triggerId);
|
||||||
|
if (!trigger) continue;
|
||||||
|
await applyTask(action, { prop: trigger, targetIds }, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the following:
|
||||||
|
* After triggers
|
||||||
|
* Children of the prop
|
||||||
|
* After-children triggers
|
||||||
|
* @param action
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyDefaultAfterPropTasks(
|
||||||
|
action: EngineAction, prop, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||||
|
await applyChildren(action, prop, targetIds, userInput);
|
||||||
|
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the following:
|
||||||
|
* After triggers
|
||||||
|
* After-children triggers
|
||||||
|
* @param action
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyAfterTasksSkipChildren(
|
||||||
|
action: EngineAction, prop, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||||
|
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of tasks containing the following:
|
||||||
|
* After triggers
|
||||||
|
* After-children triggers
|
||||||
|
* @param action
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyAfterPropTasksForSingleChild(
|
||||||
|
action: EngineAction, prop, childProp, targetIds: string[], userInput
|
||||||
|
) {
|
||||||
|
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||||
|
await applyTask(action, { prop: childProp, targetIds }, userInput);
|
||||||
|
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the trigger tasks for a given trigger path
|
||||||
|
* @param action
|
||||||
|
* @param prop
|
||||||
|
* @param targetIds
|
||||||
|
* @param triggerPath
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function applyTriggers(
|
||||||
|
action: EngineAction, prop, targetIds: string[], triggerPath: string, userInput
|
||||||
|
) {
|
||||||
|
const triggerIds = get(prop?.triggers, triggerPath);
|
||||||
|
if (!triggerIds) return;
|
||||||
|
for (const triggerId of triggerIds) {
|
||||||
|
const trigger = await getSingleProperty(action.creatureId, triggerId);
|
||||||
|
if (!trigger) continue;
|
||||||
|
await applyTask(action, { prop: trigger, targetIds }, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a task over its targets, incrementing task step by 1
|
||||||
|
* @param task
|
||||||
|
* @param targetIds
|
||||||
|
* @returns Copies of the task, but with a single target each
|
||||||
|
*/
|
||||||
|
export async function applyTaskToEachTarget(
|
||||||
|
action: EngineAction, task: PropTask, targetIds: string[] = task.targetIds, userInput
|
||||||
|
) {
|
||||||
|
if (targetIds.length <= 1) throw 'Must have multiple targets to split a task';
|
||||||
|
// If there are targets, apply a new task to each target
|
||||||
|
for (const targetId of targetIds) {
|
||||||
|
await applyTask(action, {
|
||||||
|
...task,
|
||||||
|
targetIds: [targetId]
|
||||||
|
}, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { getVariables } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
// Combine all the action results into the scope at present
|
||||||
|
export async function getEffectiveActionScope(action: EngineAction) {
|
||||||
|
const scope = await getVariables(action.creatureId);
|
||||||
|
delete scope._id;
|
||||||
|
delete scope._creatureId;
|
||||||
|
// Combine the applied results
|
||||||
|
for (const result of action.results) {
|
||||||
|
// Pop keys that are not longer used by a busy property
|
||||||
|
if (result.popScope) {
|
||||||
|
for (const key in result.popScope) {
|
||||||
|
if (!result.popScope[key]) continue;
|
||||||
|
// If the popped keys have previous results, return to them
|
||||||
|
if (scope[key]?.previous) {
|
||||||
|
scope[key] = scope[key]?.previous;
|
||||||
|
} else {
|
||||||
|
// just remove the busy flag, the prop has been consumed
|
||||||
|
delete scope[key]?._busy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For keys that have just started being used by a busy property
|
||||||
|
if (result.pushScope) {
|
||||||
|
for (const key in result.pushScope) {
|
||||||
|
// If the pushed keys already exist and are busy,
|
||||||
|
// save the previous results and overwrite
|
||||||
|
// the key
|
||||||
|
if (scope[key]?._busy) {
|
||||||
|
scope[key] = {
|
||||||
|
...result.pushScope[key],
|
||||||
|
previous: scope[key],
|
||||||
|
_busy: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
scope[key] = {
|
||||||
|
...result.pushScope[key],
|
||||||
|
_busy: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assign other scope changes without bashing the scope[key].previous field
|
||||||
|
if (result.scope) {
|
||||||
|
for (const key in result.scope) {
|
||||||
|
if (scope[key]?.previous || scope[key]?._busy) {
|
||||||
|
scope[key] = {
|
||||||
|
...result.scope[key],
|
||||||
|
previous: scope[key].previous,
|
||||||
|
_busy: scope[key]._busy,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
scope[key] = result.scope[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import logErrors from './logErrors';
|
|
||||||
import { Context, toPrimitiveOrString } from '/imports/parser/resolve';
|
import { Context, toPrimitiveOrString } from '/imports/parser/resolve';
|
||||||
import {
|
import {
|
||||||
aggregateCalculationEffects,
|
aggregateCalculationEffects,
|
||||||
@@ -7,12 +6,14 @@ import {
|
|||||||
} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
|
} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
|
||||||
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
import resolve from '/imports/parser/resolve';
|
import resolve from '/imports/parser/resolve';
|
||||||
import { getEffectiveActionScope } from '/imports/api/engine/actions/ActionEngine';
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
|
||||||
// TODO move this whole file to Actions.ts
|
// TODO Redo the work of
|
||||||
// Redo the work of imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js
|
// imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js
|
||||||
// But in the action scope
|
// But in the action scope
|
||||||
export default async function recalculateCalculation(calcObj, action, parseLevel = 'reduce', context, scope) {
|
export default async function recalculateCalculation(
|
||||||
|
calcObj, action, parseLevel = 'reduce', context, scope
|
||||||
|
) {
|
||||||
if (!calcObj?.parseNode) return;
|
if (!calcObj?.parseNode) return;
|
||||||
calcObj._parseLevel = parseLevel;
|
calcObj._parseLevel = parseLevel;
|
||||||
if (!scope) {
|
if (!scope) {
|
||||||
75
app/imports/api/engine/action/functions/spendResources.ts
Normal file
75
app/imports/api/engine/action/functions/spendResources.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import applyTask from '/imports/api/engine/action/tasks/applyTask';
|
||||||
|
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
export default async function spendResources(
|
||||||
|
action: EngineAction, prop, targetIds: string[], result: TaskResult, userInput
|
||||||
|
) {
|
||||||
|
// Use uses
|
||||||
|
if (prop.usesLeft) {
|
||||||
|
result.mutations.push({
|
||||||
|
targetIds,
|
||||||
|
updates: [{
|
||||||
|
propId: prop._id,
|
||||||
|
inc: { usesUsed: 1, usesLeft: -1 },
|
||||||
|
type: prop.type,
|
||||||
|
}],
|
||||||
|
contents: [{
|
||||||
|
name: 'Uses left',
|
||||||
|
value: `${prop.usesLeft - 1}`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all the resources consumed and damage them
|
||||||
|
if (prop.resources?.attributesConsumed?.length) {
|
||||||
|
for (const att of prop.resources.attributesConsumed) {
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
const statToDamage = getFromScope(att.variableName, scope);
|
||||||
|
await recalculateCalculation(att.quantity, action, 'reduce');
|
||||||
|
await applyTask(action, {
|
||||||
|
prop,
|
||||||
|
targetIds: [action.creatureId],
|
||||||
|
subtaskFn: 'damageProp',
|
||||||
|
params: {
|
||||||
|
operation: 'increment',
|
||||||
|
value: +att.quantity?.value || 0,
|
||||||
|
targetProp: statToDamage,
|
||||||
|
},
|
||||||
|
}, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all the items consumed and consume them
|
||||||
|
if (prop.resources?.itemsConsumed?.length) {
|
||||||
|
for (const itemConsumed of prop.resources.itemsConsumed) {
|
||||||
|
await recalculateCalculation(itemConsumed.quantity, action, 'reduce');
|
||||||
|
if (!itemConsumed.itemId) {
|
||||||
|
throw 'No ammo was selected';
|
||||||
|
}
|
||||||
|
const item = getSingleProperty(action.creatureId, itemConsumed.itemId);
|
||||||
|
if (!item || item.root.id !== prop.root.id) {
|
||||||
|
throw 'The prop\'s ammo was not found on the creature';
|
||||||
|
}
|
||||||
|
const quantity = +itemConsumed?.quantity?.value;
|
||||||
|
if (
|
||||||
|
!quantity ||
|
||||||
|
!isFinite(quantity)
|
||||||
|
) continue;
|
||||||
|
await applyTask(action, {
|
||||||
|
prop,
|
||||||
|
targetIds,
|
||||||
|
subtaskFn: 'consumeItemAsAmmo',
|
||||||
|
params: {
|
||||||
|
value: quantity,
|
||||||
|
item,
|
||||||
|
},
|
||||||
|
}, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met
|
|||||||
import { doActionWork } from '/imports/api/engine/actions/doAction';
|
import { doActionWork } from '/imports/api/engine/actions/doAction';
|
||||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
||||||
|
|
||||||
|
// TODO Migrate this to the new action engine
|
||||||
|
|
||||||
const doAction = new ValidatedMethod({
|
const doAction = new ValidatedMethod({
|
||||||
name: 'creatureProperties.doCastSpell',
|
name: 'creatureProperties.doCastSpell',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -7,9 +7,11 @@ import rollDice from '/imports/parser/rollDice';
|
|||||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
|
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
|
||||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
||||||
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
import recalculateCalculation from '../../actions/applyPropertyByType/shared/recalculateCalculation';
|
||||||
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
// TODO Migrate this to the new action engine
|
||||||
|
|
||||||
const doCheck = new ValidatedMethod({
|
const doCheck = new ValidatedMethod({
|
||||||
name: 'creatureProperties.doCheck',
|
name: 'creatureProperties.doCheck',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
21
app/imports/api/engine/action/methods/insertAction.ts
Normal file
21
app/imports/api/engine/action/methods/insertAction.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import EngineActions, { EngineAction, ActionSchema } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||||
|
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
export const insertAction: ValidatedMethod = new ValidatedMethod({
|
||||||
|
name: 'actions.insertAction',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
action: ActionSchema
|
||||||
|
}).validator({ clean: true }),
|
||||||
|
run: async function ({ action }: { action: EngineAction }) {
|
||||||
|
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||||
|
// First remove all other actions on this creature
|
||||||
|
// only do one action at a time, don't wait for this to finish
|
||||||
|
EngineActions.removeAsync({ creatureId: action.creatureId });
|
||||||
|
// Force a random id even if one was provided, we may use it later as the seed for PRNG
|
||||||
|
delete action._id;
|
||||||
|
return await EngineActions.insertAsync(action);
|
||||||
|
},
|
||||||
|
});
|
||||||
34
app/imports/api/engine/action/methods/runAction.ts
Normal file
34
app/imports/api/engine/action/methods/runAction.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions';
|
||||||
|
import { getCreature } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
export const runAction = new ValidatedMethod({
|
||||||
|
name: 'actions.runAction',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
action: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
userInput: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
stepThrough: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
}
|
||||||
|
}).validator(),
|
||||||
|
run: async function ({ actionId, userInput }: { actionId: string, userInput?: any }) {
|
||||||
|
const action = await EngineActions.findOneAsync(actionId);
|
||||||
|
if (!action) throw 'Action not found';
|
||||||
|
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||||
|
const originalAction = EJSON.clone(action);
|
||||||
|
applyAction(action, userInput);
|
||||||
|
// Persist changes to the action
|
||||||
|
const writePromise = writeChangedAction(originalAction, action);
|
||||||
|
return writePromise;
|
||||||
|
},
|
||||||
|
});
|
||||||
33
app/imports/api/engine/action/tasks/Task.ts
Normal file
33
app/imports/api/engine/action/tasks/Task.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
type Task = PropTask | DamagePropTask | ItemAsAmmoTask;
|
||||||
|
|
||||||
|
export default Task;
|
||||||
|
|
||||||
|
interface BaseTask {
|
||||||
|
prop: { [key: string]: any };
|
||||||
|
targetIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropTask extends BaseTask {
|
||||||
|
subtaskFn?: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DamagePropTask extends BaseTask {
|
||||||
|
subtaskFn: 'damageProp';
|
||||||
|
params: {
|
||||||
|
/**
|
||||||
|
* Use getPropertyTitle(prop) to set the title
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
operation: 'increment' | 'set';
|
||||||
|
value: number;
|
||||||
|
targetProp: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemAsAmmoTask extends BaseTask {
|
||||||
|
subtaskFn: 'consumeItemAsAmmo';
|
||||||
|
params: {
|
||||||
|
value: number;
|
||||||
|
item: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
73
app/imports/api/engine/action/tasks/TaskResult.ts
Normal file
73
app/imports/api/engine/action/tasks/TaskResult.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* The result of running a task containing all the changes that need to be made to the listed
|
||||||
|
* targets
|
||||||
|
* Each mutation may apply to a different subset of targets
|
||||||
|
*/
|
||||||
|
export default class TaskResult {
|
||||||
|
propId: string;
|
||||||
|
// The targets of the original task
|
||||||
|
targetIds: string[];
|
||||||
|
scope: any;
|
||||||
|
// Consume pushed changes from the local scope, every change pushed must be popped later
|
||||||
|
popScope?: any;
|
||||||
|
// Push changes to the scope if the same task intends to consume them in later steps
|
||||||
|
// These changes will be marked as _busy until they are consumed
|
||||||
|
// This allows a property to run in between steps of the same property type without
|
||||||
|
// bashing the variables used to maintain state between steps while still exposing
|
||||||
|
// those variables to triggers that need to change them
|
||||||
|
// If multiple properties use the same variable at once, the values used by outer
|
||||||
|
// properties can be found on variable.previous
|
||||||
|
pushScope?: any;
|
||||||
|
mutations: Mutation[];
|
||||||
|
constructor(propId: string, targetIds: string[]) {
|
||||||
|
this.propId = propId;
|
||||||
|
this.targetIds = targetIds;
|
||||||
|
this.mutations = [];
|
||||||
|
this.scope = {};
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
// Which creatures the mutation is applied to
|
||||||
|
// A mutation may apply to all, or a subset of, the result's targets and the acting creature
|
||||||
|
targetIds: string[];
|
||||||
|
// What changes in the database
|
||||||
|
updates?: Update[];
|
||||||
|
// What properties get added
|
||||||
|
// TODO make these properties a LibraryNode type
|
||||||
|
inserts?: any[];
|
||||||
|
// What properties get deleted
|
||||||
|
removals?: Removal[];
|
||||||
|
// Logged when this is applied
|
||||||
|
contents?: LogContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Update = {
|
||||||
|
propId: string;
|
||||||
|
type: string,
|
||||||
|
set?: any;
|
||||||
|
inc?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Removal = {
|
||||||
|
propId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogContent = {
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
inline?: boolean;
|
||||||
|
context?: any;
|
||||||
|
silenced?: boolean;
|
||||||
|
}
|
||||||
131
app/imports/api/engine/action/tasks/applyDamagePropTask.ts
Normal file
131
app/imports/api/engine/action/tasks/applyDamagePropTask.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import { DamagePropTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { applyTriggers } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
|
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||||
|
|
||||||
|
export default async function applyDamagePropTask(
|
||||||
|
task: DamagePropTask, action: EngineAction, result: TaskResult, userInput
|
||||||
|
): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
|
||||||
|
if (task.targetIds.length > 1) {
|
||||||
|
throw 'This subtask can only be called on a single target';
|
||||||
|
}
|
||||||
|
const targetId = task.targetIds[0];
|
||||||
|
|
||||||
|
let { value } = task.params;
|
||||||
|
const { title, operation } = task.params;
|
||||||
|
let targetProp = task.params.targetProp;
|
||||||
|
|
||||||
|
// Set the scope properties
|
||||||
|
result.pushScope = {};
|
||||||
|
if (prop.operation === 'increment') {
|
||||||
|
if (value >= 0) {
|
||||||
|
result.pushScope['~damage'] = { value };
|
||||||
|
} else {
|
||||||
|
result.pushScope['~healing'] = { value: -value };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.pushScope['~set'] = { value };
|
||||||
|
}
|
||||||
|
// Store which property we're targeting
|
||||||
|
if (targetId === action.creatureId) {
|
||||||
|
result.pushScope['~attributeDamaged'] = { _propId: targetProp._id };
|
||||||
|
} else {
|
||||||
|
result.pushScope['~attributeDamaged'] = targetProp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the before triggers which may change scope properties
|
||||||
|
await applyTriggers(action, targetProp, [action.creatureId], 'damageProperty.before', userInput);
|
||||||
|
|
||||||
|
// Refetch the scope properties
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
result.popScope = {
|
||||||
|
'~damage': 1, '~healing': 1, '~set': 1, '~attributeDamaged': 1,
|
||||||
|
};
|
||||||
|
value = +value;
|
||||||
|
if (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 there are no targets, just log the result that would apply and end
|
||||||
|
if (!task.targetIds?.length) {
|
||||||
|
// Get the locally equivalent stat with the same variable name
|
||||||
|
const statName = getPropertyTitle(targetProp);
|
||||||
|
result.appendLog({
|
||||||
|
name: title,
|
||||||
|
value: `${statName}${operation === 'set' ? ' set to' : ''}` +
|
||||||
|
` ${value}`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}, task.targetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let damage, newValue, increment;
|
||||||
|
targetProp = await getSingleProperty(targetId, targetPropId);
|
||||||
|
|
||||||
|
if (!targetProp) return;
|
||||||
|
|
||||||
|
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: title,
|
||||||
|
value: `${getPropertyTitle(targetProp)} set to ${value}`,
|
||||||
|
inline: true,
|
||||||
|
silenced: prop.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: prop.silent,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await applyTriggers(action, prop, [action.creatureId], 'damageProperty.after', userInput);
|
||||||
|
}
|
||||||
55
app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts
Normal file
55
app/imports/api/engine/action/tasks/applyItemAsAmmoTask.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import {
|
||||||
|
applyDefaultAfterPropTasks, applyTriggers
|
||||||
|
} from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||||
|
import {
|
||||||
|
getEffectiveActionScope
|
||||||
|
} from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||||
|
import { ItemAsAmmoTask } from '/imports/api/engine/action/tasks/Task';
|
||||||
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
||||||
|
import { getPropertyChildren } from '/imports/api/engine/loadCreatures';
|
||||||
|
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||||
|
|
||||||
|
export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action: EngineAction, result: TaskResult, userInput): Promise<void> {
|
||||||
|
const prop = task.prop;
|
||||||
|
const { item } = task.params
|
||||||
|
let { value } = task.params;
|
||||||
|
|
||||||
|
if (item.type !== 'item') throw 'Must use an item as ammo';
|
||||||
|
|
||||||
|
// Store the ammo item and value in the scope
|
||||||
|
result.scope['#ammo'] = { propId: item._id };
|
||||||
|
result.pushScope = { ['~ammoConsumed']: { value } };
|
||||||
|
|
||||||
|
// Apply the before triggers
|
||||||
|
await applyTriggers(action, item, [action.creatureId], 'ammo.before', userInput);
|
||||||
|
|
||||||
|
// Refetch the scope properties
|
||||||
|
const scope = await getEffectiveActionScope(action);
|
||||||
|
result.popScope = {
|
||||||
|
'~ammoConsumed': 1,
|
||||||
|
};
|
||||||
|
value = scope['~ammoConsumed']?.value || 0;
|
||||||
|
|
||||||
|
const itemChildren = await getPropertyChildren(action.creatureId, item);
|
||||||
|
|
||||||
|
// Do the quantity adjustment
|
||||||
|
// Check if property has quantity
|
||||||
|
result.mutations.push({
|
||||||
|
targetIds: task.targetIds,
|
||||||
|
updates: [{
|
||||||
|
propId: item._id,
|
||||||
|
inc: { quantity: -value },
|
||||||
|
type: 'item',
|
||||||
|
}],
|
||||||
|
// Log the item name as a heading if it has child properties to apply
|
||||||
|
contents: itemChildren.length ? [{
|
||||||
|
name: getPropertyTitle(item) || 'Ammo',
|
||||||
|
inline: false,
|
||||||
|
silenced: prop.silent,
|
||||||
|
}] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await applyTriggers(action, item, [action.creatureId], 'ammo.after', userInput);
|
||||||
|
return applyDefaultAfterPropTasks(action, item, task.targetIds, userInput);
|
||||||
|
}
|
||||||
49
app/imports/api/engine/action/tasks/applyTask.ts
Normal file
49
app/imports/api/engine/action/tasks/applyTask.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||||
|
import Task 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';
|
||||||
|
|
||||||
|
export default async function applyTask(action: EngineAction, task: Task, userInput?): Promise<void> {
|
||||||
|
action.taskCount += 1;
|
||||||
|
if (action.taskCount > 100) throw 'Only 100 properties can be applied at once';
|
||||||
|
|
||||||
|
if (task.subtaskFn) {
|
||||||
|
const result = new TaskResult(task.prop._id, task.targetIds);
|
||||||
|
action.results.push(result);
|
||||||
|
switch (task.subtaskFn) {
|
||||||
|
case 'damageProp':
|
||||||
|
return applyDamagePropTask(task, action, result, userInput);
|
||||||
|
case 'consumeItemAsAmmo':
|
||||||
|
return applyItemAsAmmoTask(task, action, result, userInput);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get property
|
||||||
|
const prop = task.prop;
|
||||||
|
|
||||||
|
// Ensure the prop exists
|
||||||
|
if (!prop) throw new Meteor.Error('Not found', 'Property could not be found');
|
||||||
|
|
||||||
|
// If the property is deactivated by a toggle, skip it
|
||||||
|
if (prop.deactivatedByToggle) return;
|
||||||
|
|
||||||
|
// Before triggers
|
||||||
|
if (prop.triggerIds?.before?.length) {
|
||||||
|
for (const triggerId of prop.triggerIds.before) {
|
||||||
|
const trigger = await getSingleProperty(action.creatureId, triggerId);
|
||||||
|
if (!trigger) continue;
|
||||||
|
await applyTask(action, { prop: trigger, targetIds: task.targetIds }, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a result an push it to the action results, pass it to the apply function to modify
|
||||||
|
const result = new TaskResult(task.prop._id, task.targetIds);
|
||||||
|
result.scope[`#${prop.type}`] = prop;
|
||||||
|
action.results.push(result);
|
||||||
|
|
||||||
|
// Apply the property
|
||||||
|
return applyProperties[prop.type]?.(task, action, result, userInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs';
|
|
||||||
import {
|
|
||||||
getCreature, getVariables, getPropertiesOfType, replaceLinkedVariablesWithProps
|
|
||||||
} from '/imports/api/engine/loadCreatures';
|
|
||||||
import { groupBy, remove } from 'lodash';
|
|
||||||
|
|
||||||
export default class ActionContext {
|
|
||||||
constructor(creatureId, targetIds = [], method, invocationId) {
|
|
||||||
// Get the creature
|
|
||||||
this.creature = getCreature(creatureId)
|
|
||||||
// Store the details for pausing for user interaction
|
|
||||||
this.invocationId = invocationId;
|
|
||||||
this.userInputStep = 0;
|
|
||||||
|
|
||||||
if (!this.creature) {
|
|
||||||
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
|
|
||||||
}
|
|
||||||
// Create a log
|
|
||||||
this.log = CreatureLogSchema.clean({
|
|
||||||
creatureId: creatureId,
|
|
||||||
creatureName: this.creature.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the variables of the acting creature
|
|
||||||
this.creature.variables = getVariables(creatureId);
|
|
||||||
replaceLinkedVariablesWithProps(this.creature.variables);
|
|
||||||
delete this.creature.variables._id;
|
|
||||||
delete this.creature.variables._creatureId;
|
|
||||||
// Alias as scope
|
|
||||||
this.scope = this.creature.variables;
|
|
||||||
|
|
||||||
// Get the targets and their variables
|
|
||||||
this.targets = [];
|
|
||||||
targetIds.forEach(targetId => {
|
|
||||||
let target;
|
|
||||||
if (targetId === creatureId) {
|
|
||||||
target = this.creature;
|
|
||||||
} else {
|
|
||||||
target = getCreature(targetId);
|
|
||||||
target.variables = getVariables(targetId);
|
|
||||||
delete target.variables._id;
|
|
||||||
delete target.variables._creatureId;
|
|
||||||
}
|
|
||||||
this.targets.push(target);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store a reference to the method for inserting the log
|
|
||||||
this.method = method;
|
|
||||||
|
|
||||||
// Get triggers
|
|
||||||
this.triggers = getPropertiesOfType(creatureId, 'trigger');
|
|
||||||
// Remove deleted or inactive triggers
|
|
||||||
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
|
|
||||||
// Sort triggers by order
|
|
||||||
this.triggers.sort((a, b) => a.order - b.order);
|
|
||||||
// Group the triggers into triggers.<event>.<timing> or
|
|
||||||
// triggers.doActionProperty.<propertyType>.<timing>
|
|
||||||
this.triggers = groupBy(this.triggers, 'event');
|
|
||||||
for (const event in this.triggers) {
|
|
||||||
if (event === 'doActionProperty') {
|
|
||||||
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
|
|
||||||
for (const propertyType in this.triggers[event]) {
|
|
||||||
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.triggers[event] = groupBy(this.triggers[event], 'timing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addLog(content) {
|
|
||||||
if (content.name || content.value) {
|
|
||||||
this.log.content.push(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeLog() {
|
|
||||||
insertCreatureLogWork({
|
|
||||||
log: this.log,
|
|
||||||
creature: this.creature,
|
|
||||||
method: this.method,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
|||||||
import action from './applyPropertyByType/applyAction';
|
|
||||||
import ammo from './applyPropertyByType/applyItemAsAmmo'
|
|
||||||
import adjustment from './applyPropertyByType/applyAdjustment';
|
|
||||||
import branch from './applyPropertyByType/applyBranch';
|
|
||||||
import buff from './applyPropertyByType/applyBuff';
|
|
||||||
import buffRemover from './applyPropertyByType/applyBuffRemover';
|
|
||||||
import damage from './applyPropertyByType/applyDamage';
|
|
||||||
import folder from './applyPropertyByType/applyFolder';
|
|
||||||
import note from './applyPropertyByType/applyNote';
|
|
||||||
import roll from './applyPropertyByType/applyRoll';
|
|
||||||
import savingThrow from './applyPropertyByType/applySavingThrow';
|
|
||||||
import toggle from './applyPropertyByType/applyToggle';
|
|
||||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
|
||||||
import { TreeNode } from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
|
|
||||||
|
|
||||||
const applyPropertyByType = {
|
|
||||||
action,
|
|
||||||
ammo,
|
|
||||||
adjustment,
|
|
||||||
branch,
|
|
||||||
buff,
|
|
||||||
buffRemover,
|
|
||||||
damage,
|
|
||||||
folder,
|
|
||||||
note,
|
|
||||||
propertySlot: folder,
|
|
||||||
roll,
|
|
||||||
savingThrow,
|
|
||||||
spell: action,
|
|
||||||
toggle,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function applyProperty(node: TreeNode<CreatureProperty>, actionContext: ActionContext, ...rest) {
|
|
||||||
if (node.doc.deactivatedByToggle) return;
|
|
||||||
actionContext.scope[`#${node.doc.type}`] = node.doc;
|
|
||||||
applyPropertyByType[node.doc.type]?.(node, actionContext, ...rest);
|
|
||||||
}
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations';
|
|
||||||
import recalculateCalculation from './shared/recalculateCalculation';
|
|
||||||
import rollDice from '/imports/parser/rollDice';
|
|
||||||
import applyProperty from '../applyProperty';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
|
|
||||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature';
|
|
||||||
import { TreeNode, hasAncestorRelationship } from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import { Action } from '/imports/api/properties/Actions';
|
|
||||||
import { LogContent } from '/imports/api/creature/log/LogContentSchema';
|
|
||||||
import { Item } from '/imports/api/properties/Items';
|
|
||||||
|
|
||||||
interface Ammo extends Item {
|
|
||||||
type: 'ammo'
|
|
||||||
adjustment: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function applyAction(node: TreeNode<Action>, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const prop = node.doc;
|
|
||||||
if (prop.target === 'self') actionContext.targets = [actionContext.creature];
|
|
||||||
const targets = actionContext.targets;
|
|
||||||
|
|
||||||
// Log the name and summary
|
|
||||||
const content: LogContent = { name: prop.name, };
|
|
||||||
if (prop.summary?.text) {
|
|
||||||
recalculateInlineCalculations(prop.summary, actionContext);
|
|
||||||
content.value = prop.summary.value;
|
|
||||||
}
|
|
||||||
if (!prop.silent) actionContext.addLog(content);
|
|
||||||
|
|
||||||
// Spend the resources
|
|
||||||
const failed = await spendResources(prop, actionContext);
|
|
||||||
if (failed) return;
|
|
||||||
|
|
||||||
const attack = prop.attackRoll;
|
|
||||||
|
|
||||||
// Attack if there is an attack roll
|
|
||||||
if (attack && attack.calculation) {
|
|
||||||
if (targets.length) {
|
|
||||||
for (const target of targets) {
|
|
||||||
await applyAttackToTarget({ attack, target, actionContext });
|
|
||||||
// Apply the children, but only to the current target
|
|
||||||
actionContext.targets = [target];
|
|
||||||
await applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await applyAttackWithoutTarget({ attack, actionContext });
|
|
||||||
await applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
if (prop.actionType === 'event' && prop.variableName) {
|
|
||||||
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
|
||||||
delete actionContext.scope['~attackHit'];
|
|
||||||
delete actionContext.scope['~attackMiss'];
|
|
||||||
delete actionContext.scope['~criticalHit'];
|
|
||||||
delete actionContext.scope['~criticalMiss'];
|
|
||||||
delete actionContext.scope['~attackRoll'];
|
|
||||||
|
|
||||||
recalculateCalculation(attack, actionContext);
|
|
||||||
const scope = actionContext.scope;
|
|
||||||
const {
|
|
||||||
resultPrefix,
|
|
||||||
result,
|
|
||||||
criticalHit,
|
|
||||||
criticalMiss,
|
|
||||||
} = rollAttack(attack, scope);
|
|
||||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
|
||||||
if (scope['~attackAdvantage']?.value === 1) {
|
|
||||||
name += ' (Advantage)';
|
|
||||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
|
||||||
name += ' (Disadvantage)';
|
|
||||||
}
|
|
||||||
if (!criticalMiss) {
|
|
||||||
scope['~attackHit'] = { value: true }
|
|
||||||
}
|
|
||||||
if (!criticalHit) {
|
|
||||||
scope['~attackMiss'] = { value: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
actionContext.addLog({
|
|
||||||
name,
|
|
||||||
value: `${resultPrefix}\n**${result}**`,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
|
||||||
const scope = actionContext.scope;
|
|
||||||
delete scope['~attackHit'];
|
|
||||||
delete scope['~attackMiss'];
|
|
||||||
delete scope['~criticalHit'];
|
|
||||||
delete scope['~criticalMiss'];
|
|
||||||
delete scope['~attackDiceRoll'];
|
|
||||||
delete scope['~attackRoll'];
|
|
||||||
|
|
||||||
recalculateCalculation(attack, actionContext);
|
|
||||||
|
|
||||||
const {
|
|
||||||
resultPrefix,
|
|
||||||
result,
|
|
||||||
criticalHit,
|
|
||||||
criticalMiss,
|
|
||||||
} = rollAttack(attack, scope);
|
|
||||||
|
|
||||||
if (target.variables.armor) {
|
|
||||||
const armor = target.variables.armor.value;
|
|
||||||
|
|
||||||
let name = criticalHit ? 'Critical Hit!' :
|
|
||||||
criticalMiss ? 'Critical Miss!' :
|
|
||||||
result > armor ? 'Hit!' : 'Miss!';
|
|
||||||
if (scope['~attackAdvantage']?.value === 1) {
|
|
||||||
name += ' (Advantage)';
|
|
||||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
|
||||||
name += ' (Disadvantage)';
|
|
||||||
}
|
|
||||||
|
|
||||||
actionContext.addLog({
|
|
||||||
name,
|
|
||||||
value: `${resultPrefix}\n**${result}**`,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
if (criticalMiss || result < armor) {
|
|
||||||
scope['~attackMiss'] = { value: true };
|
|
||||||
} else {
|
|
||||||
scope['~attackHit'] = { value: true };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: 'Target has no `armor`',
|
|
||||||
});
|
|
||||||
actionContext.addLog({
|
|
||||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
|
||||||
value: `${resultPrefix}\n**${result}**`,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rollAttack(attack, scope) {
|
|
||||||
const rollModifierText = numberToSignedString(attack.value, true);
|
|
||||||
let value, resultPrefix;
|
|
||||||
if (scope['~attackAdvantage']?.value === 1) {
|
|
||||||
const [a, b] = rollDice(2, 20);
|
|
||||||
if (a >= b) {
|
|
||||||
value = a;
|
|
||||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
|
||||||
} else {
|
|
||||||
value = b;
|
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
|
||||||
}
|
|
||||||
} else if (scope['~attackAdvantage']?.value === -1) {
|
|
||||||
const [a, b] = rollDice(2, 20);
|
|
||||||
if (a <= b) {
|
|
||||||
value = a;
|
|
||||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
|
||||||
} else {
|
|
||||||
value = b;
|
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = rollDice(1, 20)[0];
|
|
||||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
|
||||||
}
|
|
||||||
scope['~attackDiceRoll'] = { value };
|
|
||||||
const result = value + attack.value;
|
|
||||||
scope['~attackRoll'] = { value: result };
|
|
||||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
|
||||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyCrits(value, scope) {
|
|
||||||
let scopeCrit = scope['~criticalHitTarget']?.value;
|
|
||||||
if (scopeCrit?.parseType === 'constant') {
|
|
||||||
scopeCrit = scopeCrit.value;
|
|
||||||
}
|
|
||||||
const criticalHitTarget = scopeCrit || 20;
|
|
||||||
const criticalHit = value >= criticalHitTarget;
|
|
||||||
let criticalMiss;
|
|
||||||
if (criticalHit) {
|
|
||||||
scope['~criticalHit'] = { value: true };
|
|
||||||
} else {
|
|
||||||
criticalMiss = value === 1;
|
|
||||||
if (criticalMiss) {
|
|
||||||
scope['~criticalMiss'] = { value: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { criticalHit, criticalMiss };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function spendResources(prop: Action, actionContext) {
|
|
||||||
// Check Uses
|
|
||||||
if (!prop.usesLeft || prop.usesLeft <= 0) {
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Resources
|
|
||||||
if (prop.insufficientResources) {
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Items
|
|
||||||
const spendLog: string[] = [];
|
|
||||||
const gainLog: string[] = [];
|
|
||||||
const ammoToApply: TreeNode<Ammo>[] = [];
|
|
||||||
try {
|
|
||||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
|
||||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
|
||||||
if (!itemConsumed.itemId) {
|
|
||||||
throw 'No ammo was selected for this prop';
|
|
||||||
}
|
|
||||||
const item = CreatureProperties.findOne(itemConsumed.itemId) as Item;
|
|
||||||
if (!item || item.root.id !== prop.root.id) {
|
|
||||||
throw 'The prop\'s ammo was not found on the creature';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!itemConsumed?.quantity?.value ||
|
|
||||||
!isFinite(+itemConsumed.quantity.value)
|
|
||||||
) return;
|
|
||||||
const quantityConsumed = +itemConsumed.quantity.value;
|
|
||||||
|
|
||||||
let logName = item.name;
|
|
||||||
if (quantityConsumed > 1 || quantityConsumed < -1) {
|
|
||||||
logName = item.plural || logName;
|
|
||||||
}
|
|
||||||
if (quantityConsumed > 0) {
|
|
||||||
spendLog.push(logName + ': ' + quantityConsumed);
|
|
||||||
} else if (quantityConsumed < 0) {
|
|
||||||
gainLog.push(logName + ': ' + -quantityConsumed);
|
|
||||||
}
|
|
||||||
// So long as the item isn't an ancestor of the current prop apply it
|
|
||||||
// If it was an ancestor this would be an infinite loop
|
|
||||||
if (!hasAncestorRelationship(item, prop)) {
|
|
||||||
ammoToApply.push({
|
|
||||||
doc: {
|
|
||||||
...item,
|
|
||||||
// Use ammo pseudo-type
|
|
||||||
type: 'ammo',
|
|
||||||
// Store the adjustment to be applied
|
|
||||||
adjustment: quantityConsumed,
|
|
||||||
},
|
|
||||||
children: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: e.toString(),
|
|
||||||
});
|
|
||||||
console.error(e);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// No more errors should be thrown after this line
|
|
||||||
|
|
||||||
// Use uses
|
|
||||||
if (prop.usesLeft) {
|
|
||||||
CreatureProperties.update(prop._id, {
|
|
||||||
$inc: { usesUsed: 1 }
|
|
||||||
}, {
|
|
||||||
//@ts-expect-error no typings for collection 2 selector
|
|
||||||
selector: prop
|
|
||||||
});
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Uses left',
|
|
||||||
value: prop.usesLeft - 1,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Damage stats
|
|
||||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
|
||||||
recalculateCalculation(attConsumed.quantity, actionContext);
|
|
||||||
|
|
||||||
if (!attConsumed.quantity?.value) return;
|
|
||||||
const quantityConsumed = +attConsumed.quantity.value;
|
|
||||||
if (!attConsumed.variableName) return;
|
|
||||||
const stat = actionContext.scope[attConsumed.variableName];
|
|
||||||
if (!stat) {
|
|
||||||
spendLog.push(attConsumed.variableName + ': ' + ' not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
damagePropertyWork({
|
|
||||||
prop: stat,
|
|
||||||
operation: 'increment',
|
|
||||||
value: attConsumed.quantity.value,
|
|
||||||
actionContext,
|
|
||||||
});
|
|
||||||
if (quantityConsumed > 0) {
|
|
||||||
spendLog.push(stat.name + ': ' + quantityConsumed);
|
|
||||||
} else if (quantityConsumed < 0) {
|
|
||||||
gainLog.push(stat.name + ': ' + -quantityConsumed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply the ammo children
|
|
||||||
for (const node of ammoToApply) {
|
|
||||||
await applyProperty(node, actionContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log all the spending
|
|
||||||
if (gainLog.length && !prop.silent) actionContext.addLog({
|
|
||||||
name: 'Gained',
|
|
||||||
value: gainLog.join('\n'),
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
if (spendLog.length && !prop.silent) actionContext.addLog({
|
|
||||||
name: 'Spent',
|
|
||||||
value: spendLog.join('\n'),
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import recalculateCalculation from './shared/recalculateCalculation';
|
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
|
|
||||||
export default function applyAdjustment(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const prop = node.doc
|
|
||||||
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
|
||||||
|
|
||||||
if (!prop.amount) {
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate the amount
|
|
||||||
recalculateCalculation(prop.amount, actionContext);
|
|
||||||
|
|
||||||
const value = +prop.amount.value;
|
|
||||||
if (!isFinite(value)) {
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (damageTargets?.length) {
|
|
||||||
damageTargets.forEach(target => {
|
|
||||||
let stat = target.variables[prop.stat];
|
|
||||||
if (!stat?.type) {
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
|
||||||
});
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
damagePropertyWork({
|
|
||||||
prop: stat,
|
|
||||||
operation: prop.operation,
|
|
||||||
value,
|
|
||||||
actionContext,
|
|
||||||
});
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Attribute damage',
|
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
||||||
` ${value}`,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!prop.silent) actionContext.addLog({
|
|
||||||
name: 'Attribute damage',
|
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
|
||||||
` ${value}`,
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import applyProperty from '../applyProperty';
|
|
||||||
import recalculateCalculation from './shared/recalculateCalculation';
|
|
||||||
import rollDice from '/imports/parser/rollDice';
|
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
|
|
||||||
export default async function applyBranch(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const scope = actionContext.scope;
|
|
||||||
const targets = actionContext.targets;
|
|
||||||
const prop = node.doc
|
|
||||||
switch (prop.branchType) {
|
|
||||||
case 'if':
|
|
||||||
recalculateCalculation(prop.condition, actionContext);
|
|
||||||
if (prop.condition?.value) applyChildren(node, actionContext);
|
|
||||||
break;
|
|
||||||
case 'index':
|
|
||||||
if (node.children.length) {
|
|
||||||
recalculateCalculation(prop.condition, actionContext);
|
|
||||||
if (!isFinite(prop.condition?.value)) {
|
|
||||||
actionContext.addLog({
|
|
||||||
name: 'Branch Error',
|
|
||||||
value: 'Index did not resolve into a valid number'
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let index = Math.floor(prop.condition?.value);
|
|
||||||
if (index < 1) index = 1;
|
|
||||||
if (index > node.children.length) index = node.children.length;
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
applyProperty(node.children[index - 1], actionContext);
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'hit':
|
|
||||||
if (scope['~attackHit']?.value) {
|
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' });
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'miss':
|
|
||||||
if (scope['~attackMiss']?.value) {
|
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' });
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'failedSave':
|
|
||||||
if (scope['~saveFailed']?.value) {
|
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' });
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'successfulSave':
|
|
||||||
if (scope['~saveSucceeded']?.value) {
|
|
||||||
if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', });
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'random':
|
|
||||||
if (node.children.length) {
|
|
||||||
let index = rollDice(1, node.children.length)[0] - 1;
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
applyProperty(node.children[index], actionContext);
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'eachTarget':
|
|
||||||
if (targets.length) {
|
|
||||||
targets.forEach(target => {
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
actionContext.targets = [target]
|
|
||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'choice': {
|
|
||||||
console.log('paused waiting for user input');
|
|
||||||
let { index } = await getUserInput({
|
|
||||||
index: 'number',
|
|
||||||
}, actionContext);
|
|
||||||
console.log('resuming with input ' + index);
|
|
||||||
if (!isFinite(index) || index < 0) index = 0;
|
|
||||||
if (index > node.children.length - 1) index = node.children.length - 1;
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
console.log('applying child ', index);
|
|
||||||
console.log(node.children[index]);
|
|
||||||
applyProperty(node.children[index], actionContext);
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import {
|
|
||||||
renewDocIds,
|
|
||||||
} from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
|
||||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex';
|
|
||||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
import resolve, { map, toString } from '/imports/parser/resolve';
|
|
||||||
import accessor from '/imports/parser/parseTree/accessor';
|
|
||||||
import logErrors from './shared/logErrors';
|
|
||||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs';
|
|
||||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX';
|
|
||||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations';
|
|
||||||
|
|
||||||
export default function applyBuff(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const prop = node.doc
|
|
||||||
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
|
||||||
|
|
||||||
// Mark the buff as dirty for recalculation
|
|
||||||
prop.dirty = true;
|
|
||||||
|
|
||||||
// Then copy the descendants of the buff to the targets
|
|
||||||
let propList = [prop];
|
|
||||||
function addChildrenToPropList(children, { skipCrystalize } = { skipCrystalize: false }) {
|
|
||||||
children.forEach(child => {
|
|
||||||
if (skipCrystalize) child.node._skipCrystalize = true;
|
|
||||||
propList.push(child.node);
|
|
||||||
// recursively add the child's children, but don't crystalize nested buffs
|
|
||||||
addChildrenToPropList(child.children, {
|
|
||||||
skipCrystalize: skipCrystalize || child.node.type === 'buff'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addChildrenToPropList(node.children);
|
|
||||||
if (!prop.skipCrystalization) {
|
|
||||||
crystalizeVariables({ propList, actionContext });
|
|
||||||
}
|
|
||||||
|
|
||||||
buffTargets.forEach(target => {
|
|
||||||
const targetPropList = EJSON.clone(propList);
|
|
||||||
// Move the properties to the target by replacing the old subtree parent and root with the '
|
|
||||||
// target id
|
|
||||||
renewDocIds({
|
|
||||||
docArray: targetPropList,
|
|
||||||
idMap: {
|
|
||||||
[prop.parentId]: target._id,
|
|
||||||
[prop.root.id]: target._id,
|
|
||||||
},
|
|
||||||
collectionMap: { [prop.root.collection]: 'creatures' }
|
|
||||||
});
|
|
||||||
// Apply the buff
|
|
||||||
CreatureProperties.batchInsert(targetPropList);
|
|
||||||
|
|
||||||
//Log the buff
|
|
||||||
let logValue = prop.description?.value
|
|
||||||
if (prop.description?.text) {
|
|
||||||
recalculateInlineCalculations(prop.description, actionContext);
|
|
||||||
logValue = prop.description?.value;
|
|
||||||
}
|
|
||||||
if ((prop.name || prop.description?.value) && !prop.silent) {
|
|
||||||
if (target._id === actionContext.creature._id) {
|
|
||||||
// Targeting self
|
|
||||||
actionContext.addLog({
|
|
||||||
name: prop.name,
|
|
||||||
value: logValue,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Targeting other
|
|
||||||
insertCreatureLog.call({
|
|
||||||
log: {
|
|
||||||
creatureId: target._id,
|
|
||||||
content: [{
|
|
||||||
name: prop.name,
|
|
||||||
value: logValue,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
|
|
||||||
// Don't apply the children of the buff, they get copied to the target instead
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces all variables with their resolved values
|
|
||||||
* except variables of the form `~target.thing.total` become `thing.total`
|
|
||||||
*/
|
|
||||||
function crystalizeVariables({ propList, actionContext }) {
|
|
||||||
propList.forEach(prop => {
|
|
||||||
if (prop._skipCrystalize) {
|
|
||||||
delete prop._skipCrystalize;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Iterate through all the calculations and crystalize them
|
|
||||||
computedSchemas[prop.type].computedFields().forEach(calcKey => {
|
|
||||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
|
||||||
const calcObj = get(prop, key);
|
|
||||||
if (!calcObj?.parseNode) return;
|
|
||||||
calcObj.parseNode = map(calcObj.parseNode, node => {
|
|
||||||
// Skip nodes that aren't symbols or accessors
|
|
||||||
if (
|
|
||||||
node.parseType !== 'accessor'
|
|
||||||
) return node;
|
|
||||||
// Handle variables
|
|
||||||
if (node.name === '~target') {
|
|
||||||
// strip ~target
|
|
||||||
if (node.parseType === 'accessor') {
|
|
||||||
node.name = node.path.shift();
|
|
||||||
if (!node.path.length) {
|
|
||||||
return accessor.create({ name: node.name })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Can't strip symbols
|
|
||||||
actionContext.addLog({
|
|
||||||
name: 'Error',
|
|
||||||
value: 'Variable `~target` should not be used without a property: ~target.property',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
} else {
|
|
||||||
// Resolve all other variables
|
|
||||||
const { result, context } = resolve('reduce', node, actionContext.scope);
|
|
||||||
logErrors(context.errors, actionContext);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
calcObj.calculation = toString(calcObj.parseNode);
|
|
||||||
calcObj.hash = cyrb53(calcObj.calculation);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// For each key in the schema
|
|
||||||
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
|
|
||||||
// That ends in .inlineCalculations
|
|
||||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
|
||||||
const inlineCalcObj = get(prop, key);
|
|
||||||
if (!inlineCalcObj) return;
|
|
||||||
|
|
||||||
// If there is no text, skip
|
|
||||||
if (!inlineCalcObj.text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace all the existing calculations
|
|
||||||
let index = -1;
|
|
||||||
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
|
|
||||||
index += 1;
|
|
||||||
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the value to the uncomputed string
|
|
||||||
inlineCalcObj.value = inlineCalcObj.text;
|
|
||||||
|
|
||||||
// Write a new hash
|
|
||||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
|
||||||
if (inlineCalcHash === inlineCalcObj.hash) {
|
|
||||||
// Skip if nothing changed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inlineCalcObj.hash = inlineCalcHash;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
|
|
||||||
export default function applyFolder(node, actionContext) {
|
|
||||||
// Apply triggers
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
// Apply children
|
|
||||||
applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
|
|
||||||
import applyProperty from '../applyProperty';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity';
|
|
||||||
|
|
||||||
export default function applyItemAsAmmo(node, actionContext) {
|
|
||||||
// The item node should come without children, since it is not part of the original action tree
|
|
||||||
const prop = node.doc
|
|
||||||
// Get all the item's descendant properties
|
|
||||||
const properties = getPropertyDescendants(actionContext.creature._id, prop._id);
|
|
||||||
properties.sort((a, b) => a.order - b.order);
|
|
||||||
const propertyForest = nodeArrayToTree(properties);
|
|
||||||
|
|
||||||
// Apply the item
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
|
|
||||||
// Do the quantity adjustment
|
|
||||||
const itemProp = { ...prop, type: 'item' };
|
|
||||||
delete itemProp.adjustment;
|
|
||||||
adjustQuantityWork({
|
|
||||||
property: itemProp,
|
|
||||||
operation: 'increment',
|
|
||||||
value: prop.adjustment,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate the change to quantity
|
|
||||||
prop.quantity -= prop.adjustment;
|
|
||||||
|
|
||||||
// Log the item name as a heading if it's not silent and has child properties to apply
|
|
||||||
if (!prop.silent && propertyForest.length) {
|
|
||||||
actionContext.addLog({
|
|
||||||
name: prop.name || 'Ammo',
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
|
|
||||||
// Apply the item's children
|
|
||||||
propertyForest.forEach(node => applyProperty(node, actionContext));
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations';
|
|
||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
|
|
||||||
export default async function applyNote(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const prop = node.doc
|
|
||||||
|
|
||||||
// Log Name, summary
|
|
||||||
let content = { name: prop.name };
|
|
||||||
if (prop.summary?.text) {
|
|
||||||
recalculateInlineCalculations(prop.summary, actionContext);
|
|
||||||
content.value = prop.summary.value;
|
|
||||||
}
|
|
||||||
if (content.name || content.value) {
|
|
||||||
actionContext.addLog(content);
|
|
||||||
}
|
|
||||||
// Log description
|
|
||||||
if (prop.description?.text) {
|
|
||||||
recalculateInlineCalculations(prop.description, actionContext);
|
|
||||||
actionContext.addLog({ value: prop.description.value });
|
|
||||||
}
|
|
||||||
// Apply children
|
|
||||||
await applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
|
||||||
import logErrors from './shared/logErrors';
|
|
||||||
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
|
||||||
import resolve, { toString } from '/imports/parser/resolve';
|
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
|
|
||||||
export default function applyRoll(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
|
||||||
const prop = node.doc
|
|
||||||
|
|
||||||
if (prop.roll?.calculation) {
|
|
||||||
const logValue = [];
|
|
||||||
|
|
||||||
// roll the dice only and store that string
|
|
||||||
recalculateCalculation(prop.roll, actionContext, undefined, 'compile');
|
|
||||||
const { result: rolled, context } = resolve('roll', prop.roll.valueNode, actionContext.scope);
|
|
||||||
if (rolled.parseType !== 'constant') {
|
|
||||||
logValue.push(toString(rolled));
|
|
||||||
}
|
|
||||||
logErrors(context.errors, actionContext);
|
|
||||||
|
|
||||||
// Reset the errors so we don't log the same errors twice
|
|
||||||
context.errors = [];
|
|
||||||
|
|
||||||
// Resolve the roll to a final value
|
|
||||||
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
|
|
||||||
logErrors(context.errors, actionContext);
|
|
||||||
|
|
||||||
// Store the result
|
|
||||||
if (reduced.parseType === 'constant') {
|
|
||||||
prop.roll.value = reduced.value;
|
|
||||||
} else if (reduced.parseType === 'error') {
|
|
||||||
prop.roll.value = null;
|
|
||||||
} else {
|
|
||||||
prop.roll.value = toString(reduced);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't end up with a constant or a number of finite value, give up
|
|
||||||
if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) {
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
const value = reduced.value;
|
|
||||||
|
|
||||||
actionContext.scope[prop.variableName] = { value };
|
|
||||||
logValue.push(`**${value}**`);
|
|
||||||
|
|
||||||
if (!prop.silent) {
|
|
||||||
actionContext.addLog({
|
|
||||||
name: prop.name,
|
|
||||||
value: logValue.join('\n'),
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return applyChildren(node, actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import applyProperty from '/imports/api/engine/actions/applyProperty';
|
|
||||||
|
|
||||||
export default async function applyChildren(node, actionContext) {
|
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
|
||||||
for (const child of node.children) {
|
|
||||||
await applyProperty(child, actionContext);
|
|
||||||
}
|
|
||||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function logErrors(errors, actionContext){
|
|
||||||
errors?.forEach(error => {
|
|
||||||
if (error.type !== 'info'){
|
|
||||||
actionContext.addLog({name: 'Error', value: error.message});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers';
|
|
||||||
import clean from '/imports/api/engine/computation/utility/cleanProp.testFn';
|
|
||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const prop = clean({
|
|
||||||
id: 'propWithTags',
|
|
||||||
type: 'action',
|
|
||||||
tags: ['yes1', 'notUsed', 'no1', 'yes2', 'no2', 'or1', 'or2'],
|
|
||||||
});
|
|
||||||
const positiveProp = clean({
|
|
||||||
id: 'propWithTags',
|
|
||||||
type: 'action',
|
|
||||||
tags: ['yes1', 'notUsed', 'yes2', 'or1', 'or2'],
|
|
||||||
});
|
|
||||||
assert.isTrue(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
targetTags: ['yes1'],
|
|
||||||
}), prop),
|
|
||||||
'Trigger matches on a single target tag'
|
|
||||||
);
|
|
||||||
assert.isTrue(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
targetTags: ['yes1', 'yes2'],
|
|
||||||
}), prop),
|
|
||||||
'Trigger matches on a multiple target tags'
|
|
||||||
);
|
|
||||||
assert.isFalse(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
targetTags: ['yes1'],
|
|
||||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
|
||||||
}), prop),
|
|
||||||
'Trigger correctly fails to match when not tags are present'
|
|
||||||
);
|
|
||||||
assert.isFalse(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
|
||||||
}), prop),
|
|
||||||
'Trigger correctly fails to match when only not tags are present'
|
|
||||||
);
|
|
||||||
assert.isTrue(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
extraTags: [{ operation: 'NOT', tags: ['no1'] }]
|
|
||||||
}), positiveProp),
|
|
||||||
'Trigger matches when only not tags are present'
|
|
||||||
);
|
|
||||||
assert.isTrue(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
|
||||||
}), positiveProp),
|
|
||||||
'Trigger matches when OR tags are present'
|
|
||||||
);
|
|
||||||
assert.isTrue(
|
|
||||||
triggerMatchTags(clean({
|
|
||||||
type: 'trigger',
|
|
||||||
targetTags: ['missing1'],
|
|
||||||
extraTags: [{ operation: 'OR', tags: ['or1'] }]
|
|
||||||
}), positiveProp),
|
|
||||||
'Trigger matches when only OR tags are present'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
|
|
||||||
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
|
||||||
import { getPropertyDescendants } from '/imports/api/engine/loadCreatures';
|
|
||||||
import { TreeNode, docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import applyProperty from '/imports/api/engine/actions/applyProperty';
|
|
||||||
import { difference, intersection } from 'lodash';
|
|
||||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
|
|
||||||
|
|
||||||
export async function applyNodeTriggers(node: TreeNode<any>, timing, actionContext) {
|
|
||||||
const prop = node.doc;
|
|
||||||
const type = prop.type;
|
|
||||||
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
|
|
||||||
if (triggers) {
|
|
||||||
for (const trigger of triggers) {
|
|
||||||
await applyTrigger(trigger, prop, actionContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyTriggers(triggers = [], prop, actionContext) {
|
|
||||||
// Apply the triggers
|
|
||||||
for (const trigger of triggers) {
|
|
||||||
await applyTrigger(trigger, prop, actionContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyTrigger(trigger, prop, actionContext) {
|
|
||||||
// If there is a prop we are applying the trigger from,
|
|
||||||
// don't fire if the tags don't match
|
|
||||||
if (prop && !triggerMatchTags(trigger, prop)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent trigger from firing if it's inactive
|
|
||||||
if (trigger.inactive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent triggers from firing if their condition is false
|
|
||||||
if (trigger.condition?.parseNode) {
|
|
||||||
recalculateCalculation(trigger.condition, actionContext);
|
|
||||||
if (!trigger.condition.value?.value) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent triggers from firing themselves in a loop
|
|
||||||
if (trigger.firing) {
|
|
||||||
/*
|
|
||||||
log.content.push({
|
|
||||||
name: trigger.name || 'Trigger',
|
|
||||||
value: 'Trigger can\'t fire itself',
|
|
||||||
inline: true,
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trigger.firing = true;
|
|
||||||
|
|
||||||
// Fire the trigger
|
|
||||||
const content = {
|
|
||||||
name: trigger.name || 'Trigger',
|
|
||||||
value: trigger.description,
|
|
||||||
inline: false,
|
|
||||||
}
|
|
||||||
if (trigger.description?.text) {
|
|
||||||
recalculateInlineCalculations(trigger.description, actionContext);
|
|
||||||
content.value = trigger.description.value;
|
|
||||||
}
|
|
||||||
if (!trigger.silent) actionContext.addLog(content);
|
|
||||||
|
|
||||||
// Get all the trigger's properties and apply them
|
|
||||||
const properties = getPropertyDescendants(actionContext.creature._id, trigger._id);
|
|
||||||
properties.sort((a, b) => a.order - b.order);
|
|
||||||
const propertyForest = nodeArrayToTree(properties);
|
|
||||||
for (const node of propertyForest) {
|
|
||||||
await applyProperty(node, actionContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.firing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function triggerMatchTags(trigger, prop) {
|
|
||||||
let matched = false;
|
|
||||||
const propTags = getEffectivePropTags(prop);
|
|
||||||
// Check the target tags
|
|
||||||
if (
|
|
||||||
!trigger.targetTags?.length ||
|
|
||||||
difference(trigger.targetTags, propTags).length === 0
|
|
||||||
) {
|
|
||||||
matched = true;
|
|
||||||
}
|
|
||||||
// Check the extra tags
|
|
||||||
if (trigger.extraTags) {
|
|
||||||
for (const extra of trigger.extraTags) {
|
|
||||||
if (extra.operation === 'OR') {
|
|
||||||
if (matched) break;
|
|
||||||
if (
|
|
||||||
!extra.tags.length ||
|
|
||||||
difference(extra.tags, propTags).length === 0
|
|
||||||
) {
|
|
||||||
matched = true;
|
|
||||||
}
|
|
||||||
} else if (extra.operation === 'NOT') {
|
|
||||||
if (
|
|
||||||
extra.tags.length &&
|
|
||||||
intersection(extra.tags, propTags).length > 0
|
|
||||||
) {
|
|
||||||
matched = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
|
||||||
import { docsToForest } from '/imports/api/parenting/parentingFunctions';
|
|
||||||
import {
|
|
||||||
getPropertyAncestors, getPropertyDescendants
|
|
||||||
} from '/imports/api/engine/loadCreatures';
|
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
|
||||||
import applyProperty from './applyProperty';
|
|
||||||
import ActionContext from '/imports/api/engine/actions/ActionContext';
|
|
||||||
|
|
||||||
const doAction = new ValidatedMethod({
|
|
||||||
name: 'creatureProperties.doAction',
|
|
||||||
validate: new SimpleSchema({
|
|
||||||
actionId: SimpleSchema.RegEx.Id,
|
|
||||||
targetIds: {
|
|
||||||
type: Array,
|
|
||||||
defaultValue: [],
|
|
||||||
maxCount: 20,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
'targetIds.$': {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
|
||||||
},
|
|
||||||
scope: {
|
|
||||||
type: Object,
|
|
||||||
blackbox: true,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
invocationId: {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
|
||||||
optional: true,
|
|
||||||
}
|
|
||||||
}).validator(),
|
|
||||||
applyOptions: {
|
|
||||||
throwStubExceptions: false,
|
|
||||||
},
|
|
||||||
mixins: [RateLimiterMixin],
|
|
||||||
rateLimit: {
|
|
||||||
numRequests: 10,
|
|
||||||
timeInterval: 5000,
|
|
||||||
},
|
|
||||||
async run({ actionId, targetIds = [], scope, invocationId }) {
|
|
||||||
console.log('do Action running');
|
|
||||||
// Get action context
|
|
||||||
const action = CreatureProperties.findOne(actionId);
|
|
||||||
if (!action) throw new Meteor.Error('not-found', 'The action was not found');
|
|
||||||
const creatureId = action.root.id;
|
|
||||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
|
||||||
|
|
||||||
// Check permissions
|
|
||||||
assertEditPermission(actionContext.creature, this.userId);
|
|
||||||
actionContext.targets.forEach(target => {
|
|
||||||
assertEditPermission(target, this.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ancestors = getPropertyAncestors(creatureId, action._id);
|
|
||||||
ancestors.sort((a, b) => a.order - b.order);
|
|
||||||
|
|
||||||
const properties = getPropertyDescendants(creatureId, action._id);
|
|
||||||
properties.push(action);
|
|
||||||
properties.sort((a, b) => a.order - b.order);
|
|
||||||
|
|
||||||
// Do the action
|
|
||||||
await doActionWork({ properties, ancestors, actionContext, methodScope: scope });
|
|
||||||
|
|
||||||
// Recompute all involved creatures
|
|
||||||
if (Meteor.isServer) {
|
|
||||||
Creatures.updateAsync({
|
|
||||||
_id: { $in: [creatureId, ...targetIds] }
|
|
||||||
}, {
|
|
||||||
$set: { dirty: true },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default doAction;
|
|
||||||
|
|
||||||
export async function doActionWork({
|
|
||||||
properties, ancestors, actionContext, methodScope = {},
|
|
||||||
}) {
|
|
||||||
// get the docs
|
|
||||||
const ancestorScope = getAncestorScope(ancestors);
|
|
||||||
const propertyForest = docsToForest(properties);
|
|
||||||
if (propertyForest.length !== 1) {
|
|
||||||
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the ancestry and method scope in the context scope
|
|
||||||
Object.assign(actionContext.scope, ancestorScope, methodScope);
|
|
||||||
|
|
||||||
// Apply the top level property, it is responsible for applying its children
|
|
||||||
// recursively
|
|
||||||
await applyProperty(propertyForest[0], actionContext);
|
|
||||||
// Insert the log
|
|
||||||
actionContext.writeLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes ancestors are in tree order already
|
|
||||||
function getAncestorScope(ancestors) {
|
|
||||||
const scope = {};
|
|
||||||
ancestors.forEach(prop => {
|
|
||||||
scope[`#${prop.type}`] = prop;
|
|
||||||
});
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import '/imports/api/simpleSchemaConfig';
|
|
||||||
//import testTypes from './testTypes/index';
|
|
||||||
import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn';
|
|
||||||
import { doActionWork } from './doAction';
|
|
||||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
|
||||||
|
|
||||||
function cleanProp(prop) {
|
|
||||||
let schema = CreatureProperties.simpleSchema(prop);
|
|
||||||
return schema.clean(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanCreature(creature) {
|
|
||||||
let schema = Creatures.simpleSchema(creature);
|
|
||||||
return schema.clean(creature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fake ActionContext to test actions with
|
|
||||||
const creatureId = 'actionTestCreatureId';
|
|
||||||
const creatureName = 'Action Test Creature';
|
|
||||||
const testActionContext = {
|
|
||||||
creature: cleanCreature({
|
|
||||||
_id: creatureId,
|
|
||||||
}),
|
|
||||||
log: CreatureLogSchema.clean({
|
|
||||||
creatureId: creatureId,
|
|
||||||
creatureName: creatureName,
|
|
||||||
}),
|
|
||||||
scope: {},
|
|
||||||
addLog(content) {
|
|
||||||
if (content.name || content.value) {
|
|
||||||
this.log.content.push(content);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
writeLog: () => { },
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = cleanProp({
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
const actionAncestors = [];
|
|
||||||
|
|
||||||
describe('Do Action', function () {
|
|
||||||
it('Does an empty action', function () {
|
|
||||||
doActionWork({
|
|
||||||
properties: [action],
|
|
||||||
ancestors: actionAncestors,
|
|
||||||
actionContext: testActionContext,
|
|
||||||
methodScope: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//testTypes.forEach(test => it(test.text, test.fn));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Action utility functions', function () {
|
|
||||||
it('Triggers match tags', applyTriggers);
|
|
||||||
})
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|
||||||
import { set } from 'lodash';
|
|
||||||
|
|
||||||
// Reminder: throwStubExceptions: true is the default, and only
|
|
||||||
// possible when run() is not async
|
|
||||||
// For async run() stub exceptions never stop the client from sending
|
|
||||||
// the call to the server
|
|
||||||
|
|
||||||
// Dict of invocationId: {steps: {earlyAnswers, resolve, reject}}
|
|
||||||
// either resolve functions waiting for the user's input or early answers that were provided
|
|
||||||
// before the resolves could be set up
|
|
||||||
let userInputRequests = {};
|
|
||||||
let provideUserInput;
|
|
||||||
|
|
||||||
if (Meteor.isClient) {
|
|
||||||
provideUserInput = function (invocationId, step, answers, callback) {
|
|
||||||
Meteor.call('answerUserInputRequest', { invocationId, step, answers }, callback);
|
|
||||||
// Do the same work on the client without using a stub
|
|
||||||
answerInputRequestWork({ invocationId, step, answers });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { userInputRequests, provideUserInput };
|
|
||||||
|
|
||||||
export default async function getUserInput(questions, actionContext) {
|
|
||||||
// get the invocation details from the action context
|
|
||||||
const invocationId = actionContext.invocationId;
|
|
||||||
const step = actionContext.userInputStep;
|
|
||||||
actionContext.userInputStep += 1; // increment userInput step every time
|
|
||||||
|
|
||||||
// If the answers are already waiting, just return them
|
|
||||||
if (userInputRequests[invocationId]?.[step]?.earlyAnswers) {
|
|
||||||
return userInputRequests[invocationId][step].earlyAnswers;
|
|
||||||
}
|
|
||||||
// On the client, store the questions to be answered
|
|
||||||
if (Meteor.isClient) {
|
|
||||||
set(userInputRequests, `${invocationId}[${step}]`, { questions });
|
|
||||||
}
|
|
||||||
// Create a place for the answers to go when they are provided
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
set(userInputRequests, `${invocationId}[${step}]`, { resolve, reject });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function answerInputRequestWork({ invocationId, step, answers }) {
|
|
||||||
console.log('running answerUserInputRequest');
|
|
||||||
const invocation = userInputRequests[invocationId];
|
|
||||||
if (!invocation) {
|
|
||||||
// Call order on the server is guaranteed, so the invocation must have been created
|
|
||||||
// Before we can update it
|
|
||||||
throw new Meteor.Error('Not found', 'The method this answer is updating does not exist');
|
|
||||||
}
|
|
||||||
if (invocation[step]?.resolve) {
|
|
||||||
// If there is a resolve waiting for this response, provide it
|
|
||||||
invocation[step].resolve(answers);
|
|
||||||
} else {
|
|
||||||
// Otherwise just store the response as early answers
|
|
||||||
invocation[step] = {
|
|
||||||
earlyAnswers: answers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
|
||||||
// This function is not defined on the client so that it has no stub function
|
|
||||||
// This allows it to be called while still simulating an awaited async method
|
|
||||||
// See https://guide.meteor.com/2.8-migration.html#the-limitations
|
|
||||||
new ValidatedMethod({
|
|
||||||
name: 'answerUserInputRequest',
|
|
||||||
validate: new SimpleSchema({
|
|
||||||
invocationId: SimpleSchema.RegEx.Id,
|
|
||||||
step: SimpleSchema.Integer,
|
|
||||||
answers: {
|
|
||||||
type: Object,
|
|
||||||
blackbox: true,
|
|
||||||
},
|
|
||||||
}).validator(),
|
|
||||||
applyOptions: {
|
|
||||||
throwStubExceptions: false,
|
|
||||||
},
|
|
||||||
mixins: [RateLimiterMixin],
|
|
||||||
rateLimit: {
|
|
||||||
numRequests: 20,
|
|
||||||
timeInterval: 5000,
|
|
||||||
},
|
|
||||||
run: answerInputRequestWork,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import './doCastSpell';
|
|
||||||
import './doCheck';
|
|
||||||
@@ -163,12 +163,13 @@ export function getPropertyDescendants(creatureId, propertyId) {
|
|||||||
if (
|
if (
|
||||||
prop.left > property.left
|
prop.left > property.left
|
||||||
&& prop.right < property.right
|
&& prop.right < property.right
|
||||||
|
&& prop.removed !== true
|
||||||
) {
|
) {
|
||||||
props.push(prop);
|
props.push(prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cloneProps = EJSON.clone(props);
|
const cloneProps = EJSON.clone(props).sort((a, b) => a.left - b.left);
|
||||||
return cloneProps
|
return cloneProps;
|
||||||
} else {
|
} else {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
...getFilter.descendants(property),
|
...getFilter.descendants(property),
|
||||||
|
|||||||
@@ -315,13 +315,13 @@ export function renewDocIds({ docArray, collectionMap = {}, idMap = {} }) {
|
|||||||
// Give new ids and map the changes as {oldId: newId}
|
// Give new ids and map the changes as {oldId: newId}
|
||||||
docArray.forEach(doc => {
|
docArray.forEach(doc => {
|
||||||
const oldId = doc._id;
|
const oldId = doc._id;
|
||||||
const newId = idMap[oldId] || randomSrc.id();
|
const newId = oldId in idMap ? idMap[oldId] : randomSrc.id();
|
||||||
doc._id = newId;
|
doc._id = newId;
|
||||||
idMap[oldId] = newId;
|
idMap[oldId] = newId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the id from the map if it exists, leave unchanged otherwise
|
// Get the id from the map if it exists, leave unchanged otherwise
|
||||||
const remap = id => idMap[id] || id
|
const remap = id => id in idMap ? idMap[id] : id
|
||||||
|
|
||||||
// If there are references by id that need to be maintained when copying from
|
// If there are references by id that need to be maintained when copying from
|
||||||
// a library, here is where we would update them
|
// a library, here is where we would update them
|
||||||
|
|||||||
6
app/imports/api/utility/getPropertyTitle.ts
Normal file
6
app/imports/api/utility/getPropertyTitle.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||||
|
|
||||||
|
export default function getPropertyTitle(prop) {
|
||||||
|
if (prop.name) return prop.name;
|
||||||
|
return getPropertyName(prop.type);
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||||
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
|
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
|
||||||
import doAction from '/imports/api/engine/actions/doAction';
|
//TODO import doAction from '/imports/api/engine/actions/doAction';
|
||||||
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
||||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import doAction from '/imports/api/engine/actions/doAction';
|
//TODO import doAction from '/imports/api/engine/actions/doAction';
|
||||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||||
|
|
||||||
|
|||||||
@@ -153,3 +153,4 @@ export default {
|
|||||||
min-width: 42px;
|
min-width: 42px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../../../../../api/engine/action/methods/doCheck
|
||||||
@@ -106,4 +106,4 @@ export default {
|
|||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>../../../../../api/engine/action/methods/doCheck
|
||||||
@@ -84,4 +84,4 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>../../../../../api/engine/action/methods/doCastSpell
|
||||||
@@ -138,3 +138,4 @@ export default {
|
|||||||
color: rgba(0, 0, 0, 0.54) !important;
|
color: rgba(0, 0, 0, 0.54) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../../../../../api/engine/action/methods/doCheck
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyViewerMixin from '/imports/client/ui/properties/viewers/shared/propertyViewerMixin';
|
import propertyViewerMixin from '/imports/client/ui/properties/viewers/shared/propertyViewerMixin';
|
||||||
import doAction from '/imports/api/engine/actions/doAction';
|
//TODO import doAction from '/imports/api/engine/actions/doAction';
|
||||||
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
import ActionConditionView from '/imports/client/ui/properties/components/actions/ActionConditionView.vue';
|
||||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
@@ -254,3 +254,4 @@ export default {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
../../../../api/engine/action/methods/doCastSpell
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||||
import doAction from '/imports/api/engine/actions/doAction.js';
|
//TODO import doAction from '/imports/api/engine/actions/doAction.js';
|
||||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const accessor = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
toString(node) {
|
toString(node) {
|
||||||
if (!node.path) return `${node.name}`;
|
if (!node.path?.length) return `${node.name}`;
|
||||||
return `${node.name}.${node.path.join('.')}`;
|
return `${node.name}.${node.path.join('.')}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user