Skip to content

2014 Monk (5/7): Way of the Four Elements disciplines - #1736

Open
Sandersland wants to merge 1 commit into
stagingfrom
feat/1503-2014-four-elements
Open

2014 Monk (5/7): Way of the Four Elements disciplines#1736
Sandersland wants to merge 1 commit into
stagingfrom
feat/1503-2014-four-elements

Conversation

@Sandersland

Copy link
Copy Markdown
Owner

Summary

Slice 5 of 7 in #1313 (2014 Monk). Implements the 2014-only Way of the Four Elements subclass — Elemental Attunement + a 16-discipline ki-fueled catalog (PHB'14 pp. 80–81, not in SRD 5.1) — riding the generic subclass-choice machinery (#899) end to end rather than reviving the retired discipline engine (34f5a4c).

  • Seed: backend/prisma/seed/disciplines.ts — 16 GrantedAbility rows (source: "discipline", all EDITION_2014), transcribed from PHB'14 with page citations. Corrects two errors the deleted engine's seed carried: Fist of Four Thunders is 2d8 (thunderwave), not 3d8; Eternal Mountain Defense's own text gates it at monk level 13, not 17.
  • Subclass: new Subclass row "Way of the Four Elements" (EDITION_2014, slug monk-way-of-the-four-elements); "Warrior of the Elements" retagged EDITION_2024 (its real 2014 predecessor now exists as its own subclass, so the two can no longer share an untagged row).
  • Cast: new lib/classes/disciplines.tsmaxKiPerDiscipline (PHB'14 p.80's per-cast ki cap: 2/3/4/5/6 at L3/5/9/13/17), the 7-discipline concentration set, and a new EffectScaling mode "poolStep" (ki-overspend scaling, the generalised successor to the retired discipline-only "focus" mode). One ABILITY_REGISTRY entry ("disciplines") — no new route file.
  • Schema: widens CharacterEventType with castDiscipline (plain ADD VALUE migration; the other four retired discipline events stay retired).
  • Swap rule (owner decision, 2026-08-03): PHB'14's "replace one discipline when you learn a new one" is built this slice via a new subclassChoiceSwapCadence rule (the choose-N analog of swapCadenceFor) and a LevelUpSubmission.subclassChoicesForgotten field, netted against learns the same way the existing spell swap works — no new LevelUpStepKind.

