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 => {
|
||||
let schema = LibraryNodes.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
|
||||
@@ -226,5 +226,3 @@ Creatures.attachSchema(CreatureSchema);
|
||||
|
||||
export default Creatures;
|
||||
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 Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
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 { 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 targetId = Random.id();
|
||||
|
||||
describe('Interrupt action system', function () {
|
||||
let unload: (() => void) | undefined = undefined;
|
||||
const dummySubscription = {
|
||||
onStop(fn) {
|
||||
unload = fn;
|
||||
}
|
||||
};
|
||||
const dummySubscription = Tracker.autorun(() => undefined)
|
||||
this.timeout(8000);
|
||||
before(async function () {
|
||||
// Remove old data
|
||||
await Promise.all([
|
||||
@@ -48,7 +46,7 @@ describe('Interrupt action system', function () {
|
||||
loadCreature(creatureId, dummySubscription);
|
||||
});
|
||||
after(function () {
|
||||
unload?.();
|
||||
dummySubscription.stop();
|
||||
});
|
||||
it('writes notes to the log', async function () {
|
||||
const action = await runActionById(note1Id);
|
||||
@@ -86,7 +84,7 @@ describe('Interrupt action system', function () {
|
||||
it('Halts execution of choice branches', async function () {
|
||||
let userInputRequested = false;
|
||||
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');
|
||||
});
|
||||
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?) {
|
||||
const action: Action = {
|
||||
const action: EngineAction = {
|
||||
creatureId: prop.root.id,
|
||||
rootPropId: prop._id,
|
||||
results: [],
|
||||
taskCount: 0,
|
||||
targetIds,
|
||||
};
|
||||
return Actions.insertAsync(action);
|
||||
return EngineActions.insertAsync(action);
|
||||
}
|
||||
|
||||
async function runActionById(propId, userInputFn = () => 0) {
|
||||
const prop = await CreatureProperties.findOneAsync(propId);
|
||||
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';
|
||||
await applyAction(action, userInputFn, { simulate: true });
|
||||
return action;
|
||||
}
|
||||
|
||||
function allUpdates(action: Action) {
|
||||
function allUpdates(action: EngineAction) {
|
||||
const updates: Update[] = [];
|
||||
action.results.forEach(result => {
|
||||
result.mutations.forEach(mutation => {
|
||||
@@ -158,7 +229,31 @@ function allUpdates(action: Action) {
|
||||
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[] = [];
|
||||
action.results.forEach(result => {
|
||||
result.mutations.forEach(mutation => {
|
||||
@@ -170,8 +265,9 @@ function allLogContent(action: Action) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId, adjustedStatId,
|
||||
adjustmentIncrementId, adjustmentSetId, rollId;
|
||||
let note1Id, folderId, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId,
|
||||
adjustedStatId, adjustmentIncrementId, adjustmentSetId, rollId, buffId,
|
||||
removeParentBuffId, removeTaggedBuffsId, removeOneTaggedBuffId, taggedBuffId, secondTaggedBuffId, buffAttChildId;
|
||||
|
||||
const propForest = [
|
||||
// Apply a simple note
|
||||
@@ -255,6 +351,63 @@ const propForest = [
|
||||
{ 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
|
||||
{
|
||||
_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 applyProperty from '../applyProperty';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
||||
import { getPropertyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove';
|
||||
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||
import TaskResult from 'imports/api/engine/action/tasks/TaskResult';
|
||||
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||
import { findLast, filter, difference, intersection } from 'lodash';
|
||||
import { getPropertiesOfType, getPropertyAncestors } from '/imports/api/engine/loadCreatures';
|
||||
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) {
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
export default function applyBuffRemoverProperty(
|
||||
task: PropTask, action: EngineAction, result: TaskResult, userInput
|
||||
) {
|
||||
const prop = task.prop;
|
||||
|
||||
const prop = node.doc
|
||||
|
||||
// Log Name
|
||||
if (prop.name && !prop.silent) {
|
||||
actionContext.addLog({ name: prop.name });
|
||||
// Log Name
|
||||
result.appendLog({
|
||||
name: getPropertyTitle(prop),
|
||||
}, task.targetIds)
|
||||
}
|
||||
|
||||
// Remove buffs
|
||||
if (prop.targetParentBuff) {
|
||||
// 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');
|
||||
if (!nearestBuff) {
|
||||
actionContext.addLog({
|
||||
result.appendLog({
|
||||
name: 'Error',
|
||||
value: 'Buff remover does not have a parent buff to remove',
|
||||
});
|
||||
}, task.targetIds);
|
||||
return;
|
||||
}
|
||||
removeBuff(nearestBuff, actionContext, prop);
|
||||
removeBuff(nearestBuff, prop, result);
|
||||
} else {
|
||||
// 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 => {
|
||||
if (buff.inactive) return false;
|
||||
if (buffRemoverMatchTags(prop, buff)) return true;
|
||||
@@ -42,7 +43,7 @@ export default function applyBuffRemover(node, actionContext) {
|
||||
if (prop.removeAll) {
|
||||
// Remove all matching buffs
|
||||
targetedBuffs.forEach(buff => {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
removeBuff(buff, prop, result);
|
||||
});
|
||||
} else {
|
||||
// Sort in reverse order
|
||||
@@ -50,19 +51,23 @@ export default function applyBuffRemover(node, actionContext) {
|
||||
// Remove the one with the highest order
|
||||
const buff = targetedBuffs[0];
|
||||
if (buff) {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
removeBuff(buff, prop, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
applyChildren(node, actionContext);
|
||||
applyDefaultAfterPropTasks(action, prop, task.targetIds, userInput);
|
||||
}
|
||||
|
||||
function removeBuff(buff, actionContext, prop) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Removed',
|
||||
value: `${buff.name || 'Buff'}`
|
||||
function removeBuff(buff: any, prop, result: TaskResult) {
|
||||
result.mutations.push({
|
||||
targetIds: result.targetIds,
|
||||
removals: [{ propId: buff._id }],
|
||||
contents: [{
|
||||
name: 'Removed',
|
||||
value: `${buff.name || 'Buff'}`,
|
||||
silenced: prop.silent,
|
||||
}],
|
||||
});
|
||||
softRemove({ _id: buff._id, collection: CreatureProperties });
|
||||
}
|
||||
|
||||
function buffRemoverMatchTags(buffRemover, prop) {
|
||||
@@ -1,16 +1,4 @@
|
||||
import { some, intersection, difference, remove, includes } from 'lodash';
|
||||
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';
|
||||
// TODO
|
||||
|
||||
export default function applyDamage(node, 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';
|
||||
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';
|
||||
// TODO
|
||||
|
||||
export default function applySavingThrow(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -1,6 +1,4 @@
|
||||
import recalculateCalculation from './shared/recalculateCalculation';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
|
||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
|
||||
// TODO
|
||||
|
||||
export default function applyToggle(node, 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 {
|
||||
aggregateCalculationEffects,
|
||||
@@ -7,12 +6,14 @@ import {
|
||||
} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
|
||||
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||
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
|
||||
// Redo the work of imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js
|
||||
// TODO Redo the work of
|
||||
// imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js
|
||||
// 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;
|
||||
calcObj._parseLevel = parseLevel;
|
||||
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 ActionContext from '/imports/api/engine/actions/ActionContext';
|
||||
|
||||
// TODO Migrate this to the new action engine
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCastSpell',
|
||||
validate: new SimpleSchema({
|
||||
@@ -7,9 +7,11 @@ import rollDice from '/imports/parser/rollDice';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
|
||||
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';
|
||||
|
||||
// TODO Migrate this to the new action engine
|
||||
|
||||
const doCheck = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCheck',
|
||||
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 (
|
||||
prop.left > property.left
|
||||
&& prop.right < property.right
|
||||
&& prop.removed !== true
|
||||
) {
|
||||
props.push(prop);
|
||||
}
|
||||
}
|
||||
const cloneProps = EJSON.clone(props);
|
||||
return cloneProps
|
||||
const cloneProps = EJSON.clone(props).sort((a, b) => a.left - b.left);
|
||||
return cloneProps;
|
||||
} else {
|
||||
return CreatureProperties.find({
|
||||
...getFilter.descendants(property),
|
||||
|
||||
@@ -315,13 +315,13 @@ export function renewDocIds({ docArray, collectionMap = {}, idMap = {} }) {
|
||||
// Give new ids and map the changes as {oldId: newId}
|
||||
docArray.forEach(doc => {
|
||||
const oldId = doc._id;
|
||||
const newId = idMap[oldId] || randomSrc.id();
|
||||
const newId = oldId in idMap ? idMap[oldId] : randomSrc.id();
|
||||
doc._id = newId;
|
||||
idMap[oldId] = newId;
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||
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 AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<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 { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
|
||||
|
||||
|
||||
@@ -153,3 +153,4 @@ export default {
|
||||
min-width: 42px;
|
||||
}
|
||||
</style>
|
||||
../../../../../api/engine/action/methods/doCheck
|
||||
@@ -106,4 +106,4 @@ export default {
|
||||
min-width: 72px;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
../../../../../api/engine/action/methods/doCheck
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
<script lang="js">
|
||||
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 AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
@@ -254,3 +254,4 @@ export default {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
../../../../api/engine/action/methods/doCastSpell
|
||||
@@ -153,7 +153,7 @@
|
||||
<script lang="js">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.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 ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
|
||||
@@ -93,7 +93,7 @@ const accessor = {
|
||||
}
|
||||
},
|
||||
toString(node) {
|
||||
if (!node.path) return `${node.name}`;
|
||||
if (!node.path?.length) return `${node.name}`;
|
||||
return `${node.name}.${node.path.join('.')}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user