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>
38 KiB
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. SeeREADME.mdinside the zip for the integration contract;index.html<style>block for the full design system;src/*.jsxfor component structure. This supersedestheriapolis-codex-ui-implementation-plan.mdand 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 inCorebefore 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
- Replace MonoGame + Myra + CodexUI with Godot 4 (C#). Modern, composable
Control-node UI; no more hand-rolling NineSlice and layout primitives. - Preserve
Theriapolis.Corebyte-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. - Adopt the React prototype's design language as the new in-game UI.
CharacterCreator.zipis 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. - 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.
- 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.
- Tests still green. All ~700 Core/worldgen/determinism/save tests pass
unchanged. Project builds and runs
dotnet testand the headlessTheriapolis.ToolsCLI exactly as before. - One Godot project replacing two C# projects (
Theriapolis.Game+Theriapolis.Desktop). The Godot project referencesTheriapolis.Coredirectly 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'sdata/*.jsonshape diverges from the liveContent/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,Asidesummary 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
Themeresource (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/*.jsonfiles —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:
# 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)→WorldStateSave.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/godotbranch offmainpost-Phase-7. - Re-run the §3 audit; record actual LOC and screen count.
- Install Godot 4.x (mono build) +
.NET 8SDK; confirmdotnet --versionmatches Core's target. - Create
Theriapolis.Godot.csprojreferencingTheriapolis.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 resultingWorldState. - Confirm hash matches
Theriapolis.Tools worldgen-dump --seed 12345. - Update
Architecture/CoreNoDependencyTests.csto forbidGodot.*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:Node2Dwith_Drawrendering tile colours fromWorldState.Tiles. MirrorWorldMapRenderer.cscolour logic.Camera2Dnode with mouse-wheel zoom; reproduceC.WORLD_TILE_PIXELS-aware coordinate model.LineFeatureNode.cs: instantiate oneLine2Dper polyline inWorldState.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/*.jsonandContent/Gfx/*.pnginto the Godot project'sres://filesystem; configure.csprojto keep them in sync (or symlink). - Convert each PNG atlas into Godot
AtlasTextureresources. Tile atlas, NPC atlas, CodexUI atlas — three separate themes. - Write a
ContentLoaderautoload that exposes the samestring ContentDataDirectory/ContentGfxDirectorycontract Game1 had, so Core's data loading code (Loaders.csetc.) 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:TileMapshowing the 3×3 world-tile window around the player (C.TACTICAL_WINDOW_WORLD_TILES).- Streaming: re-implement the 64-tile chunk loader from
TacticalRenderer.csusingTileMap.SetCellcalls bounded to the visible viewport. PlayerSprite/NpcSprite→AnimatedSprite2Dnodes. 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 fromCharacterCreator.zip/index.htmlinto 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/--rulemapped to Theme constants; display + body fonts as Theme default fonts; standard spacing/radius constants.Theme/Variations/dark.tresandTheme/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 asFontFileresources at the sizes the prototype uses (verify by spot-rendering each). UI/Widgets/CodexPopover.cs: reusable hover-popover Control implementing theTraitName/SkillChip/BonusPillpattern fromsrc/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, notwindow.inner*).UI/Widgets/CodexStepper.cs: the.stepper/.stepwidget — 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.htmllocally withpython3 -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 matchingapp.jsx:codex-headerband: title + folio counter ("Folio III of VIII").CodexStepper(from M5) bound to the 8 steps (see below).pagetwo-column body: per-step scene (left) + Aside (right).nav-bar: Back / validation banner / Next.
- One scene per step under
Scenes/CharacterCreation/Steps/. Direct port ofsrc/steps.jsx:StepClade— clade picker grid; selecting a clade auto-defaults species perapp.jsxuseEffect([cladeId]).StepSpecies— species cards filtered by clade.StepClass— calling cards; level-1 features filtered fromlevel_tableper the prototype's contract.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 asStepClass, one card per available subclass for the chosen calling). Data shape from the liveContent/Data/classes.json.StepBackground— history cards.StepStats— ability assignment, drag-drop. Reproduce the payload contract fromsteps.jsxhandleDrop/dropToPoolexactly:- 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 mirrorsstatHistoryarray.
StepSkills— class skill picks above background-locked skills. Skill chips use the M5CodexPopover(SkillChip + BonusPill).StepReview— name entry + final summary. The "Confirm & Begin" button does whatapp.jsxstep's TODO told us to: hand the final character object to Core (see README §4 "Wire the 'Confirm & Begin' handoff").
UI/Aside.tscn+ script: portsAsidefromapp.jsx— clade/species flavor, total ability scores including bonuses (sum ofstatAssign[ab] + clade.ability_mods[ab] + species.ability_mods[ab]), size + speed, skill list. Bound to a CoreCharacterDraftmodel via Godot signals.- Validation: per-step
Validate()mirroringvalidate(i)inapp.jsxexactly (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
firstIncompleterule. 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.
WorldGenProgressScreen— progress bar driven by Core stage callbacks.WorldMapScreen— wraps the M2 world map node + minimap + UI overlays.PlayScreen— wraps the M4 tactical node + HUD overlay (HP, stamina, minimap).PauseMenuScreen—PopupPaneloverlay; pauses Core sim tick.SaveLoadScreen—ItemListof saves;Save.Read/Save.Writecalls unchanged.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.
CombatHUDScreen— turn order, ability bar, target reticle, damage numbers. Damage-number animations move fromSpriteBatchtoAnimatedSprite2Dor tween-drivenLabelnodes.DungeonScreen(Phase-7) — variant ofPlayScreenfor bounded interiors; same tactical render path, different camera bounds + exit-tile logic.LevelUpScreen— stat allocation grid; reuse character-creation widgets.InventoryScreen/ShopScreen— drag-drop between containers using Godot's native drag-drop API (replacesDragDropController.cs).QuestLogScreen/ReputationScreen/DefeatedScreen— mostly text-list views.- Dialogue → combat handoff (Phase-7): the
InteractionScreen"settle this here" branch closes the popup and instantiatesCombatHUDScreen.tscnwith 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 fromInputManager.cs+ the new GodotInputMapactions. Adapt to Core via anInputAdapterautoload exposing the same enum-typed action API the screens use today.PlayerController→_PhysicsProcess-driven node consuming the adapter.Platform/SavePaths.cs→ useOS.GetUserDataDir()(Godot's cross-platform per-user dir); confirm the directory matches MonoGame'sLocalApplicationDatapath 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, setdisplay/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'swidth=1440viewport meta is not a constraint — the codex aesthetic should fill the screen. Add a developer-only--windowedCLI 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/andTheriapolis.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.Desktopwith the Godot launch command. - "Project Architecture": replace
Theriapolis.Gameparagraph withTheriapolis.Godot. - Architecture-test description: forbid
Godot.*(notMicrosoft.Xna.*) in Core.
- "Build & Run Commands": replace
- 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(), noRandomNumberGeneratorGodot 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.csreads 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 thatTheriapolis.Core.dllhas noGodot.*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/godotbranch;mainkeeps shipping MonoGame patches if needed during the 6–7 week window. - After M10 ships, MonoGame branch is tagged
monogame-finaland frozen. No back-merges frommainintoport/godotafter M5 (CodexUI parity point) — too much surface diverges. - Rollback plan: if the port stalls past M6 with unsolvable Godot-side
blockers, abandon
port/godotand reconsider §2's "ImGui.NET / replace Myra only" alternative. M0–M5 work is salvageable for a future attempt.
10. Risks & open questions
Real risks
- Data-schema reconciliation.
CharacterCreator.zip/data/*.jsonpredates Phase 6.5 (subclasses, levelling, hybrids, scent tags). The liveContent/Data/*.jsonhas 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. - 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. - 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.
- Tactical chunk streaming performance.
TileMap.SetCellis well optimised but the 64-tile chunk loader was tuned againstSpriteBatch. Mitigation: if M4 reveals a perf issue, fall back toMultiMeshInstance2Dfor tile rendering. - 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). - 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.
- Drag-drop fidelity (M6 stats step + M8 inventory). Godot's native
drag-drop is event-driven; the React prototype uses HTML5
dataTransferwith a documented payload shape. Mitigation: port the payload contract verbatim ({from:"pool", value, idx}/{from:"slot", value, ability}), serialise asVariantdictionaries; if native API doesn't suffice for a custom drag preview, write a thin_GuiInput-driven shim onControlnodes.
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)
- Subclass step UX. Dedicated 8th step, not an inline picker on the Calling card. Implementation in M6 step #4.
- 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. - 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. - Default theme. Parchment. Dark and blood ship as player-selectable variations in the existing settings screen.
Open questions for the user / project lead
- Pixel-art shader stack. CodexUI uses MonoGame
SpriteBatchwith point filtering; do any tactical-render screens depend onSamplerState.PointClampbehaviour that needs reproducing in Godot? (Default Godot project import settings setFilter: Nearestfor pixel art — likely fine, confirm at M5.) - Audio. Phase 7 may or may not have introduced audio. If so, M9 grows
to include
AudioStreamPlayerintegration. - 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:
Theriapolis.Game/andTheriapolis.Desktop/are deleted from the repo.dotnet testis green (≥ Phase-7 test count, none removed except verified Game-only tests, plus the new save-compat + architecture tests).- A save written by
monogame-finalloads in the Godot build, plays through Old Howl mine, saves, reloads — all without errors. - The Godot build exports cleanly to Windows desktop and runs the same end-to-end smoke test.
CLAUDE.mdis updated to reflect the new project layout and build commands; the architecture test forbidsGodot.*in Core.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.