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

33 KiB
Raw Blame History

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).

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:

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 — 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 contractTheriapolis.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:

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:

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:

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:

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:

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 — 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 CodexAbilityRows 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). The change:

// 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:

// 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:

// 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:

grep -E '^\s*--' _design_handoff/character_creation/from_design/index.html | head -40

The definitive list at the time of writing:

--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.