feat: add source-owned combat mechanic programs
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { evaluateMechanicProgram, type MechanicProgram } from '$lib/calculations/mechanic';
|
||||
import { createDraft } from './loadout.js';
|
||||
import {
|
||||
buildWeaponMechanicTimeline,
|
||||
mechanicModifiersAt,
|
||||
weaponCriticalChance
|
||||
} from './loadout-combat.js';
|
||||
|
||||
describe('loadout mechanic timeline', () => {
|
||||
it('emits deterministic critical hits from modeled critical chance', () => {
|
||||
const profile = createDraft().combatProfile;
|
||||
profile.encounterSeconds = 4;
|
||||
profile.hitRate = 100;
|
||||
profile.shotsPerBurst = 100;
|
||||
profile.burstPauseSeconds = 0;
|
||||
|
||||
const timeline = buildWeaponMechanicTimeline(
|
||||
{ rpm: 60, magazine: 100, reload_speed: 0 },
|
||||
profile,
|
||||
50
|
||||
);
|
||||
const criticalHits = timeline
|
||||
.filter((event) => event.kind === 'weapon_hit')
|
||||
.map((event) => event.values?.critical);
|
||||
|
||||
expect(criticalHits).toEqual([false, true, false, true, false]);
|
||||
});
|
||||
|
||||
it('derives critical chance through the configured binding transform', () => {
|
||||
expect(weaponCriticalChance(
|
||||
{ critical: 80 },
|
||||
[{
|
||||
attributeId: 'critical',
|
||||
stage: 'critical_chance',
|
||||
operator: 'add',
|
||||
weight: 0.5,
|
||||
hardCap: 30,
|
||||
curveType: 'none'
|
||||
}]
|
||||
)).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('per-shot mechanic modifiers', () => {
|
||||
const program: MechanicProgram = {
|
||||
id: 'phase-order',
|
||||
name: 'Phase order',
|
||||
pools: [{
|
||||
key: 'stacks',
|
||||
label: 'Stacks',
|
||||
scope: 'agent',
|
||||
initial: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
expiresAfterSeconds: 10
|
||||
}],
|
||||
clauses: [
|
||||
{
|
||||
id: 'stack-on-hit',
|
||||
when: 'weapon_hit',
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [{ kind: 'add', pool: 'stacks', value: 1 }],
|
||||
outcomes: [
|
||||
{
|
||||
id: 'instant-a',
|
||||
label: 'Instant A',
|
||||
phase: 'on_event',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'equipped_weapon',
|
||||
value: 2,
|
||||
basis: 'flat',
|
||||
attributeId: 'weapon-damage'
|
||||
},
|
||||
{
|
||||
id: 'after-hit',
|
||||
label: 'After hit',
|
||||
phase: 'after_event',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'equipped_weapon',
|
||||
value: 100,
|
||||
basis: 'flat',
|
||||
attributeId: 'weapon-damage'
|
||||
},
|
||||
{
|
||||
id: 'active-stacks',
|
||||
label: 'Active stacks',
|
||||
phase: 'while_active',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'equipped_weapon',
|
||||
value: 3,
|
||||
basis: 'flat',
|
||||
attributeId: 'weapon-damage',
|
||||
statePool: 'stacks',
|
||||
perStack: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'second-instant',
|
||||
when: 'weapon_hit',
|
||||
reducer: { kind: 'none' },
|
||||
mutations: [],
|
||||
outcomes: [{
|
||||
id: 'instant-b',
|
||||
label: 'Instant B',
|
||||
phase: 'on_event',
|
||||
kind: 'attribute_modifier',
|
||||
scope: 'equipped_weapon',
|
||||
value: 5,
|
||||
basis: 'flat',
|
||||
attributeId: 'weapon-damage'
|
||||
}]
|
||||
}
|
||||
]
|
||||
};
|
||||
const evaluation = evaluateMechanicProgram(
|
||||
program,
|
||||
[
|
||||
{ kind: 'weapon_hit', at: 1, targetId: 'target' },
|
||||
{ kind: 'weapon_hit', at: 2, targetId: 'target' }
|
||||
],
|
||||
{ mode: 'custom', encounterSeconds: 5 }
|
||||
);
|
||||
const source = {
|
||||
status: 'OK' as const,
|
||||
result: evaluation,
|
||||
program,
|
||||
encounterSeconds: 5
|
||||
};
|
||||
|
||||
it('combines same-attribute on-event effects and excludes after-event effects', () => {
|
||||
expect(mechanicModifiersAt(source, 1, 'weapon_hit')).toEqual([{
|
||||
attributeId: 'weapon-damage',
|
||||
scope: 'equipped_weapon',
|
||||
value: 7
|
||||
}]);
|
||||
});
|
||||
|
||||
it('uses the pre-hit active stack state when the triggering hit adds another stack', () => {
|
||||
expect(mechanicModifiersAt(source, 1.5, 'weapon_hit')[0]?.value).toBe(3);
|
||||
expect(mechanicModifiersAt(source, 2, 'weapon_hit')[0]?.value).toBe(10);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user