feat: expand ISAC content modeling
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 28s
Validate and deploy staging / deploy (push) Successful in 1m3s

This commit is contained in:
jb
2026-08-03 01:23:20 -04:00
parent d9924a95b9
commit 57068b61cd
90 changed files with 9635 additions and 1270 deletions
+115
View File
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest';
import {
MOD_SECONDARY_CAPACITY,
normalizeModSecondaryPool,
} from './secondary-pool';
const endpoints = (
level1Min: number | string,
level1Max: number | string,
level20Min: number | string,
level20Max: number | string,
) => ({ level1Min, level1Max, level20Min, level20Max });
const range = (statId: string) => ({
statId,
values: {
Specialized: endpoints('1.5', '2.5', '3.5', '4.5'),
Superior: endpoints(2, 3, 4, 5),
'High-end': endpoints(3, 4, 5, 6),
},
});
describe('shared Mod secondary pool', () => {
it('defines secondary capacity for every Mod quality', () => {
expect(MOD_SECONDARY_CAPACITY).toEqual({
Standard: 0,
Specialized: 1,
Superior: 2,
'High-end': 2,
});
});
it('normalizes Attribute IDs and valid Level 1/20 endpoints', () => {
const result = normalizeModSecondaryPool([
range(' critical-hit-chance '),
range('critical-hit-damage'),
]);
expect(result.error).toBeNull();
expect(result.pool).toEqual([
{
statId: 'critical-hit-chance',
values: {
Specialized: endpoints(1.5, 2.5, 3.5, 4.5),
Superior: endpoints(2, 3, 4, 5),
'High-end': endpoints(3, 4, 5, 6),
},
},
{
statId: 'critical-hit-damage',
values: {
Specialized: endpoints(1.5, 2.5, 3.5, 4.5),
Superior: endpoints(2, 3, 4, 5),
'High-end': endpoints(3, 4, 5, 6),
},
},
]);
});
it('rejects duplicate Attributes after ID normalization', () => {
const result = normalizeModSecondaryPool([
range('critical-hit-chance'),
range(' critical-hit-chance '),
]);
expect(result).toEqual({
pool: [],
error: 'Choose a unique Attribute for every shared pool option.',
});
});
it.each([
[
'missing quality ranges',
{
...range('critical-hit-chance'),
values: {
Specialized: endpoints(1, 2, 3, 4),
Superior: endpoints(2, 3, 4, 5),
},
},
],
[
'reversed ranges',
{
...range('critical-hit-chance'),
values: {
...range('critical-hit-chance').values,
Specialized: endpoints(2, 1, 3, 4),
},
},
],
[
'Level 20 regression',
{
...range('critical-hit-chance'),
values: {
...range('critical-hit-chance').values,
Specialized: endpoints(2, 3, 1, 2),
},
},
],
])('rejects %s', (_label, invalidRange) => {
const result = normalizeModSecondaryPool([
invalidRange,
range('critical-hit-damage'),
]);
expect(result).toEqual({
pool: [],
error:
'Configure valid Level 1 and Level 20 ranges for Specialized, Superior, and High-end quality.',
});
});
});
+102
View File
@@ -0,0 +1,102 @@
export const MOD_QUALITIES = [
'Standard',
'Specialized',
'Superior',
'High-end',
] as const;
export const MOD_SECONDARY_QUALITIES = [
'Specialized',
'Superior',
'High-end',
] as const;
export const MOD_SECONDARY_CAPACITY = {
Standard: 0,
Specialized: 1,
Superior: 2,
'High-end': 2,
} as const satisfies Record<(typeof MOD_QUALITIES)[number], number>;
export type ModSecondaryQuality = (typeof MOD_SECONDARY_QUALITIES)[number];
export type ModSecondaryRange = {
statId: string;
values: Record<
ModSecondaryQuality,
{
level1Min: number;
level1Max: number;
level20Min: number;
level20Max: number;
}
>;
};
export function normalizeModSecondaryPool(input: unknown): {
pool: ModSecondaryRange[];
error: string | null;
} {
if (!Array.isArray(input) || input.length < 2 || input.length > 50) {
return {
pool: [],
error: 'Configure between two and 50 shared secondary Attributes.',
};
}
const pool = input.map((candidate) => {
const range = candidate as Partial<ModSecondaryRange>;
return {
statId: typeof range.statId === 'string' ? range.statId.trim() : '',
values: Object.fromEntries(
MOD_SECONDARY_QUALITIES.map((quality) => {
const values = range.values?.[quality] as
| Partial<ModSecondaryRange['values'][ModSecondaryQuality]>
| undefined;
return [
quality,
{
level1Min: Number(values?.level1Min),
level1Max: Number(values?.level1Max),
level20Min: Number(values?.level20Min),
level20Max: Number(values?.level20Max),
},
];
}),
) as ModSecondaryRange['values'],
};
});
if (
pool.some((range) => !range.statId) ||
new Set(pool.map((range) => range.statId)).size !== pool.length
) {
return {
pool: [],
error: 'Choose a unique Attribute for every shared pool option.',
};
}
if (
pool.some((range) =>
MOD_SECONDARY_QUALITIES.some((quality) => {
const values = range.values[quality];
return (
Object.values(values).some((value) => !Number.isFinite(value)) ||
values.level1Max < values.level1Min ||
values.level20Max < values.level20Min ||
values.level20Min < values.level1Min ||
values.level20Max < values.level1Max
);
}),
)
) {
return {
pool: [],
error:
'Configure valid Level 1 and Level 20 ranges for Specialized, Superior, and High-end quality.',
};
}
return { pool, error: null };
}