467 lines
12 KiB
TypeScript
467 lines
12 KiB
TypeScript
import { assert } from 'chai';
|
||
import {
|
||
allLogContent,
|
||
allMutations,
|
||
allUpdates,
|
||
createTestCreature,
|
||
getRandomIds,
|
||
removeAllCreaturesAndProps,
|
||
runActionById,
|
||
TestCreature
|
||
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
|
||
import { LogContent, Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
|
||
import Alea from 'alea';
|
||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||
|
||
const [
|
||
creatureId, targetCreatureId, targetCreature2Id, emptyActionId, selfActionId, attackActionId,
|
||
usesActionId, attackMissId, attackNoTargetId, usesResourcesActionId, ammoId, resourceAttId,
|
||
consumeAmmoId, consumeResourceId, noUsesActionId, insufficientResourcesActionId,
|
||
attributeResetByEventId, eventActionId, advantageAttackId, advantageEffectId, disadvantageAttackId, disadvantageEffectId,
|
||
] = getRandomIds(100);
|
||
|
||
const actionTestCreature: TestCreature = {
|
||
_id: creatureId,
|
||
props: [
|
||
// Empty
|
||
{
|
||
_id: emptyActionId,
|
||
type: 'action',
|
||
summary: { text: 'Summary text 1 + 1 = {1 + 1}' }
|
||
},
|
||
// Attack that targets self
|
||
{
|
||
_id: selfActionId,
|
||
type: 'action',
|
||
target: 'self',
|
||
},
|
||
// Attack that hits
|
||
{
|
||
_id: attackActionId,
|
||
type: 'action',
|
||
attackRoll: { calculation: '10' },
|
||
},
|
||
// Attack that misses
|
||
{
|
||
_id: attackMissId,
|
||
type: 'action',
|
||
attackRoll: { calculation: '-5' },
|
||
},
|
||
// Attack that has Advantage
|
||
{
|
||
_id: advantageAttackId,
|
||
type: 'action',
|
||
attackRoll: { calculation: '0' },
|
||
tags: ['hasAdvantage'],
|
||
},
|
||
{
|
||
_id: advantageEffectId,
|
||
type: 'effect',
|
||
operation: 'advantage',
|
||
targetByTags: true,
|
||
targetTags: ['hasAdvantage'],
|
||
},
|
||
// Attack that has Disadvantage
|
||
{
|
||
_id: disadvantageAttackId,
|
||
type: 'action',
|
||
attackRoll: { calculation: '0' },
|
||
tags: ['hasDisadvantage'],
|
||
},
|
||
{
|
||
_id: disadvantageEffectId,
|
||
type: 'effect',
|
||
operation: 'disadvantage',
|
||
targetByTags: true,
|
||
targetTags: ['hasDisadvantage'],
|
||
},
|
||
// Attack that has no target
|
||
{
|
||
_id: attackNoTargetId,
|
||
type: 'action',
|
||
attackRoll: { calculation: '1' },
|
||
},
|
||
// Disable crits
|
||
{
|
||
type: 'attribute',
|
||
attributeType: 'stat',
|
||
variableName: '~criticalHitTarget',
|
||
baseValue: { calculation: '21' },
|
||
},
|
||
{
|
||
type: 'attribute',
|
||
attributeType: 'stat',
|
||
variableName: '~criticalMissTarget',
|
||
baseValue: { calculation: '0' },
|
||
},
|
||
// Has uses
|
||
{
|
||
_id: usesActionId,
|
||
type: 'action',
|
||
uses: { calculation: '3' },
|
||
usesUsed: 1,
|
||
reset: 'longRest',
|
||
},
|
||
// Not enough uses
|
||
{
|
||
_id: noUsesActionId,
|
||
type: 'action',
|
||
uses: { calculation: '5' },
|
||
usesUsed: 5,
|
||
reset: 'longRest',
|
||
},
|
||
// Uses Resources
|
||
{
|
||
_id: ammoId,
|
||
type: 'item',
|
||
quantity: 12,
|
||
tags: ['ammo']
|
||
},
|
||
{
|
||
_id: resourceAttId,
|
||
type: 'attribute',
|
||
name: 'Resource Name',
|
||
attributeType: 'stat',
|
||
baseValue: { calculation: '7' },
|
||
variableName: 'resourceVar',
|
||
},
|
||
{
|
||
_id: usesResourcesActionId,
|
||
type: 'action',
|
||
resources: {
|
||
itemsConsumed: [{
|
||
_id: consumeAmmoId,
|
||
tag: 'ammo',
|
||
quantity: { calculation: '3' },
|
||
itemId: ammoId,
|
||
}],
|
||
attributesConsumed: [{
|
||
_id: consumeResourceId,
|
||
variableName: 'resourceVar',
|
||
quantity: { calculation: '2' },
|
||
}],
|
||
conditions: [],
|
||
}
|
||
},
|
||
{
|
||
_id: insufficientResourcesActionId,
|
||
type: 'action',
|
||
resources: {
|
||
attributesConsumed: [{
|
||
_id: consumeResourceId,
|
||
variableName: 'resourceVar',
|
||
quantity: { calculation: '9001' },
|
||
}],
|
||
itemsConsumed: [],
|
||
conditions: [],
|
||
}
|
||
},
|
||
// Events and resetting attributes
|
||
{
|
||
_id: attributeResetByEventId,
|
||
type: 'attribute',
|
||
name: 'Attribute Reset By testEvent Event',
|
||
attributeType: 'stat',
|
||
baseValue: { calculation: '27' },
|
||
damage: 13,
|
||
variableName: 'resetByEventAtt',
|
||
reset: 'testEvent'
|
||
},
|
||
{
|
||
_id: eventActionId,
|
||
type: 'action',
|
||
actionType: 'event',
|
||
variableName: 'testEvent',
|
||
},
|
||
],
|
||
}
|
||
|
||
const actionTargetCreature: TestCreature = {
|
||
_id: targetCreatureId,
|
||
props: [
|
||
{
|
||
type: 'attribute',
|
||
attributeType: 'stat',
|
||
variableName: 'armor',
|
||
baseValue: { calculation: '10' },
|
||
}
|
||
]
|
||
}
|
||
|
||
const actionTargetCreature2: TestCreature = {
|
||
_id: targetCreature2Id,
|
||
props: [
|
||
{
|
||
type: 'attribute',
|
||
attributeType: 'stat',
|
||
variableName: 'armor',
|
||
baseValue: { calculation: '10' },
|
||
}
|
||
]
|
||
}
|
||
|
||
describe('Apply Action Properties', function () {
|
||
// Increase timeout
|
||
this.timeout(8000);
|
||
|
||
before(async function () {
|
||
await removeAllCreaturesAndProps();
|
||
await createTestCreature(actionTestCreature);
|
||
await createTestCreature(actionTargetCreature);
|
||
await createTestCreature(actionTargetCreature2);
|
||
});
|
||
|
||
it('should generate random numbers reliably given consistent seeds', function () {
|
||
const aleaFraction = Alea('test', 'seeds');
|
||
const randomNumbers = [aleaFraction(), aleaFraction(), aleaFraction()];
|
||
assert.deepEqual(randomNumbers, [
|
||
0.19889510236680508, 0.9176857066340744, 0.042551583144813776
|
||
]);
|
||
});
|
||
|
||
it('should run empty actions', async function () {
|
||
const action = await runActionById(emptyActionId);
|
||
assert.exists(action);
|
||
assert.deepEqual(allMutations(action), [{
|
||
contents: [{
|
||
name: 'Action',
|
||
value: 'Summary text 1 + 1 = 2',
|
||
}],
|
||
targetIds: [],
|
||
}]);
|
||
});
|
||
|
||
it('should target self when set', async function () {
|
||
const action = await runActionById(selfActionId);
|
||
assert.exists(action);
|
||
assert.deepEqual(allMutations(action), [{
|
||
contents: [{
|
||
name: 'Action',
|
||
}],
|
||
targetIds: [creatureId],
|
||
}]);
|
||
});
|
||
|
||
it('should make attack rolls against no targets', async function () {
|
||
const action = await runActionById(attackNoTargetId, []);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: [],
|
||
}, {
|
||
contents: [{
|
||
name: 'To Hit',
|
||
value: '1d20 [10] + 1\n**11**',
|
||
inline: true,
|
||
}],
|
||
targetIds: [],
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
})
|
||
|
||
it('should make attack rolls against multiple creatures', async function () {
|
||
const action = await runActionById(attackActionId, [
|
||
targetCreatureId,
|
||
targetCreature2Id,
|
||
]);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: [targetCreatureId, targetCreature2Id]
|
||
}, {
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Hit!',
|
||
value: '1d20 [10] + 10\n**20**',
|
||
}],
|
||
targetIds: [targetCreatureId],
|
||
}, {
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Hit!',
|
||
value: '1d20 [10] + 10\n**20**',
|
||
}],
|
||
targetIds: [targetCreature2Id],
|
||
},
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('should make attack rolls that use uses', async function () {
|
||
const action = await runActionById(usesActionId, [targetCreatureId]);
|
||
const expectedUpdates: Update[] = [
|
||
{
|
||
propId: usesActionId,
|
||
type: 'action',
|
||
inc: { usesUsed: 1, usesLeft: -1 },
|
||
}
|
||
]
|
||
assert.deepEqual(allUpdates(action), expectedUpdates);
|
||
});
|
||
|
||
it('should fail to make attacks that have no uses left', async function () {
|
||
const action = await runActionById(noUsesActionId, [targetCreatureId]);
|
||
const expectedContent: LogContent[] = [
|
||
{
|
||
name: 'Action'
|
||
}, {
|
||
name: 'Error',
|
||
value: 'Action does not have enough uses left'
|
||
}
|
||
]
|
||
assert.deepEqual(allLogContent(action), expectedContent);
|
||
});
|
||
|
||
it('should make attack rolls that miss', async function () {
|
||
const action = await runActionById(attackMissId, [targetCreatureId]);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: [targetCreatureId],
|
||
}, {
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Miss!',
|
||
value: '1d20 [10] − 5\n**5**', // DiceCloud uses unicode minus
|
||
}],
|
||
targetIds: [targetCreatureId],
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('should make attack rolls that roll with advantage', async function () {
|
||
const prop = await CreatureProperties.findOneAsync(advantageAttackId);
|
||
assert(prop);
|
||
assert(prop.type === 'action')
|
||
assert.equal(prop.attackRoll?.advantage, 1, 'The attack roll should have advantage');
|
||
const action = await runActionById(advantageAttackId, [targetCreatureId]);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: [targetCreatureId],
|
||
}, {
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Hit! (Advantage)',
|
||
value: '1d20 [ ~~10~~, 11 ] + 0\n**11**',
|
||
}],
|
||
targetIds: [targetCreatureId],
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('should make attack rolls that roll with disadvantage', async function () {
|
||
const prop = await CreatureProperties.findOneAsync(disadvantageAttackId);
|
||
assert(prop);
|
||
assert(prop.type === 'action');
|
||
assert.equal(prop.attackRoll?.disadvantage, 1, 'The attack roll should have disadvantage');
|
||
const action = await runActionById(disadvantageAttackId, [targetCreatureId]);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: [targetCreatureId],
|
||
}, {
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Hit! (Disadvantage)',
|
||
value: '1d20 [ 10, ~~11~~ ] + 0\n**10**',
|
||
}],
|
||
targetIds: [targetCreatureId],
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('actions should consume resources', async function () {
|
||
const action = await runActionById(usesResourcesActionId, []);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{ name: 'Action' }],
|
||
targetIds: []
|
||
},
|
||
{
|
||
contents: [{
|
||
inline: true,
|
||
name: 'Stat damaged',
|
||
value: '−2 Resource Name',
|
||
}],
|
||
targetIds: [creatureId],
|
||
updates: [{
|
||
inc: {
|
||
damage: 2,
|
||
value: -2
|
||
},
|
||
propId: resourceAttId,
|
||
type: 'attribute'
|
||
}],
|
||
},
|
||
{
|
||
targetIds: [],
|
||
updates: [
|
||
{
|
||
inc: {
|
||
quantity: -3
|
||
},
|
||
propId: ammoId,
|
||
type: 'item',
|
||
}
|
||
]
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('should handle insufficient resources', async function () {
|
||
const action = await runActionById(insufficientResourcesActionId, []);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{
|
||
name: 'Action'
|
||
}, {
|
||
name: 'Error',
|
||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||
}],
|
||
targetIds: [],
|
||
},
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
it('should reset attributes when events happen', async function () {
|
||
const action = await runActionById(eventActionId, []);
|
||
const expectedMutations: Mutation[] = [
|
||
{
|
||
contents: [{
|
||
name: 'Action'
|
||
}],
|
||
targetIds: [],
|
||
},
|
||
{
|
||
contents: [
|
||
{
|
||
inline: true,
|
||
name: 'Stat restored',
|
||
value: '+13 Attribute Reset By testEvent Event',
|
||
},
|
||
],
|
||
targetIds: [creatureId],
|
||
updates: [
|
||
{
|
||
inc: {
|
||
damage: -13,
|
||
value: 13,
|
||
},
|
||
propId: attributeResetByEventId,
|
||
type: 'attribute',
|
||
},
|
||
],
|
||
}
|
||
];
|
||
assert.deepEqual(allMutations(action), expectedMutations);
|
||
});
|
||
|
||
});
|