Skip to content

2014 Monk (3/7): Way of the Open Hand — Wholeness of Body, Tranquility, 2014 Quivering Palm - #1733

Merged
Sandersland merged 1 commit into
stagingfrom
feat/1501-2014-open-hand
Aug 5, 2026
Merged

2014 Monk (3/7): Way of the Open Hand — Wholeness of Body, Tranquility, 2014 Quivering Palm#1733
Sandersland merged 1 commit into
stagingfrom
feat/1501-2014-open-hand

Conversation

@Sandersland

Copy link
Copy Markdown
Owner

Summary

  • Forks Way of the Open Hand (SRD 5.1 p.78, the only monastic tradition in the SRD) into its own EDITION_2014-only subclass (monk-way-of-the-open-hand) — a separate subclass from Warrior of the Open Hand (SRD 5.2), not a fork sharing one slug. Warrior of the Open Hand's Subclass row + 4 feature rows are retagged EDITION_2024 in the same commit (bound, per A 2024 character can pick Path of the Totem Warrior and get zero subclass features #1559).
  • Content verified directly against dnd5eapi.co's 2014 and 2024 API text:
    • Open Hand Technique — Addle's duration genuinely forks: 2014 "can't take reactions until the end of your next turn" (no save) vs 2024 "can't make Opportunity Attacks until the start of its next turn". The pre-existing 2024 row text was a hybrid of neither edition ("take reactions... start of its next turn") — corrected to verified SRD 5.2 wording.
    • Wholeness of Body — 2014 is an action healing a flat 3 × monk level (no die), once/long rest; 2024 is a bonus action rolling Martial Arts die + Wis mod. Row-owned pool (resourceKey/resourceRecharge/resourceTotals, Tracker: 2024 class features & resources retabulation to SRD 5.2 — the eleven per-class issues (wave 2 of #1522) #1134's fixed-total discriminator). Gets its own DERIVED_ACTIONS key (wholenessOfBodyAction) and frontend resolver since cost + heal formula both differ.
    • Tranquility (L11) — new to 2014, reminder-only; 2024 has Fleet Step instead.
    • Quivering Palm — 2014's outcome mapping is inverted from 2024's: a failed save drops the target to 0 HP outright, a success takes the full 10d10 necrotic (never halved). Cost forks 3 ki vs 4 focus via monkPoolKey.
  • open-hand-technique.ts/quivering-palm.ts/character-serialize.ts's rider gates now recognize either Open Hand slug (a monk resolves to at most one).

Test plan

  • npm run typecheck (root) — green
  • npm run lint (root) — green
  • npm run test -w backend — 365 files / 4433 tests green
  • npm run test -w frontend — 330 files / 3489 tests green
  • npx prisma db seed — green twice, idempotent
  • scripts/check-subclass-substring.sh — green (no substring matching)
  • fallow audit — clean (pre-commit hook passed)
  • New content-pin + integration tests (monk-2014-open-hand.test.ts), dedicated cross-edition setSubclass rejection test against the real seeded catalog (open-hand-cross-edition-1501.test.ts)
  • RED→GREEN per changed function: resolveQuiveringPalmDamage, openHandRiderSummary, DERIVED_ACTIONS gating, actionResolvers.ts

Slice 3/7 of #1313. Depends on #1500 (merged). Fully parallel with #1502/#1503 — did not touch shadow-arts.ts or warrior-of-elements.ts/disciplines.

Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn

Closes #1501

…y, 2014 Quivering Palm (#1501)

Forks Way of the Open Hand (SRD 5.1 p.78, the only monastic tradition in the
SRD) into its own EDITION_2014-only subclass — a SEPARATE subclass from
Warrior of the Open Hand (SRD 5.2), not a fork sharing one slug, matching
the plan's "the 2014 and 2024 names differ" call. New slug
"monk-way-of-the-open-hand" (SUBCLASS_SLUGS/SUBCLASS_IDENTITY), a new
Subclass row (EDITION_2014), and monk.ts's "warrior of the open hand" row
retagged EDITION_2024 in the same commit (bound per #1559's
resolveSubclassId/assertEverySubclassEditionPopulated requirement).

Content, verified against dnd5eapi.co's 2014 and 2024 API text directly:
- Open Hand Technique: Addle's duration text actually forks — 2014 "can't
  take reactions until the end of YOUR next turn" (the monk's own turn, no
  save) vs 2024 "can't make Opportunity Attacks until the start of ITS next
  turn" (the target's). The pre-existing 2024 row said "take reactions...
  start of its next turn", a hybrid of neither edition — corrected to the
  verified SRD 5.2 wording, reconciling the open-hand-technique.ts/monk.ts
  disagreement the issue flagged.
- Wholeness of Body: 2014 is an ACTION healing a flat 3 x monk level (no
  die), once per long rest — a materially different shape from 2024's bonus
  action + Martial Arts die + Wis mod. Row-owned pool (resourceKey/
  resourceRecharge/resourceTotals, #1134's fixed-total discriminator) rather
  than a resourceFn. Gets its own DERIVED_ACTIONS key
  (wholenessOfBodyAction) and frontend resolver, since the cost and heal
  formula both differ from the 2024 key.
- Tranquility (L11): new to 2014, no 2024 counterpart (2024 has Fleet Step
  instead) — reminder-only action row.
- Quivering Palm: 2014's outcome mapping is INVERTED from 2024's, not
  reworded — a failed save drops the target to 0 HP outright, a successful
  one takes the full 10d10 necrotic (never halved). Cost forks 3 ki vs 4
  focus via monkPoolKey. Transcribed as SRD 5.1 states it.

open-hand-technique.ts/quivering-palm.ts/character-serialize.ts's rider
gates now recognize EITHER Open Hand slug (a monk can only ever hold one),
threading `rulesEdition` through their selects.

Tests: RED/GREEN per changed function (resolveQuiveringPalmDamage,
openHandRiderSummary, DERIVED_ACTIONS gating, actionResolvers), new seed
content-pin + integration suites (monk-2014-open-hand.test.ts), a dedicated
cross-edition setSubclass rejection test against the real seeded catalog
(open-hand-cross-edition-1501.test.ts), and updates to every fixture/count
the Open Hand fork touches (class-subclasses.fixture.ts,
test-feature-rows.fixture.ts, feature-edition.test.ts's tagged-feature
ledger, monk-2024-content.test.ts's per-partition counts,
class-feature-migration.test.ts's populated-row predicate). `prisma db seed`
verified green and idempotent twice.

Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @Sandersland's task in 4m 32s —— View job


Code Review: 2014 Monk — Way of the Open Hand

  • Read PR diff and changed files
  • Review seed data (monk-features.ts, subclasses.ts)
  • Review logic files (quivering-palm.ts, open-hand-technique.ts, actions.ts)
  • Review serialization (character-serialize.ts)
  • Review frontend changes (actionResolvers.ts, sheet.ts)
  • Review tests
  • Post findings

Overall this is clean work. The fork-into-separate-subclasses decision is correct — the names, levels, and mechanics all differ enough that a same-slug-both-editions shape would have been misleading. The inverted Quivering Palm outcome (fail → 0 HP, success → full damage) is correctly transcribed and the resolveQuiveringPalmDamage branching reflects it faithfully. Two items to flag, one a type-safety nit and one a UX display quirk.


Finding 1 — character-serialize.ts: OPEN_HAND_SLUGS inferred as string[], not readonly SubclassSlug[]

backend/src/lib/character/character-serialize.ts line 85:

const OPEN_HAND_SLUGS = ["monk-warrior-of-the-open-hand", "monk-way-of-the-open-hand"];

The parallel constants in quivering-palm.ts (line 146) and open-hand-technique.ts (line 117) are both declared readonly SubclassSlug[]:

const OPEN_HAND_SLUGS: readonly SubclassSlug[] = ["monk-warrior-of-the-open-hand", "monk-way-of-the-open-hand"];

The character-serialize.ts version is functionally correct — SubclassSlug extends string so .includes(slug) type-checks — but the annotation omission means TypeScript won't catch a typo in either array element here (e.g. "monk-way-of-open-hand"), where the other two files would error immediately. Trivial fix: add : readonly SubclassSlug[] and import the type from subclass-slug.js.

Fix this →


Finding 2 — actionResolvers.ts: wholenessOfBodyAction.healRoll produces a "0d1 + N" label

frontend/src/features/session/actionResolvers.ts line 254:

healRoll: (c) => {
  const monkLevel = c.classes?.find((cls) => cls.name.toLowerCase() === "monk")?.level ?? 0;
  return { count: 0, faces: 1, modifier: 3 * monkLevel };
},

rollSpec with count: 0 correctly produces total = modifier = 3 × monkLevel (no dice rolled). But formatRollSpec({ count: 0, faces: 1, modifier: 18 }) renders as "0d1 + 18", which means the action card button will read "Regain 0d1 + 18 HP" rather than "Regain 18 HP".

Confirmed via dice.ts:

let label = `${critCount(spec)}d${faces}`;  // → "0d1"
if (modifier > 0) label += ` + ${modifier}`;  // → "0d1 + 18"

The behavior is correct (rolled total = 18 for a L6 monk), only the label is awkward. If you want a clean label without touching the shared formatRollSpec, one option is a modifier-only spec { count: 1, faces: 1, modifier: 3 * monkLevel - 1 } (renders "1d1 + 17", still ugly), or you could guard the label path in turnOptions.ts's formatRollSpec call for count=0 specs. Alternatively, accept "0d1 + 18" as a deliberate edge-case tradeoff — the number is right, the label is just non-idiomatic. Low-severity, but worth a conscious decision since it'll be visible on screen.


Everything else: confirmed correct

  • resolveQuiveringPalmDamage 2014 branch (fail → appliedDamage: 0, success → full rawDamage): correctly inverted, unit-tested with both outcomes. ✓
  • quiveringPalmCost(edition) (EDITION_2014 → 3, else → 4) and **monkPoolKey(edition)** (routes to "ki"/"focus") compose correctly in setQuiveringPalm`. ✓
  • addleClause(edition): verified both clauses against each edition's SRD source; the reaction-scope (all reactions vs Opportunity Attacks only) and clock-owner (your vs its) genuinely differ. ✓
  • Wholeness of Body row-owned pool (resourceKey: "wholenessOfBody", resourceTotals: [{ minLevel: 6, total: 1 }], resourceRecharge: "longRest"): correctly uses the Tracker: 2024 class features & resources retabulation to SRD 5.2 — the eleven per-class issues (wave 2 of #1522) #1134 fixed-total discriminator rather than a resourceFn. ✓
  • EDITION_2024 tags on wholenessOfBody/fleetStep: correctly added before a 2014 sibling could wrongly inherit them. ✓
  • ACTION_EFFECT_FN.wholenessOfBodyAction: same spend+heal-passthrough shape as the 2024 key; the fallow-ignore is accurate — the two keys must stay distinct because their DERIVED_ACTIONS entries differ in cost and grantSubclassSlugs. ✓
  • open-hand-cross-edition-1501.test.ts: exercises crossEditionRejection at the real seeded catalog (correct test layer — not mocked). ✓
  • monk-2014-open-hand.test.ts: pins exact feature descriptions, level gates, pool descriptor, and integration-layer deriveResources correctness. ✓
  • Row count math in file header (36 + 37 = 73, fork is a wash): correct. ✓

@Sandersland
Sandersland merged commit 64ca03c into staging Aug 5, 2026
7 checks passed
@Sandersland
Sandersland deleted the feat/1501-2014-open-hand branch August 5, 2026 02:09
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.

2014 Monk (3/7): Way of the Open Hand — Wholeness of Body, Tranquility, 2014 Quivering Palm

1 participant