Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
# 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,
|
||||
17–19 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 all three themes.** Parchment (default), dark (leather &
|
||||
candlelight), blood (warm crimson). The React prototype already designed
|
||||
them; Godot's Theme + ThemeVariation system makes shipping all three
|
||||
approximately free. Theme is a runtime player setting, not a dev tweak.
|
||||
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`, ...) | `Theme` resource type variations (one per theme) |
|
||||
| `[data-theme="dark"]` / `[data-theme="blood"]` | `ThemeVariation` resources, switched via player setting|
|
||||
| `.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/dark.tres` and `Theme/Variations/blood.tres`: ThemeVariations
|
||||
for the other two themes. Player setting selects the active variation.
|
||||
- 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** |
|
||||
|
||||
≈ **6–7 calendar weeks** assuming a single full-time engineer, no
|
||||
significant blockers, and the post-Phase-7 baseline is genuinely stable. Pad
|
||||
20% (~7–8 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 M6–M8. 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 3–4 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 6–7 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. M0–M5 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. **Default theme.** Parchment. Dark and blood ship as player-selectable
|
||||
variations in the existing settings screen.
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user