Backend-only — no frontend wiring (matches the issue's scope).

Test plan

  • npm run lint / npm run typecheck (root, all workspaces) green
  • Full backend suite green (370 files / 4460 tests, incl. new discipline unit + route + level-up-ceremony + reconciliation tests)
  • prisma db seed run twice: exactly 16 source: "discipline" rows, all EDITION_2014, zero edition: NULL
  • assertEverySubclassEditionPopulated passes (verified via the full class-feature-population.test.ts run)
  • scripts/check-enum-narrowing.sh, check-seed-data-modules.sh, check-catalog-id-edition-guard.sh, check-class-ts-migration.sh, check-subclass-substring.sh all pass
  • fallow audit clean (two new/incidental duplicate findings suppressed with reasons; one pre-existing cross-file duplicate reduced to non-blocking)

🤖 Generated with Claude Code

https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn

Slice 5/7 of the 2014 Monk epic (#1313). Rides the generic subclass-choice
machinery (#899) end to end instead of reviving the retired discipline
engine: choicesKnown["fourElementsDisciplines"] + learn/forgetSubclassChoice
(with crossEditionRejection already covering the option lookup), the
existing reconcileSubclassChoices/clampChoicesToCaps pair for level-down (no
new reconciler), and one ABILITY_REGISTRY entry ("disciplines") for the cast.

- Seed 16 disciplines (PHB'14 pp.80-81, not in SRD 5.1) as GrantedAbility
  rows, source "discipline", all EDITION_2014 — corrects two errors in the
  structural-reference-only deleted seed (34f5a4c^): Fist of Four Thunders
  is 2d8 not 3d8 (thunderwave), Eternal Mountain Defense's own PHB'14 text
  gates it at monk level 13, not 17.
- New lib/classes/disciplines.ts: maxKiPerDiscipline (PHB'14 p.80's per-cast
  ki cap), the 7-discipline concentration set, and a ki-scaled EffectSpec via
  a new EffectScaling "poolStep" mode (the generalised successor to the
  retired discipline-only "focus" scaling).
- Widen CharacterEventType with castDiscipline (the only one of the five
  retired 2014 discipline events that returns — migration is a plain
  ADD VALUE, no enum-narrowing concerns).
- Retag "Warrior of the Elements" EDITION_2024 (its 2014 predecessor, Way of
  the Four Elements, is now a real from-scratch discipline menu, not a
  shared/untagged row) and add the "Way of the Four Elements" Subclass row
  EDITION_2014, slug monk-way-of-the-four-elements.
- Owner decision (2026-08-03): build the PHB'14 "replace one discipline when
  you learn a new one" swap rule this slice, not as unrestricted forget.
  subclassChoiceSwapCadence (types.ts) is a new choose-N-cadence rule
  alongside swapCadenceFor (spells), reused generically for any future
  choose-N feature (#1516) — canSwap rides the existing subclassChoice step
  (no new swap-only step needed, since a choose-N swap is legal exactly when
  a new pick is available) and a new LevelUpSubmission.subclassChoicesForgotten
  field nets against learns the same way the spell swap already does.

Backend-only — no frontend wiring in this slice (matches the issue's scope).

Closes #1503

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn
@Sandersland
Sandersland force-pushed the feat/1503-2014-four-elements branch from 8cbcb14 to eaec03b Compare August 5, 2026 02:54
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @Sandersland's task in 5m 34s —— View job


Code Review: 2014 Monk — Way of the Four Elements

  • Read diff and key changed files
  • Review seed data (disciplines.ts, monk-features.ts, subclasses.ts)
  • Review lib/classes/disciplines.ts (new capability file)
  • Review leveling changes (level-up-plan, level-up-submission, level-up-transaction)
  • Review route handlers and contracts
  • Review tests
  • Post findings

Overall Assessment

Excellent slice. The mechanics are well-modelled, the poolStep EffectScaling generalisation is the right abstraction, the subclassChoiceSwapCadence + subclassChoicesForgotten approach cleanly mirrors the spell-swap pattern without a new step kind, and the reconciler reuse via generic reconcileSubclassChoices is exactly right. One correctness bug in two seed rows; one minor schema inconsistency. Everything else is clean.


Finding 1 — Content Bug: saveEffect: "half" missing for two disciplines

Files: backend/prisma/seed/disciplines.ts lines ~113–127 and ~160–175

Fist of Unbroken Air and Water Whip both deal half damage on a successful save per PHB'14 p.81, but their seed rows omit saveEffect: "half". Every other save-damage discipline in the same file has it:

Discipline saveEffect set? PHB'14 says
Fist of Four Thunders "half" half on save
Fist of Unbroken Air (absent) "takes half as much damage" on success
Sweeping Cinder Strike "half" half on save
Water Whip (absent) "takes half damage" on success
Gong of the Summit "half" half on save
Flames of the Phoenix "half" half on save
Breath of Winter "half" half on save

catalogEffectSpec passes saveEffect through as row.saveEffect ?? null. Because these two rows lack the field, the wire EffectSpec.saveEffect is null — and the frontend will show no half-damage hint on a successful save for two of the most commonly-used disciplines (Fist of Unbroken Air and Water Whip both available at L3, Water Whip is the iconic L3 ki trick).

The fix is two lines in disciplines.ts:

// Fist of Unbroken Air (~line 116)
saveEffect: "half",

// Water Whip (~line 169)
saveEffect: "half",

No migration needed — these are seed-only columns on GrantedAbility. Fix this →


Finding 2 — Minor schema inconsistency: roll validator allows 0

File: packages/contracts/src/ability-ops.ts line ~51

roll: z.number().nonnegative().optional(),

The contract allows roll: 0, but disciplines.ts:resolveDisciplineCast immediately rejects it for any damage discipline (roll <= 0 → throw). This is not a bug (the server-side guard catches it), but it differs from the convention in castShadowArtOpSchema where roll uses .positive(). A z.number().int().positive() here would make the contract self-documenting and consistent with the shadow-arts sibling.


Substantive things that are correct (and worth naming)

  • maxKiPerDiscipline formula min(6, 2 + floor((monkLevel-1)/4)) → 2/3/4/5/6 at L3/L5/L9/L13/L17 matches PHB'14 p.80's table exactly. Unit test covers all breakpoints. ✓

  • CONCENTRATION_DISCIPLINES set — 7 entries are correct (gust of wind, hold person, gaseous form, fly, stoneskin, wall of fire, wall of stone all require concentration per PHB'14; thunderwave, burning hands, shatter, fireball, cone of cold, and the non-spell disciplines do not). ✓

  • Fist of Four Thunders corrected from 3d8 → 2d8 — thunderwave's PHB'14 damage is 2d8. ✓

  • Eternal Mountain Defense gated at minLevel: 13, not 11 or 17 — the PHB'14 text states the 13th-level gate explicitly on the discipline itself; the seed header's explanation is accurate. ✓

  • Warrior of the Elements retagged EDITION_2024 once its 2014 predecessor was authored with its own slug — this avoids the "shared row resolves against nothing" trap that any untagged subclass row hits once its Subclass row stops being NULL-edition. ✓

  • No new reconciler entryreconcileSubclassChoices (Enabler: generalize subclass choice-count + reconciler beyond Fighter/Monk (level-up Choose-N dep) #899's generic mechanism) handles fourElementsDisciplines for free; discipline-reconciliation.test.ts proves this and pins the 8-entry LEVEL_GATED_RECONCILERS count. ✓

  • subclassChoicesForgotten ordering — forgets before learns in STEP_OP_BUILDERS.subclassChoice mirrors the spell-swap pattern; the comment correctly calls out why this matters even for the RAW-disallowed re-pick edge. ✓

  • assertSubclassChoiceForgets scope — correctly rejects forgets on a key with no canSwap step, limits to 1 forget per choiceKey per level-up, and defers entry-existence to applyForgetSubclassChoiceOp at apply time (sound design decision documented in-comment). ✓

  • subclassChoiceSwapCadence placement in types.ts — correct; keeps level-up-plan.ts import-clean of Prisma transaction types. ✓

  • deployments.md example updated — removing castDiscipline from the blocked-enum example is correct (it's now a live enum value again); adding disciplinesReconciled is correct (still retired). ✓

  • no-disciplines-known-key.test.ts — grep-enforced guard against accidentally resurrecting disciplinesKnown (the pre-retirement orphaned JSON key) is the right shape of invariant test. ✓


Verdict: One content bug in the seed data (two disciplines missing saveEffect: "half") that should be fixed before merge. Everything else is solid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant