Files
shd.build/src/lib/calculations/conditional.test.ts
T
jb dd84dff055
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s
feat: add community publishing and news syndication
2026-08-01 21:42:09 -04:00

572 lines
15 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
TRIGGER_ACTION_REGISTRY,
evaluateConditionalProgram,
summarizeTrigger,
validateConditionalProgram,
validateTriggerConfiguration,
type ConditionalEvent,
type ConditionalProgram,
type TriggerConfiguration,
} from './conditional.js';
const modifier = {
id: 'weapon-damage',
scope: 'equipped_weapon' as const,
application: 'active_modifier' as const,
attributeId: 'weapon-damage',
value: 3,
};
const program = (
trigger: TriggerConfiguration,
overrides: Partial<ConditionalProgram> = {},
): ConditionalProgram => ({
id: 'test-effect',
trigger,
lifecycle: {
cooldownSeconds: 0,
durationSeconds: 10,
maxStacks: 10,
stacksPerActivation: 1,
},
outcomes: [modifier],
...overrides,
});
const shot = (
at: number,
targetId: string | null,
hit = true,
headshot = false,
): ConditionalEvent => ({ kind: 'shot', at, targetId, hit, headshot });
describe('trigger action registry', () => {
it('owns every action and validates discriminated parameters', () => {
expect(Object.keys(TRIGGER_ACTION_REGISTRY)).toHaveLength(16);
expect(
validateTriggerConfiguration('same_target_hits_within_window', {
hitsRequired: 9,
windowSeconds: 3,
}),
).toMatchObject({ ok: true });
expect(
validateTriggerConfiguration('same_target_hits_within_window', {
hitsRequired: 0,
windowSeconds: -1,
}),
).toEqual({
ok: false,
errors: [
'Hits required must be a positive integer.',
'Window seconds must be greater than 0.',
],
});
expect(
validateTriggerConfiguration('while_firing', { unexpected: true }),
).toMatchObject({ ok: false });
expect(
validateTriggerConfiguration('target_hit_within_distance', { meters: 0 }),
).toEqual({
ok: false,
errors: ['Distance must be greater than 0.'],
});
});
it('generates user-facing summaries from typed values', () => {
expect(
summarizeTrigger({
action: 'distinct_enemies_within_window',
params: { enemiesRequired: 4, windowSeconds: 2.5 },
}),
).toBe('Hit 4 distinct enemies within 2.5 seconds.');
expect(
summarizeTrigger({
action: 'continuous_firing_duration',
params: { seconds: 1 },
}),
).toBe('Fire continuously for 1 second.');
expect(
summarizeTrigger({
action: 'target_hit_within_distance',
params: { meters: 20 },
}),
).toBe('Hit a target within 20 meters.');
});
it('validates unique health bands and their outcome relationships', () => {
const result = validateConditionalProgram(
program(
{
action: 'health_threshold_bands',
params: {
bands: [
{ thresholdPercent: 50, outcomeKey: 'low' },
{ thresholdPercent: 20, outcomeKey: 'critical' },
],
},
},
{
outcomes: [
{ ...modifier, id: 'low', outcomeKey: 'low' },
{ ...modifier, id: 'critical', outcomeKey: 'critical', value: 10 },
],
},
),
);
expect(result).toMatchObject({ ok: true });
});
it('rejects negative instant outcomes but permits negative Attribute modifiers', () => {
expect(
validateConditionalProgram(
program(
{
action: 'headshot',
params: {},
},
{
outcomes: [
{
id: 'damage',
scope: 'target',
application: 'direct_damage',
basis: 'flat',
value: -10,
},
],
},
),
),
).toEqual({
ok: false,
errors: ['Instant outcome "damage" cannot use a negative value.'],
});
expect(
validateConditionalProgram(
program(
{
action: 'headshot',
params: {},
},
{
outcomes: [{ ...modifier, value: -10 }],
},
),
),
).toMatchObject({ ok: true });
});
});
describe('evaluateConditionalProgram', () => {
it('counts repeated hits against the same target inside the window', () => {
const result = evaluateConditionalProgram(
program({
action: 'same_target_hits_within_window',
params: { hitsRequired: 3, windowSeconds: 1 },
}),
[
shot(0, 'alpha'),
shot(0.2, 'bravo'),
shot(0.4, 'alpha'),
shot(0.8, 'alpha'),
],
{ mode: 'encounter', encounterSeconds: 5 },
);
expect(result).toMatchObject({
status: 'OK',
activationCount: 1,
maxStacksReached: 1,
});
expect(result.activations[0]).toMatchObject({
at: 0.8,
stacksBefore: 0,
stacksAfter: 1,
});
});
it('counts each distinct enemy only once per window', () => {
const result = evaluateConditionalProgram(
program({
action: 'distinct_enemies_within_window',
params: { enemiesRequired: 3, windowSeconds: 2 },
}),
[
shot(0, 'alpha'),
shot(0.2, 'alpha'),
shot(0.4, 'bravo'),
shot(0.6, 'charlie'),
],
{ mode: 'encounter', encounterSeconds: 5 },
);
expect(result.activationCount).toBe(1);
expect(result.activations[0].at).toBe(0.6);
});
it('resets consecutive hits after a miss or target change', () => {
const result = evaluateConditionalProgram(
program({
action: 'consecutive_same_target_hits',
params: { hitsRequired: 3 },
}),
[
shot(0, 'alpha'),
shot(0.1, 'alpha'),
shot(0.2, null, false),
shot(0.3, 'alpha'),
shot(0.4, 'bravo'),
shot(0.5, 'bravo'),
shot(0.6, 'bravo'),
],
{ mode: 'encounter', encounterSeconds: 5 },
);
expect(result.activationCount).toBe(1);
expect(result.activations[0].at).toBe(0.6);
});
it('starts cooldown on activation and applies the activation after its triggering event', () => {
const result = evaluateConditionalProgram(
program(
{
action: 'headshot',
params: {},
},
{
lifecycle: {
cooldownSeconds: 3,
durationSeconds: 5,
maxStacks: 3,
stacksPerActivation: 1,
},
},
),
[
shot(1, 'alpha', true, true),
shot(2, 'alpha', true, true),
shot(4, 'alpha', true, true),
],
{ mode: 'encounter', encounterSeconds: 10 },
);
expect(result.activations).toHaveLength(2);
expect(result.activations.map((activation) => activation.at)).toEqual([
1, 4,
]);
expect(result.activations[0].effectiveAfter).toBeGreaterThan(1);
expect(result.activations[1]).toMatchObject({
stacksBefore: 1,
stacksAfter: 2,
expiresAt: 9,
cooldownUntil: 7,
});
});
it('treats an explicit non-headshot hit as a body shot', () => {
const result = evaluateConditionalProgram(
program({
action: 'body_shot',
params: {},
}),
[
shot(1, 'alpha', true, false),
shot(2, 'alpha', true, true),
shot(3, 'alpha', false, false),
],
{ mode: 'encounter', encounterSeconds: 5 },
);
expect(result.activations.map((activation) => activation.at)).toEqual([1]);
});
it('refreshes one shared duration and clears every stack on expiry', () => {
const result = evaluateConditionalProgram(
program(
{
action: 'damage_taken',
params: {},
},
{
lifecycle: {
cooldownSeconds: 0,
durationSeconds: 3,
maxStacks: 3,
stacksPerActivation: 1,
},
},
),
[
{ kind: 'damage_taken', at: 0 },
{ kind: 'damage_taken', at: 1 },
{ kind: 'damage_taken', at: 5 },
],
{ mode: 'encounter', encounterSeconds: 10 },
);
expect(
result.activations.map(({ stacksBefore, stacksAfter }) => ({
stacksBefore,
stacksAfter,
})),
).toEqual([
{ stacksBefore: 0, stacksAfter: 1 },
{ stacksBefore: 1, stacksAfter: 2 },
{ stacksBefore: 0, stacksAfter: 1 },
]);
expect(result.intervals).toEqual([
{ start: 0.000001, end: 1.000001, stacks: 1, outcomeKey: null },
{ start: 1.000001, end: 4, stacks: 2, outcomeKey: null },
{ start: 5.000001, end: 8, stacks: 1, outcomeKey: null },
]);
});
it('resets firing progress on reload without removing an active buff', () => {
const result = evaluateConditionalProgram(
program(
{
action: 'continuous_firing_duration',
params: { seconds: 2 },
},
{
lifecycle: {
cooldownSeconds: 0,
durationSeconds: 10,
maxStacks: 4,
stacksPerActivation: 1,
},
},
),
[
{ kind: 'firing_started', at: 0 },
shot(0.5, 'alpha'),
{ kind: 'reload_started', at: 3 },
{ kind: 'reload_completed', at: 4 },
{ kind: 'firing_started', at: 4 },
{ kind: 'firing_stopped', at: 7 },
],
{ mode: 'encounter', encounterSeconds: 12 },
);
expect(result.activations.map((activation) => activation.at)).toEqual([
2, 6,
]);
expect(result.activations[1]).toMatchObject({
stacksBefore: 1,
stacksAfter: 2,
expiresAt: 16,
});
expect(result.uptimePercent).toBeCloseTo((10 / 12) * 100);
});
it('activates continuous firing only once in each uninterrupted segment', () => {
const result = evaluateConditionalProgram(
program({
action: 'continuous_firing_duration',
params: { seconds: 2 },
}),
[
{ kind: 'firing_started', at: 0 },
{ kind: 'firing_stopped', at: 7 },
],
{ mode: 'encounter', encounterSeconds: 10 },
);
expect(result.activations.map((activation) => activation.at)).toEqual([2]);
});
it('uses only the strongest matching health band', () => {
const result = evaluateConditionalProgram(
program(
{
action: 'health_threshold_bands',
params: {
bands: [
{ thresholdPercent: 50, outcomeKey: 'low' },
{ thresholdPercent: 20, outcomeKey: 'critical' },
],
},
},
{
lifecycle: {
cooldownSeconds: 0,
durationSeconds: 0,
maxStacks: 1,
stacksPerActivation: 1,
},
outcomes: [
{ ...modifier, id: 'low', outcomeKey: 'low', value: 5 },
{ ...modifier, id: 'critical', outcomeKey: 'critical', value: 10 },
],
},
),
[],
{
mode: 'encounter',
encounterSeconds: 20,
healthPercent: 15,
},
);
expect(result.selectedOutcomeKey).toBe('critical');
expect(result.intervals).toEqual([
{ start: 0, end: 20, stacks: 1, outcomeKey: 'critical' },
]);
});
it('allows a direct proc-rate override and reports per-stack instant outcomes', () => {
const result = evaluateConditionalProgram(
program(
{
action: 'weapon_kill',
params: {},
},
{
lifecycle: {
cooldownSeconds: 0,
durationSeconds: 20,
maxStacks: 3,
stacksPerActivation: 1,
},
outcomes: [
{
id: 'blast',
scope: 'target',
application: 'direct_damage',
basis: 'weapon_base_percent',
value: 25,
},
],
},
),
[],
{
mode: 'encounter',
encounterSeconds: 30,
directProcsPerMinute: 6,
},
);
expect(result.activations.map((activation) => activation.at)).toEqual([
10, 20, 30,
]);
expect(result.outcomeEvents.map((event) => event.value)).toEqual([
25, 50, 75,
]);
});
it('uses real trigger events when the whole-program activation override is zero', () => {
const result = evaluateConditionalProgram(
program({
action: 'weapon_kill',
params: {},
}),
[{ kind: 'enemy_killed', at: 4 }],
{
mode: 'encounter',
encounterSeconds: 30,
directProcsPerMinute: 0,
},
);
expect(result.activations.map((activation) => activation.at)).toEqual([4]);
});
it('models headshot kills from enemy-killed timeline events', () => {
const result = evaluateConditionalProgram(
program({
action: 'headshot_kill',
params: {},
}),
[
{ kind: 'enemy_killed', at: 2, headshot: false },
{ kind: 'enemy_killed', at: 7, headshot: true },
],
{ mode: 'encounter', encounterSeconds: 15 },
);
expect(result.activations.map((activation) => activation.at)).toEqual([7]);
});
it('activates on successful target hits while the target is within the configured distance', () => {
const inRange = evaluateConditionalProgram(
program({
action: 'target_hit_within_distance',
params: { meters: 20 },
}),
[
shot(1, 'alpha', false),
shot(2, null),
shot(3, 'alpha'),
shot(4, 'bravo'),
],
{
mode: 'encounter',
encounterSeconds: 10,
distanceMeters: 20,
},
);
expect(inRange.activations.map((activation) => activation.at)).toEqual([
3, 4,
]);
const outOfRange = evaluateConditionalProgram(
program({
action: 'target_hit_within_distance',
params: { meters: 20 },
}),
[shot(3, 'alpha')],
{
mode: 'encounter',
encounterSeconds: 10,
distanceMeters: 21,
},
);
expect(outOfRange.activationCount).toBe(0);
});
it('requires a target-distance assumption for distance-limited hit events', () => {
const result = evaluateConditionalProgram(
program({
action: 'target_hit_within_distance',
params: { meters: 20 },
}),
[shot(3, 'alpha')],
{
mode: 'encounter',
encounterSeconds: 10,
},
);
expect(result.activationCount).toBe(0);
});
it('keeps target proximity as a persistent condition rather than a hit event', () => {
const result = evaluateConditionalProgram(
program({
action: 'within_distance',
params: { meters: 20 },
}),
[],
{
mode: 'encounter',
encounterSeconds: 10,
distanceMeters: 20,
},
);
expect(result.activationCount).toBe(1);
expect(result.intervals).toEqual([
{ start: 0, end: 10, stacks: 10, outcomeKey: null },
]);
});
it('keeps while-firing effects active only inside firing segments', () => {
const result = evaluateConditionalProgram(
program({
action: 'while_firing',
params: {},
}),
[
{ kind: 'firing_started', at: 1 },
{ kind: 'firing_stopped', at: 3 },
{ kind: 'firing_started', at: 5 },
{ kind: 'reload_started', at: 8 },
],
{ mode: 'encounter', encounterSeconds: 10 },
);
expect(result.intervals).toEqual([
{ start: 1, end: 3, stacks: 10, outcomeKey: null },
{ start: 5, end: 8, stacks: 10, outcomeKey: null },
]);
expect(result.uptimePercent).toBe(50);
});
});