Editor-first Title screen from mock: warm atmosphere (GPUParticles embers, AnimationPlayer glow, palette-uniform shader bg), logo block, keyboard/mouse menu (New Game -> shell, Quit works, rest inert), footer. Plus a single /VERSION source of truth read by both the API (startup + /version) and the client (synced into project.godot config/version, read via ProjectSettings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.6 KiB
Title screen (M3) + unified version — design
Milestone: M3 (roadmap "Title screen"). Last screen of the M3 visual-foundation block.
Charter: §16 (mockups are the UI bible; editor-first scenes per ADR 0001), §4 (client/API are separate processes over HTTP), §2 (presentation/state).
Mockup: mockups/Title Screen.dc.html.
Branch: feature/title-screen off dev.
1. Goal
The entry point — the game boots to a Title screen. Faithful to the mock: a dark, warm, atmospheric first impression (drifting embers, a pulsing off-screen fire glow, a key-art slot), the CODE OF CONQUEST logo block, a keyboard/mouse-driven menu, and a footer. Plus the cross-cutting piece the Title surfaces first: a single source of truth for the app version shared by the client and the API.
2. Scope decisions (settled in brainstorming)
- Version single-source: a root
/VERSIONfile (0.01-alpha). The API reads it at startup; the client bakes it intoproject.godot'sapplication/config/versionvia a sync script and reads it throughProjectSettings. No runtime coupling — the client knows its own version offline. Rejected: client fetching the version over HTTP (conflates client build with server build; fails offline); codegen into both (overkill now). - Menu behavior:
New Game→ change scene to the Main Window shell (the one real destination today; the real flow later is Title → character creation → shell).Quit→ quit the app.Continue/Load Game/Settings/Creditsare faithful-but-inert placeholders (their screens are later milestones). Keyboard (↑/↓select,Enteractivate) and mouse (hover selects, click activates). - Motion: implement the ambient motion — drifting embers + a pulsing fire glow.
- Boot: Title becomes the project's
run/main_scene.
3. Version — the unified source
/VERSION 0.01-alpha (one line, LF; the only human-edited version)
API (Python)
api/app/version.py:get_version() -> strreads/VERSION(path resolved relative to the repo root, mirroring howcanon_logresolves the schema dir) and strips whitespace; a module-levelVERSIONconstant caches it.api/app/main.py:FastAPI(title="coc-rpg proxy", version=VERSION)and aGET /versionreturning{"version": VERSION}.- Test:
get_version()equals the/VERSIONfile contents; the FastAPI app'sversionmatches;GET /versionreturns it.
Client (Godot)
scripts/sync-version(repo-root script, bash): reads/VERSIONand writesconfig/version="<v>"into[application]ofclient/project.godot(adds the key if absent). Run on every version bump. Documented inclient/docs/README.md.client/project.godot:application/config/version="0.01-alpha"(written by the sync script; committed).client/scripts/util/version.gd—class_name Version, staticfunc string() -> StringreturnsProjectSettings.get_setting("application/config/version", ""); if empty, returns aLAST_RESORTconstant ("0.0.0-dev") so the footer can never be blank. Afunc footer() -> Stringreturns"v%s · BUILT IN GODOT 4.7 · © 2026" % string().- Test:
Version.string()is non-empty and equals the project setting;footer()contains the version.
Alignment
/VERSION is the source; bump it and run scripts/sync-version. The API test guards that the API reads the file correctly; the client's config/version is written from the same file by the sync script. A repo-level equality check (project.godot vs /VERSION) is a later nicety (noted, not built now — nothing is deployed).
4. Title scene — architecture (editor-first, ADR 0001)
client/scenes/title/TitleScreen.tscn + client/scripts/ui/title/title_screen.gd. Layout authored in the .tscn; the script does on-load work only.
Authored node tree (.tscn)
Root TitleScreen (Control, fixed 1920×1080, theme assigned, no full-rect anchors on root — the M3-a collapse rule; script keeps size = get_viewport_rect().size).
- Background atmosphere (bottom of the z-order):
Background— aColorRect(full rect) fed by a small warm-radial shader (title_background.gdshader) whose colours arePaletteuniforms set in_ready()(theDarkBayprecedent — renders on F6, editor shows the fallback solid). Uniform colours: a warm centre, a mid brown, the near-black stage edge.KeyArt— dashed art-slot placeholder (right side), a mono caption "KEY ART DROPS IN HERE" (charter: art slots stay dashed placeholders).Glow— aColorRect/TextureRectwarm radial at the bottom, itsmodulate:apulsed by anAnimationPlayer(Glowanimation, ~6s loop, 0.5↔0.8).Vignette— the existingVignetteOverlaysurface, full rect.Embers— aGPUParticles2Demitting small warm dots rising ~460px over 7–11s with fade (aParticleProcessMaterial: upward gravity, lifetime randomness, alpha curve). Authored so it previews in-editor.
- Content column (
MarginContainer/VBoxContainer, left, padding ~120px, vertically centered):KickerLabel — "A DARK FANTASY RPG",TitleKickervariation.LogoLabel — "CODE OF\nCONQUEST",TitleLogovariation.Rule— a smallColorRect(220×2), colourPalette.GOLDapplied in_ready()(a decorative accent; the mock's gold→transparent fade is not worth a gradient asset here).TaglineLabel — "The world has real problems, and it does not care about you.",Accentvariation.MenuVBoxContainer — 6 authored rows, one per menu item (fixed list). Each row is anHBoxContainernamed for its action, carryingmetadata/action(a StringName), containing aMarkerLabel ("◆", gold) and aVBoxContainerwith aLabel(item name) and, forContinue, aSubLabel ("CHAPTER II · VEXCCA · 14:22:07"). Authored labels: New Game, Continue, Load Game, Settings, Credits, Quit.
- Footer:
FooterLeftLabel (Mono) — the version line (bound fromVersion.footer()in_ready; authored placeholder text so it previews).FooterRightLabel (Mono) — "[ ↑ ↓ ] CHOOSE [ ↵ ] ENTER".
Theme additions (built from Palette, per the established pattern)
Add to ThemeKeys + build_game_theme.gd, regenerate game_theme.tres:
TitleLogo— Label variation: serif (Georgia-equivalent) bold, ~116px, colourCREAM.TitleKicker— Label variation: mono, ~13px, wide tracking, colourGOLD.
(The tagline uses the existing Accent; the footer uses Mono. Any new warm background colours go in Palette as new tokens, not hex literals in the shader/scene.)
Script (title_screen.gd, on-load/logic only)
class_name TitleScreen extends Control.signal menu_activated(action: StringName)— the testable seam (like the dock'sscreen_requested)._ready(): viewport-fit (+size_changed); set the background shader'sPaletteuniforms; harvest the menu rows (_rows: Arrayfrom theMenucontainer, reading each row'sactionmeta) into an ordered list; bindFooterLeft.text = Version.new().footer(); connectmenu_activated→_on_menu_activated; apply the initial selection highlight.- Input:
_unhandled_inputhandlesui_up/ui_down(move_sel, wrapping) andui_accept(activate_sel); each row's mouse-enter sets_sel; mouse-click activates that row. Selection highlight = toggle the row'sMarkeropacity (1 selected / 0 not) + the label colour (CREAM_BRIGHTselected /MUTED_MONOnot), fromPalette. _activate(i): emitmenu_activated(_rows[i].action)._on_menu_activated(action):&"new_game"→get_tree().change_scene_to_file("res://scenes/shell/MainWindowShell.tscn");&"quit"→get_tree().quit(); anything else → no-op (inert placeholder). Kept out of_activateso tests assert the emitted action without triggering a real scene change / quit.
5. Boot
client/project.godot: set run/main_scene="res://scenes/title/TitleScreen.tscn". Launching the game (and a future export) opens the Title; New Game → the shell.
6. §2 ledger
- Version value, menu selection index, boot scene — state/presentation, code-owned.
- No AI text anywhere on this screen (the DM enters at the shell). The
/versionendpoint is metadata, not a role. menu_activatedfor inert items is a dangling seam (no destination yet), exactly like the dock's routing seam.
7. Testing
Version(client):string()non-empty and equalsProjectSettingsapplication/config/version;footer()contains the version and the "BUILT IN GODOT 4.7" text.- API version:
get_version()==/VERSIONfile contents (whitespace-stripped); the FastAPI appversionmatches;GET /versionreturns{"version": …}. TitleScreen: selection logic —↓/↑move_seland wrap across the 6 rows; a row's mouse-enter sets_sel. Activation — with a watchedmenu_activated, activating the New Game row emits&"new_game"and the Quit row emits&"quit"(no real scene change/quit in the test). Node-presence: the 6 menu rows mount with theiractionmeta; footer shows a version.- Visual gate (required): human F6 — the logo, the warm background + vignette, drifting embers, the pulsing glow, selection highlight moving with keyboard/mouse, and New Game → the shell. Headless can't prove the atmosphere.
8. Out of scope
- Character creation, save/load, settings, credits screens (later milestones) — those menu items are inert.
- Real key art (dashed slot).
- A repo-level version drift-check / pre-commit hook (noted; nothing is deployed).
- Persisted "last selected" / save-slot info on Continue (the sub-line is authored placeholder text).