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:
67
server/src/modules/leveling/lib/compute-applicable-steps.ts
Normal file
67
server/src/modules/leveling/lib/compute-applicable-steps.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user