Files

750 lines
38 KiB
Markdown
Raw Permalink Normal View History

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