Close the 0.9.3 UI polish milestone
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Skipped

Add deterministic captures 14-21 for the remaining screen-design states, fix the panel/button layout issues they exposed, and record 21/21 compare and judge results. Bump the source to 0.9.3 and move the completed milestone to DONE.md.
This commit is contained in:
zwitschi committed 2026-11-02 17:57:40 +01:00
1 parent e805a91ad3
commit c1cd4898e8
32 files changed
+440 -160

No files matched your search

+8 -1
View File
@@ -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 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
Semantic Versioning. 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 ### Changed
+75
View File
@@ -1660,3 +1660,78 @@ redesign.
blocks a build. 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. 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.
+29 -26
View File
@@ -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. 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 - **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, 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. handoff; it complements the unit tests that only exercise panels.
- **Destructive actions (2026-11-02):** `SettingsPanel._open_destructive_dialog()` - **Destructive actions (2026-11-02):** `SettingsPanel._open_destructive_dialog()`
owns one confirmation instance for reset/replay; repeated presses focus the 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()` 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 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. 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 - **Version and roadmap renumber (2026-11-02):** source is now **0.9.3**; the
completed map/NPC work is the patch content. Postponed work is renumbered to completed map/NPC work and UI polish are the patch content. Next work is
**0.9.3 UI**, **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the **0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the **0.10.0**
**0.10.0** minor release candidate. `TODO.md` plans only 0.x patch/minor minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
increments; no major-version scope is defined yet. No release build or tag has been produced major-version scope is defined yet. No release build or tag has been produced
for 0.9.2. for 0.9.3.
- **Priority change (2026-11-02):** the map optimization (originally planned for 0.9.5) and the pre-build - **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit and
NPC revisit are complete and in `DONE.md`. The postponed 0.9.3 UI, 0.9.4 NPC 0.9.3 UI polish are complete and in `DONE.md`. The 0.9.4 NPC detail, 0.9.5
detail, 0.9.5 textures and 0.9.6 integration/0.10.0 release work are next. The textures and 0.9.6 integration/0.10.0 release work are next. The current source
current source version is 0.9.2; no release build or tag has been produced. 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 - **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 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 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 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 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. 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 - **Mockups complete (v0.3.2, updated 0.9.3):** `docs/screens/designs/` holds AI references for
catalogue screens. Only the six staged captures (01–06) are compared/judged; the other 13 all 21 catalogue screens, and every entry now has a committed current-build capture.
have no `capture`, so adding staged captures for the wand panel, timeline and escalation HUD `capture_screenshots.gd` covers scenes 01–13, `capture_ui_states.gd` covers UI states 14–21,
would extend automated visual checks. `generate` now calls `_load_env_file()`. 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 gallery - **Staged captures (v0.3.3, updated 2026-11-02):** `capture_screenshots.gd` emits the scene
shots; states 7–10 switch to The Joke Shop at elapsed 620 s, retry the wand until it alerts, 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. 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 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 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 `recorder.close_all_open()`, because `to_dict()` only lists closed incidents.
HUD mockup is third-person now; `judge` still classes it `review` (no diff). `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 - **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 (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 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. - 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. - 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()`. - `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). - 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 - 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 (`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` - 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. 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. - `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). - 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/`. 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 ## Open Questions
@@ -754,10 +757,10 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Plan vs code ## Plan vs code
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of `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, 2026-11-02 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
the Blender asset library, the realism pass, and the visual-alignment pass. `TODO.md` holds only UI/accessibility pass, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
the realism follow-ups and optional future ideas. The project uses a custom headless runner, not 0.9.4–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
GUT, and Gitea Actions, not GitHub Actions. GitHub Actions.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu --> <!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
+1 -1
View File
@@ -176,7 +176,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
## Versioning and Delivery ## Versioning and Delivery
`config/version` in `project.godot` is the single version source (currently `0.9.2`). Releases are tagged `v<version>`; 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<version>`; 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 ## Design Direction
+9 -7
View File
@@ -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 > **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. > 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 > **Current source: v0.9.3** — the UI/accessibility polish pass on top of the
> NPC final-context review; the release tag is pending. > 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 > Releases are built automatically when a `vX.Y.Z` tag is pushed
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use > (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline > `./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 ## Development Status
v0.9.2 is the current source version: the Hollywood and Joke Shop map v0.9.3 is the current source version: the completed UI/accessibility polish
rebuilds, the NPC final-context review and the HUD/wand string fixes. It builds pass (dialogue, button states, focus/navigation, modal close, long-string layout,
on v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality and destructive-action guards and full screen capture coverage), on top of v0.9.2's
audio/accessibility pass, v0.8's tutorial/campaign/resume work and the v0.7.x Hollywood/Joke Shop map rebuilds and NPC final-context review. It builds on
venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md) 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 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). verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
+12 -81
View File
@@ -5,10 +5,10 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
## Gap Analysis and 0.x Roadmap ## 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 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 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. release-candidate foundation, not yet a 0.10.0 release candidate.
### Documentation audit (completed 2026-11-01) ### 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 | | 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 | | 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 | | 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 | | 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.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.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 | | 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 | | 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 | | 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 | | 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+ | | 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 **Current priority:** map optimization, the **pre-build NPC revisit** and the
complete (both moved to `DONE.md` on 2026-11-02). **0.9.3 UI polish** is active; **0.9.3 UI polish** pass are complete (all moved to `DONE.md` on 2026-11-02).
0.9.4 NPC-detail and 0.9.5 texture patches, then 0.9.6 integration and the 0.10.0 **0.9.4 NPC-detail** is active; 0.9.5 texture refinement, 0.9.6 integration and
release candidate, are queued. No release build or tag has been produced for the the 0.10.0 release candidate are queued. The source version is now 0.9.3; no
current source version. release build or tag has been produced for it.
## Patch 0.9.3 — Dialog, button and UI polish ## Patch 0.9.4 — NPC visual optimization
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
Goal: make the eight character variants hold up in portrait, conversation and crowd 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 views without changing the `npc_base`/`Rig/Head` contract or the skinned/procedural
+1 -1
View File
@@ -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. - **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
- **PATCH** — bug fixes and small polish that do not change behavior. - **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<version>` (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<version>` (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 ## 3. Context and Scope
+11 -9
View File
@@ -261,6 +261,9 @@ python3 tools/screen_designs.py generate --draft --only wand --force
# (requires OPENROUTER_API_KEY) # (requires OPENROUTER_API_KEY)
python3 tools/screen_designs.py generate 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 # Prop gallery (studio line-up) for close-range prop checks
godot --path . --script res://src/test/capture_prop_gallery.gd 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 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, The catalogue covers 21 screens: the thirteen scene captures plus the start menu,
pause overlay, settings, shift review, radial menu, emergency radio, wand pause overlay, settings, shift review, radial menu, emergency radio, crowd panic,
screening, Joke Shop queue, TV Studio arrival, crowd panic, responder arrival, and responder arrival. All 21 have an AI reference in `assets/images/ui/`
body-camera timeline, and the escalation HUD. All 21 have an AI reference in (snapshot in `docs/screens/designs/`) and a committed capture, so every entry is
`assets/images/ui/` (snapshot in `docs/screens/designs/`); the eleven screens with compared or judged against the running game.
staged captures are also compared or judged against the running game.
`compare` flags gross drift: wrong aspect ratio, undersized frame, near-blank `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 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 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 `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 `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 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 scene captures, UI-state captures and prop gallery into `docs/screenshots/` (using
no display is available) and then runs `screen_designs.py compare`. `xvfb-run` when no display is available) and then runs `screen_designs.py compare`.
+1
View File
@@ -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 - 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. - 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. - 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. - 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. - 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.
+2 -2
View File
@@ -3,9 +3,9 @@
Date: 2026-11-02 Date: 2026-11-02
Model: automatic cheapest image+JSON selection (`z-ai/glm-5.3-flash` at run time) Model: automatic cheapest image+JSON selection (`z-ai/glm-5.3-flash` at run time)
Command: `python3 tools/screen_designs.py judge` 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 Accepted captures are in [`docs/screens/`](screens/README.md); the committed
gallery is in [`docs/screenshots/`](screenshots/README.md). gallery is in [`docs/screenshots/`](screenshots/README.md).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+9 -1
View File
@@ -1,6 +1,6 @@
# Screenshots # 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 ## Image previews
@@ -19,3 +19,11 @@ Committed gallery captures generated from the current build by src/test/capture_
| <img src="11-precision-walk.png" width="280" alt="11-precision-walk.png"> | [`11-precision-walk.png`](11-precision-walk.png) | | <img src="11-precision-walk.png" width="280" alt="11-precision-walk.png"> | [`11-precision-walk.png`](11-precision-walk.png) |
| <img src="12-joke-shop.png" width="280" alt="12-joke-shop.png"> | [`12-joke-shop.png`](12-joke-shop.png) | | <img src="12-joke-shop.png" width="280" alt="12-joke-shop.png"> | [`12-joke-shop.png`](12-joke-shop.png) |
| <img src="13-studio-arrival.png" width="280" alt="13-studio-arrival.png"> | [`13-studio-arrival.png`](13-studio-arrival.png) | | <img src="13-studio-arrival.png" width="280" alt="13-studio-arrival.png"> | [`13-studio-arrival.png`](13-studio-arrival.png) |
| <img src="14-start-menu.png" width="280" alt="14-start-menu.png"> | [`14-start-menu.png`](14-start-menu.png) |
| <img src="15-pause-overlay.png" width="280" alt="15-pause-overlay.png"> | [`15-pause-overlay.png`](15-pause-overlay.png) |
| <img src="16-settings-panel.png" width="280" alt="16-settings-panel.png"> | [`16-settings-panel.png`](16-settings-panel.png) |
| <img src="17-shift-review.png" width="280" alt="17-shift-review.png"> | [`17-shift-review.png`](17-shift-review.png) |
| <img src="18-emergency-radio.png" width="280" alt="18-emergency-radio.png"> | [`18-emergency-radio.png`](18-emergency-radio.png) |
| <img src="19-radial-menu.png" width="280" alt="19-radial-menu.png"> | [`19-radial-menu.png`](19-radial-menu.png) |
| <img src="20-crowd-panic.png" width="280" alt="20-crowd-panic.png"> | [`20-crowd-panic.png`](20-crowd-panic.png) |
| <img src="21-responder-arrival.png" width="280" alt="21-responder-arrival.png"> | [`21-responder-arrival.png`](21-responder-arrival.png) |
+1
View File
@@ -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. 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. 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. 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.
+26 -1
View File
@@ -196,9 +196,34 @@ strings and the maximum supported HUD scale.
`smoke_timeline.gd` covers review → timeline → review. These are end-to-end `smoke_timeline.gd` covers review → timeline → review. These are end-to-end
scene smokes, not just panel-unit tests. 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 ## Remaining audit work
- Player-facing labels are now routed through `content/strings.en.json`; numeric - 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. 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. - 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.
+1 -1
View File
@@ -12,7 +12,7 @@ config_version=5
config/name="Gig Line" config/name="Gig Line"
config/description="Workplace security simulation about observation, access verification, and proportionate escalation." 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" run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility") config/features=PackedStringArray("4.7", "GL Compatibility")
+2
View File
@@ -107,6 +107,7 @@ func _init() -> void:
var buttons := HBoxContainer.new() var buttons := HBoxContainer.new()
vbox.add_child(buttons) vbox.add_child(buttons)
action_button = Button.new() action_button = Button.new()
action_button.custom_minimum_size = Vector2(190, 0)
action_button.text = Strings.lookup("ui.radio_send_assist") action_button.text = Strings.lookup("ui.radio_send_assist")
action_button.button_down.connect(func(): _start_hold()) action_button.button_down.connect(func(): _start_hold())
action_button.button_up.connect(func(): _stop_hold(false)) action_button.button_up.connect(func(): _stop_hold(false))
@@ -114,6 +115,7 @@ func _init() -> void:
UiStyles.apply_button(action_button, 16) UiStyles.apply_button(action_button, 16)
buttons.add_child(action_button) buttons.add_child(action_button)
cancel_button = Button.new() cancel_button = Button.new()
cancel_button.custom_minimum_size = Vector2(140, 0)
cancel_button.text = Strings.lookup("ui.radio_cancel") cancel_button.text = Strings.lookup("ui.radio_cancel")
cancel_button.pressed.connect(func(): cancelled.emit()) cancel_button.pressed.connect(func(): cancelled.emit())
UiStyles.apply_button(cancel_button, 16) UiStyles.apply_button(cancel_button, 16)
+28 -16
View File
@@ -76,14 +76,25 @@ func _build() -> void:
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
column.add_child(title) 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() 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_value = _add_label_row(sens_row, "Mouse look sensitivity")
_sensitivity = _add_slider(sens_row, 0.0005, 0.0060, 0.0001) _sensitivity = _add_slider(sens_row, 0.0005, 0.0060, 0.0001)
_sensitivity.value_changed.connect(_on_sensitivity_changed) _sensitivity.value_changed.connect(_on_sensitivity_changed)
var font_row := VBoxContainer.new() 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_value = _add_label_row(font_row, "HUD font scale")
_font_scale = _add_slider(font_row, 0.75, 1.75, 0.05) _font_scale = _add_slider(font_row, 0.75, 1.75, 0.05)
_font_scale.value_changed.connect(_on_font_scale_changed) _font_scale.value_changed.connect(_on_font_scale_changed)
@@ -91,15 +102,15 @@ func _build() -> void:
_invert_y = CheckButton.new() _invert_y = CheckButton.new()
_invert_y.text = Strings.lookup("ui.settings_invert") _invert_y.text = Strings.lookup("ui.settings_invert")
_invert_y.toggled.connect(_on_invert_y_toggled) _invert_y.toggled.connect(_on_invert_y_toggled)
column.add_child(_invert_y) content.add_child(_invert_y)
_head_bob = CheckButton.new() _head_bob = CheckButton.new()
_head_bob.text = Strings.lookup("ui.settings_head_bob") _head_bob.text = Strings.lookup("ui.settings_head_bob")
_head_bob.toggled.connect(_on_head_bob_toggled) _head_bob.toggled.connect(_on_head_bob_toggled)
column.add_child(_head_bob) content.add_child(_head_bob)
var diff_row := HBoxContainer.new() var diff_row := HBoxContainer.new()
column.add_child(diff_row) content.add_child(diff_row)
var diff_label := Label.new() var diff_label := Label.new()
diff_label.text = Strings.lookup("ui.settings_difficulty") diff_label.text = Strings.lookup("ui.settings_difficulty")
diff_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL diff_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -111,13 +122,13 @@ func _build() -> void:
diff_row.add_child(_difficulty) diff_row.add_child(_difficulty)
var audio_row := HBoxContainer.new() var audio_row := HBoxContainer.new()
column.add_child(audio_row) content.add_child(audio_row)
audio_row.add_child(_volume_label("Master volume")) 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")) 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")) 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) _master.value_changed.connect(func(v): settings["master_volume"] = v)
_sfx.value_changed.connect(func(v): settings["sfx_volume"] = v) _sfx.value_changed.connect(func(v): settings["sfx_volume"] = v)
_ambience.value_changed.connect(func(v): settings["ambience_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 = CheckButton.new()
_reduce_motion.text = Strings.lookup("ui.settings_reduce_motion") _reduce_motion.text = Strings.lookup("ui.settings_reduce_motion")
_reduce_motion.toggled.connect(func(v): settings["reduce_motion"] = v) _reduce_motion.toggled.connect(func(v): settings["reduce_motion"] = v)
column.add_child(_reduce_motion) content.add_child(_reduce_motion)
_captions = CheckButton.new() _captions = CheckButton.new()
_captions.text = Strings.lookup("ui.settings_captions") _captions.text = Strings.lookup("ui.settings_captions")
_captions.toggled.connect(func(v): settings["captions_enabled"] = v) _captions.toggled.connect(func(v): settings["captions_enabled"] = v)
column.add_child(_captions) content.add_child(_captions)
var replay_row := HBoxContainer.new() var replay_row := HBoxContainer.new()
replay_row.alignment = BoxContainer.ALIGNMENT_CENTER replay_row.alignment = BoxContainer.ALIGNMENT_CENTER
column.add_child(replay_row) content.add_child(replay_row)
var replay := Button.new() var replay := Button.new()
replay.name = "ReplayTutorialButton" replay.name = "ReplayTutorialButton"
replay.text = Strings.lookup("ui.settings_replay") 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.text = Strings.lookup("ui.settings_controls_help")
controls_title.add_theme_font_size_override("font_size", 13) controls_title.add_theme_font_size_override("font_size", 13)
controls_title.modulate = Color(0.75, 0.8, 0.9) controls_title.modulate = Color(0.75, 0.8, 0.9)
column.add_child(controls_title) content.add_child(controls_title)
var grid := GridContainer.new() var grid := GridContainer.new()
grid.columns = 4 grid.columns = 4
grid.add_theme_constant_override("h_separation", 10) grid.add_theme_constant_override("h_separation", 10)
grid.add_theme_constant_override("v_separation", 6) grid.add_theme_constant_override("v_separation", 6)
column.add_child(grid) content.add_child(grid)
for action in InputBindings.REMAPPABLE: for action in InputBindings.REMAPPABLE:
var label := Label.new() var label := Label.new()
label.text = str(action).replace("_", " ").capitalize() label.text = str(action).replace("_", " ").capitalize()
@@ -207,12 +218,13 @@ func _volume_label(text: String) -> Label:
label.custom_minimum_size = Vector2(82, 0) label.custom_minimum_size = Vector2(82, 0)
return label 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() var slider := HSlider.new()
slider.min_value = min_v slider.min_value = min_v
slider.max_value = max_v slider.max_value = max_v
slider.step = step slider.step = step
slider.custom_minimum_size = Vector2(420, 24) slider.custom_minimum_size = Vector2(min_width, 24)
parent.add_child(slider) parent.add_child(slider)
return slider return slider
+7 -4
View File
@@ -36,11 +36,14 @@ func _init() -> void:
dim.set_anchors_preset(Control.PRESET_FULL_RECT) dim.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_child(dim) 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() var center := VBoxContainer.new()
center.set_anchors_preset(Control.PRESET_CENTER) center.custom_minimum_size = Vector2(760, 0)
center.alignment = BoxContainer.ALIGNMENT_CENTER center.alignment = BoxContainer.ALIGNMENT_CENTER
center.add_theme_constant_override("separation", 12) center.add_theme_constant_override("separation", 8)
root.add_child(center) center_host.add_child(center)
var title := Label.new() var title := Label.new()
title.text = Strings.lookup("review.title") title.text = Strings.lookup("review.title")
@@ -92,7 +95,7 @@ func _init() -> void:
center.add_child(_quit_button) center.add_child(_quit_button)
func _style_review_button(button: Button) -> void: 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) UiStyles.apply_button(button, 22)
func show_review(review: Dictionary, scenario: Resource, history: Array = [], func show_review(review: Dictionary, scenario: Resource, history: Array = [],
+5
View File
@@ -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 button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
var margin_h := 12 if compact else 18 var margin_h := 12 if compact else 18
var margin_v := 7 if compact else 10 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 normal := _box(SURFACE, margin_h, margin_v)
var hover := _box(SURFACE_HOVER, margin_h, margin_v) var hover := _box(SURFACE_HOVER, margin_h, margin_v)
var pressed := _box(SURFACE_PRESSED, margin_h, margin_v) var pressed := _box(SURFACE_PRESSED, margin_h, margin_v)
+202
View File
@@ -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()])
+1 -1
View File
@@ -25,7 +25,7 @@ func test_shared_button_states_are_complete() -> bool:
"the disabled state is visually distinct from normal") "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" button.text = "A very long localized action label that must not push the panel beyond the viewport"
var d := TestUtils.check(button.clip_text 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") "long action labels ellipsize instead of forcing a wide panel")
button.free() button.free()
return a and b and c and d return a and b and c and d
+1
View File
@@ -18,5 +18,6 @@ if [[ -z "${DISPLAY:-}" ]]; then
fi fi
"${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_screenshots.gd -- --out=res://docs/screenshots "${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 "${RUN[@]}" "$GODOT" --path . --script res://src/test/capture_prop_gallery.gd -- --out=res://docs/screenshots
python3 tools/screen_designs.py compare python3 tools/screen_designs.py compare
+8 -8
View File
@@ -228,7 +228,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Radial action menu", "title": "Radial action menu",
"source": "ADR-0009 · content/strings.en.json", "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 " "description": "A round doughnut menu centred on screen, 12 slices in "
"de-escalation order: Stop, Look, Point, No, Ask, Inspect, " "de-escalation order: Stop, Look, Point, No, Ask, Inspect, "
"Wand, Warn, Block, Reason, Recognize, Radio. The focused " "Wand, Warn, Block, Reason, Recognize, Radio. The focused "
@@ -250,7 +250,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Shift review", "title": "Shift review",
"source": "PLAN reputation / shift review · architecture.md ShiftReview", "source": "PLAN reputation / shift review · architecture.md ShiftReview",
"ref": None, "ref": "17-shift-review.png",
"description": "End-of-shift performance review: access decisions, " "description": "End-of-shift performance review: access decisions, "
"escalation choices, incident list, battery and crowd " "escalation choices, incident list, battery and crowd "
"outcomes, and reputation/score feedback.", "outcomes, and reputation/score feedback.",
@@ -271,7 +271,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Start menu", "title": "Start menu",
"source": "MEMORY session shell (StartMenu, layer 20)", "source": "MEMORY session shell (StartMenu, layer 20)",
"ref": None, "ref": "14-start-menu.png",
"description": "The boot screen: a venue-selection menu (Hollywood " "description": "The boot screen: a venue-selection menu (Hollywood "
"Boulevard, The Joke Shop, TV Studio) before a shift.", "Boulevard, The Joke Shop, TV Studio) before a shift.",
"prompt": ( "prompt": (
@@ -290,7 +290,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Pause overlay", "title": "Pause overlay",
"source": "MEMORY session shell (PauseOverlay, layer 18)", "source": "MEMORY session shell (PauseOverlay, layer 18)",
"ref": None, "ref": "15-pause-overlay.png",
"description": "Pause menu: resume, restart, settings, and quit-to-menu.", "description": "Pause menu: resume, restart, settings, and quit-to-menu.",
"prompt": ( "prompt": (
"Realistic 3D game pause menu overlay: buttons labelled Resume, " "Realistic 3D game pause menu overlay: buttons labelled Resume, "
@@ -309,7 +309,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Settings", "title": "Settings",
"source": "MEMORY session shell (SettingsPanel, layer 22)", "source": "MEMORY session shell (SettingsPanel, layer 22)",
"ref": None, "ref": "16-settings-panel.png",
"description": "Settings: look sensitivity and HUD font scale controls.", "description": "Settings: look sensitivity and HUD font scale controls.",
"prompt": ( "prompt": (
"Realistic 3D game settings screen: sliders labelled look sensitivity " "Realistic 3D game settings screen: sliders labelled look sensitivity "
@@ -325,7 +325,7 @@ SCREENS = [
"kind": "ui", "kind": "ui",
"title": "Emergency radio", "title": "Emergency radio",
"source": "PLAN level 5 · strings radio.emergency_dispatched", "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 " "description": "Hold-to-confirm emergency radio panel with a confirmation "
"prompt and a cooldown indicator.", "prompt and a cooldown indicator.",
"prompt": ( "prompt": (
@@ -413,7 +413,7 @@ SCREENS = [
"kind": "scene", "kind": "scene",
"title": "Crowd panic", "title": "Crowd panic",
"source": "PLAN.md Public Crowd Reaction · CrowdSystem.reaction_for_event", "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 " "description": "After a panic event the sidewalk empties away from the "
"door: people running toward the street, one person still " "door: people running toward the street, one person still "
"near the steps, the guard between them and the entrance.", "near the steps, the guard between them and the entrance.",
@@ -434,7 +434,7 @@ SCREENS = [
"kind": "scene", "kind": "scene",
"title": "Responder arrival", "title": "Responder arrival",
"source": "PLAN.md Level 5 Emergency Response · radio_system.gd responder dispatch", "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 " "description": "A dispatched responder walks up the steps toward a locked "
"door while the entrance is being cleared; blue light washes " "door while the entrance is being cleared; blue light washes "
"the facade and the lock indicator reads red.", "the facade and the lock indicator reads red.",