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
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
+75
View File
@@ -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.
+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.
- **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.
<!-- 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
`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
+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
> 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).
+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
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
+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.
- **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
+11 -9
View File
@@ -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`.
+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
- 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.
+2 -2
View File
@@ -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).
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
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_
| <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="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.
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.
+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
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.
+1 -1
View File
@@ -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")
+2
View File
@@ -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)
+28 -16
View File
@@ -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
+7 -4
View File
@@ -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 = [],
+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
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)
+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")
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
+1
View File
@@ -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
+8 -8
View File
@@ -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.",