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

100 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.gd``class_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).