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>
33 KiB
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
- 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.
- Reuse the existing MonoGame SpriteBatch rendering stack — same
SpriteBatch, sameSpriteFontsystem, same content-pipeline patterns the world/tactical renderers use. No new framework dependency. - Co-exist with Myra, which stays in tree for utility screens (pause
menu, save/load slot picker). The Game1 screen stack treats
CodexUIscreens and Myra screens identically — both implementIScreen. - 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
CodexUIprimitives. - 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.csprojandTheriapolis.Desktop.csproj). - C# 12 / .NET 8 (
<LangVersion>12</LangVersion>). Theriapolis.Coremust remain MonoGame-free — enforced byArchitecture/CoreNoDependencyTests.cs. AllCodexUIcode lives inTheriapolis.Game/.- Existing screen contract —
Theriapolis.Game/Screens/IScreen.cs:Initialize(Game1),Update(GameTime),Draw(GameTime, SpriteBatch),Deactivate(),Reactivate(). NewCodexScreenbase class implements this. - Content pipeline — Desktop project's
.csprojalready wildcards..\Content\Gfx\**to output. Add a newContent/Gfx/codex/subfolder for the new asset set. DesktopRuntimeIdentifierscurrently includewin-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.mgcbtoTheriapolis.Desktopand 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
SpriteFontentry toContent.mgcbwith the source TTF and the desired character set. - The
.mgcbbuild produces.xnbfiles that MonoGame'sContentManagerloads at runtime. - Pros: baked atlases, fastest runtime load, no extra runtime deps.
- Cons: requires
mgcbtool 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 withSpacing(defaultRowGap), optionalPadding, optionalHAlign(left / center / right / stretch).Row— horizontal stack with same parameters; defaultColGap.Grid— fixed-column grid; takesColumns(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
_hoverEnterTimeper widget; show popover afterHoverDelay = 0.4s. - On show, position popover relative to trigger widget; clamp to viewport edges; flip above/below if not enough room below.
- Hide on
MouseLeftfrom both trigger and popover bounds.
For text input (CodexTextBox):
- Subscribe to
Game1.Window.TextInputevent in the screen'sInitialize; route incomingcharto the focused TextBox viaCodexInput.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
CodexPoolDiehas apayload = { 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 (1–2 days).
- Implement
CodexWidget,CodexLayout,CodexInput,CodexTheme. - Implement
NineSlicerenderer. - Implement
CodexAtlaswith 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 (2–3 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 (3–4 days).
CodexCharacterCreationScreenshell +CodexAside.- All 7 step files.
- Wire to existing
CharacterBuilder(no changes there). - Replace
TitleScreen.OnNewWorldClickedto push the new screen.
M-E — Asset authoring + visual polish (2–4 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.mdto reflect "now matches the design." - Update
CLAUDE.mdif any project conventions changed.
Total: ~10–14 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
CodexLayoutcontainers. Measure + arrange of nested rows and columns produces expected child bounds. - Unit-test
DragDropControllerlifecycle. 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 newCodexUI/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:
- Font path: A or B? Recommend B (FontStashSharp) for v1 iteration speed, switch later if profiling demands.
- 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.
- 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×).
- 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. - Animation. The plan calls hover transitions out of scope. Confirm no card-flip / step-transition animations are wanted in v1.
- 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
CodexUIis 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.