Files
TheriapolisV3/theriapolis-rpg-implementation-plan-godot-port.md
Christopher Wiebe 953bb985ad M5: Codex design system (dark theme) + kitchen-sink
Programmatic Theme builder + reusable popover and stepper widgets,
ported from CharacterCreator.zip's :root design tokens. Kitchen-sink
scene exercises every primitive for visual eyeballing.

CodexPalette.cs:
  Color tokens lifted verbatim from the React prototype's `:root`
  block (--bg, --ink, --gild, --seal, etc.). Variable names mirror
  the CSS so the audit trail stays readable. Spacing locked at the
  prototype's normal density (--gap=24, --pad=28, --radius=2).

  Scope cut: only the Dark theme ships. The React prototype designed
  Parchment, Dark, and Blood as switchable variations — user direction
  during M5 is that only Dark (leather + candlelight) is wanted for
  this game. Parchment/Blood code dropped, plan doc updated to match
  (§1 goal #5, §4.5 UI map, §5 M5 scope, §10 resolved decisions #4).
  No runtime theme switcher.

CodexTheme.Build():
  Programmatically constructs a Godot Theme from CodexPalette.Dark
  plus CodexSpacing/CodexType tokens. Configures Panel, Card,
  CodexPopover styleboxes; Label variations for H1..H4, CodexTitle,
  Eyebrow, Meta, ValidationOk/Error, CardName/Body/Meta, StepperNum/
  Name; Button + PrimaryButton + GhostButton variants; LineEdit,
  CheckBox, scrollbar styling.

  Fonts: looks for CormorantGaramond / CrimsonPro / JetBrainsMono
  TTFs in res://Fonts/ (or Content/Fonts/) and graceful-falls-back to
  Godot defaults if missing. M5 ships with no fonts in repo; user can
  drop them in later for typography parity with the React prototype.

CodexPopover.cs:
  Hoverable text trigger + floating PanelContainer, mirrors
  src/trait-hint.jsx. Viewport-clamps horizontally and vertically;
  flips above the trigger if there's no room below; 80 ms grace
  period when moving cursor from trigger to popover. Detriment
  variant uses the seal-coloured stylebox. Future TraitName /
  SkillChip / BonusPill widgets layer className differences on top.

CodexStepper.cs:
  Roman-numeral horizontal stepper with Pending / Active / Complete /
  Locked states. Active step gets a 2-px gild underline, Complete
  shows a ✓ in seal-red, Locked shows ✕ + 0.45 modulate. Emits
  StepClicked(int) for non-locked rows. M5 is decorative — M6 wires
  the signal to the character-creation state machine.

KitchenSink.cs + Main.cs --codex-test:
  Verification scene rendering every primitive (header, stepper,
  buttons, inputs, cards, trait popovers). Clicks log to console.
  Fonts default to Godot's Noto Sans until res://Fonts/ is populated.

Closes M5 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M6 (title + character creation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:29:22 -07:00

750 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Theriapolis — Godot Port — Design & Implementation Plan
**Status:** Proposed (drafted 2026-04-30).
Targets the codebase state **after Phase 7 ships**: SAVE_SCHEMA_VERSION ≥ 7,
~700+ tests green, dungeons + PoI interiors + dialogue→combat handoff live,
1719 screens in `Theriapolis.Game`, CodexUI as the in-game UI framework.
**Audience:** the agent who will land the Godot port. Read §3 (pre-flight audit)
before writing code — the size and shape of the rewrite surface determines milestone
ordering. Read §10 (risks) before committing to dates.
**Governing docs:**
- `theriapolis-rpg-implementation-plan.md` §12 (binding hard rules — all still
apply post-port)
- `CharacterCreator.zip` (at repo root) — the **canonical visual + interaction
spec** for character creation and, by extension, the design language for
every other screen. See `README.md` inside the zip for the integration
contract; `index.html` `<style>` block for the full design system; `src/*.jsx`
for component structure. **This supersedes `theriapolis-codex-ui-implementation-plan.md`
and the existing CodexUI implementation as design authority for the port.**
- `theriapolis-codex-ui-implementation-plan.md` — historical only. CodexUI is
the "pale imitation" of the React prototype that we're discarding.
- `theriapolis-rpg-implementation-plan-phase7.md` §1.10 (Phase-7 carryover
items — verify each is in `Core` before port begins)
- `CLAUDE.md` "Hard Rules" section — every rule remains in force; the port
changes the rendering host, not the rules.
**All hard rules from the original plan §12 remain in force.** No engine code
in `Theriapolis.Core`, all RNG via `SeededRng`, all magic numbers in
`Constants.cs`, determinism + linear-feature exclusion contracts unchanged. The
architecture test gets a one-line update (the forbidden namespace becomes
`Godot` instead of `Microsoft.Xna`).
---
## 1. Goals & non-goals
### Goals
1. **Replace MonoGame + Myra + CodexUI with Godot 4 (C#).** Modern, composable
`Control`-node UI; no more hand-rolling NineSlice and layout primitives.
2. **Preserve `Theriapolis.Core` byte-for-byte.** Zero changes to worldgen,
RNG, polylines, settlements, dungeons, save schema, or any deterministic
subsystem. The architecture test continues to enforce engine-free Core.
3. **Adopt the React prototype's design language as the new in-game UI.**
`CharacterCreator.zip` is the spec: parchment / dark / blood themes via
CSS custom properties, illuminated-codex typography (Cormorant Garamond,
Crimson Pro, Cinzel, etc.), trait/skill hover popovers with viewport
clamping, floating Aside summary, codex-styled stepper, foil/seal/rule
visual vocabulary. Every screen — not just character creation — adopts
this design language.
4. **Behavioural parity for every shipped screen.** Title, character
creation, world map, play (tactical), combat HUD, inventory, save/load,
level-up, shop, quest log, reputation, defeated, pause, interaction,
dungeon, plus the Phase-7 dialogue→combat overlay. *Visual* parity with
today's CodexUI is **not** a goal — the React prototype is the new
baseline. Behavioural parity (what each screen *does*) is the goal.
5. **Ship the dark theme only.** The React prototype designed Parchment,
Dark, and Blood; the user's call during M5 is that only Dark (leather +
candlelight) is wanted for this game. Parchment and Blood are dropped
from scope. No runtime theme switcher — there is one theme.
6. **Tests still green.** All ~700 Core/worldgen/determinism/save tests pass
unchanged. Project builds and runs `dotnet test` and the headless
`Theriapolis.Tools` CLI exactly as before.
7. **One Godot project replacing two C# projects** (`Theriapolis.Game` +
`Theriapolis.Desktop`). The Godot project references `Theriapolis.Core`
directly and ships as the new desktop entry point.
### Non-goals
- **No new gameplay features.** This is a port + UI rebuild, not a phase.
Defer all "while we're in there" gameplay temptations. The character-creation
surface gains a Subclass step (§5.6) only because Phase 6.5 already shipped
subclass selection and the existing CodexUI surfaces it; the React prototype
predates 6.5 and doesn't include it. This is a forced reconciliation, not new
scope.
- **No mobile / web export** in this port. Godot supports them; we're not
exercising them yet. (The React prototype is web-native; if a future phase
wants a true web client, that's a separate effort.)
- **No save-format change.** Saves written by the MonoGame build must load in
the Godot build and vice versa. SAVE_SCHEMA_VERSION does not bump.
- **No Godot-side determinism.** Core is the deterministic boundary. Anything
Godot does with rendering/animation/UI state is presentation-only.
- **No embedding the React prototype.** We are not shipping a webview, an
Electron sidecar, or any HTML rendering inside Godot. The zip is a *design
spec*, not a build artifact.
- **No data-schema migration in this port.** If `CharacterCreator.zip`'s
`data/*.json` shape diverges from the live `Content/Data/*.json` (it almost
certainly does — the zip predates Phase 6.5), the live schema wins. The
React prototype's data files are reference material for *UI structure*, not
for *content*.
---
## 2a. The CharacterCreator.zip baseline
The user has a React-via-Babel character-creation prototype (Claude Design
output, ~345 KB) that they prefer over the in-game CodexUI implementation.
CodexUI was a "pale imitation" of this prototype; rather than re-port the
imitation, we go back to the source. The prototype is at the repo root as
`CharacterCreator.zip` and contains:
- `index.html` — host page with the **complete design system** in a single
`<style>` block: CSS custom properties for three themes (parchment / dark /
blood), density toggle (compact/normal), font pairings, page layout,
stepper, cards, chips, popovers, drag affordances. This is the visual
source of truth.
- `src/main.jsx`, `src/app.jsx` — wizard shell, 7-step state machine,
per-step validation, forward-navigation gating, `Aside` summary
computation (sums clade + species ability mods).
- `src/steps.jsx` — all 7 step components (`StepClade`, `StepSpecies`,
`StepClass`, `StepBackground`, `StepStats`, `StepSkills`, `StepReview`).
- `src/data.jsx` — runtime constants (ABILITIES, SKILL_LABEL, SKILL_DESC,
SKILL_ABILITY, SIZE_LABEL, LANGUAGES, ITEM_NAME, STANDARD_ARRAY,
abilityMod, signed). The Godot port copies the *constants*, not the JS.
- `src/trait-hint.jsx` — the hover-popover primitives (TraitName,
LanguageChip, SkillChip, BonusPill) with viewport clamping behaviour.
Reusable across every screen.
- `src/portrait.jsx` — placeholder portrait component (silhouette /
heraldry / placeholder modes).
- `data/{clades,species,classes,backgrounds}.json` — content data **as
shaped for the UI**. Field-level docs in the README §"File map".
- `README.md` — explicit integration instructions written for downstream
developers; covers state shape, drag-drop payloads, popover clamping,
theme system. Read this once before starting M5.
**What we use from the zip:**
- The CSS design system → ported to a Godot `Theme` resource (M5).
- The 7-step wizard structure, validation rules, Aside layout → ported to
Godot Control nodes (M6).
- The hover-popover behaviour → reusable Godot widget (M5).
- The drag-drop ability-assignment payload contract → Godot's drag-drop API.
**What we don't use from the zip:**
- The React/Babel runtime — irrelevant in Godot.
- The HTML/JSX components — replaced by Godot scenes + C# scripts.
- The `data/*.json` files — `Content/Data/*.json` (the live game data) is
authoritative for content. The zip's data files are reference material for
UI structure only.
## 2b. Why now / why Godot
The MonoGame UI ceiling — Myra is dead-upstream and CodexUI is hand-rolled —
means every new screen costs disproportionate time. Phase 7 added two more
screens (dungeon, dialogue→combat) on top of an already 17-screen surface; the
trend is bad. Godot's `Control` node tree, theme system, and editor-driven
scene composition are an order of magnitude cheaper for the kind of UI
authoring this game needs.
The port is *cheap relative to the alternatives* because `Theriapolis.Core`
was designed engine-agnostic from day one (architecture test enforced). Every
deterministic system — the part of this codebase that is *expensive* to
rebuild — survives unchanged. We are replacing the rendering shell and the UI
toolkit, not the game.
Alternatives considered and rejected:
- **Stay on MonoGame, replace Myra only** (e.g. ImGui.NET, FontStashSharp +
CodexUI). Cheaper short-term, but doubles down on a hand-rolled UI
framework with no editor tooling. Doesn't fix the actual ceiling.
- **Avalonia + MonoGame.** Awkward windowing model; mixing two render loops
is fragile.
- **Unity.** Heavier port (Mono → Unity's CLR, asset pipeline overhaul,
licensing), worse 2D pixel-art tooling.
- **Stride.** Less mature 2D pipeline, smaller community.
Godot 4 with C# wins on: native 2D pixel-art workflow, editor scene authoring,
mature `Control`-node UI, MIT licence, `.NET 8` first-class support.
---
## 3. Pre-flight audit (what we're rewriting)
Run before kickoff to confirm post-Phase-7 numbers; the table below is the
**2026-04-30 snapshot** and is only the starting point.
| Subsystem | LOC | Files | Notes |
|----------------------|-------:|------:|---------------------------------------------------|
| `Theriapolis.Game/CodexUI` | 4,492 | 28 | Hand-rolled UI framework — biggest rewrite |
| `Theriapolis.Game/Screens` | 5,014 | 17 | Screen logic; ~50% UI wiring, ~50% game logic |
| `Theriapolis.Game/Rendering` | 1,317 | 9 | World/tactical renderers, camera, atlases, sprites|
| `Theriapolis.Game/Input,Platform,UI`| 548 | 6 | Input mapping, save paths, clipboard |
| `Theriapolis.Game/Game1.cs` + ScreenManager | 166 | 2 | App shell + screen stack |
| `Theriapolis.Desktop` | 37 | 1 | Will be deleted; Godot project replaces it |
| **Total rewrite** | **~11,500** | **63** | |
**Untouched:**
- `Theriapolis.Core` (all generation, simulation, save/load, polylines)
- `Theriapolis.Tools` (CLI worldgen-dump, settlement-report, tile-inspect)
- `Theriapolis.Tests` (xUnit)
- `Content/Data/*.json` (biomes, factions, dungeon templates, etc.)
- `Content/Gfx/*.png` (sprites, tiles, atlases)
- `Content/Fonts/*.ttf`
**Re-run before kickoff:**
```bash
# Confirm Core is still MonoGame-free (must return zero matches)
grep -rn "Microsoft\.Xna\|MonoGame" Theriapolis.Core/
# Confirm test count
dotnet test --no-build --logger "console;verbosity=minimal" | tail
```
---
## 4. Architecture
### 4.1 New project layout
```
Core (no engine deps) ← unchanged
Tools (ImageSharp, headless) ← unchanged
Tests (xUnit) ← unchanged
Theriapolis.Godot/ ← NEW; replaces Game + Desktop
project.godot
Theriapolis.Godot.csproj ← references Core
Scenes/ ← .tscn scene files (one per screen)
UI/ ← .tscn + .gd-style C# Control scripts
Theme/ ← Godot Theme resource for CodexUI styling
Rendering/ ← Camera2D, world/tactical Node2D scripts
Input/ ← Godot InputMap + adapter to Core actions
Platform/ ← SavePaths, Clipboard (Godot APIs)
Autoload/ ← Game state singleton bridging to Core
```
### 4.2 The Core boundary
Core's public surface is unchanged. The Godot side talks to Core through the
same APIs `Theriapolis.Game` used:
- `WorldGenerator.Generate(seed, dataDir)``WorldState`
- `Save.Write(path, snapshot)` / `Save.Read(path)` (whatever the Phase-7 save
API ended up named — verify post-7)
- Resolver, encounter lifecycle, dungeon stamping, quest engine — all
invoked exactly as before.
### 4.3 The architecture test
Update `Architecture/CoreNoDependencyTests.cs` to forbid the `Godot`
namespace in `Theriapolis.Core` in addition to (or instead of) MonoGame.
This is a one-line change. **Keep both bans for the duration of the port** so
the in-flight branch can't accidentally regress.
### 4.4 Render model
The seamless-zoom contract (CLAUDE.md "Seamless Zoom Model") survives. The
implementation moves:
| Concept | MonoGame | Godot |
|--------------------------|-----------------------------|-----------------------------------------|
| Camera | `Rendering/Camera2D.cs` (custom) | `Camera2D` node + zoom property |
| World tile rendering | `WorldMapRenderer` + `SpriteBatch` | `TileMap` or `Node2D._Draw` over `WorldState` |
| Tactical tile rendering | `TacticalRenderer` + atlas | `TileMap` with chunked `set_cell` updates |
| Polyline rendering | `LineFeatureRenderer` (SpriteBatch lines) | `Line2D` node per polyline |
| Sprite (player/NPC) | `PlayerSprite`/`NpcSprite` | `AnimatedSprite2D` |
| Atlas loading | `TileAtlas`/`TacticalAtlas`/`CodexAtlas` | `AtlasTexture` resources |
| Game loop | `Game1.Update`/`Draw` | `_Process` / `_Draw` on root node |
`Line2D` for polylines is a meaningful upgrade — anti-aliased, width-tapered,
texture-able lines without the manual quad strip work `LineFeatureRenderer`
does today.
### 4.5 UI model
The React prototype's design system maps onto Godot's `Theme` + `Control`
system. The mapping below is from **the React prototype** (the new spec), not
from CodexUI (which is being deleted). For CodexUI → React-prototype
correspondences, see the existing CodexUI source as a behavioural reference
only; do not preserve its visual choices.
| React prototype (CharacterCreator.zip) | Godot equivalent |
|-----------------------------------------------------|--------------------------------------------------------|
| CSS custom properties (`--bg`, `--ink`, `--gild`, ...) | Single `Theme` resource constructed in `CodexTheme.cs`|
| `.codex-header`, `.codex-title`, `.codex-sub` | `PanelContainer` + `Label` with display font theme |
| `.stepper` / `.step` / `.step.active/.complete/.locked` | Custom `HBoxContainer` script with state-driven theme |
| `.page` (two-column main + aside) | `HSplitContainer` or `HBoxContainer` (fixed ratio) |
| `.card` (Calling/History/Species cards) | `PanelContainer` + `StyleBoxTexture` (nine-slice) |
| `.chip` (skill/feature/language chips) | `PanelContainer` + `Label`, themed |
| `.btn.primary` / `.btn.ghost` | `Button` with theme variations |
| `.aside` (right-rail summary) | `MarginContainer` + `VBoxContainer` (signal-bound) |
| `.popover` (TraitName/SkillChip/BonusPill hover) | Custom `PopupPanel` widget — viewport-clamp logic |
| HTML5 `dataTransfer` drag-drop in StepStats | `_GetDragData` / `_CanDropData` / `_DropData` |
| Cormorant Garamond / Crimson Pro / Cinzel / Uncial | `FontFile` resources bundled in `res://` |
| `loadData()` (fetch JSON) | Existing `ContentLoader` (Core) → unchanged |
**Theme switching is a runtime player setting**, not a developer toggle. The
React prototype's `tweaks-panel.jsx` is **not** ported — it was a dev
affordance. Theme selection lives in the existing settings screen. The
prototype's density toggle (compact vs. normal spacing) is **dropped from
scope**: ship only the normal spacing values (`--gap: 24px`, `--pad: 28px`).
The character-creation flow is the densest UI in the game and ships first
(M6) as the pilot for the design language. Every other screen inherits the
Theme + widget vocabulary established there.
---
## 5. Milestones
Ten milestones, each independently demoable. Build them in this order; each
unblocks the next.
### M0. Pre-flight (½ day)
- Create `port/godot` branch off `main` post-Phase-7.
- Re-run the §3 audit; record actual LOC and screen count.
- Install Godot 4.x (mono build) + `.NET 8` SDK; confirm `dotnet --version`
matches Core's target.
- Create `Theriapolis.Godot.csproj` referencing `Theriapolis.Core`;
hello-world scene loads + builds.
**Exit criteria:** `dotnet build` of the new csproj succeeds; Godot opens the
empty project; CI (or local `dotnet test`) is unchanged from main.
### M1. Headless parity (1 day)
Goal: prove Core works untouched under Godot's csproj.
- From a Godot autoload script, call `WorldGenerator.Generate(12345)` and
log the FNV hash of the resulting `WorldState`.
- Confirm hash matches `Theriapolis.Tools worldgen-dump --seed 12345`.
- Update `Architecture/CoreNoDependencyTests.cs` to forbid `Godot.*` in Core
(in addition to MonoGame).
**Exit criteria:** identical worldgen hash from Tools CLI and Godot host. No
test regressions.
### M2. World map render (3 days)
The lowest-risk visual milestone — pure data → pixels, no UI.
- `Rendering/WorldMapNode.cs`: `Node2D` with `_Draw` rendering tile colours
from `WorldState.Tiles`. Mirror `WorldMapRenderer.cs` colour logic.
- `Camera2D` node with mouse-wheel zoom; reproduce
`C.WORLD_TILE_PIXELS`-aware coordinate model.
- `LineFeatureNode.cs`: instantiate one `Line2D` per polyline in
`WorldState.Rivers`/`Roads`/`Rails`. Width and colour from constants.
- Manual visual diff vs `Theriapolis.Tools worldgen-dump --seed 12345
--out world.png`; pixel-diff utility optional.
**Exit criteria:** generate world from seed 12345 in Godot, compare to tools
PNG output side-by-side; rivers/roads/rails visually correct, including
bridges and wye-junctions. No determinism drift.
### M3. Asset pipeline (2 days)
- Copy `Content/Data/*.json` and `Content/Gfx/*.png` into the Godot project's
`res://` filesystem; configure `.csproj` to keep them in sync (or symlink).
- Convert each PNG atlas into Godot `AtlasTexture` resources. Tile atlas, NPC
atlas, CodexUI atlas — three separate themes.
- Write a `ContentLoader` autoload that exposes the same `string
ContentDataDirectory` / `ContentGfxDirectory` contract Game1 had, so
Core's data loading code (`Loaders.cs` etc.) is unchanged.
- Verify `biomes.json` / `factions.json` / dungeon room templates load.
**Exit criteria:** Core can read all data files via the Godot path; PNG atlases
render correctly in a test scene.
### M4. Tactical render (3 days)
- `Rendering/TacticalNode.cs`: `TileMap` showing the 3×3 world-tile window
around the player (`C.TACTICAL_WINDOW_WORLD_TILES`).
- Streaming: re-implement the 64-tile chunk loader from `TacticalRenderer.cs`
using `TileMap.SetCell` calls bounded to the visible viewport.
- `PlayerSprite` / `NpcSprite` → `AnimatedSprite2D` nodes. Walk-cycle frames
drive from the existing per-sprite frame-index logic (Core-side).
- Camera follow + smooth zoom from world-map view → tactical view at the
zoom threshold.
**Exit criteria:** walk around a generated world, tactical view streams
chunks correctly, sprites animate, camera transitions are smooth.
### M5. Codex design system in Godot (4 days)
Port the React prototype's design system to a Godot Theme. This is the
foundation every subsequent screen builds on.
- Extract the `<style>` block from `CharacterCreator.zip/index.html` into a
written audit (one-time, ~½ day): every CSS custom property → Theme
constant; every selector → Theme type/variation; every nine-slice candidate
identified.
- `Theme/codex.tres`: Godot Theme with the parchment palette as default —
`--bg` / `--ink` / `--gild` / `--seal` / `--rule` mapped to Theme constants;
display + body fonts as Theme default fonts; standard spacing/radius
constants.
- ~~Theme variations for Parchment + Blood~~ — dropped during M5 per
user decision; Dark only.
- Fonts: copy Cormorant Garamond, Crimson Pro, Spectral, EB Garamond, Cinzel,
Uncial Antiqua, JetBrains Mono into `res://Fonts/`. Bundle as `FontFile`
resources at the sizes the prototype uses (verify by spot-rendering each).
- `UI/Widgets/CodexPopover.cs`: reusable hover-popover Control implementing
the `TraitName` / `SkillChip` / `BonusPill` pattern from
`src/trait-hint.jsx`. Includes the viewport-clamp + flip-above-or-below
behaviour the React version has (README §"Hover popovers and viewport
clamping" — `documentElement.clientWidth/Height`, not `window.inner*`).
- `UI/Widgets/CodexStepper.cs`: the `.stepper` / `.step` widget — Roman
numerals, locked/active/complete states, click-to-jump with gating.
Reused by character creation and any future multi-step flow.
- "Kitchen sink" scene showing each primitive (button primary/ghost, card,
chip, popover, stepper, aside, page header, validation banner) under all
three themes. Side-by-side comparison against screenshots taken from the
React prototype (open `index.html` locally with `python3 -m http.server`).
**Exit criteria:** kitchen-sink scene visually matches React-prototype
screenshots across all three themes; the popover widget passes a manual
viewport-edge test (trigger near each edge, popover stays in-bounds).
### M6. Title + character creation (5 days)
The character-creation flow is M5's first real customer. Direct port of the
React prototype's structure.
- `Scenes/TitleScreen.tscn`: vertical button stack on a parchment field with
the codex title (Cinzel / Uncial Antiqua treatment) and a version label.
Plain — exists mostly to validate the design system in a non-trivial
composition.
- `Scenes/CharacterCreation.tscn`: top-level scene matching `app.jsx`:
- `codex-header` band: title + folio counter ("Folio III of VIII").
- `CodexStepper` (from M5) bound to the 8 steps (see below).
- `page` two-column body: per-step scene (left) + Aside (right).
- `nav-bar`: Back / validation banner / Next.
- One scene per step under `Scenes/CharacterCreation/Steps/`. Direct
port of `src/steps.jsx`:
1. `StepClade` — clade picker grid; selecting a clade auto-defaults
species per `app.jsx` `useEffect([cladeId])`.
2. `StepSpecies` — species cards filtered by clade.
3. `StepClass` — calling cards; level-1 features filtered from
`level_table` per the prototype's contract.
4. `StepSubclass` — **NEW vs. React prototype**. The prototype is
pre-Phase-6.5 and has no subclass picker. **Confirmed scope: a
dedicated step**, not an inline picker on the Calling card.
Adopts the M5 design vocabulary (cards laid out the same way as
`StepClass`, one card per available subclass for the chosen
calling). Data shape from the live `Content/Data/classes.json`.
5. `StepBackground` — history cards.
6. `StepStats` — ability assignment, **drag-drop**. Reproduce the
payload contract from `steps.jsx` `handleDrop` /
`dropToPool` exactly:
- pool→slot, slot→slot (swap), slot→pool
- payload shape: `{from:"pool", value, idx}` or
`{from:"slot", value, ability}`
Use Godot's `_GetDragData` / `_CanDropData` / `_DropData`.
Roll-history list mirrors `statHistory` array.
7. `StepSkills` — class skill picks above background-locked skills.
Skill chips use the M5 `CodexPopover` (SkillChip + BonusPill).
8. `StepReview` — name entry + final summary. The "Confirm & Begin"
button does what `app.jsx` step's TODO told us to: hand the
final character object to Core (see README §4 "Wire the
'Confirm & Begin' handoff").
- `UI/Aside.tscn` + script: ports `Aside` from `app.jsx` — clade/species
flavor, total ability scores including bonuses (sum of
`statAssign[ab] + clade.ability_mods[ab] + species.ability_mods[ab]`),
size + speed, skill list. Bound to a Core `CharacterDraft` model via
Godot signals.
- Validation: per-step `Validate()` mirroring `validate(i)` in `app.jsx`
exactly (clade picked, species picked, ..., assigned 6/6 abilities, picked
N/N skills, name non-empty).
- Forward-navigation gating: a step is locked iff some earlier step has
unmet validation, per the prototype's `firstIncomplete` rule. Backward
navigation always allowed.
**Schema reconciliation sub-task (½ day, kicks off M6):** before writing UI,
diff the React prototype's `data/*.json` against `Content/Data/*.json`.
For each entity (clade / species / class / background), produce a one-page
field map noting where the live schema is a superset (Phase 6.5 added
fields), where it differs in shape, and where the prototype assumed a
field the live schema lacks. Live schema wins on all conflicts. The map
becomes the contract the step scripts read against.
**Exit criteria:** create a character end-to-end across all 8 steps, hash
the resulting `PlayerCharacter` blob, confirm it matches the same character
created via the MonoGame build using the same inputs (modulo subclass step,
which the React prototype lacks). Side-by-side screenshots of each step in
parchment / dark / blood themes match the prototype's design language.
### M7. Play loop screens (5 days)
Port in dependency order; each is ~½–1 day.
1. `WorldGenProgressScreen` — progress bar driven by Core stage callbacks.
2. `WorldMapScreen` — wraps the M2 world map node + minimap + UI overlays.
3. `PlayScreen` — wraps the M4 tactical node + HUD overlay (HP, stamina,
minimap).
4. `PauseMenuScreen` — `PopupPanel` overlay; pauses Core sim tick.
5. `SaveLoadScreen` — `ItemList` of saves; `Save.Read`/`Save.Write` calls
unchanged.
6. `InteractionScreen` — dialogue tree UI; densest text rendering after
character creation.
**Exit criteria:** load a save from the MonoGame build, walk around, save,
reload — bytes identical.
### M8. Combat + dungeon screens (4 days)
The Phase-7-shipped surface.
1. `CombatHUDScreen` — turn order, ability bar, target reticle, damage
numbers. Damage-number animations move from `SpriteBatch` to
`AnimatedSprite2D` or tween-driven `Label` nodes.
2. `DungeonScreen` (Phase-7) — variant of `PlayScreen` for bounded
interiors; same tactical render path, different camera bounds + exit-tile
logic.
3. `LevelUpScreen` — stat allocation grid; reuse character-creation widgets.
4. `InventoryScreen` / `ShopScreen` — drag-drop between containers using
Godot's native drag-drop API (replaces `DragDropController.cs`).
5. `QuestLogScreen` / `ReputationScreen` / `DefeatedScreen` — mostly
text-list views.
6. **Dialogue → combat handoff** (Phase-7): the
`InteractionScreen` "settle this here" branch closes the popup and
instantiates `CombatHUDScreen.tscn` with the encounter pre-loaded. The
Phase-7 plumbing on the Core side is unchanged.
**Exit criteria:** play through Old Howl mine end-to-end (Phase-7 showcase
content): walk in, combat, loot, walk out, return to overworld.
### M9. Input, save paths, clipboard (1 day)
The platform layer.
- `Input/InputMap.tres`: every binding from `InputManager.cs` + the new
Godot `InputMap` actions. Adapt to Core via an `InputAdapter` autoload
exposing the same enum-typed action API the screens use today.
- `PlayerController` → `_PhysicsProcess`-driven node consuming the adapter.
- `Platform/SavePaths.cs` → use `OS.GetUserDataDir()` (Godot's
cross-platform per-user dir); confirm the directory matches MonoGame's
`LocalApplicationData` path so saves remain interoperable, *or* migrate
on first run with an explicit migration helper.
- `Platform/Clipboard.cs` → `DisplayServer.ClipboardSet`/`ClipboardGet`.
- **Window mode: borderless fullscreen at native desktop resolution.**
In `project.godot`, set `display/window/size/mode = 3`
(`Fullscreen` — borderless, native resolution). Stretch settings:
`display/window/stretch/mode = "canvas_items"`,
`stretch/aspect = "expand"`. Design the Theme so type and spacing
remain legible across common resolutions (1920×1080, 2560×1440,
3840×2160). The React prototype's `width=1440` viewport meta is
*not* a constraint — the codex aesthetic should fill the screen.
Add a developer-only `--windowed` CLI flag (or F11 toggle) for
iteration.
**Exit criteria:** every keybinding works; saves written by the MonoGame
build load in the Godot build.
### M10. Cleanup, packaging, decommission (2 days)
- Delete `Theriapolis.Game/` and `Theriapolis.Desktop/`. Remove from the
solution.
- Drop MonoGame + Myra packages from `Theriapolis.Core.csproj` (there should
be none, but verify) and the new Godot csproj's transitive graph.
- Update `CLAUDE.md`:
- "Build & Run Commands": replace `dotnet run --project Theriapolis.Desktop`
with the Godot launch command.
- "Project Architecture": replace `Theriapolis.Game` paragraph with
`Theriapolis.Godot`.
- Architecture-test description: forbid `Godot.*` (not `Microsoft.Xna.*`)
in Core.
- Configure Godot export presets for Windows / Linux / macOS desktop.
- Smoke-test export builds on each target platform.
**Exit criteria:** `Theriapolis.Game` / `Theriapolis.Desktop` are gone;
`dotnet test` is green; an exported Windows build runs and plays through Old
Howl mine.
---
## 6. Total estimate
| Phase | Work | Days |
|-------|-----------------------------------|-----:|
| M0 | Pre-flight | 0.5 |
| M1 | Headless parity | 1 |
| M2 | World map render | 3 |
| M3 | Asset pipeline | 2 |
| M4 | Tactical render | 3 |
| M5 | Codex design system in Godot | 4 |
| M6 | Title + character creation | 5 |
| M7 | Play loop screens | 5 |
| M8 | Combat + dungeon screens | 4 |
| M9 | Input, save paths, clipboard | 1 |
| M10 | Cleanup, packaging, decommission | 2 |
| **Total** | | **30.5** |
≈ **67 calendar weeks** assuming a single full-time engineer, no
significant blockers, and the post-Phase-7 baseline is genuinely stable. Pad
20% (~78 weeks) for the unknowns called out in §10. M5 grew by 1 day vs.
the original plan to absorb the design-system audit and the reusable popover
+ stepper widgets, both of which pay back in M6M8. M6 grew by 1 day to
absorb the schema reconciliation and the Subclass step. If the agent doing
the port can run M2/M3/M4 in parallel via worktrees, shave 34 days.
---
## 7. Determinism & save compatibility
The bright line: **anything that touches `WorldState`, `PlayerCharacter`,
`Save.*`, or the Phase-7 dungeon/encounter state is Core's job, not Godot's.**
- All RNG is Core. No `randf()`, no `RandomNumberGenerator` Godot calls in
gameplay paths. Only presentation-layer randomness (e.g. animation jitter)
may use Godot RNG, and only in code paths that don't feed back into Core.
- Save compatibility test: `Tests/SaveCompat/MonogameToGodotTests.cs` reads
a fixture save written by the MonoGame build (committed to the repo) and
asserts every Core-loadable field round-trips identically.
- Per-stage hash tests (`WorldgenDeterminismTests`, `Phase23DeterminismTests`,
any Phase-7 additions) continue to run unchanged.
---
## 8. Testing strategy
### What stays
- All `Theriapolis.Tests/` xUnit tests run unchanged. They reference Core
only and don't care about the host.
- All `Theriapolis.Tools/` CLI behaviour (`worldgen-dump`,
`settlement-report`, `tile-inspect`) is unchanged.
### What's added
- `Tests/SaveCompat/MonogameToGodotTests.cs` — one fixture save per major
schema event (post-Phase-3, post-Phase-6, post-Phase-7), each loaded and
hashed for equality.
- `Tests/Architecture/NoGodotInCoreTests.cs` — twin of the existing MonoGame
ban; reflection check that `Theriapolis.Core.dll` has no `Godot.*`
references.
- A small `Theriapolis.Godot/Tests/` Godot-side script set (one .tscn per
visual milestone) for manual visual-diff comparison vs MonoGame screenshots.
Not run in CI.
### What's gone
- Any tests that exercise `Theriapolis.Game/`-specific code (there should be
none — Game has no test coverage today by design — verify before deleting).
---
## 9. Migration & rollback
- The port runs on a `port/godot` branch; `main` keeps shipping MonoGame
patches if needed during the 67 week window.
- After M10 ships, MonoGame branch is tagged `monogame-final` and frozen.
No back-merges from `main` into `port/godot` after M5 (CodexUI parity
point) — too much surface diverges.
- **Rollback plan:** if the port stalls past M6 with unsolvable Godot-side
blockers, abandon `port/godot` and reconsider §2's "ImGui.NET / replace
Myra only" alternative. M0M5 work is salvageable for a future attempt.
---
## 10. Risks & open questions
### Real risks
1. **Data-schema reconciliation.** `CharacterCreator.zip/data/*.json`
predates Phase 6.5 (subclasses, levelling, hybrids, scent tags). The live
`Content/Data/*.json` has additional fields and possibly different shapes
per Phase-6.5 deviations. The React prototype's UI assumes the older
shape. *Mitigation:* the M6 schema-reconciliation sub-task produces a
field map before any UI code is written; the live schema wins on all
conflicts; the React component's data assumptions are adapted to
match. Do this audit in writing — do not let it leak into ad-hoc
step-by-step "oh we also need..." discoveries.
2. **Phase-6.5 features absent from the React prototype.** Specifically: no
subclass picker (README §"Known caveats"), no levelling beyond level 1,
no hybrid-character clade pairing, no scent-mask UI. *Mitigation:* the
plan adds an explicit Subclass step (M6 step #4); levelling stays in
`LevelUpScreen` (M8) where it already lives; hybrids are a clade-picker
variant (M6 StepClade); scent UI lives elsewhere (likely InventoryScreen
or a future settings surface). All of these adopt the M5 design
language; none invent new visual idioms.
3. **Codex-aesthetic completeness.** The React prototype only designed the
character-creation surface. Other screens (combat HUD, world map
overlays, dungeon UI) need design decisions extending the codex
vocabulary. *Mitigation:* M5's reusable widget set (popover, stepper,
panel/card/chip, button variants) covers ~80% of every screen; the
remaining ~20% (combat-specific overlays, minimap framing) gets one
design decision per screen made during M7/M8 with the M5 vocabulary
as the base. If a screen needs a brand-new visual idiom, flag it
instead of inventing one — defer to a polish pass.
4. **Tactical chunk streaming performance.** `TileMap.SetCell` is well
optimised but the 64-tile chunk loader was tuned against `SpriteBatch`.
*Mitigation:* if M4 reveals a perf issue, fall back to `MultiMeshInstance2D`
for tile rendering.
5. **Save path divergence.** `OS.GetUserDataDir()` defaults to
`%APPDATA%/Godot/app_userdata/<project>` on Windows; MonoGame writes
under `%LOCALAPPDATA%/Theriapolis`. *Mitigation:* override Godot's
user-data path explicitly to match the MonoGame path, or ship a one-shot
migration on first launch (preferred).
6. **C# hot-reload ergonomics in Godot.** Godot 4's C# tooling is solid but
not as snappy as MonoGame's iteration loop. *Mitigation:* set up
editor-side debugging early (M0), accept slower iteration.
7. **Drag-drop fidelity** (M6 stats step + M8 inventory). Godot's native
drag-drop is event-driven; the React prototype uses HTML5 `dataTransfer`
with a documented payload shape. *Mitigation:* port the payload contract
verbatim (`{from:"pool", value, idx}` / `{from:"slot", value, ability}`),
serialise as `Variant` dictionaries; if native API doesn't suffice for a
custom drag preview, write a thin `_GuiInput`-driven shim on `Control`
nodes.
### Non-risks (called out so we don't worry about them)
- **Core porting.** Core does not port. It is referenced.
- **Determinism.** Determinism lives in Core and is not touched.
- **Test breakage.** Tests reference Core only.
- **Tools CLI.** Tools references Core only.
### Resolved decisions (locked in 2026-04-30)
1. **Subclass step UX.** Dedicated 8th step, not an inline picker on the
Calling card. Implementation in M6 step #4.
2. **Density toggle.** Dropped from scope. Ship only the prototype's normal
spacing values (`--gap: 24px`, `--pad: 28px`). The React prototype's
compact mode was a dev-time experiment, not a player feature.
3. **Window mode.** Borderless fullscreen at native desktop resolution by
default. The Theme must scale legibly across 1920×1080 / 2560×1440 /
3840×2160. F11 (or `--windowed`) toggles to a windowed mode for
iteration. Configured in M9.
4. **Theme.** Dark only (leather + candlelight palette). Parchment and
Blood dropped from scope during M5; no runtime theme switcher.
### Open questions for the user / project lead
1. **Pixel-art shader stack.** CodexUI uses MonoGame `SpriteBatch` with
point filtering; do any tactical-render screens depend on
`SamplerState.PointClamp` behaviour that needs reproducing in Godot?
(Default Godot project import settings set `Filter: Nearest` for pixel
art — likely fine, confirm at M5.)
2. **Audio.** Phase 7 may or may not have introduced audio. If so, M9 grows
to include `AudioStreamPlayer` integration.
3. **Multi-monitor + DPI.** With borderless-fullscreen as the default, the
Godot build will pick the *primary* monitor's native resolution at
launch. Confirm that's the desired behaviour, or add a monitor-selection
setting. DPI scaling on high-density displays may need explicit handling;
defer to M9.
---
## 11. Done definition
The port is complete when, simultaneously:
1. `Theriapolis.Game/` and `Theriapolis.Desktop/` are deleted from the repo.
2. `dotnet test` is green (≥ Phase-7 test count, none removed except
verified Game-only tests, plus the new save-compat + architecture tests).
3. A save written by `monogame-final` loads in the Godot build, plays through
Old Howl mine, saves, reloads — all without errors.
4. The Godot build exports cleanly to Windows desktop and runs the same
end-to-end smoke test.
5. `CLAUDE.md` is updated to reflect the new project layout and build
commands; the architecture test forbids `Godot.*` in Core.
6. `theriapolis-rpg-implementation-plan-godot-port.md` (this file) gets a
"Status: Shipped 20XX-XX-XX" header and a §12 deviation table for any
milestone that landed differently than planned.