feat: defer advanced build calculations
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 27s
Validate and deploy staging / deploy (push) Successful in 54s

This commit is contained in:
jb
2026-08-02 03:07:12 -04:00
parent c61b9832dd
commit d09b6220ee
19 changed files with 1485 additions and 1267 deletions
+435
View File
@@ -0,0 +1,435 @@
import { describe, expect, it } from 'vitest';
import { createDraft } from './loadout.js';
import { summarizeLoadout, type LoadoutCatalog } from './loadout-summary.js';
const attribute = (
id: string,
name: string,
options: {
unit?: string;
calculationType?: string;
cap?: number | null;
} = {},
) => ({
id,
name,
category: 'Combat',
unit: options.unit ?? '',
calculation_type: options.calculationType ?? 'Additive',
cap_value: options.cap ?? null,
});
const catalog = (overrides: Partial<LoadoutCatalog> = {}): LoadoutCatalog => ({
attributes: [],
gear: [],
gearAugments: [],
brands: [],
weapons: [],
talents: [],
chipSets: [],
protocols: [],
specializations: [],
...overrides,
});
const stat = (summary: ReturnType<typeof summarizeLoadout>, id: string) =>
summary.stats.find((entry) => entry.id === id);
describe('summarizeLoadout', () => {
it('returns an empty deterministic summary for a partial draft', () => {
expect(summarizeLoadout(createDraft(), catalog())).toEqual({
stats: [],
activeBonuses: [],
equippedTalents: [],
});
});
it('ignores saved rolls that do not reference a known Attribute', () => {
const draft = createDraft();
draft.weapons[0].itemId = 'weapon';
draft.weapons[0].rolls[0] = { statId: 'unknown', value: 999 };
const summary = summarizeLoadout(
draft,
catalog({
weapons: [
{
id: 'weapon',
name: 'Test Weapon',
exotic_talent_id: null,
baseBonuses: [],
},
],
}),
);
expect(summary.stats).toEqual([]);
});
it('combines additive and multiplicative Attributes, applies caps, and counts distinct sources', () => {
const draft = createDraft();
draft.weapons[0].itemId = 'weapon';
draft.weapons[0].rolls = [
{ statId: 'additive', value: 5 },
{ statId: 'multiplicative', value: 20 },
];
const summary = summarizeLoadout(
draft,
catalog({
attributes: [
attribute('additive', 'Additive'),
attribute('multiplicative', 'Multiplicative', {
unit: '%',
calculationType: 'Multiplicative',
}),
attribute('capped', 'Capped', { cap: 25 }),
],
weapons: [
{
id: 'weapon',
name: 'Test Weapon',
exotic_talent_id: null,
baseBonuses: [
{ stat_attribute_id: 'additive', value: 10 },
{ stat_attribute_id: 'multiplicative', value: 10 },
{ stat_attribute_id: 'capped', value: 30 },
],
},
],
}),
);
expect(stat(summary, 'additive')).toEqual({
id: 'additive',
value: 15,
sourceCount: 2,
});
expect(stat(summary, 'multiplicative')?.value).toBeCloseTo(32);
expect(stat(summary, 'multiplicative')?.sourceCount).toBe(2);
expect(stat(summary, 'capped')).toEqual({
id: 'capped',
value: 25,
sourceCount: 1,
});
});
it('activates every earned set bonus and records it as a stat source', () => {
const draft = createDraft();
for (const slot of ['Holster', 'Gloves'] as const) {
draft.gear[slot].itemId = `${slot}-gear`;
draft.gear[slot].brandId = 'brand';
}
const summary = summarizeLoadout(
draft,
catalog({
attributes: [attribute('set-stat', 'Set Stat', { unit: '%' })],
gear: [
{ id: 'Holster-gear', tier: 0, base_armor_min: null },
{ id: 'Gloves-gear', tier: 0, base_armor_min: null },
],
brands: [
{
id: 'brand',
name: 'Determinist',
effects: [
{
pieces_required: 1,
stat_attribute_id: 'set-stat',
value: 5,
},
{
pieces_required: 2,
stat_attribute_id: 'set-stat',
value: 10,
},
],
},
],
}),
);
expect(stat(summary, 'set-stat')).toEqual({
id: 'set-stat',
value: 15,
sourceCount: 2,
});
expect(summary.activeBonuses).toEqual([
expect.objectContaining({ pieces: 1, count: 2, value: 5 }),
expect.objectContaining({ pieces: 2, count: 2, value: 10 }),
]);
});
it('interpolates Skill Chip bonuses and Talent descriptions from High-end count', () => {
const draft = createDraft();
for (let index = 0; index < 3; index += 1) {
draft.chips[index].setId = 'chip-set';
draft.chips[index].quality = index === 0 ? 'High-end' : 'Superior';
}
const summary = summarizeLoadout(
draft,
catalog({
attributes: [attribute('skill-damage', 'Skill Damage', { unit: '%' })],
chipSets: [
{
id: 'chip-set',
name: 'Signal',
specialization_ability_id: 'ability',
twoPiece: {
stat_attribute_id: 'skill-damage',
min_value: 10,
max_value: 40,
},
talent: {
name: 'Signal Talent',
description_template: 'Gain {1} Skill Damage.',
effects: [
{
stat_attribute_id: 'skill-damage',
min_value: 15,
max_value: 45,
},
],
rule: null,
},
},
],
}),
);
expect(stat(summary, 'skill-damage')).toEqual({
id: 'skill-damage',
value: 20,
sourceCount: 1,
});
expect(summary.equippedTalents).toEqual([
expect.objectContaining({
key: 'chips:chip-set',
tier: 1,
abilityId: 'ability',
description: 'Gain 25% Skill Damage.',
}),
]);
});
it('scales Protocol primary stats and Talent descriptions independently', () => {
const draft = createDraft();
draft.protocol = { itemId: 'protocol', level: 50, talentUpgrades: 5 };
const summary = summarizeLoadout(
draft,
catalog({
attributes: [
attribute('health', 'Health'),
attribute('healing', 'Healing', { unit: '%' }),
],
protocols: [
{
id: 'protocol',
name: 'Survival',
primaryEffect: {
stat_attribute_id: 'health',
base_value: 100,
},
secondaryEffects: [{ stat_attribute_id: 'healing', value: 5 }],
talent: {
name: 'Recovery',
description_template: 'Restore {1}.',
effects: [{ stat_attribute_id: 'healing', base_value: 2.5 }],
rule: null,
},
},
],
}),
);
expect(stat(summary, 'health')?.value).toBe(200);
expect(stat(summary, 'healing')?.value).toBe(5);
expect(summary.equippedTalents[0]).toMatchObject({
key: 'protocol:protocol',
tier: 5,
description: 'Restore 5%.',
});
});
it('applies static specialization rank effects but excludes combat mechanic outcomes', () => {
const draft = createDraft();
draft.specialization.focusId = 'focus';
draft.specialization.nodeRanks.node = 2;
const summary = summarizeLoadout(
draft,
catalog({
attributes: [
attribute('static', 'Static'),
attribute('mechanic', 'Mechanic'),
],
specializations: [
{
id: 'specialization',
focuses: [
{
id: 'focus',
nodes: [
{
id: 'node',
name: 'Steady Hand',
ranks: [
{
rank: 2,
effects: [
{
effect_type: 'Attribute',
stat_attribute_id: 'static',
value: 12,
},
],
combatEffects: [
{
rule: {
clauses: [
{
outcomes: [
{
kind: 'attribute_modifier',
attributeId: 'mechanic',
value: 99,
},
],
},
],
},
},
],
},
],
},
],
},
],
},
],
}),
);
expect(stat(summary, 'static')).toEqual({
id: 'static',
value: 12,
sourceCount: 1,
});
expect(stat(summary, 'mechanic')).toBeUndefined();
});
it('renders the selected Talent tier without applying its mechanic outcomes to stats', () => {
const draft = createDraft();
draft.weapons[0].itemId = 'weapon';
draft.weapons[0].tier = 2;
draft.weapons[0].talent1Id = 'talent';
const rule = {
clauses: [
{
outcomes: [
{
kind: 'attribute_modifier',
attributeId: 'talent-stat',
value: 100,
},
],
},
],
};
const summary = summarizeLoadout(
draft,
catalog({
attributes: [
attribute('talent-stat', 'Talent Stat', { unit: '%' }),
attribute('shots', 'Shots', { unit: 'Shots' }),
],
weapons: [
{
id: 'weapon',
name: 'Test Weapon',
exotic_talent_id: null,
baseBonuses: [],
},
],
talents: [
{
id: 'talent',
name: 'Measured',
template: 'Gain {1} for {2}.',
tiers: [
{
tier: 'I',
effects: [
{ stat_attribute_id: 'talent-stat', value: 10 },
{ stat_attribute_id: 'shots', value: 2 },
],
},
{
tier: 'II',
effects: [
{ stat_attribute_id: 'talent-stat', value: 20 },
{ stat_attribute_id: 'shots', value: 3 },
],
},
],
rule,
},
],
}),
);
expect(summary.equippedTalents).toEqual([
expect.objectContaining({
id: 'talent',
tier: 2,
description: 'Gain 20% for 3 Shots.',
rule,
}),
]);
expect(stat(summary, 'talent-stat')).toBeUndefined();
});
it('uses the authored tier description when a Talent has no template row', () => {
const draft = createDraft();
draft.weapons[0].itemId = 'weapon';
draft.weapons[0].talent1Id = 'talent';
const summary = summarizeLoadout(
draft,
catalog({
attributes: [attribute('bonus', 'Bonus', { unit: '%' })],
weapons: [
{
id: 'weapon',
name: 'Test Weapon',
exotic_talent_id: null,
baseBonuses: [],
},
],
talents: [
{
id: 'talent',
name: 'Measured',
template: '',
tiers: [
{
tier: 'I',
description: 'Gain {1} while active.',
effects: [{ stat_attribute_id: 'bonus', value: 10 }],
},
],
},
],
}),
);
expect(summary.equippedTalents[0]?.description).toBe(
'Gain 10% while active.',
);
});
});