feat(01-02): implement compute-applicable-steps (D-10 step ordering)

Task 5 GREEN phase. Pure function returning ordered StepKind[] for a
level-up wizard, conditional on:
- targetLevel (boost levels, skill-increase levels, even/odd)
- hasFreeArchetype (D-13)
- isCaster (D-18)
- classProgressionHasChoiceType (D-19)

Step ordering per D-10:
  class-features → class-feature-choice → boost → skill-increase
  → feat-class/feat-skill (even levels) → feat-general (3,7,11,15,19)
  → feat-ancestry (5,9,13,17) → feat-archetype → spellcaster → review

Invariants: always starts with class-features, always ends with review.
13 tests passing.
This commit is contained in:
2026-04-27 14:14:35 +02:00
parent de07fc8f78
commit d9ed18c86c

View File

@@ -0,0 +1,67 @@
/**
* Pure function — given the level-up parameters, returns the ordered list of
* wizard step kinds the player must walk through (D-10 step ordering).
*
* No NestJS, no Prisma, no I/O. Output is fully determined by inputs.
*/
import type { StepKind } from './types';
export interface ComputeStepsInput {
targetLevel: number;
className: string;
hasFreeArchetype: boolean;
isCaster: boolean;
isSpontaneousCaster: boolean;
classProgressionHasChoiceType: boolean;
}
/** PF2e ability-boost levels (every 5th level after 5). */
const BOOST_LEVELS: ReadonlySet<number> = new Set([5, 10, 15, 20]);
/** PF2e skill-increase steps (level 3 and every 2 levels thereafter). */
const SKILL_INCREASE_LEVELS: ReadonlySet<number> = new Set([3, 5, 7, 9, 11, 13, 15, 17, 19]);
/** General-feat slot levels (PF2e CRB). */
const GENERAL_FEAT_LEVELS: ReadonlySet<number> = new Set([3, 7, 11, 15, 19]);
/**
* Ancestry-feat slot levels (PF2e CRB) — character creation grants level 1, but
* this function is for level-ups (1 is excluded by callers / handled below).
*/
const ANCESTRY_FEAT_LEVELS: ReadonlySet<number> = new Set([1, 5, 9, 13, 17]);
/**
* Returns the ordered list of wizard step kinds for a level-up.
* Step ordering follows D-10:
* class-features → class-feature-choice (if any)
* → boost (if level ∈ {5,10,15,20})
* → skill-increase (if level ∈ {3,5,7,...,19})
* → feat-class (if even level)
* → feat-skill (if even level)
* → feat-general (if level ∈ {3,7,11,15,19})
* → feat-ancestry (if level ∈ {5,9,13,17}; excludes L1)
* → feat-archetype (if hasFreeArchetype)
* → spellcaster (if isCaster)
* → review
*/
export function computeApplicableSteps(input: ComputeStepsInput): StepKind[] {
const { targetLevel, hasFreeArchetype, isCaster, classProgressionHasChoiceType } = input;
const steps: StepKind[] = ['class-features'];
if (classProgressionHasChoiceType) steps.push('class-feature-choice');
if (BOOST_LEVELS.has(targetLevel)) steps.push('boost');
if (SKILL_INCREASE_LEVELS.has(targetLevel)) steps.push('skill-increase');
if (targetLevel % 2 === 0) {
steps.push('feat-class');
steps.push('feat-skill');
}
if (GENERAL_FEAT_LEVELS.has(targetLevel)) steps.push('feat-general');
if (ANCESTRY_FEAT_LEVELS.has(targetLevel) && targetLevel !== 1) {
steps.push('feat-ancestry');
}
if (hasFreeArchetype) steps.push('feat-archetype');
if (isCaster) steps.push('spellcaster');
steps.push('review');
return steps;
}