Skip to content

feat(openworlds): create — subrace handling on the Creation Plane (#377, #315 AC5) - #669

Merged
100yenadmin merged 1 commit into
mainfrom
feat/create-subrace-377
Jun 5, 2026
Merged

feat(openworlds): create — subrace handling on the Creation Plane (#377, #315 AC5)#669
100yenadmin merged 1 commit into
mainfrom
feat/create-subrace-377

Conversation

@100yenadmin

Copy link
Copy Markdown
Member

Closes #377. Implements #315 AC5 (subrace handling on the Creation Plane).

What changed

Subrace is now an optional second-tier lineage choice on the Creation Plane, threaded through the ability preview, the review/summary surfaces, the portrait filter, and the startProviderSession seam.

Additive, render-only (viewer JSX, no build step). Validated by transforming screen-create.jsx through viewer/openworlds/vendor/babel-standalone-7.29.0.min.js (62393 transformed bytes, plus a negative control proving the validator fails on broken JSX).

viewer/openworlds/screen-create.jsx

  • AC1 — data model: RACES entries gain an optional subraces map — elf (high/wood), dwarf (mountain/hill), halfling (lightfoot/stout), gnome (forest/rock). Each subrace declares name, optional body, and a bonus delta that stacks on the base race.
    • Drow decision (documented in source): drow stays a standalone top-level race (it already has its own RACES entry, its own CHA delta, and a Minthara gallery face tagged race:"drow"), not folded into elf.subraces. Only High/Wood live under elf so the gallery filter + race tags stay consistent with what's already shipped.
  • Helpers: subracesForRace / hasSubraces / mergeBonus (additive, non-mutating) / effectiveRaceBonus (base + chosen subrace) / subraceLineageName.
  • AC2 — UI: new SubracePicker renders brass/sketch radio chips, gated on the selected race having a non-empty subraces map. A synthesized "Standard" option (subrace: null) is first and is the default, so base-race-only / homebrew heroes are never blocked. Switching race clears a stale subrace (a Wood Elf pick can't linger onto Dwarf). role="radiogroup"/role="radio" + data-worldos-testid hooks for agent driving.
  • AC3 — ability stacking: the StepAbilities preview, the StepReview summary, and the live right-rail summary all read effectiveRaceBonus(hero). Mountain Dwarf shows STR +2 / CON +2; no surface is left reading the bare base-race bonus.
  • AC4 — bindHero spec: spec.subrace is serialized (string slug, empty string when none chosen) across the startProviderSession seam alongside race. Engine-side wiring is out of scope (filed separately if needed).
  • Initial hero state gains a subrace: null slot.
  • [playtest] Creation Plane: image selection still incorrect — race-aware filter + larger gallery + dead-filter #315 AC5 — portraits: portraitChoicesForRace(race, subrace) ANDs in the subrace when the gallery carries subrace-tagged faces, else degrades to the race-level pool (never an empty grid — the curated 12-face set is race-only today). Preserves the existing fix(openworlds): portrait gallery empty for 5 of 11 races (dwarf, halfling, gnome, dragonborn, half-orc) #379/fix(openworlds): portrait gallery empty-state fallback (Closes #379) #390 race-level fallback.

viewer/tests/test_openworlds_static.py

  • test_openworlds_create_subrace_handling_is_wired: parses RACES per-race and asserts the four subrace maps + bonus deltas, the drow-standalone decision, the SubracePicker / radiogroup / Standard-default UI, the single effectiveRaceBonus stacking path, the race-switch reset, the bindHero spec.subrace, and the subrace-aware portrait filter.

Verification

  • JSX transforms cleanly through the vendored babel-standalone (no build step); negative control confirms the validator rejects broken JSX.
  • Runtime smoke of the pure helpers: Mountain Dwarf STR+2/CON+2, Hill Dwarf CON+2/WIS+1, High Elf DEX+2/INT+1, Wood Elf DEX+2/WIS+1, Standard = base-only, mergeBonus non-mutating, lineage name = "Wood Elf", and wood-elf portrait grid stays non-empty.
  • viewer/tests/test_openworlds_static.py, test_portrait_art.py, test_building_universe.py, and the non-subprocess test_portrait_gen.py classes: 96 passed.
  • One pre-existing failure unrelated to this PR: test_portrait_gen.py::PortraitGenRealSubprocessTests::test_null_default_returns_placeholder_no_network fails with ModuleNotFoundError: No module named 'pydantic' — it spawns the real engine subprocess via uv run and the engine venv in this environment lacks the dep. That test file is byte-identical to HEAD; this PR touches only the JSX screen + its static guard.

Findings skipped

None — all four acceptance criteria of #377 are implemented.

Out of scope (untouched)

Parity lane, combat/table/character screens, the server.py combat/lockout area, scripts/play_party.sh, qa/run_duo.sh. No viewer/server.py change was needed (this is a wire-up + data change in the screen).

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@100yenadmin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes and 45 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c65afb51-c0d1-4e08-b34a-5d87a257220f

📥 Commits

Reviewing files that changed from the base of the PR and between 411a07f and 5c5a47a.

📒 Files selected for processing (2)
  • viewer/openworlds/screen-create.jsx
  • viewer/tests/test_openworlds_static.py

Comment @coderabbitai help to get the list of available commands and usage tips.

…, #315 AC5)

The character creator offered no subrace choice. This adds subrace as an
OPTIONAL second-tier lineage choice and threads it through the ability
preview, the review/summary surfaces, the portrait filter, and the
startProviderSession seam.

Additive, render-only (viewer JSX, no build step). Validated by transforming
screen-create.jsx through viewer/openworlds/vendor/babel-standalone-7.29.0.min.js.

Changes (viewer/openworlds/screen-create.jsx):
- RACES: optional `subraces` map on elf (high/wood), dwarf (mountain/hill),
  halfling (lightfoot/stout), gnome (forest/rock). Each subrace declares
  name/body and a `bonus` DELTA that stacks on the base race.
  Drow DECISION documented: drow stays a STANDALONE top-level race (its own
  RACES entry + Minthara gallery face), NOT folded into elf.subraces — matches
  the already-shipped data fork. (AC1)
- Helpers: subracesForRace / hasSubraces / mergeBonus (additive, non-mutating) /
  effectiveRaceBonus (base + chosen subrace) / subraceLineageName.
- StepRace: new SubracePicker renders brass/sketch radio chips, gated on the
  race having a non-empty subraces map; a synthesized "Standard" (subrace: null)
  option is first and is the default so base-race-only/homebrew heroes are never
  blocked. Switching race clears a stale subrace. (AC2)
- Ability stacking: StepAbilities preview, StepReview summary, and the live
  right-rail summary all read effectiveRaceBonus(hero) — Mountain Dwarf shows
  STR +2 / CON +2. No surface left reading the bare base-race bonus. (AC3)
- bindHero: spec.subrace serialized (empty string when none) across the
  startProviderSession seam. Engine-side wiring out of scope. (AC4)
- Initial hero state gains `subrace: null`.
- portraitChoicesForRace(race, subrace): ANDs in subrace when the gallery
  carries subrace-tagged faces, else degrades to the race-level pool (never an
  empty grid — the curated faces are race-only today). Preserves the existing
  #379/#390 race-level fallback. (#315 AC5)

Test guard (viewer/tests/test_openworlds_static.py):
- test_openworlds_create_subrace_handling_is_wired: parses RACES per-race,
  asserts the four subrace maps + bonus deltas, the drow-standalone decision,
  the SubracePicker/radiogroup/Standard-default UI, the single effectiveRaceBonus
  stacking path, the race-switch reset, the bindHero spec.subrace, and the
  subrace-aware portrait filter.

Findings skipped: none — all four AC of #377 implemented.
@100yenadmin
100yenadmin force-pushed the feat/create-subrace-377 branch from c5facde to 5c5a47a Compare June 5, 2026 18:30
@100yenadmin
100yenadmin merged commit b4a84cf into main Jun 5, 2026
13 checks passed
@100yenadmin
100yenadmin deleted the feat/create-subrace-377 branch June 5, 2026 18:30
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.

feat(openworlds): create — subrace handling in race + portrait selection (#315 AC5)

1 participant