Files
TheriapolisV3/theriapolis-codex-ui-implementation-plan.md
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

708 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Theriapolis — `CodexUI` Module
## Design & Implementation Plan for a Custom MonoGame UI Layer
**Status:** Proposed. Targets the codebase state as of Phase 5 M5 complete
(329 tests green, Myra 1.0.0.204 still in tree, character creator written
against Myra in [CharacterCreationScreen.cs](Theriapolis.Game/Screens/CharacterCreationScreen.cs)).
**Audience:** A future Claude Code session implementing this module. The
session won't have any of the conversation history that produced this plan
— this document is the complete brief.
**Goal:** Replace the Myra-built character creator with a custom MonoGame
SpriteBatch-based UI module that **exactly mimics the visual design** from
Claude Design's React prototype. Build the module so it can be reused for
future stylized screens (title, inventory, save/load showcase). Keep Myra
in tree for utility screens (pause menu, slot picker) — this is a hybrid,
not a replacement.
---
## 1. Visual source of truth
The complete visual reference is at:
```
_design_handoff/character_creation/from_design/
├── README.md — design's own integration notes
├── index.html — full HTML + CSS (the `<style>` block is the design system)
├── src/ — React JSX components (data flow, state model)
└── data/ — JSON content (identical to Content/Data/)
```
**The agent must run the React prototype before starting:**
```bash
cd _design_handoff/character_creation/from_design
python3 -m http.server 8000
# open http://localhost:8000/
```
Click through every step. Observe the parchment background, gilded card
borders, serif display fonts (Cinzel for headings, Cormorant Garamond for
body), monospace tags (JetBrains Mono), the stepper's locked/active/complete
states, the card hover/selected states, the bonus pills with sourced
popovers, the drag-drop ability slots, the trait hover popovers. **Every
visual detail in `CodexUI` should match what the React prototype shows.**
The CSS variables block at the top of `index.html`'s `<style>` is the
authoritative color/typography/spacing reference. See §A1 below for the
extracted token table.
The current Myra port is in
[Theriapolis.Game/Screens/CharacterCreationScreen.cs](Theriapolis.Game/Screens/CharacterCreationScreen.cs)
— the *behavior* and *state model* port faithfully (validation, step
gating, click-pick-place stat assignment, auto-assign, roll history). The
*visual* port does not. Read it for behavior, not appearance.
---
## 2. Goals & non-goals
### Goals
1. **Pixel-perfect visual match to the React design** — parchment
background, gilded card frames with selected-state glow, serif display
fonts, codex header layout, stepper with ✕/→/✓ marks, card grids with
hover and selected states.
2. **Reuse the existing MonoGame SpriteBatch rendering stack** — same
`SpriteBatch`, same `SpriteFont` system, same content-pipeline patterns
the world/tactical renderers use. No new framework dependency.
3. **Co-exist with Myra**, which stays in tree for utility screens (pause
menu, save/load slot picker). The Game1 screen stack treats `CodexUI`
screens and Myra screens identically — both implement `IScreen`.
4. **Reusable primitive set.** The character creator is the first
deliverable but not the only one. Title screen, inventory, save/load
showcase will all call into the same `CodexUI` primitives.
5. **Runtime-checkable assets.** Missing fonts or textures should fail
loudly at content-load time with a clear message, not at first
render. Same loud-fail discipline as `ContentLoader`.
### Non-goals
- **Replacing Myra entirely.** Pause menu, slot picker, debug overlays
stay Myra. They don't need to be beautiful.
- **A general-purpose UI framework.** This is a small set of widgets
hand-tailored to the Theriapolis codex aesthetic. It is not meant to
scale to arbitrary game UIs.
- **Cross-platform mobile/touch.** Desktop only — same as the existing game.
- **Animations.** A subtle hover transition is fine; full motion design
(e.g. card-flip transitions, ink-bleed entrances) is out of scope.
- **Localization / RTL.** English LTR only. The font and layout choices
assume English.
- **Accessibility passes** (screen reader, high-contrast mode, reduced
motion). Worth noting as a future concern; not in v1.
---
## 3. Stack & constraints
- **MonoGame DesktopGL 3.8.2** (already in `Theriapolis.Game.csproj` and
`Theriapolis.Desktop.csproj`).
- **C# 12 / .NET 8** (`<LangVersion>12</LangVersion>`).
- **`Theriapolis.Core` must remain MonoGame-free** — enforced by
`Architecture/CoreNoDependencyTests.cs`. All `CodexUI` code lives in
`Theriapolis.Game/`.
- **Existing screen contract** — `Theriapolis.Game/Screens/IScreen.cs`:
`Initialize(Game1)`, `Update(GameTime)`, `Draw(GameTime, SpriteBatch)`,
`Deactivate()`, `Reactivate()`. New `CodexScreen` base class implements
this.
- **Content pipeline** — Desktop project's `.csproj` already wildcards
`..\Content\Gfx\**` to output. Add a new `Content/Gfx/codex/` subfolder
for the new asset set. Desktop `RuntimeIdentifiers` currently include
`win-x64; linux-x64; osx-x64; osx-arm64` — assets must be cross-platform
PNGs, no platform-specific paths.
- **Fonts** — see §6 for two options (MonoGame Content Pipeline vs
SpriteFontPlus / FontStashSharp). Pick one before starting.
---
## 4. Module file layout
```
Theriapolis.Game/
CodexUI/
Core/
CodexScreen.cs // base class — IScreen + widget tree + input dispatch
CodexWidget.cs // base class for every widget; bounds, hover, click, parent ref
CodexLayout.cs // layout helpers: row, column, grid, padding, alignment
CodexInput.cs // wraps MouseState + KeyboardState; edge-detect, hover delta
CodexTheme.cs // color tokens, density tokens, font tokens (see §A1)
NineSlice.cs // 9-slice texture renderer for scalable bordered panels
Atlas.cs // strongly-typed loader for the codex asset set
Widgets/
CodexLabel.cs // wrapped serif text, multi-line, alignment
CodexButton.cs // primary / ghost / small variants per the design
CodexPanel.cs // 9-sliced parchment panel with optional title bar
CodexCard.cs // selectable card with hover + selected glow + click handler
CodexChip.cs // trait / skill / language pill (multiple subtypes)
CodexBonusPill.cs // small +N / N pill with sourced popover
CodexStepper.cs // 7-step horizontal stepper with locked/active/complete states
CodexAbilityRow.cs // ability slot + drop target + final-score readout + bar
CodexPoolDie.cs // draggable d20-like value tile for the stat pool
CodexCheckboxRow.cs // skill row with check/locked/unavailable states
CodexTextBox.cs // single-line text input (for the name field)
CodexHoverPopover.cs // floating popover with title + description + tag (trait hover)
CodexOrnamentRule.cs // horizontal hairline rule with central ornament glyph
Screens/
CodexCharacterCreationScreen.cs // the wizard, ported from Myra
CodexAside.cs // right-column live summary panel
Steps/
StepClade.cs // one file per step, mirrors steps.jsx
StepSpecies.cs
StepClass.cs
StepBackground.cs
StepStats.cs // most complex; drag-drop ability assignment
StepSkills.cs
StepReview.cs
Drag/
DragDropController.cs // captures source on TouchDown, follows cursor with ghost,
// resolves drop on TouchUp; Escape cancels
```
**14 widget files + 7 step files + 6 core files + 2 screen files = ~29
new C# files.** Plan for ~3000 lines of code total (rough order of
magnitude — codex aesthetic widgets are mostly drawing logic + state).
---
## 5. Visual primitive catalog
Each row maps a React design element → the C# `CodexUI` widget that
implements it. The agent should keep this table next to the design open in
a browser as they implement.
| React selector / component | CodexUI widget | Notes |
|----------------------------|----------------------|-------|
| `body { background: parchment }` | `CodexScreen` background | Tile a parchment PNG over the full viewport |
| `.codex-header` (title + folio sub) | `CodexLabel` (display font) | Two-line stacked label, serif large + small |
| `.stepper` row | `CodexStepper` | 7 step bullets with `locked / active / complete` visual states; ✕ / Roman / ✓ marks |
| `.step.active` | `CodexStepper` active state | Gilded ring around the bullet, ink-darkened name |
| `.card` | `CodexCard` | 9-sliced parchment frame; hover = subtle gild halo; selected = strong gild halo |
| `.card.selected` | `CodexCard.IsSelected` | Stronger gild halo + corner ornament |
| `.scent-aura` overlay | `CodexCard.SpecialAura` | Optional radial gild glow behind the card on hover (clade step only) |
| `.sigil` | Sprite atlas | One PNG per clade (canidae, felidae, etc.) — clade icons |
| `.mod` (`.pos` / `.neg`) | `CodexBonusPill` | Small bordered pill, gilded for + and rust-red for |
| `.trait-chips` row | `CodexLayout.WrapRow`| Horizontal flex-wrap of chips — needs a flow layout helper |
| `.trait-name` chip | `CodexChip` (Trait variant) | Bordered pill; hover = full popover with name + description + tag |
| `.skill-chip` (`.from-bg` / `.from-cls`) | `CodexChip` (Skill variant) | Two color variants based on source; popover shows name + governing ability + description |
| `.language-chip` | `CodexChip` (Language variant) | Italic serif text in a bordered pill |
| `.popover` (rich) | `CodexHoverPopover` | Floating panel above/below trigger; viewport-clamped; appears after small hover delay |
| `.btn.primary` | `CodexButton.Primary`| Filled gild background, ink text, embossed border |
| `.btn.ghost` | `CodexButton.Ghost` | Transparent fill, ink border, ink text |
| `.btn.small` | `CodexButton.Small` | Smaller padding/font, used for inline reroll/auto-assign |
| `.pool` row + `.die` | `CodexPoolDie` + `DragDropController` | Draggable value tile; visual = a stylized small ornamented stone |
| `.ability-row` (full) | `CodexAbilityRow` | Composite: name + bonus pill + slot + final score + horizontal progress bar |
| `.slot` (drop target) | `CodexAbilityRow.Slot` | Empty = dashed border; filled = gilded; drop target tracked by DragDropController |
| `.bar` + `.bar-fill` | `CodexAbilityRow.ProgressBar` | Inset 9-slice; fill colored by relative score |
| `.skill-grid-by-ability` | `CodexLayout.Grid` | 3-column grid; each column = `.skill-group` |
| `.skill-group` | `CodexPanel` (no border, just heading) | Heading is the ability long-name + short-tag |
| `.skill-row` | `CodexCheckboxRow` | `[locked]` / `[unavailable]` / `[checked]` / default; click toggles when class option |
| `input[type=text]` (name) | `CodexTextBox` | Single-line, serif italic, gilded underline |
| `.review-grid` blocks | `CodexPanel` | Each block = small panel with heading + content + `Edit ` link |
| `.kit-grid` items | `CodexLayout.WrapRow` of `CodexChip` | One chip per starting-kit item; equipped items get a corner mark |
| `.divider` / horizontal rule | `CodexOrnamentRule` | Two hairlines flanking a small central diamond glyph |
Anything in the React design that doesn't appear above (e.g. the Tweaks
panel, the fonts dropdown) is **out of scope** — the wizard ships with
one fixed theme baked in.
---
## 6. Asset list
### 6.1 Textures (PNG, sized for 1× rendering at the target window resolution)
Place under `Content/Gfx/codex/`. Add the directory pattern to
`Theriapolis.Desktop.csproj`'s `<Content Include>` block (already wildcards
`Gfx/**`).
| Asset | Size | Purpose | Notes |
|---|---|---|---|
| `parchment_bg.png` | 512×512 (tileable) | Full-viewport background | Subtle wear pattern; tiles seamlessly |
| `parchment_card.png` | 192×192 9-slice | Card body fill | 24 px corners, 16 px inner edges |
| `gild_frame.png` | 64×64 9-slice | Card border (default) | 16 px corners, gilded line |
| `gild_frame_selected.png` | 64×64 9-slice | Card border (selected) | Thicker gild + inner glow |
| `gild_button_primary.png` | 96×32 9-slice | Primary button background | Gild fill with embossed bevel |
| `ink_button_ghost.png` | 96×32 9-slice | Ghost button border | Ink line, no fill |
| `wax_seal.png` | 64×64 sprite | Confirm button accent | Optional; red wax with crest |
| `ornament_diamond.png` | 16×16 sprite | Center of horizontal rule | Small gold diamond |
| `stepper_bullet_locked.png`| 24×24 sprite | Stepper bullet (locked) | ✕ over greyed parchment |
| `stepper_bullet_active.png`| 24×24 sprite | Stepper bullet (active) | Gild filled with Roman numeral |
| `stepper_bullet_done.png` | 24×24 sprite | Stepper bullet (complete)| Gild ring with ✓ |
| `chip_trait.png` | 96×24 9-slice | Trait chip background | Subtle parchment fill, ink border |
| `chip_skill_bg.png` | 96×24 9-slice | Skill chip — from background | Gild edge variant |
| `chip_skill_class.png` | 96×24 9-slice | Skill chip — from class | Standard ink edge variant |
| `chip_language.png` | 96×24 9-slice | Language chip | Italicized rendering, ink border |
| `pool_die.png` | 32×40 sprite | Stat pool tile | Stylized small standing stone with engraved face |
| `slot_empty.png` | 64×40 9-slice | Empty ability slot | Dashed ink border |
| `slot_filled.png` | 64×40 9-slice | Filled ability slot | Gilded border, parchment fill |
| `bar_track.png` | 16×8 9-slice | Progress bar track | Inset shadow |
| `bar_fill.png` | 16×8 9-slice | Progress bar fill | Gilded gradient |
| `popover_bg.png` | 96×96 9-slice | Hover popover background | Parchment with darker ink border, drop shadow |
| `clade_sigil_canidae.png` | 48×48 sprite | Canidae icon | Wolf-head heraldry mark |
| `clade_sigil_felidae.png` | 48×48 sprite | Felidae icon | Lion-head heraldry mark |
| `clade_sigil_mustelidae.png` | 48×48 sprite | Mustelidae icon | |
| `clade_sigil_ursidae.png` | 48×48 sprite | Ursidae icon | |
| `clade_sigil_cervidae.png` | 48×48 sprite | Cervidae icon | |
| `clade_sigil_bovidae.png` | 48×48 sprite | Bovidae icon | |
| `clade_sigil_leporidae.png`| 48×48 sprite | Leporidae icon | |
**Total: 27 asset files.** All can be hand-painted in Aseprite or
generated via Pixellab (the Tile Generation tooling already in this repo
— see `theriapolis-tile-generation-handoff.md` for the existing prompt
patterns). The 7 clade sigils are the most art-direction-sensitive
authoring task; the rest are repeating border patterns and small icons.
### 6.2 Fonts
The design uses **Cinzel** (small caps display), **Cormorant Garamond**
(serif body), **JetBrains Mono** (small caps tags), with **EB Garamond**
and **Spectral** as alternates. All five are available on Google Fonts as
TTF.
Pick **one** of two integration paths:
**Path A: MonoGame Content Pipeline** (preferred for shipped games)
- Install MonoGame Content Builder Tool: `dotnet tool install -g
dotnet-mgcb-editor`
- Add `Content.mgcb` to `Theriapolis.Desktop` and reference it in the
`.csproj` (commented-out reference is already there from Phase 0).
- For each font + size pair (Cinzel-Bold-32, Cinzel-Bold-18,
CormorantGaramond-Italic-14, etc.), add a `SpriteFont` entry to
`Content.mgcb` with the source TTF and the desired character set.
- The `.mgcb` build produces `.xnb` files that MonoGame's `ContentManager`
loads at runtime.
- **Pros:** baked atlases, fastest runtime load, no extra runtime deps.
- **Cons:** requires `mgcb` tool installed, fixed font sizes (one .xnb per
size).
**Path B: SpriteFontPlus / FontStashSharp** (faster iteration)
- Add NuGet: `FontStashSharp.MonoGame`.
- Load TTF files at runtime, render glyphs into a runtime-managed atlas.
- Any size at any time without rebuilding content.
- **Pros:** no content pipeline setup, runtime size flexibility.
- **Cons:** extra runtime dependency, slightly slower first-render per new
glyph/size, atlas churn risk if sizes vary widely.
**Recommendation:** Path B for v1 (faster iteration during the styling
pass), revisit Path A if profiling shows atlas churn.
In either case, register the loaded fonts on a `CodexTheme.Fonts` static
table keyed by semantic name:
```csharp
public static class CodexFonts {
public static SpriteFont DisplayLarge; // Cinzel-Bold-32 (codex header)
public static SpriteFont DisplayMedium; // Cinzel-Bold-22 (h2)
public static SpriteFont DisplaySmall; // Cinzel-Bold-16 (h3)
public static SpriteFont SerifBody; // CormorantGaramond-Regular-16
public static SpriteFont SerifItalic; // CormorantGaramond-Italic-14
public static SpriteFont MonoTag; // JetBrainsMono-Medium-10
public static SpriteFont MonoTagSmall; // JetBrainsMono-Medium-9
}
```
Loaded once at game startup; all widgets read from this table.
---
## 7. Theme tokens
Extract from the React design's `:root` CSS variables. The agent should
reproduce these exactly. See appendix §A1 for the full table; here's the
canonical names the C# code should use:
```csharp
public static class CodexColors {
// Backgrounds
public static Color Bg = HexColor("#f3ead4"); // parchment
public static Color BgDeep = HexColor("#e8dcb8"); // worn parchment shadow
public static Color Panel = HexColor("#ede0bf"); // card fill
public static Color PanelInset = HexColor("#e3d4a8"); // sunken inner panel
// Inks
public static Color Ink = HexColor("#2a1f12"); // primary serif text
public static Color InkSoft = HexColor("#5a4a32"); // secondary text
public static Color InkMute = HexColor("#8a7858"); // tertiary text
// Accents
public static Color Gild = HexColor("#a47118"); // gilded borders + select halo
public static Color GildBright = HexColor("#d4a23e"); // hover halo / pressed primary
public static Color Seal = HexColor("#8a2818"); // wax-red, danger / negative bonus
public static Color Rule = HexColor("#c8b88a"); // hairline rule color
}
public static class CodexDensity {
public const int RowGap = 6;
public const int ColGap = 8;
public const int CardPad = 14;
public const int PanelPad = 16;
public const int ChipPad = 6;
public const int ButtonPad = 10;
}
```
Density tokens correspond to the React design's `--gap` family; CardPad
and PanelPad let cards and aside panels keep consistent breathing room.
---
## 8. Layout primitives
The widget tree is hand-built in C# (no XAML). Every widget exposes:
```csharp
public abstract class CodexWidget {
public Rectangle Bounds { get; protected set; } // screen-space, set by parent during layout
public bool Visible { get; set; } = true;
public bool Enabled { get; set; } = true;
public CodexWidget? Parent { get; set; }
public virtual Point Measure(Point available); // returns desired size
public virtual void Arrange(Rectangle bounds); // sets Bounds; recurses to children
public virtual void Update(GameTime gt, CodexInput input);
public virtual void Draw(SpriteBatch sb, GameTime gt);
// Hit-testing helpers
public bool ContainsPoint(Point p) => Bounds.Contains(p);
}
```
Layout containers are widgets that arrange their children:
- **`Column`** — vertical stack with `Spacing` (default `RowGap`), optional
`Padding`, optional `HAlign` (left / center / right / stretch).
- **`Row`** — horizontal stack with same parameters; default `ColGap`.
- **`Grid`** — fixed-column grid; takes `Columns` (count) and a list of
child widgets, lays them left-to-right then wraps.
- **`WrapRow`** — flow layout: lay children left-to-right, wrap to next
line when width exceeded.
- **`Padding`** — single-child decorator that adds breathing room.
- **`Center`** — single-child decorator that centers the child horizontally
and vertically within its assigned bounds.
Implementation note: do **measure-then-arrange** in two passes to support
content-driven sizing (a card sizes to fit its name + chips). Avoid React-
style flex math; just stack and clamp.
---
## 9. Input + state primitives
`CodexInput` wraps `MouseState` + `KeyboardState` and produces edge-detected
events:
```csharp
public sealed class CodexInput {
public Point MousePosition { get; private set; }
public Point MouseDelta { get; private set; }
public bool LeftButtonDown { get; private set; }
public bool LeftJustPressed { get; private set; }
public bool LeftJustReleased { get; private set; }
public bool RightJustPressed { get; private set; }
public bool KeyJustPressed(Keys k);
public bool KeyDown(Keys k);
public string TextEnteredThisFrame { get; private set; } // for CodexTextBox
}
```
`CodexScreen.Update` calls `_input.Tick()` once per frame, then dispatches
to the widget tree. Each widget tracks its own hover/pressed state from
`input.MousePosition` against `Bounds`.
For hover popovers (`CodexHoverPopover`):
- Track `_hoverEnterTime` per widget; show popover after `HoverDelay = 0.4s`.
- On show, position popover relative to trigger widget; clamp to viewport
edges; flip above/below if not enough room below.
- Hide on `MouseLeft` from both trigger and popover bounds.
For text input (`CodexTextBox`):
- Subscribe to `Game1.Window.TextInput` event in the screen's
`Initialize`; route incoming `char` to the focused TextBox via
`CodexInput.TextEnteredThisFrame`.
- Handle Backspace, Enter, arrow keys via `KeyJustPressed`.
---
## 10. Drag-and-drop
`DragDropController` is a screen-level helper that owns the drag lifecycle:
```csharp
public sealed class DragDropController {
public bool IsDragging { get; private set; }
public object? Payload { get; private set; } // what's being dragged
public CodexWidget? GhostWidget { get; private set; } // visual that follows the cursor
public void BeginDrag(object payload, CodexWidget ghost);
public void Update(GameTime gt, CodexInput input); // position ghost; detect drop or cancel
public void Draw(SpriteBatch sb); // draw ghost on top of everything else
public event System.Action<object, Point>? OnDrop; // (payload, dropPositionScreen)
public event System.Action<object>? OnCancel;
}
```
Source widgets call `controller.BeginDrag(payload, ghost)` on `LeftJustPressed`
when their bounds contain the cursor. Drop targets register interest with
the controller; on `LeftJustReleased`, the controller hit-tests
registered targets at the drop position and fires `OnDrop`.
Escape cancels: in `Update`, if `IsDragging && input.KeyJustPressed(Keys.Escape)`,
call `OnCancel` and clear the drag.
For the character creator's stat pool:
- Source: each `CodexPoolDie` has a `payload = { from: "pool", value: int, idx: int }`.
- Drop target: each `CodexAbilityRow.Slot`.
- Slot-to-pool drop: the pool itself registers as a drop target.
- Slot-to-slot drop: the slot is both source and target.
Match the payload shape from the React design's `handleDrop` /
`dropToPool` functions in `steps.jsx` so the logic is portable.
---
## 11. Character creator port
Once the primitives exist, the character creator port is mostly
"translate `steps.jsx` to C# screen + step files." The state model is
already implemented in the existing
[CharacterCreationScreen.cs](Theriapolis.Game/Screens/CharacterCreationScreen.cs)
— port that state model verbatim, swap the Myra widgets for `CodexUI`
widgets.
Steps map 1:1 to the design's `steps.jsx`:
| React component | C# step file | Key widgets |
|---|---|---|
| `StepClade` | `Steps/StepClade.cs` | Predator/prey section headers, `CodexCard` grid with `CodexBonusPill` mods + `CodexChip` traits + clade sigil sprite |
| `StepSpecies` | `Steps/StepSpecies.cs` | 3-per-row grid filtered by clade; size + speed + mods + traits |
| `StepClass` | `Steps/StepClass.cs` | 2-per-row grid with `★ Suits Clade` ribbon overlay; level-1 features as chips |
| `StepBackground` | `Steps/StepBackground.cs` | 2-per-row grid; flavor body text + feature chip + skill chips |
| `StepStats` | `Steps/StepStats.cs` | Method tabs, pool row of `CodexPoolDie`, six `CodexAbilityRow`s with drag-drop, roll history line |
| `StepSkills` | `Steps/StepSkills.cs` | 3-column grid by ability; `CodexCheckboxRow` per skill |
| `StepReview` | `Steps/StepReview.cs` | Name input (`CodexTextBox`), `CodexPanel` summary blocks with `Edit ` buttons, starting-kit chip wrap |
The `CodexAside` panel mirrors the React `<Aside />` from `app.jsx` — five
summary blocks (Name, Lineage, Calling+History, Abilities, Skills) plus
the optional Selected detail line.
The wizard validation, step gating, and "Confirm & Begin" handoff to
`WorldGenProgressScreen` are unchanged from the existing implementation —
copy that logic verbatim.
---
## 12. Integration with the existing screen system
### 12.1 Game1 changes
Game1 already pushes `CharacterCreationScreen` on the new-game flow (see
[TitleScreen.cs](Theriapolis.Game/Screens/TitleScreen.cs)). The change:
```csharp
// In TitleScreen.OnNewWorldClicked, the only diff is the screen type:
_game.Screens.Push(new CodexUI.Screens.CodexCharacterCreationScreen(seed));
```
The old Myra `CharacterCreationScreen.cs` can be **deleted** when the
new screen ships and its tests pass — it's a 1:1 functional replacement.
### 12.2 Asset loading
Add a single `CodexAtlas` instance owned by `Game1`:
```csharp
// In Game1.LoadContent (or whatever the existing pattern is):
CodexAtlas.LoadAll(GraphicsDevice, ContentDataDirectory);
```
`CodexAtlas` loads every PNG in `Content/Gfx/codex/` and exposes them as
strongly-typed properties. The fonts load from the chosen path (mgcb or
FontStashSharp) at the same time.
Loud-fail on missing assets:
```csharp
// In CodexAtlas.LoadAll:
foreach (var name in REQUIRED_ASSETS) {
string path = Path.Combine(gfxDir, "codex", name);
if (!File.Exists(path))
throw new FileNotFoundException($"Codex asset missing: {path}");
}
```
### 12.3 Coexistence with Myra
The Myra screens (`PauseMenuScreen`, `SaveLoadScreen`, etc.) keep using
`Myra.Graphics2D.UI.Desktop`. The new `CodexScreen` base class doesn't
touch Myra at all. The screen stack treats them identically — both
implement `IScreen`. No conflicts.
A reasonable convention: `CodexUI` namespaces live under
`Theriapolis.Game.CodexUI.*`, Myra-using screens live in
`Theriapolis.Game.Screens.*` (existing). The two systems never share
widget types.
---
## 13. Milestones
Suggested implementation order. Each milestone is a ship point — branch
mergeable, tests passing, something visually demonstrable.
**M-A — Core + atlas + fonts (12 days).**
- Implement `CodexWidget`, `CodexLayout`, `CodexInput`, `CodexTheme`.
- Implement `NineSlice` renderer.
- Implement `CodexAtlas` with all 27 asset slots — using **placeholder
PNGs** (solid colors with text labels) so the structure works before
art is authored.
- Load fonts via chosen path.
- Build a test screen that draws a parchment background + 9-sliced card
+ a label so end-to-end pipeline is verified.
**M-B — Atomic widgets (23 days).**
- `CodexLabel`, `CodexButton` (3 variants), `CodexPanel`, `CodexChip` (3
variants), `CodexBonusPill`, `CodexOrnamentRule`, `CodexTextBox`.
- Each widget is verified in the test screen.
**M-C — Composite widgets (2 days).**
- `CodexCard` (with hover + selected states), `CodexStepper`,
`CodexCheckboxRow`, `CodexAbilityRow`, `CodexPoolDie`,
`CodexHoverPopover`.
- `DragDropController`.
**M-D — Character creator port (34 days).**
- `CodexCharacterCreationScreen` shell + `CodexAside`.
- All 7 step files.
- Wire to existing `CharacterBuilder` (no changes there).
- Replace `TitleScreen.OnNewWorldClicked` to push the new screen.
**M-E — Asset authoring + visual polish (24 days, mostly art).**
- Replace placeholder PNGs with finished art (parchment textures,
gilded borders, clade sigils, ornament glyphs, wax seal).
- Final color/spacing pass against the React design with the prototype
open side-by-side.
- Smoke-test by running the game and clicking through the wizard.
**M-F — Cleanup (½ day).**
- Delete the old Myra `CharacterCreationScreen.cs`.
- Update `_design_handoff/character_creation/IMPLEMENTATION_STATUS.md`
to reflect "now matches the design."
- Update `CLAUDE.md` if any project conventions changed.
**Total: ~1014 days.** Front-loaded with infrastructure (M-A through
M-C); the wizard port (M-D) is mostly translation; the visual polish
(M-E) is where the codex aesthetic lands.
---
## 14. Testing strategy
### 14.1 What to test
- **Unit-test `NineSlice`.** Given a source rect and a target rect,
verify it produces the right 9 sub-quads. No MonoGame dependency
needed if the math is in a static helper.
- **Unit-test `CodexLayout` containers.** Measure + arrange of nested
rows and columns produces expected child bounds.
- **Unit-test `DragDropController` lifecycle.** Begin → update positions
→ drop on registered target → fires OnDrop with correct payload.
- **Smoke-test the screen.** Headless render — push the screen,
call Update + Draw a few frames, assert no exceptions. (Existing test
pattern: see `Theriapolis.Tests/` — add a new `CodexUI/` folder.)
### 14.2 What NOT to test
- Pixel-exact render output. Visual regression testing on rendered
output is fragile; defer to manual visual check against the React
prototype.
- Font kerning / metrics. Trust the font system.
### 14.3 Architecture test
Add to `Theriapolis.Tests/Architecture/CoreNoDependencyTests.cs`: assert
that `Theriapolis.Game.CodexUI.*` types do **not** reference Myra (so
we don't accidentally cross-contaminate). The reflection check is the
same pattern as the existing Core-no-MonoGame check.
---
## 15. Open questions
These should be answered before M-A starts:
1. **Font path: A or B?** Recommend B (FontStashSharp) for v1 iteration
speed, switch later if profiling demands.
2. **Asset authoring source.** Hand-painted in Aseprite, or generated via
the Pixellab tooling already in the repo? The clade sigils benefit
from hand-direction; the 9-slice borders are simple enough to author
either way.
3. **Window resolution assumption.** The React design assumes ~1280×800
minimum; what's the game's target? Confirm before authoring assets at
1× scale (vs 2× or 3×).
4. **Theme switching.** The design's Tweaks panel supports light/dark/
custom themes. The plan assumes the game ships with one fixed theme
(the parchment codex). Confirm before adding theme-switching support
to `CodexTheme`.
5. **Animation.** The plan calls hover transitions out of scope. Confirm
no card-flip / step-transition animations are wanted in v1.
6. **Aside detail panel scope.** The current Myra port uses a click-to-show
detail line in the aside panel as a substitute for hover popovers. With
real hover popovers (`CodexHoverPopover`), is the aside detail line
still wanted? Probably yes for keyboard navigation (no hover when no
pointer).
---
## 16. What this plan does **not** cover
- Title screen redesign. Save for after the character creator ships and
the primitives are proven.
- Inventory screen redesign. Same — defer until M3+ inventory mechanics
shake out and the codex primitives are mature.
- HUD / combat overlay redesign. M5 ships those in Myra; rebuilding them
with `CodexUI` is a much later concern (different aesthetic — combat
HUD wants high-contrast functional, not parchment-decorative).
- The pause menu. Stays Myra. It's utility UI.
---
## Appendix A1 — Color & typography tokens (extracted from `index.html`)
Run this to extract the canonical CSS variables yourself:
```bash
grep -E '^\s*--' _design_handoff/character_creation/from_design/index.html | head -40
```
The definitive list at the time of writing:
```css
--bg: #f3ead4; /* parchment */
--bg-deep: #e8dcb8;
--panel: #ede0bf;
--panel-inset: #e3d4a8;
--ink: #2a1f12;
--ink-soft: #5a4a32;
--ink-mute: #8a7858;
--gild: #a47118;
--gild-bright: #d4a23e;
--seal: #8a2818;
--rule: #c8b88a;
--serif-display: 'Cinzel', 'Cormorant Garamond', serif;
--serif-body: 'Cormorant Garamond', 'EB Garamond', serif;
--mono: 'JetBrains Mono', monospace;
```
(If the React design has been updated since this plan was written,
re-extract from `index.html` and update `CodexColors` / `CodexFonts`
accordingly.)
---
*Theriapolis CodexUI Implementation Plan — 2026-04-25*
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 5 plan series.*
*Implementer: a future Claude Code session — read this whole file before starting M-A.*