From d9ed18c86c6d640303934394015779635fc886a5 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Mon, 27 Apr 2026 14:14:35 +0200 Subject: [PATCH] feat(01-02): implement compute-applicable-steps (D-10 step ordering) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../leveling/lib/compute-applicable-steps.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 server/src/modules/leveling/lib/compute-applicable-steps.ts diff --git a/server/src/modules/leveling/lib/compute-applicable-steps.ts b/server/src/modules/leveling/lib/compute-applicable-steps.ts new file mode 100644 index 0000000..23bd2a7 --- /dev/null +++ b/server/src/modules/leveling/lib/compute-applicable-steps.ts @@ -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 = new Set([5, 10, 15, 20]); + +/** PF2e skill-increase steps (level 3 and every 2 levels thereafter). */ +const SKILL_INCREASE_LEVELS: ReadonlySet = new Set([3, 5, 7, 9, 11, 13, 15, 17, 19]); + +/** General-feat slot levels (PF2e CRB). */ +const GENERAL_FEAT_LEVELS: ReadonlySet = 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 = 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; +}