708 lines
33 KiB
Markdown
708 lines
33 KiB
Markdown
|
|
# 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 (1–2 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 (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).**
|
|||
|
|
- `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 (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.md`
|
|||
|
|
to reflect "now matches the design."
|
|||
|
|
- Update `CLAUDE.md` if 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 `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.*
|