Files
code_of_conquest_dnd/docs/superpowers/specs/2026-07-11-title-screen-design.md
Phillip Tarrant 0249cbcf09 docs(spec): Title screen (M3) + unified version design
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>
2026-07-11 10:19:17 -05:00

9.6 KiB
Raw Blame History

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 /VERSION file (0.01-alpha). The API reads it at startup; the client bakes it into project.godot's application/config/version via a sync script and reads it through ProjectSettings. 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 / Credits are faithful-but-inert placeholders (their screens are later milestones). Keyboard (/ select, Enter activate) 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() -> str reads /VERSION (path resolved relative to the repo root, mirroring how canon_log resolves the schema dir) and strips whitespace; a module-level VERSION constant caches it.
  • api/app/main.py: FastAPI(title="coc-rpg proxy", version=VERSION) and a GET /version returning {"version": VERSION}.
  • Test: get_version() equals the /VERSION file contents; the FastAPI app's version matches; GET /version returns it.

Client (Godot)

  • scripts/sync-version (repo-root script, bash): reads /VERSION and writes config/version="<v>" into [application] of client/project.godot (adds the key if absent). Run on every version bump. Documented in client/docs/README.md.
  • client/project.godot: application/config/version="0.01-alpha" (written by the sync script; committed).
  • client/scripts/util/version.gdclass_name Version, static func string() -> String returns ProjectSettings.get_setting("application/config/version", ""); if empty, returns a LAST_RESORT constant ("0.0.0-dev") so the footer can never be blank. A func footer() -> String returns "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 — a ColorRect (full rect) fed by a small warm-radial shader (title_background.gdshader) whose colours are Palette uniforms set in _ready() (the DarkBay precedent — 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 — a ColorRect/TextureRect warm radial at the bottom, its modulate:a pulsed by an AnimationPlayer (Glow animation, ~6s loop, 0.5↔0.8).
    • Vignette — the existing VignetteOverlay surface, full rect.
    • Embers — a GPUParticles2D emitting small warm dots rising ~460px over 711s with fade (a ParticleProcessMaterial: upward gravity, lifetime randomness, alpha curve). Authored so it previews in-editor.
  • Content column (MarginContainer/VBoxContainer, left, padding ~120px, vertically centered):
    • Kicker Label — "A DARK FANTASY RPG", TitleKicker variation.
    • Logo Label — "CODE OF\nCONQUEST", TitleLogo variation.
    • Rule — a small ColorRect (220×2), colour Palette.GOLD applied in _ready() (a decorative accent; the mock's gold→transparent fade is not worth a gradient asset here).
    • Tagline Label — "The world has real problems, and it does not care about you.", Accent variation.
    • Menu VBoxContainer — 6 authored rows, one per menu item (fixed list). Each row is an HBoxContainer named for its action, carrying metadata/action (a StringName), containing a Marker Label ("◆", gold) and a VBoxContainer with a Label (item name) and, for Continue, a Sub Label ("CHAPTER II · VEXCCA · 14:22:07"). Authored labels: New Game, Continue, Load Game, Settings, Credits, Quit.
  • Footer:
    • FooterLeft Label (Mono) — the version line (bound from Version.footer() in _ready; authored placeholder text so it previews).
    • FooterRight Label (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, colour CREAM.
  • TitleKicker — Label variation: mono, ~13px, wide tracking, colour GOLD.

(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's screen_requested).
  • _ready(): viewport-fit (+size_changed); set the background shader's Palette uniforms; harvest the menu rows (_rows: Array from the Menu container, reading each row's action meta) into an ordered list; bind FooterLeft.text = Version.new().footer(); connect menu_activated_on_menu_activated; apply the initial selection highlight.
  • Input: _unhandled_input handles ui_up/ui_down (move _sel, wrapping) and ui_accept (activate _sel); each row's mouse-enter sets _sel; mouse-click activates that row. Selection highlight = toggle the row's Marker opacity (1 selected / 0 not) + the label colour (CREAM_BRIGHT selected / MUTED_MONO not), from Palette.
  • _activate(i): emit menu_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 _activate so 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 /version endpoint is metadata, not a role.
  • menu_activated for inert items is a dangling seam (no destination yet), exactly like the dock's routing seam.

7. Testing

  • Version (client): string() non-empty and equals ProjectSettings application/config/version; footer() contains the version and the "BUILT IN GODOT 4.7" text.
  • API version: get_version() == /VERSION file contents (whitespace-stripped); the FastAPI app version matches; GET /version returns {"version": …}.
  • TitleScreen: selection logic — / move _sel and wrap across the 6 rows; a row's mouse-enter sets _sel. Activation — with a watched menu_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 their action meta; 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).