Tested and fixed adjustments

This commit is contained in:
ThaumRystra
2023-11-17 17:34:48 +02:00
parent 95c3e882d7
commit 59a9433dc7
3 changed files with 193 additions and 78 deletions

View File

@@ -22,11 +22,16 @@ let LogContentSchema = new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
// This log entry was silenced
silenced: {
type: Boolean,
optional: true,
},
context: { context: {
type: Object, type: Object,
optional: true, optional: true,
}, },
'context.errors':{ 'context.errors': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.errorCount, maxCount: STORAGE_LIMITS.errorCount,

View File

@@ -8,7 +8,7 @@ import rollDice from '/imports/parser/rollDice';
const Actions = new Mongo.Collection<ActionWithId>('actions'); const Actions = new Mongo.Collection<ActionWithId>('actions');
interface Action { export interface Action {
creatureId: string; creatureId: string;
rootPropId: string; rootPropId: string;
targetIds?: string[]; targetIds?: string[];
@@ -27,6 +27,7 @@ interface ActionWithId extends Action {
type Task = { type Task = {
propId: string; propId: string;
targetIds: string[]; targetIds: string[];
step?: number,
} }
type TaskResult = { type TaskResult = {
@@ -34,17 +35,11 @@ type TaskResult = {
targetIds: string[]; targetIds: string[];
scope: any; scope: any;
mutations: Mutation[]; mutations: Mutation[];
step?: number;
deferred?: boolean;
deferredState?: any;
} }
class PartialTaskResult { class PartialTaskResult {
scope: any; scope: any;
mutations: Mutation[]; mutations: Mutation[];
step?: number;
deferred?: boolean;
deferredState?: any;
constructor() { constructor() {
this.scope = {}; this.scope = {};
this.mutations = []; this.mutations = [];
@@ -66,17 +61,19 @@ type Mutation = {
// Which creatures the mutation is applied to // Which creatures the mutation is applied to
targetIds: string[]; targetIds: string[];
// What changes in the database // What changes in the database
updates?: { updates?: Update[];
propId: string;
set?: any;
inc?: any;
type: string,
}[];
// Logged when this is applied // Logged when this is applied
contents?: LogContent[]; contents?: LogContent[];
} }
type LogContent = { export type Update = {
propId: string;
type: string,
set?: any;
inc?: any;
}
export type LogContent = {
name?: string; name?: string;
value?: string; value?: string;
inline?: boolean; inline?: boolean;
@@ -123,6 +120,10 @@ const ActionSchema = new SimpleSchema({
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
'taskQueue.$.step': {
type: Number,
optional: true,
},
'taskQueue.$.targetIds': { 'taskQueue.$.targetIds': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
@@ -202,11 +203,20 @@ const ActionSchema = new SimpleSchema({
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
// Required, because CreatureProperties.update requires a selector of { type }
'results.$.mutations.$.updates.$.type': {
type: String,
},
'results.$.mutations.$.updates.$.set': { 'results.$.mutations.$.updates.$.set': {
type: Object, type: Object,
optional: true, optional: true,
blackbox: true, blackbox: true,
}, },
'results.$.mutations.$.updates.$.inc': {
type: Object,
optional: true,
blackbox: true,
},
'results.$.mutations.$.contents': { 'results.$.mutations.$.contents': {
type: Array, type: Array,
optional: true, optional: true,
@@ -262,9 +272,13 @@ export async function runAction(actionId: string, userInput?) {
return writePromise; return writePromise;
} }
async function applyNextTask(action, userInput?) { // TODO create a function to get the effective value of a property,
// simulating all the result updates in the action so far
async function applyNextTask(action: Action, userInput?) {
// Get the next task // Get the next task
const task = action.taskQueue[0]; const task = action.taskQueue.shift();
if (!task) throw 'Next task does not exist';
// Get the property from the action's task properties or the creature's properties // Get the property from the action's task properties or the creature's properties
let prop; let prop;
const taskProp = action.taskProperties[task.propId]; const taskProp = action.taskProperties[task.propId];
@@ -278,26 +292,15 @@ async function applyNextTask(action, userInput?) {
if (prop.deactivatedByToggle) return; if (prop.deactivatedByToggle) return;
// Apply the property // Apply the property
const result: TaskResult | undefined = await applyPropertyByType[prop.type]?.(prop, task, action, userInput); const result: PartialTaskResult = await applyPropertyByType[prop.type]?.(prop, task, action, userInput);
// store the task's details and save the result
if (result) { result.scope[`#${prop.type}`] = prop;
// store the task's details and save the result action.results.push({
result.scope[`#${prop.type}`] = prop; propId: task.propId,
result.propId = task.propId; targetIds: task.targetIds,
result.targetIds = task.targetIds; scope: result.scope,
if (result.deferred) { mutations: result.mutations,
delete result.deferred; });
result.step = (result.step || 0) + 1;
action.deferredResults[task.propId] = result;
} else {
// There was a result and it wasn't deferred, we can remove this task from the queue
action.taskQueue.shift();
action.results.push(result);
}
} else if (!action.userInputNeeded) {
// Prevent accidental infinite loops if we don't remove the task, but also don't break for input
throw 'The only time result can be undefined is if we are waiting for user input';
}
} }
function writeChangedAction(original: ActionWithId, changed: ActionWithId) { function writeChangedAction(original: ActionWithId, changed: ActionWithId) {
@@ -360,15 +363,21 @@ function createResult(): PartialTaskResult {
} }
// Combine all the action results into the scope at present // Combine all the action results into the scope at present
export function getEffectiveActionScope(action) { export function getEffectiveActionScope(action: Action) {
const scope = getVariables(action.creatureId); const scope = getVariables(action.creatureId);
// First combine the applied results
for (const result of action.results) { for (const result of action.results) {
Object.assign(scope, result.scope); Object.assign(scope, result.scope);
} }
// Then the deferred results
// Warning: order is not guaranteed here
for (const id in action.deferredResults) {
const result = action.deferredResults[id];
Object.assign(scope, result.scope);
}
return scope; return scope;
} }
type DamageProp = { type DamageProp = {
_id?: string; _id?: string;
operation: 'increment' | 'set'; operation: 'increment' | 'set';
@@ -547,18 +556,30 @@ const applyPropertyByType = {
} }
break; break;
case 'choice': { case 'choice': {
// If there is no input to consume, return no result, but mark the action as requiring input // Step 0, halt the action to get user input
if (!userInput) { if (!task.step) {
// Mark the action as needing user input so that it halts
action.userInputNeeded = pick(prop, ['_id', 'type', 'branchType']); action.userInputNeeded = pick(prop, ['_id', 'type', 'branchType']);
return; // Put this task back in the queue, but at step 1
action.taskQueue.push({
...task,
step: 1,
});
return result;
} }
const children = await getPropertyChildren(action.creatureId, prop._id); // Step 1 consume the user input
let index = userInput.choice; else if (task.step === 1) {
if (!isFinite(index) || index < 0) index = 0; if (!userInput) {
if (index > children.length - 1) index = children.length - 1; throw 'User input was required for this step'
pushPropAndTriggers(action, children[index], targets); }
pushAfterChildrenTriggers(action, prop, targets); const children = await getPropertyChildren(action.creatureId, prop._id);
break; let index = userInput.choice;
if (!isFinite(index) || index < 0) index = 0;
if (index > children.length - 1) index = children.length - 1;
pushPropAndTriggers(action, children[index], targets);
pushAfterChildrenTriggers(action, prop, targets);
}
return result;
} }
} }
@@ -566,9 +587,7 @@ const applyPropertyByType = {
}, },
async adjustment(prop, task: Task, action: Action): Promise<PartialTaskResult> { async adjustment(prop, task: Task, action: Action): Promise<PartialTaskResult> {
const result = createResult();
let result = action.deferredResults[task.propId];
if (!result) result = createResult();
const queueChildren = async function (targetIds) { const queueChildren = async function (targetIds) {
await pushChildren(action, prop, targetIds); await pushChildren(action, prop, targetIds);
@@ -576,9 +595,10 @@ const applyPropertyByType = {
} }
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds; const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
task.targetIds = damageTargets;
// Step 0, get the operation and value and push the damage pseudo prop to the queue // Step 0, get the operation and value and push the damage pseudo prop to the queue
if (!result.step) { if (!task.step) {
if (!prop.amount) { if (!prop.amount) {
queueChildren(task.targetIds); queueChildren(task.targetIds);
@@ -587,7 +607,6 @@ const applyPropertyByType = {
// Evaluate the amount // Evaluate the amount
recalculateCalculation(prop.amount, action, 'reduce'); recalculateCalculation(prop.amount, action, 'reduce');
const value = +prop.amount.value; const value = +prop.amount.value;
if (!isFinite(value)) { if (!isFinite(value)) {
queueChildren(task.targetIds); queueChildren(task.targetIds);
@@ -596,7 +615,6 @@ const applyPropertyByType = {
if (damageTargets?.length) { if (damageTargets?.length) {
for (const targetId of damageTargets) { for (const targetId of damageTargets) {
queueChildren([targetId]);
const statId = getVariables(targetId)?.[prop.stat]?._propId; const statId = getVariables(targetId)?.[prop.stat]?._propId;
if (!statId) continue; if (!statId) continue;
@@ -610,22 +628,37 @@ const applyPropertyByType = {
}, [targetId]); }, [targetId]);
continue; continue;
} }
// Do the damage
pushDamagePropertyTasks(action, { pushDamagePropertyTasks(action, {
type: 'damageProp', type: 'damageProp',
value, value,
operation: prop.operation, operation: prop.operation,
targetPropId: stat._id, targetPropId: stat._id,
}, stat, [targetId], result); }, stat, [targetId], result);
// Do the next step of this property
action.taskQueue.push({
...task,
step: 1,
});
} }
} }
result.deferred = true;
result.deferredState = { value };
} }
// Step 1, Log the results // Step 1, Log the results
else if (result.step === 1) { else if (task.step === 1) {
const value = result.deferredState.value; const scope = getEffectiveActionScope(action);
let value;
if (prop.operation === 'increment') {
if (prop.value >= 0) {
value = scope['~damage']?.value;
} else {
value = -scope['~healing']?.value;
}
} else {
value = scope['~set']?.value;
}
if (damageTargets?.length) { if (damageTargets?.length) {
for (const targetId of damageTargets) { for (const targetId of damageTargets) {
await queueChildren([targetId]);
result.appendLog({ result.appendLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
@@ -635,6 +668,7 @@ const applyPropertyByType = {
}, [targetId]); }, [targetId]);
} }
} else { } else {
await queueChildren(task.targetIds);
result.appendLog({ result.appendLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
@@ -662,7 +696,6 @@ const applyPropertyByType = {
} else { } else {
value = scope['~set']?.value; value = scope['~set']?.value;
} }
let damage, newValue, increment; let damage, newValue, increment;
if (task.targetIds.length) { if (task.targetIds.length) {
for (const targetId of task.targetIds) { for (const targetId of task.targetIds) {

View File

@@ -4,7 +4,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn';
import Creatures from '/imports/api/creature/creatures/Creatures'; import Creatures from '/imports/api/creature/creatures/Creatures';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import Actions, { createAction, runAction } from '/imports/api/engine/actions/Actions'; import Actions, { Action, Update, LogContent, createAction, runAction } from '/imports/api/engine/actions/Actions';
import computeCreature from '/imports/api/engine/computeCreature'; import computeCreature from '/imports/api/engine/computeCreature';
let creatureId; let creatureId;
@@ -23,31 +23,60 @@ describe('Interrupt action system', function () {
computeCreature(creatureId); computeCreature(creatureId);
}); });
it('writes notes to the log', async function () { it('writes notes to the log', async function () {
assert.equal( const action = await runActionById(note1Id);
await testRunActionById(note1Id), assert.deepEqual(
'Note 1 summary. 1 + 1 = 2' allLogContent(action),
[{ value: 'Note 1 summary. 1 + 1 = 2' }]
); );
}); });
it('Applies the children of if branches', async function () { it('Applies the children of if branches', async function () {
assert.equal( let action = await runActionById(ifTruthyBranchId);
await testRunActionById(ifTruthyBranchId), assert.deepEqual(
'child of if branch' allLogContent(action),
[{ value: 'child of if branch' }]
); );
assert.isUndefined( action = await runActionById(ifFalsyBranchId);
await testRunActionById(ifFalsyBranchId) assert.deepEqual(
allLogContent(action),
[]
); );
}); });
it('Applies the children of index branches', async function () { it('Applies the children of index branches', async function () {
assert.equal( const action = await runActionById(indexBranchId);
await testRunActionById(indexBranchId), assert.deepEqual(
'child 2 of index branch' allLogContent(action),
[{ value: 'child 2 of index branch' }]
); );
}); });
it('Halts execution of choice branches', async function () { it('Halts execution of choice branches', async function () {
const action = await runActionById(choiceBranchId); const action = await runActionById(choiceBranchId);
if (!action) throw 'Action is expected to exist';
assert.isUndefined(action.results[0]);
assert.exists(action.userInputNeeded); assert.exists(action.userInputNeeded);
assert.deepEqual(
allLogContent(action),
[]
);
});
it('Applies adjustments', async function () {
let action = await runActionById(adjustmentSetId)
assert.deepEqual(
allUpdates(action),
[{
propId: adjustedStatId,
type: 'attribute',
set: { damage: 5, value: 3 },
}],
'Applying set adjustments should return the correct updates'
);
action = await runActionById(adjustmentIncrementId)
assert.deepEqual(
allUpdates(action),
[{
propId: adjustedStatId,
type: 'attribute',
inc: { damage: 2, value: -2 }, // damage goes up by 2, value down by 2
}],
'Applying increment adjustments should return the correct updates'
);
}); });
}); });
@@ -56,15 +85,35 @@ async function runActionById(propId) {
const actionId = await createAction(prop); const actionId = await createAction(prop);
await runAction(actionId); await runAction(actionId);
const action = await Actions.findOneAsync(actionId); const action = await Actions.findOneAsync(actionId);
if (!action) throw 'Action is expected to exist'
return action; return action;
} }
async function testRunActionById(propId) { function allUpdates(action: Action) {
const action = await runActionById(propId); const updates: Update[] = [];
return action?.results?.[action.results.length - 1]?.mutations?.[0]?.contents?.[0]?.value; action.results.forEach(result => {
result.mutations.forEach(mutation => {
mutation.updates?.forEach(update => {
updates.push(update);
});
});
});
return updates;
} }
let note1Id, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId; function allLogContent(action: Action) {
const contents: LogContent[] = [];
action.results.forEach(result => {
result.mutations.forEach(mutation => {
mutation.contents?.forEach(logContent => {
contents.push(logContent);
});
});
});
return contents;
}
let note1Id, ifTruthyBranchId, ifFalsyBranchId, indexBranchId, choiceBranchId, adjustedStatId, adjustmentIncrementId, adjustmentSetId;
const propForest = [ const propForest = [
// Apply a simple note // Apply a simple note
@@ -114,6 +163,34 @@ const propForest = [
{ type: 'note', summary: { text: 'child 3 of choice branch' } }, { type: 'note', summary: { text: 'child 3 of choice branch' } },
], ],
}, },
// Apply adjustments
{
_id: adjustedStatId = Random.id(),
type: 'attribute',
attributeType: 'stat',
variableName: 'adjustedStat',
baseValue: { calculation: '8' },
}, {
_id: adjustmentSetId = Random.id(),
type: 'adjustment',
stat: 'adjustedStat',
operation: 'set',
amount: { calculation: '3' },
target: 'self',
children: [
{ type: 'note', summary: { text: 'adjustment set applied' } },
],
}, {
_id: adjustmentIncrementId = Random.id(),
type: 'adjustment',
stat: 'adjustedStat',
operation: 'increment',
amount: { calculation: '2' },
target: 'self',
children: [
{ type: 'note', summary: { text: 'adjustment increment applied' } },
],
},
]; ];
function insertActionTestProps() { function insertActionTestProps() {