Fix talent outcome validation feedback

This commit is contained in:
JB
2026-07-27 22:01:20 -04:00
parent 37918bd411
commit 7e78d75f85
6 changed files with 248 additions and 50 deletions
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import type { ConditionalMechanicDraftV1 } from './mechanic-editor';
import { missingTalentDescriptionSlots, numberedTalentOutcomes } from './talent-description';
const draft = (outcomes: ConditionalMechanicDraftV1['clauses'][number]['outcomes']): ConditionalMechanicDraftV1 => ({
version: 1,
simulationReadiness: 'ready',
scenarioInputs: [],
pools: [],
clauses: [{
key: 'primary',
label: 'Primary',
event: 'enemy_killed',
if: { kind: 'true' },
reducer: { kind: 'none' },
mutations: [],
outcomes
}]
});
describe('talent description outcomes', () => {
it('accepts a numbered restoration outcome as a description value', () => {
const outcomes = numberedTalentOutcomes(draft([{
key: 'restore-health',
label: 'Restore Health',
phase: 'after_event',
kind: 'restoration',
scope: 'self',
resource: 'health',
delivery: 'continuous',
durationSeconds: 3,
basis: 'max_health_percent',
descriptionSlot: 1,
variants: {
I: { value: 240 },
II: { value: 336 }
}
}]));
expect(outcomes).toHaveLength(1);
expect(outcomes[0]).toMatchObject({ kind: 'restoration', descriptionSlot: 1 });
expect(missingTalentDescriptionSlots('Restore {1} over time.', outcomes)).toEqual([]);
});
it('reports exact missing slots instead of relying on outcome count', () => {
const outcomes = numberedTalentOutcomes(draft([{
key: 'damage',
label: 'Damage',
phase: 'on_event',
kind: 'direct_damage',
scope: 'target',
basis: 'flat',
descriptionSlot: 2,
variants: {
I: { value: 10 },
II: { value: 14 }
}
}]));
expect(missingTalentDescriptionSlots('{1} then {2} and {3}.', outcomes)).toEqual([1, 3]);
});
});
@@ -0,0 +1,38 @@
import type {
ConditionalMechanicDraftV1,
ConditionalMechanicOutcomeDraft
} from '$lib/calculations/mechanic-editor';
const VALUE_BEARING_OUTCOME_KINDS = new Set([
'attribute_modifier',
'direct_damage',
'restoration',
'resource_cost'
]);
export const numberedTalentOutcomes = (
draft: ConditionalMechanicDraftV1
): ConditionalMechanicOutcomeDraft[] => {
const outcomes = draft.clauses
.flatMap((clause) => clause.outcomes)
.filter((outcome) =>
VALUE_BEARING_OUTCOME_KINDS.has(outcome.kind)
&& Number.isInteger(outcome.descriptionSlot)
&& Number(outcome.descriptionSlot) > 0
)
.sort((left, right) => Number(left.descriptionSlot) - Number(right.descriptionSlot));
return [...new Map(outcomes.map((outcome) => [outcome.descriptionSlot, outcome])).values()];
};
export const missingTalentDescriptionSlots = (
template: string,
outcomes: ConditionalMechanicOutcomeDraft[]
) => {
const availableSlots = new Set(outcomes.map((outcome) => Number(outcome.descriptionSlot)));
return [...new Set(
[...template.matchAll(/\{(\d+)\}/g)]
.map((match) => Number(match[1]))
.filter((slot) => !availableSlots.has(slot))
)];
};