diff --git a/CHANGELOG.md b/CHANGELOG.md index 744dd6c..bb35e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ All notable user-facing changes to Gig Line. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use Semantic Versioning. -## [Unreleased] +## [0.9.3] — 2026-11-02 + +### Added + +- Eight committed gallery captures now cover the start menu, pause, settings, + shift review, emergency radio, action wheel, crowd panic and responder + arrival, so every screen-design reference has a deterministic current-build + capture. ### Changed diff --git a/DONE.md b/DONE.md index e5930f1..bfbbcc1 100644 --- a/DONE.md +++ b/DONE.md @@ -1660,3 +1660,78 @@ redesign. blocks a build. Completed 2026-11-02: 96 final-context captures (8 models × 3 distances × Hollywood/Joke Shop × day/night) found no P0 issues. The lanyard badge and firefighter helmet were corrected with runtime attach offsets; backpack straps, caps, low-poly hands/faces and the optional close-range flashlight wash were accepted or deferred. The 24-NPC budget is 6.93 ms, the gallery is 13 good / 0 bad, the screen-design judge is 13/13, and a clean portrait measured a 0.732 vision gap against the photoreal concept. No build, version bump or tag was created. + +## Patch 0.9.3 — Dialog, button and UI polish — completed 2026-11-02 + +Goal: make every player-facing screen read consistently and pass the Python +screen-design judge before any model/texture changes. No gameplay rules change. + +### Screen and terminology inventory + +- [x] Inventory every screen/panel: start menu, venue menu, tutorial, HUD, pause, + settings, Help, inspection, wand, radio/emergency, wheel, shift review, + timeline and campaign/continue screens. Recorded in `docs/ui-copy.md`. +- [x] Record the canonical terms for admit/refuse, warning/blocking, + assistance/emergency, body camera, shift review and campaign; the current + inspection/wand/radio/review labels use them (see `docs/ui-copy.md`). +- [x] Audit all button labels for verb-first wording, capitalization, ordering, + destructive-action confirmation and consistent confirm/cancel placement; + recorded in `docs/ui-copy.md` and fixed Quit/Apply wording. +- [x] List every hardcoded player-facing string and route it through + `content/strings.en.json`; remaining UI literals are numeric/debug-only or + dynamic data. Placeholder validation covers the new keys. +- [x] Review NPC/door-staff/radio/tutorial/help/review/campaign dialogue for + repetition, ambiguity, tone and localization length. (Reason contexts, + door-staff lines, radio/tutorial/help copy and length findings recorded in + `docs/ui-copy.md`; longest dialogue line is 56 characters.) + +### Button and panel behavior + +- [x] Audit normal/hover/pressed/focus/disabled states on every button, including + tutorial skip, venue continue, settings apply/cancel/reset, judge/review, + radio send/emergency hold and Help close. (Shared `UiStyles` states applied; + timeline boundary disabling and the keyboard-only F1 skip are documented in + `docs/ui-copy.md`.) +- [x] Verify keyboard-only and gamepad-only navigation: focus order, wrapping, + default focus, confirm/cancel behavior and no focus traps. (Focus rings, + gamepad A/B UI bindings, default focus and per-modal cancel behavior are + recorded in `docs/ui-copy.md`.) +- [x] Verify every modal closes with Esc/right-click where documented, recaptures + the mouse, and does not leave the guard frozen or the menu visible. (Single + top-modal close path, menu/settings return handling and pointer recapture are + recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help + right-click and pause Settings.) +- [x] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer + strings; fix clipping, overflow and overlapping layouts. (Target chip now + measures/caps scaled text, HUD top labels clip, and action buttons ellipsize; + regression checks are in `test_accessibility.gd` and `test_ui_buttons.gd`.) +- [x] Test destructive/irreversible actions (campaign reset, tutorial replay, + emergency send) require the intended confirmation/hold and cannot double-fire. + (Reset/replay share one guarded confirmation dialog; emergency hold submits + once and ignores the post-submit release. Covered by session/radio tests.) +- [x] Verify radio normal-send vs emergency hold, inspection/wand allow/deny, + review continue/retry/menu and Help search/filter behavior end to end. + (`smoke_ui_flows.gd` covers inspection, radio, Help, wand and review; + `smoke_campaign.gd` covers review continue/checkpoint resume.) + +### Visual and judge verification + +- [x] No UI art/reference mapping change was needed for the label-only pass; the + no-overwrite image convention was preserved. +- [x] Capture a deterministic screenshot for every dialog/button state that has a + design reference; add missing capture stages or an explicit coverage gap. + (`src/test/capture_ui_states.gd` adds 14–21: start menu, pause, settings, shift + review, emergency radio, radial menu, crowd panic and responder arrival. No + reference is left without a committed capture; the crowd-panic frame is a + staged 70-frame group shot and the responder is parked mid-approach.) +- [x] Ran `tools/check_screenshots.sh` and `screen_designs.py compare`: 21 good, + 0 bad. +- [x] Ran the judge with automatic model selection: 21/21 match, 0 review, 0 diff. +- [x] Recorded the judge model and result in `docs/screen-design-report.md`; the + generated captures remain local/gitignored. +- [x] Add a short UI copy/button checklist to `docs/smoke-checklist.md`. +- [x] Exit: all screens have consistent terminology and button behavior, long + strings/scale pass, and the Python compare/judge gate has no unexplained + diff. + +Completed 2026-11-02: all 0.9.3 UI checks are closed. The eight previously uncaptured UI states (14–21) are now deterministic gallery shots; `tools/check_screenshots.sh` reports 21 good / 0 bad and the automatic judge 21/21 match, 0 diff. The source version is now 0.9.3; no release build or tag has been produced for it yet. diff --git a/MEMORY.md b/MEMORY.md index 506b8ff..8475c02 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -63,7 +63,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o release build, version bump or tag was created at this gate. - **UI flow smoke (2026-11-02):** `smoke_ui_flows.gd` boots the real scene and exercises inspection allow/deny, normal vs emergency radio, Help filtering, - wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.3/0.9.6 + wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6 handoff; it complements the unit tests that only exercise panels. - **Destructive actions (2026-11-02):** `SettingsPanel._open_destructive_dialog()` owns one confirmation instance for reset/replay; repeated presses focus the @@ -91,16 +91,16 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o states and font colours for the dark session/UI shell. Use `UiStyles.apply_button()` for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design. -- **Version and roadmap renumber (2026-11-02):** source is now **0.9.2**; the - completed map/NPC work is the patch content. Postponed work is renumbered to - **0.9.3 UI**, **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the - **0.10.0** minor release candidate. `TODO.md` plans only 0.x patch/minor - increments; no major-version scope is defined yet. No release build or tag has been produced - for 0.9.2. -- **Priority change (2026-11-02):** the map optimization (originally planned for 0.9.5) and the pre-build - NPC revisit are complete and in `DONE.md`. The postponed 0.9.3 UI, 0.9.4 NPC - detail, 0.9.5 textures and 0.9.6 integration/0.10.0 release work are next. The - current source version is 0.9.2; no release build or tag has been produced. +- **Version and roadmap renumber (2026-11-02):** source is now **0.9.3**; the + completed map/NPC work and UI polish are the patch content. Next work is + **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the **0.10.0** + minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no + major-version scope is defined yet. No release build or tag has been produced + for 0.9.3. +- **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit and + 0.9.3 UI polish are complete and in `DONE.md`. The 0.9.4 NPC detail, 0.9.5 + textures and 0.9.6 integration/0.10.0 release work are next. The current source + version is 0.9.3; no release build or tag has been produced. - **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test @@ -248,17 +248,19 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o material (`baseColorTexture` + `metallicRoughnessTexture`, NEAREST sampling). Colours are sRGB in the texture, so the old `srgb_to_linear`/`_linear_rgba` helpers were removed. `audit_glb` fails if a prop is not exactly one material or misses TEXCOORD_0/images. -- **Mockups complete (v0.3.2):** `docs/screens/designs/` holds AI references for all 19 - catalogue screens. Only the six staged captures (01–06) are compared/judged; the other 13 - have no `capture`, so adding staged captures for the wand panel, timeline and escalation HUD - would extend automated visual checks. `generate` now calls `_load_env_file()`. -- **Staged captures (v0.3.3, updated 2026-11-02):** `capture_screenshots.gd` emits the gallery - shots; states 7–10 switch to The Joke Shop at elapsed 620 s, retry the wand until it alerts, +- **Mockups complete (v0.3.2, updated 0.9.3):** `docs/screens/designs/` holds AI references for + all 21 catalogue screens, and every entry now has a committed current-build capture. + `capture_screenshots.gd` covers scenes 01–13, `capture_ui_states.gd` covers UI states 14–21, + and `capture_prop_gallery.gd` covers the prop gallery. `generate` now calls `_load_env_file()`. +- **Staged captures (v0.3.3, updated 2026-11-02):** `capture_screenshots.gd` emits the scene + gallery; states 7–10 switch to The Joke Shop at elapsed 620 s, retry the wand until it alerts, then jump to 760 s after staging for night lighting. LATE now has its own routine pool. Shot 12 closes any stale wand/inspection modal before the venue beauty shot (the wand fallback can leave `hud.show_wand` open without `wand_open`). The timeline needs - `recorder.close_all_open()`, because `to_dict()` only lists closed incidents. The escalation - HUD mockup is third-person now; `judge` still classes it `review` (no diff). + `recorder.close_all_open()`, because `to_dict()` only lists closed incidents. + `capture_ui_states.gd` adds 14–21; crowd panic stages a group 70 frames into the flee and + responder arrival parks the responder with the door locked. `check_screenshots.sh` writes all + captures to `docs/screenshots/` before `compare`; judge is 21/21 match. - **Prop collision (v0.4.0):** `_add_prop_model()` fits a `StaticBody3D` from the model bounds (cylinder for bollard/bin/hydrant, box otherwise) and adds a `NavigationObstacle3D`; NPC agents enable avoidance and move with the safe velocity from `velocity_computed`. The navmesh stays a @@ -732,7 +734,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o - Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI. - Access loadout is implemented; do not re-fix `override_access` as if it were still unread. - `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`. -- Headless suite is **261/261**; the windowed smokes, screenshot compare and judge pass. Remaining visual follow-ups are in `TODO.md`. +- Headless suite is **261/261**; Python tooling is **97/97**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`. - Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args). - GDScript inference trap that broke the main scene: a `var` typed as a base class (`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a @@ -742,8 +744,9 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o - Do not snapshot right after changing a transform in the same frame: `capture_screenshots.gd` needs a state/frame gap or the capture uses the old camera orientation. - `EventScheduler` probabilities are per simulated second, not per call. Never poll it per frame. -- Screenshot staging lives in `src/test/capture_screenshots.gd` (guard at z≈10.5 facing the door). - `docs/screenshots/` copies are manual: copy from `user://screenshots/`. +- Screenshot staging lives in `src/test/capture_screenshots.gd` (guard at z≈10.5 facing the door) + and `src/test/capture_ui_states.gd` (UI states 14–21). `tools/check_screenshots.sh` renders all + captures into `docs/screenshots/`; do not regenerate them by hand. ## Open Questions @@ -754,10 +757,10 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o ## Plan vs code `PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of -2026-11-01 the playable build covers all three venues, controls, access/radio/evidence/review, -the Blender asset library, the realism pass, and the visual-alignment pass. `TODO.md` holds only -the realism follow-ups and optional future ideas. The project uses a custom headless runner, not -GUT, and Gitea Actions, not GitHub Actions. +2026-11-02 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3 +UI/accessibility pass, and the Blender + realism asset pipeline. `TODO.md` holds the remaining +0.9.4–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not +GitHub Actions. diff --git a/PLAN.md b/PLAN.md index dbc868e..d28cf43 100644 --- a/PLAN.md +++ b/PLAN.md @@ -176,7 +176,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD ## Versioning and Delivery -`config/version` in `project.godot` is the single version source (currently `0.9.2`). Releases are tagged `v`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. +`config/version` in `project.godot` is the single version source (currently `0.9.3`). Releases are tagged `v`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. ## Design Direction diff --git a/README.md b/README.md index be542db..dd1a915 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ precision walk, the Joke Shop's Sunset Strip frontage, the TV Studio arrival, an > **Latest published: v0.9.1** — unified Godot 4.7.2 CI/release environment, plus > v0.9.0's production-quality and v0.8 tutorial/campaign/resume work. > -> **Current source: v0.9.2** — the Hollywood/Joke Shop map rebuilds and the -> NPC final-context review; the release tag is pending. +> **Current source: v0.9.3** — the UI/accessibility polish pass on top of the +> Hollywood/Joke Shop map rebuilds and the NPC final-context review; the release +> tag is pending. > Releases are built automatically when a `vX.Y.Z` tag is pushed > (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use > `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline @@ -125,11 +126,12 @@ Full setup, every run/test/smoke command, and the controls table are in ## Development Status -v0.9.2 is the current source version: the Hollywood and Joke Shop map -rebuilds, the NPC final-context review and the HUD/wand string fixes. It builds -on v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality and -audio/accessibility pass, v0.8's tutorial/campaign/resume work and the v0.7.x -venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md) +v0.9.3 is the current source version: the completed UI/accessibility polish +pass (dialogue, button states, focus/navigation, modal close, long-string layout, +destructive-action guards and full screen capture coverage), on top of v0.9.2's +Hollywood/Joke Shop map rebuilds and NPC final-context review. It builds on +v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality pass, v0.8's +tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md) for the implemented list and [`TODO.md`](TODO.md) for the 0.x roadmap. Manual verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md). diff --git a/TODO.md b/TODO.md index 84d28e0..d6f1424 100644 --- a/TODO.md +++ b/TODO.md @@ -5,10 +5,10 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these ## Gap Analysis and 0.x Roadmap -Baseline: **v0.9.2** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume, +Baseline: **v0.9.3** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume, difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete escalation/access/radio loop, shift review, the Blender asset library, 261 headless + 97 Python -tests, a clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong +tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong release-candidate foundation, not yet a 0.10.0 release candidate. ### Documentation audit (completed 2026-11-01) @@ -33,93 +33,24 @@ release-candidate foundation, not yet a 0.10.0 release candidate. | Area | Current baseline | Remaining gap | Risk | Milestone | | --- | --- | --- | --- | --- | | Content and balance | 3 venues with validated 12+ event matrices, recovery events and contextual dialogue | External playtest evidence, balance/fairness pass, event-weight tuning and final content lock | Repetition or unfair outcomes survive into the release candidate | 0.10.0 | -| Dialog/UI | Complete screens and panel art, but copy/button behavior has not had a dedicated audit | Terminology, button states, long-string layout and full screen coverage need a focused pass | Inconsistent UI undermines first-hour clarity | 0.9.3 | +| Dialog/UI | Audited terminology, copy, button states, focus/navigation, modal close, long strings and all 21 screen captures | Keep the capture/compare/judge gate current as content changes | Low; the remaining risk is drift after new UI is added | Complete (0.9.3) | | Animation/presentation | Procedural pose layers, portrait captures, reduced hem clipping | Thorough NPC silhouette/face/gear/pose/LOD optimization before visual QA | Close-up jank reduces perceived quality | 0.9.4 | -| Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.9.3 / 0.10.0 | -| Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.9.3 / 0.10.0 | +| Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.10.0 | +| Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.10.0 | | Performance/platform | 24-NPC headless budget (6.9 ms), Compatibility/Web/HQ paths | Target hardware matrix, Web/HQ manual profiling, frame-time/memory evidence | Platform-specific drops or startup failures | 0.9.6 / 0.10.0 | | Save lifecycle | Settings/campaign/history/checkpoint files, atomic writes | Migration fixtures from 0.8/0.9, fresh-install/upgrade verification, rollback instructions | Player progress loss on upgrade | 0.10.0 | -| QA/CI | Headless suite, smokes, screenshot compare, optional judge | Full visual judge gate, release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 | +| QA/CI | Headless suite, smokes, screenshot compare, 21/21 judge | Release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 | | Release/legal | Unsigned desktop/macOS builds, Web/HQ packages | Signing/notarization decision, checksums, AI-asset provenance, credits, privacy/body-camera and rating review | Public release blocked by trust/policy | 0.10.0 | | Documentation | Architecture/ADR/status/roadmap ownership established | Final README/changelog/status/TODO/smoke-checklist sign-off and known-limitations page | Docs drift at launch | 0.9.6 / 0.10.0 | | Future 0.x | Stable 0.x feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before release | 0.11.0+ | -**Current priority:** map optimization and the **pre-build NPC revisit** are -complete (both moved to `DONE.md` on 2026-11-02). **0.9.3 UI polish** is active; -0.9.4 NPC-detail and 0.9.5 texture patches, then 0.9.6 integration and the 0.10.0 -release candidate, are queued. No release build or tag has been produced for the -current source version. +**Current priority:** map optimization, the **pre-build NPC revisit** and the +**0.9.3 UI polish** pass are complete (all moved to `DONE.md` on 2026-11-02). +**0.9.4 NPC-detail** is active; 0.9.5 texture refinement, 0.9.6 integration and +the 0.10.0 release candidate are queued. The source version is now 0.9.3; no +release build or tag has been produced for it. -## Patch 0.9.3 — Dialog, button and UI polish - -Goal: make every player-facing screen read consistently and pass the Python -screen-design judge before any model/texture changes. No gameplay rules change. - -### Screen and terminology inventory - -- [x] Inventory every screen/panel: start menu, venue menu, tutorial, HUD, pause, - settings, Help, inspection, wand, radio/emergency, wheel, shift review, - timeline and campaign/continue screens. Recorded in `docs/ui-copy.md`. -- [x] Record the canonical terms for admit/refuse, warning/blocking, - assistance/emergency, body camera, shift review and campaign; the current - inspection/wand/radio/review labels use them (see `docs/ui-copy.md`). -- [x] Audit all button labels for verb-first wording, capitalization, ordering, - destructive-action confirmation and consistent confirm/cancel placement; - recorded in `docs/ui-copy.md` and fixed Quit/Apply wording. -- [x] List every hardcoded player-facing string and route it through - `content/strings.en.json`; remaining UI literals are numeric/debug-only or - dynamic data. Placeholder validation covers the new keys. -- [x] Review NPC/door-staff/radio/tutorial/help/review/campaign dialogue for - repetition, ambiguity, tone and localization length. (Reason contexts, - door-staff lines, radio/tutorial/help copy and length findings recorded in - `docs/ui-copy.md`; longest dialogue line is 56 characters.) - -### Button and panel behavior - -- [x] Audit normal/hover/pressed/focus/disabled states on every button, including - tutorial skip, venue continue, settings apply/cancel/reset, judge/review, - radio send/emergency hold and Help close. (Shared `UiStyles` states applied; - timeline boundary disabling and the keyboard-only F1 skip are documented in - `docs/ui-copy.md`.) -- [x] Verify keyboard-only and gamepad-only navigation: focus order, wrapping, - default focus, confirm/cancel behavior and no focus traps. (Focus rings, - gamepad A/B UI bindings, default focus and per-modal cancel behavior are - recorded in `docs/ui-copy.md`.) -- [x] Verify every modal closes with Esc/right-click where documented, recaptures - the mouse, and does not leave the guard frozen or the menu visible. (Single - top-modal close path, menu/settings return handling and pointer recapture are - recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help - right-click and pause Settings.) -- [x] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer - strings; fix clipping, overflow and overlapping layouts. (Target chip now - measures/caps scaled text, HUD top labels clip, and action buttons ellipsize; - regression checks are in `test_accessibility.gd` and `test_ui_buttons.gd`.) -- [x] Test destructive/irreversible actions (campaign reset, tutorial replay, - emergency send) require the intended confirmation/hold and cannot double-fire. - (Reset/replay share one guarded confirmation dialog; emergency hold submits - once and ignores the post-submit release. Covered by session/radio tests.) -- [x] Verify radio normal-send vs emergency hold, inspection/wand allow/deny, - review continue/retry/menu and Help search/filter behavior end to end. - (`smoke_ui_flows.gd` covers inspection, radio, Help, wand and review; - `smoke_campaign.gd` covers review continue/checkpoint resume.) - -### Visual and judge verification - -- [x] No UI art/reference mapping change was needed for the label-only pass; the - no-overwrite image convention was preserved. -- [ ] Capture a deterministic screenshot for every dialog/button state that has a - design reference; add missing capture stages or an explicit coverage gap. -- [x] Ran `tools/check_screenshots.sh` and `screen_designs.py compare`: 13 good, - 0 bad. -- [x] Ran the judge with automatic model selection: 13/13 match, 0 review, 0 diff. -- [x] Recorded the judge model and result in `docs/screen-design-report.md`; the - generated captures remain local/gitignored. -- [ ] Add a short UI copy/button checklist to `docs/smoke-checklist.md`. -- [ ] Exit: all screens have consistent terminology and button behavior, long - strings/scale pass, and the Python compare/judge gate has no unexplained - diff. - -## Postponed — Patch 0.9.4 — NPC visual optimization +## Patch 0.9.4 — NPC visual optimization Goal: make the eight character variants hold up in portrait, conversation and crowd views without changing the `npc_base`/`Rig/Head` contract or the skinned/procedural diff --git a/docs/architecture.md b/docs/architecture.md index b4c652d..ba28e74 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp - **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible. - **PATCH** — bug fixes and small polish that do not change behavior. -The single source of truth for the version is `config/version` in `project.godot` (currently `0.9.2`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-stable, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md). +The single source of truth for the version is `config/version` in `project.godot` (currently `0.9.3`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-stable, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md). ## 3. Context and Scope diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index d7ee2a6..7c9b470 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -261,6 +261,9 @@ python3 tools/screen_designs.py generate --draft --only wand --force # (requires OPENROUTER_API_KEY) python3 tools/screen_designs.py generate +# UI-state gallery (menu, pause, settings, review, radio, wheel, crowd, responder) +godot --path . --script res://src/test/capture_ui_states.gd -- --out=res://docs/screenshots + # Prop gallery (studio line-up) for close-range prop checks godot --path . --script res://src/test/capture_prop_gallery.gd @@ -276,12 +279,11 @@ python3 tools/screen_designs.py judge --only 03- # one capture python3 tools/screen_designs.py judge --dry-run # build requests, no network ``` -The catalogue covers 21 screens: the eleven README captures plus the start menu, -pause overlay, settings, shift review, radial menu, emergency radio, wand -screening, Joke Shop queue, TV Studio arrival, crowd panic, responder arrival, -body-camera timeline, and the escalation HUD. All 21 have an AI reference in -`assets/images/ui/` (snapshot in `docs/screens/designs/`); the eleven screens with -staged captures are also compared or judged against the running game. +The catalogue covers 21 screens: the thirteen scene captures plus the start menu, +pause overlay, settings, shift review, radial menu, emergency radio, crowd panic, +and responder arrival. All 21 have an AI reference in `assets/images/ui/` +(snapshot in `docs/screens/designs/`) and a committed capture, so every entry is +compared or judged against the running game. `compare` flags gross drift: wrong aspect ratio, undersized frame, near-blank frame, and large layout/palette/detail distance. It is a regression gate, not a @@ -295,8 +297,8 @@ Verdicts are classed as `match`, `review` (a mismatch below `--fail-confidence`, default 0.6 — expected when a concept mockup is more stylised than the game), or `diff` (a confident mismatch, which exits non-zero). Regenerate the designs after changing screen layout, then re-run `compare` after regenerating the game screenshots with -`src/test/capture_screenshots.gd`. +`src/test/capture_screenshots.gd` and `src/test/capture_ui_states.gd`. For a one-command drift check, run `tools/check_screenshots.sh`. It renders the -staged captures and prop gallery into `docs/screenshots/` (using `xvfb-run` when -no display is available) and then runs `screen_designs.py compare`. +scene captures, UI-state captures and prop gallery into `docs/screenshots/` (using +`xvfb-run` when no display is available) and then runs `screen_designs.py compare`. diff --git a/docs/development-status.md b/docs/development-status.md index 7da5a63..e524a93 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -39,6 +39,7 @@ Implemented so far: - Aim-free selection pass (v0.3.0 work, [ADR-0011](adr/0011-aim-free-selection-and-precision-walk.md)): the focus cone ranks candidates by angle then distance, `Tab` and the mouse wheel cycle nearby people nearest-first and pin the selection, right click clears focus with a short re-acquire grace, the HUD reports how many people are in the ring, and holding `Shift` walks at half speed for close work - Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. The wand panel lists the guest's carried items, and multiline HUD/wand strings render real line breaks. +- UI-state capture pass (0.9.3 work): `src/test/capture_ui_states.gd` adds committed captures 14–21 for the start menu, pause, settings, shift review, emergency radio, action wheel, crowd panic and responder arrival. Every one of the 21 catalogue entries now has a current-build capture; `tools/check_screenshots.sh` reports 21 good / 0 bad and the automatic judge reports 21/21 match, 0 diff. Crowd panic and responder arrival are staged presentation frames, not gameplay changes. - Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts. - Input/reliability fixes: selecting a venue releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. Mouse look now reads at `_input`, restores capture on window focus/click-back, and the HUD is mouse-transparent. Normal radio reports submit on click; emergency calls still require the hold-to-confirm path. - Production-quality pass (v0.9.0): mixer/accessibility settings, audio volume clamping, 24-NPC budget smoke, shortened coat/dress stride, and a unified Godot 4.7.2 CI/local support baseline with documented upgrade/rollback requirements. diff --git a/docs/screen-design-report.md b/docs/screen-design-report.md index 2a1946d..0095f9a 100644 --- a/docs/screen-design-report.md +++ b/docs/screen-design-report.md @@ -3,9 +3,9 @@ Date: 2026-11-02 Model: automatic cheapest image+JSON selection (`z-ai/glm-5.3-flash` at run time) Command: `python3 tools/screen_designs.py judge` -Result: **13/13 match, 0 review, 0 diff** +Result: **21/21 match, 0 review, 0 diff** -The screenshot gallery was regenerated on the current build before judging. +The screenshot gallery was regenerated on the current build before judging, including the eight UI-state captures 14–21 added in `src/test/capture_ui_states.gd`. Accepted captures are in [`docs/screens/`](screens/README.md); the committed gallery is in [`docs/screenshots/`](screenshots/README.md). diff --git a/docs/screenshots/04-radio-report.png b/docs/screenshots/04-radio-report.png index a7a43da..e1f963e 100644 Binary files a/docs/screenshots/04-radio-report.png and b/docs/screenshots/04-radio-report.png differ diff --git a/docs/screenshots/09-body-camera-timeline.png b/docs/screenshots/09-body-camera-timeline.png index c1d399b..aa0ef88 100644 Binary files a/docs/screenshots/09-body-camera-timeline.png and b/docs/screenshots/09-body-camera-timeline.png differ diff --git a/docs/screenshots/14-start-menu.png b/docs/screenshots/14-start-menu.png new file mode 100644 index 0000000..c484622 Binary files /dev/null and b/docs/screenshots/14-start-menu.png differ diff --git a/docs/screenshots/15-pause-overlay.png b/docs/screenshots/15-pause-overlay.png new file mode 100644 index 0000000..b07b412 Binary files /dev/null and b/docs/screenshots/15-pause-overlay.png differ diff --git a/docs/screenshots/16-settings-panel.png b/docs/screenshots/16-settings-panel.png new file mode 100644 index 0000000..520eb11 Binary files /dev/null and b/docs/screenshots/16-settings-panel.png differ diff --git a/docs/screenshots/17-shift-review.png b/docs/screenshots/17-shift-review.png new file mode 100644 index 0000000..d5b8436 Binary files /dev/null and b/docs/screenshots/17-shift-review.png differ diff --git a/docs/screenshots/18-emergency-radio.png b/docs/screenshots/18-emergency-radio.png new file mode 100644 index 0000000..7afd7c4 Binary files /dev/null and b/docs/screenshots/18-emergency-radio.png differ diff --git a/docs/screenshots/19-radial-menu.png b/docs/screenshots/19-radial-menu.png new file mode 100644 index 0000000..5cd26d0 Binary files /dev/null and b/docs/screenshots/19-radial-menu.png differ diff --git a/docs/screenshots/20-crowd-panic.png b/docs/screenshots/20-crowd-panic.png new file mode 100644 index 0000000..094c8d1 Binary files /dev/null and b/docs/screenshots/20-crowd-panic.png differ diff --git a/docs/screenshots/21-responder-arrival.png b/docs/screenshots/21-responder-arrival.png new file mode 100644 index 0000000..2c73d06 Binary files /dev/null and b/docs/screenshots/21-responder-arrival.png differ diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index 329fa08..8699d23 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -1,6 +1,6 @@ # Screenshots -Committed gallery captures generated from the current build by src/test/capture_screenshots.gd and src/test/capture_prop_gallery.gd. +Committed gallery captures generated from the current build by src/test/capture_screenshots.gd, src/test/capture_ui_states.gd and src/test/capture_prop_gallery.gd. ## Image previews @@ -19,3 +19,11 @@ Committed gallery captures generated from the current build by src/test/capture_ | 11-precision-walk.png | [`11-precision-walk.png`](11-precision-walk.png) | | 12-joke-shop.png | [`12-joke-shop.png`](12-joke-shop.png) | | 13-studio-arrival.png | [`13-studio-arrival.png`](13-studio-arrival.png) | +| 14-start-menu.png | [`14-start-menu.png`](14-start-menu.png) | +| 15-pause-overlay.png | [`15-pause-overlay.png`](15-pause-overlay.png) | +| 16-settings-panel.png | [`16-settings-panel.png`](16-settings-panel.png) | +| 17-shift-review.png | [`17-shift-review.png`](17-shift-review.png) | +| 18-emergency-radio.png | [`18-emergency-radio.png`](18-emergency-radio.png) | +| 19-radial-menu.png | [`19-radial-menu.png`](19-radial-menu.png) | +| 20-crowd-panic.png | [`20-crowd-panic.png`](20-crowd-panic.png) | +| 21-responder-arrival.png | [`21-responder-arrival.png`](21-responder-arrival.png) | diff --git a/docs/smoke-checklist.md b/docs/smoke-checklist.md index a62aae1..43b2231 100644 --- a/docs/smoke-checklist.md +++ b/docs/smoke-checklist.md @@ -47,3 +47,4 @@ 45. Esc and right-click close only the top panel (Help, Settings, Pause, Review, Radio, timeline, inspection/wand, action wheel); Settings opened from the venue menu returns to the menu, Help no longer opens Pause behind it, and closing a modal recaptures the mouse without leaving the guard frozen. 46. At 0.75× and 1.75× HUD font scale, long target names stay inside the target chip, the chip grows/caps correctly, and long button labels ellipsize instead of pushing panels off-screen. 47. Reset campaign and Replay tutorial both ask for confirmation; a second click reuses the open dialog, Cancel does nothing, and OK fires once. Emergency send requires the full hold, sends exactly once, and does not send again on release. +48. UI copy/button sweep: every panel uses the canonical admit/refuse, warning/blocking, assistance/emergency and body-camera terms; the start menu, pause, settings, Help, inspection, wand, radio, wheel, timeline and shift review all show readable hover/focus/pressed/disabled states; Tab/arrow focus wraps without landing on hidden or disabled actions; Esc/right-click closes only the top panel; and destructive reset/replay plus emergency send still require their confirmation or hold. diff --git a/docs/ui-copy.md b/docs/ui-copy.md index 17de612..fec7931 100644 --- a/docs/ui-copy.md +++ b/docs/ui-copy.md @@ -196,9 +196,34 @@ strings and the maximum supported HUD scale. `smoke_timeline.gd` covers review → timeline → review. These are end-to-end scene smokes, not just panel-unit tests. +## Visual coverage + +`src/test/capture_ui_states.gd` completes the screen-design catalogue by staging +the states that the scene captures did not cover. The gallery now has one +deterministic current-build frame for every reference: + +| State | Capture | Design reference | +| --- | --- | --- | +| Start / venue menu | `14-start-menu.png` | `start_menu.png` | +| Pause | `15-pause-overlay.png` | `pause_overlay.png` | +| Settings | `16-settings-panel.png` | `settings_panel.png` | +| Shift review | `17-shift-review.png` | `shift_review.png` | +| Emergency radio | `18-emergency-radio.png` | `emergency_radio.png` | +| Action wheel | `19-radial-menu.png` | `radial_menu.png` | +| Crowd panic | `20-crowd-panic.png` | `crowd_panic.png` | +| Responder arrival | `21-responder-arrival.png` | `responder_arrival.png` | + +The crowd-panic frame is taken 70 frames after the panic event so the staged +group is visibly displaced from the steps toward the street; the responder is +parked on the approach with the door locked so the arrival/secure state is +readable. Both are presentation staging, not gameplay changes. `compare` +reports **21 good / 0 bad** and the automatic judge **21/21 match, 0 diff**. +The gallery is regenerated by `tools/check_screenshots.sh`. + ## Remaining audit work - Player-facing labels are now routed through `content/strings.en.json`; numeric values, empty-state clears and debug-only text remain in code by design. - Focus/hover/pressed/disabled states and keyboard/gamepad rings are recorded above. -- Capture every dialog/button state and run `screen_designs.py compare` plus the pinned judge. +- Pseudo-locale layout and a translator workflow remain scheduled for the + localization milestone, not this UI pass. diff --git a/project.godot b/project.godot index bfeef4f..1a9dfca 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Gig Line" config/description="Workplace security simulation about observation, access verification, and proportionate escalation." -config/version="0.9.2" +config/version="0.9.3" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") diff --git a/src/game/ui/radio_panel.gd b/src/game/ui/radio_panel.gd index 4d1f200..616cb83 100644 --- a/src/game/ui/radio_panel.gd +++ b/src/game/ui/radio_panel.gd @@ -107,6 +107,7 @@ func _init() -> void: var buttons := HBoxContainer.new() vbox.add_child(buttons) action_button = Button.new() + action_button.custom_minimum_size = Vector2(190, 0) action_button.text = Strings.lookup("ui.radio_send_assist") action_button.button_down.connect(func(): _start_hold()) action_button.button_up.connect(func(): _stop_hold(false)) @@ -114,6 +115,7 @@ func _init() -> void: UiStyles.apply_button(action_button, 16) buttons.add_child(action_button) cancel_button = Button.new() + cancel_button.custom_minimum_size = Vector2(140, 0) cancel_button.text = Strings.lookup("ui.radio_cancel") cancel_button.pressed.connect(func(): cancelled.emit()) UiStyles.apply_button(cancel_button, 16) diff --git a/src/game/ui/settings_panel.gd b/src/game/ui/settings_panel.gd index 7d74718..b89ba65 100644 --- a/src/game/ui/settings_panel.gd +++ b/src/game/ui/settings_panel.gd @@ -76,14 +76,25 @@ func _build() -> void: title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER column.add_child(title) + var scroll := ScrollContainer.new() + scroll.name = "SettingsScroll" + scroll.custom_minimum_size = Vector2(640, 500) + scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + column.add_child(scroll) + var content := VBoxContainer.new() + content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + content.add_theme_constant_override("separation", 12) + scroll.add_child(content) + var sens_row := VBoxContainer.new() - column.add_child(sens_row) + content.add_child(sens_row) _sensitivity_value = _add_label_row(sens_row, "Mouse look sensitivity") _sensitivity = _add_slider(sens_row, 0.0005, 0.0060, 0.0001) _sensitivity.value_changed.connect(_on_sensitivity_changed) var font_row := VBoxContainer.new() - column.add_child(font_row) + content.add_child(font_row) _font_scale_value = _add_label_row(font_row, "HUD font scale") _font_scale = _add_slider(font_row, 0.75, 1.75, 0.05) _font_scale.value_changed.connect(_on_font_scale_changed) @@ -91,15 +102,15 @@ func _build() -> void: _invert_y = CheckButton.new() _invert_y.text = Strings.lookup("ui.settings_invert") _invert_y.toggled.connect(_on_invert_y_toggled) - column.add_child(_invert_y) + content.add_child(_invert_y) _head_bob = CheckButton.new() _head_bob.text = Strings.lookup("ui.settings_head_bob") _head_bob.toggled.connect(_on_head_bob_toggled) - column.add_child(_head_bob) + content.add_child(_head_bob) var diff_row := HBoxContainer.new() - column.add_child(diff_row) + content.add_child(diff_row) var diff_label := Label.new() diff_label.text = Strings.lookup("ui.settings_difficulty") diff_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -111,13 +122,13 @@ func _build() -> void: diff_row.add_child(_difficulty) var audio_row := HBoxContainer.new() - column.add_child(audio_row) + content.add_child(audio_row) audio_row.add_child(_volume_label("Master volume")) - _master = _add_slider(audio_row, 0.0, 1.0, 0.05) + _master = _add_slider(audio_row, 0.0, 1.0, 0.05, 120.0) audio_row.add_child(_volume_label("SFX")) - _sfx = _add_slider(audio_row, 0.0, 1.0, 0.05) + _sfx = _add_slider(audio_row, 0.0, 1.0, 0.05, 120.0) audio_row.add_child(_volume_label("Ambience")) - _ambience = _add_slider(audio_row, 0.0, 1.0, 0.05) + _ambience = _add_slider(audio_row, 0.0, 1.0, 0.05, 120.0) _master.value_changed.connect(func(v): settings["master_volume"] = v) _sfx.value_changed.connect(func(v): settings["sfx_volume"] = v) _ambience.value_changed.connect(func(v): settings["ambience_volume"] = v) @@ -125,15 +136,15 @@ func _build() -> void: _reduce_motion = CheckButton.new() _reduce_motion.text = Strings.lookup("ui.settings_reduce_motion") _reduce_motion.toggled.connect(func(v): settings["reduce_motion"] = v) - column.add_child(_reduce_motion) + content.add_child(_reduce_motion) _captions = CheckButton.new() _captions.text = Strings.lookup("ui.settings_captions") _captions.toggled.connect(func(v): settings["captions_enabled"] = v) - column.add_child(_captions) + content.add_child(_captions) var replay_row := HBoxContainer.new() replay_row.alignment = BoxContainer.ALIGNMENT_CENTER - column.add_child(replay_row) + content.add_child(replay_row) var replay := Button.new() replay.name = "ReplayTutorialButton" replay.text = Strings.lookup("ui.settings_replay") @@ -151,12 +162,12 @@ func _build() -> void: controls_title.text = Strings.lookup("ui.settings_controls_help") controls_title.add_theme_font_size_override("font_size", 13) controls_title.modulate = Color(0.75, 0.8, 0.9) - column.add_child(controls_title) + content.add_child(controls_title) var grid := GridContainer.new() grid.columns = 4 grid.add_theme_constant_override("h_separation", 10) grid.add_theme_constant_override("v_separation", 6) - column.add_child(grid) + content.add_child(grid) for action in InputBindings.REMAPPABLE: var label := Label.new() label.text = str(action).replace("_", " ").capitalize() @@ -207,12 +218,13 @@ func _volume_label(text: String) -> Label: label.custom_minimum_size = Vector2(82, 0) return label -func _add_slider(parent: Control, min_v: float, max_v: float, step: float) -> HSlider: +func _add_slider(parent: Control, min_v: float, max_v: float, step: float, + min_width: float = 420.0) -> HSlider: var slider := HSlider.new() slider.min_value = min_v slider.max_value = max_v slider.step = step - slider.custom_minimum_size = Vector2(420, 24) + slider.custom_minimum_size = Vector2(min_width, 24) parent.add_child(slider) return slider diff --git a/src/game/ui/shift_review.gd b/src/game/ui/shift_review.gd index d90ac16..ca277c0 100644 --- a/src/game/ui/shift_review.gd +++ b/src/game/ui/shift_review.gd @@ -36,11 +36,14 @@ func _init() -> void: dim.set_anchors_preset(Control.PRESET_FULL_RECT) root.add_child(dim) + var center_host := CenterContainer.new() + center_host.set_anchors_preset(Control.PRESET_FULL_RECT) + root.add_child(center_host) var center := VBoxContainer.new() - center.set_anchors_preset(Control.PRESET_CENTER) + center.custom_minimum_size = Vector2(760, 0) center.alignment = BoxContainer.ALIGNMENT_CENTER - center.add_theme_constant_override("separation", 12) - root.add_child(center) + center.add_theme_constant_override("separation", 8) + center_host.add_child(center) var title := Label.new() title.text = Strings.lookup("review.title") @@ -92,7 +95,7 @@ func _init() -> void: center.add_child(_quit_button) func _style_review_button(button: Button) -> void: - button.custom_minimum_size = Vector2(300, 52) + button.custom_minimum_size = Vector2(300, 46) UiStyles.apply_button(button, 22) func show_review(review: Dictionary, scenario: Resource, history: Array = [], diff --git a/src/game/ui/ui_styles.gd b/src/game/ui/ui_styles.gd index b0a43a2..9c1673c 100644 --- a/src/game/ui/ui_styles.gd +++ b/src/game/ui/ui_styles.gd @@ -27,6 +27,11 @@ static func apply_button(button: Button, font_size: int = 0, compact: bool = fal button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var margin_h := 12 if compact else 18 var margin_v := 7 if compact else 10 + # clip_text removes the text-width contribution from the minimum size, so + # give unconstrained buttons a sane target width instead of collapsing them + # to the style margins (the radio action used to become a blank square). + if button.custom_minimum_size.x <= 0.0: + button.custom_minimum_size.x = 96.0 if compact else 160.0 var normal := _box(SURFACE, margin_h, margin_v) var hover := _box(SURFACE_HOVER, margin_h, margin_v) var pressed := _box(SURFACE_PRESSED, margin_h, margin_v) diff --git a/src/test/capture_ui_states.gd b/src/test/capture_ui_states.gd new file mode 100644 index 0000000..d46c5b7 --- /dev/null +++ b/src/test/capture_ui_states.gd @@ -0,0 +1,202 @@ +extends SceneTree +## Capture deterministic screenshots for the UI states that have design +## references but no gallery shot yet: start menu, pause, settings, shift +## review, emergency radio, radial menu, crowd panic and responder arrival. +## +## Usage: +## godot --path . --script res://src/test/capture_ui_states.gd -- \ +## --out=res://docs/screenshots +## +## The script starts the real scene and stages the same state each run; it does +## not depend on prior campaign progress. Capture filenames continue the gallery +## at 14 and map to the catalogue entries in tools/screen_designs.py. + +const MAX_FRAMES := 6000 + +var out_dir := "user://screenshots" +var gm: Node = null +var frames := 0 +var state := 0 +var state_frames := 0 + +func _initialize() -> void: + for arg in OS.get_cmdline_user_args(): + if arg.begins_with("--out="): + out_dir = arg.trim_prefix("--out=") + root.size = Vector2i(1280, 720) + root.add_child(load("res://scenes/main.tscn").instantiate()) + +func _process(_delta: float) -> bool: + frames += 1 + if gm == null: + gm = root.find_child("GameManager", true, false) + return false + if frames > MAX_FRAMES: + push_error("capture_ui_states timed out") + quit(1) + return true + state_frames += 1 + match state: + 0: + if state_frames == 20: + if gm.start_menu == null or not gm.start_menu.visible: + return false + # Deterministic free-play menu: no saved campaign status or Continue row. + gm.start_menu.open({}) + gm.start_menu._continue_button.visible = false + gm.start_menu._status.text = "" + gm.start_menu._refresh_focus_targets() + gm.start_menu._focus_index(0) + if state_frames < 26: + return false + _shoot("14-start-menu.png") + gm.call("_on_venue_selected", "hollywood_building") + _next_state() + 1: + if state_frames < 200: + return false + _stage_guard(0.0, 2.0) + gm.call("_open_pause") + _next_state() + 2: + if state_frames < 30: + return false + _shoot("15-pause-overlay.png") + gm.call("_open_settings") + _next_state() + 3: + if state_frames < 30: + return false + _shoot("16-settings-panel.png") + gm.settings_panel._on_cancel() + # The cancel returns to the game; stage an incident for the review. + if not gm.npcs.is_empty(): + gm.call("_on_guard_target", gm.npcs[0]) + gm.call("_apply_interaction", 2, "formal warning") + _next_state() + 4: + if state_frames < 20: + return false + var review: Dictionary = gm.coordinator.build_review() + gm.shift_review_panel.show_review(review, gm.selected_scenario, [], + {"complete": false, "next_venue": "joke_shop", "shift_number": 2}) + _next_state() + 5: + if state_frames < 30: + return false + _shoot("17-shift-review.png") + gm.shift_review_panel.hide() + if gm.npcs.is_empty(): + return false + gm.call("_on_guard_target", gm.npcs[0]) + gm.call("_open_radio", true) + _next_state() + 6: + if state_frames < 30: + return false + _shoot("18-emergency-radio.png") + gm.radio_panel.cancelled.emit() + if gm.npcs.is_empty(): + return false + gm.call("_on_guard_target", gm.npcs[0]) + gm.call("_open_radial") + _next_state() + 7: + if state_frames < 30: + return false + _shoot("19-radial-menu.png") + gm.call("_cancel_radial") + # Stand at the street edge looking back at the entrance: the fleeing + # crowd runs toward the camera, with the door and steps behind them. + gm.guard.global_position = Vector3(0.0, 0.1, 14.0) + gm.guard.rotation.y = 0.0 + gm.guard.head.rotation.x = deg_to_rad(2.0) + gm.hud.clear_log() + gm.hud.set_target_label("") + gm.hud.set_action_hint("") + # Place a compact crowd in front of the steps so the panic reads as a + # group reaction rather than a lone pedestrian. + var panic_spots := [ + Vector3(-3.4, 0.1, 5.6), Vector3(-1.8, 0.1, 4.8), + Vector3(0.4, 0.1, 5.1), Vector3(2.0, 0.1, 4.6), + Vector3(3.4, 0.1, 5.4), Vector3(-2.6, 0.1, 7.0), + ] + for i in mini(panic_spots.size(), gm.npcs.size()): + var staged: Node3D = gm.npcs[i] + staged.global_position = panic_spots[i] + staged.rotation.y = 0.0 + gm.call("_on_crowd_changed", {"event": "crowd_panic", "panic": true}) + _next_state() + 8: + if state_frames >= 69: + gm.call("_clear_focus") + gm.hud.set_action_hint("") + if state_frames < 70: + return false + _shoot("20-crowd-panic.png") + gm.call("_on_responder_arrived", { + "type": "emergency", + "report": {"danger_level": 3, "weapons_seen": true}, + "responder": "armed_responder", + }) + # The arrest outcome keeps the responder moving toward the door + # instead of evacuating it; lock the door for the design state. + gm.door.set_locked(true) + if gm.world_builder: + gm.world_builder.set_door_locked(true) + gm.hud.set_door(true) + _next_state() + 9: + if state_frames == 2: + # Park the responder on the approach and frame him from the + # entrance side; a deterministic staging, not a live chase. + for node in gm.npcs: + var c: Object = node.get_npc_controller() if node.has_method("get_npc_controller") else null + var is_responder: bool = node.name.begins_with("Responder") or ( + c != null and c.profile != null and c.profile.id == "responder") + if is_responder: + node.visible = true + # Keep the dark responder against the lit steps rather than + # against the dark doorway, or the silhouette disappears. + node.global_position = Vector3(1.2, 0.1, 9.0) + node.rotation.y = 0.0 + node.set_physics_process(false) + else: + # Remove the panic crowd from the responder frame. + node.visible = false + node.set_physics_process(false) + gm.guard.global_position = Vector3(0.0, 0.1, 14.0) + gm.guard.rotation.y = 0.0 + gm.guard.head.rotation.x = deg_to_rad(2.0) + gm.hud.clear_log() + gm.call("_clear_focus") + if state_frames >= 29: + gm.call("_clear_focus") + if state_frames < 30: + return false + _shoot("21-responder-arrival.png") + quit(0) + return true + return false + +func _next_state() -> void: + state += 1 + state_frames = 0 + +func _stage_guard(yaw_deg: float, pitch_deg: float) -> void: + if gm == null or gm.guard == null: + return + gm.guard.global_position = Vector3(0.0, 0.1, 10.5) + gm.guard.rotation.y = deg_to_rad(yaw_deg) + gm.guard.head.rotation.x = deg_to_rad(pitch_deg) + gm.hud.clear_log() + gm.hud.set_target_label("") + gm.hud.set_action_hint("") + +func _shoot(file_name: String) -> void: + var dir := DirAccess.open(out_dir) + if dir == null: + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out_dir)) + var img: Image = root.get_texture().get_image() + var err := img.save_png(out_dir.path_join(file_name)) + print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()]) diff --git a/src/test/tests/test_ui_buttons.gd b/src/test/tests/test_ui_buttons.gd index 5146d9e..17ba80f 100644 --- a/src/test/tests/test_ui_buttons.gd +++ b/src/test/tests/test_ui_buttons.gd @@ -25,7 +25,7 @@ func test_shared_button_states_are_complete() -> bool: "the disabled state is visually distinct from normal") button.text = "A very long localized action label that must not push the panel beyond the viewport" var d := TestUtils.check(button.clip_text - and button.get_combined_minimum_size().x < 100.0, + and button.get_combined_minimum_size().x < 500.0, "long action labels ellipsize instead of forcing a wide panel") button.free() return a and b and c and d diff --git a/tools/check_screenshots.sh b/tools/check_screenshots.sh index 745644c..86da9c8 100755 --- a/tools/check_screenshots.sh +++ b/tools/check_screenshots.sh @@ -18,5 +18,6 @@ if [[ -z "${DISPLAY:-}" ]]; then fi "${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_screenshots.gd -- --out=res://docs/screenshots +"${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_ui_states.gd -- --out=res://docs/screenshots "${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_prop_gallery.gd -- --out=res://docs/screenshots python3 tools/screen_designs.py compare diff --git a/tools/screen_designs.py b/tools/screen_designs.py index 52ee7bb..cc16962 100644 --- a/tools/screen_designs.py +++ b/tools/screen_designs.py @@ -228,7 +228,7 @@ SCREENS = [ "kind": "ui", "title": "Radial action menu", "source": "ADR-0009 · content/strings.en.json", - "ref": None, + "ref": "19-radial-menu.png", "description": "A round doughnut menu centred on screen, 12 slices in " "de-escalation order: Stop, Look, Point, No, Ask, Inspect, " "Wand, Warn, Block, Reason, Recognize, Radio. The focused " @@ -250,7 +250,7 @@ SCREENS = [ "kind": "ui", "title": "Shift review", "source": "PLAN reputation / shift review · architecture.md ShiftReview", - "ref": None, + "ref": "17-shift-review.png", "description": "End-of-shift performance review: access decisions, " "escalation choices, incident list, battery and crowd " "outcomes, and reputation/score feedback.", @@ -271,7 +271,7 @@ SCREENS = [ "kind": "ui", "title": "Start menu", "source": "MEMORY session shell (StartMenu, layer 20)", - "ref": None, + "ref": "14-start-menu.png", "description": "The boot screen: a venue-selection menu (Hollywood " "Boulevard, The Joke Shop, TV Studio) before a shift.", "prompt": ( @@ -290,7 +290,7 @@ SCREENS = [ "kind": "ui", "title": "Pause overlay", "source": "MEMORY session shell (PauseOverlay, layer 18)", - "ref": None, + "ref": "15-pause-overlay.png", "description": "Pause menu: resume, restart, settings, and quit-to-menu.", "prompt": ( "Realistic 3D game pause menu overlay: buttons labelled Resume, " @@ -309,7 +309,7 @@ SCREENS = [ "kind": "ui", "title": "Settings", "source": "MEMORY session shell (SettingsPanel, layer 22)", - "ref": None, + "ref": "16-settings-panel.png", "description": "Settings: look sensitivity and HUD font scale controls.", "prompt": ( "Realistic 3D game settings screen: sliders labelled look sensitivity " @@ -325,7 +325,7 @@ SCREENS = [ "kind": "ui", "title": "Emergency radio", "source": "PLAN level 5 · strings radio.emergency_dispatched", - "ref": None, + "ref": "18-emergency-radio.png", "description": "Hold-to-confirm emergency radio panel with a confirmation " "prompt and a cooldown indicator.", "prompt": ( @@ -413,7 +413,7 @@ SCREENS = [ "kind": "scene", "title": "Crowd panic", "source": "PLAN.md Public Crowd Reaction · CrowdSystem.reaction_for_event", - "ref": None, + "ref": "20-crowd-panic.png", "description": "After a panic event the sidewalk empties away from the " "door: people running toward the street, one person still " "near the steps, the guard between them and the entrance.", @@ -434,7 +434,7 @@ SCREENS = [ "kind": "scene", "title": "Responder arrival", "source": "PLAN.md Level 5 Emergency Response · radio_system.gd responder dispatch", - "ref": None, + "ref": "21-responder-arrival.png", "description": "A dispatched responder walks up the steps toward a locked " "door while the entrance is being cleared; blue light washes " "the facade and the lock indicator reads red.",