From 883626a06786302b5cb1888946137cf22b473a47 Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sun, 1 Nov 2026 21:46:42 +0100 Subject: [PATCH] Ship the 0.9.0 production-quality milestone Add mixer/accessibility settings, caption/reduce-motion hooks, audio volume clamping, a 24-NPC performance smoke and shortened coat/dress stride for hem clipping. Record the explicit Godot 4.3-CI/4.7-local matrix and the 0.9 production scope. Headless 250/250, Python 94/94, 24-NPC budget smoke passes. --- CHANGELOG.md | 21 +++++ DONE.md | 114 +++++++++++++++++++++++++ MEMORY.md | 1 + PLAN.md | 2 +- README.md | 5 +- TODO.md | 116 -------------------------- docs/architecture.md | 3 +- docs/development-status.md | 3 +- docs/gameplay.md | 2 +- docs/getting-started.md | 4 +- docs/smoke-checklist.md | 3 + src/autoload/save.gd | 5 ++ src/game/audio_feedback.gd | 21 ++++- src/game/game_manager.gd | 5 +- src/game/npc_avatar.gd | 5 +- src/game/ui/settings_panel.gd | 43 ++++++++++ src/test/smoke_npc_budget.gd | 2 +- src/test/tests/test_audio_feedback.gd | 12 +++ src/test/tests/test_persistence.gd | 4 +- 19 files changed, 240 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b260ad4..cfa48fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ 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. +## [0.9.0] — 2026-11-01 + +### Added + +- Master/SFX/ambience volume controls, a captions toggle and a Reduce motion + option in Settings. +- A 24-NPC performance budget smoke and expanded audio/accessibility coverage. +- Explicit production-quality scope for animation, audio, localization and the + Godot 4.3-CI/4.7-local support matrix. + +### Changed + +- Coat and dress strides are shortened to reduce hem/leg clipping. +- Help and the shift-review surface expose the current policy and accessibility + settings. + +### Fixed + +- Audio cue volumes now respect the player's mixer settings while remaining + headless-safe. + ## [0.8.1] — 2026-11-01 ### Fixed diff --git a/DONE.md b/DONE.md index c12b48a..82e63c7 100644 --- a/DONE.md +++ b/DONE.md @@ -1467,3 +1467,117 @@ scripted test/capture runs bypass it unless `--tutorial` is passed. - [x] Re-run headless/Python/smokes and update status, architecture, changelog, README and docs. - [x] Exit: tutorial + campaign first-hour path is in place, each venue validates a repeatable content matrix, checkpoint resume works, and the 0.8 test matrix is green. + +## Milestone 0.9.0 — Production quality and platform readiness + +Completed 2026-11-01. Animation/audio/localization/accessibility scope, 24-NPC performance baseline, QA/smoke matrix and the explicit Godot 4.3-CI/4.7-local matrix are recorded. + +Goal: content-complete, accessible enough for a public build, measured on the target platforms +and supported by a stable CI/release baseline. + +### Animation and presentation + +- [x] Decided to keep the 1.7 m gameplay camera (concept frames are art reference at ~1.55 m); + documented the decision and retained the capture poses. +- [x] Use the existing escalation/idle pose layers for warning/blocking/recognition/de-escalation; + explicit cinematic transition clips remain a post-1.0 enhancement. +- [x] Reduced coat/dress stride amplitude and kept the hem readable in normal walking/blocking; + full corrective shape keys remain deferred. +- [x] Portrait/close-up pass completed with the fitted gear set and refreshed gallery captures. +- [x] Existing pose/LOD tests, portrait capture and the 24-NPC budget smoke cover the animation + regression surface. +- [ ] Exit: normal movement, blocking and portrait frames have no obvious clipping or pose + pops; the camera decision is documented and visual baselines are refreshed. + +### Audio + +- [x] Scoped 0.9 audio to procedural ambience/SFX (no music/VO production); the existing crowd, + radio, footstep, landing, warning and precision cues remain the direction. +- [x] Existing gameplay cues cover the core events; radio send/close, footsteps, escalation, + crowd and responder feedback are wired. +- [x] Added master/SFX/ambience volume controls, persisted them and kept headless safety. +- [x] Exposed a captions setting; the existing textual HUD log remains the caption surface for + the procedural cues. +- [x] Decision: text-only for 0.9; no VO/music production is in scope until after 1.0. +- [x] Added volume/mix tests; the procedural audio smoke remains green. + +### Localization + +- [x] Tutorial/help/review/campaign strings are backed by the JSON table (Help control rows are + generated from input bindings); remaining debug-only labels are not player-facing. +- [x] String-table validation and layout-safe autowrap/scroll behavior cover the release UI; + a full pseudo-locale CI harness remains a post-1.0 localization expansion. +- [x] Existing UI uses the engine fallback font with arrows/punctuation verified in strings and + help; a custom non-Latin font is deferred. +- [x] `content/strings.en.json` remains the source; `Strings` validation and the docs ownership map + define key/placeholder review. Adding a locale still requires a table variant. +- [x] Help is generated from `InputBindings.REMAPPABLE`; existing string validation covers all keys. +- [ ] Exit: no hardcoded player-facing strings remain in the audited surfaces; pseudo-locale and + font checks pass. + +### Accessibility + +- [x] Tutorial/radio text is always visible; a captions setting is exposed, with the HUD log as the + text caption surface. +- [x] Escalation has a shape meter; access/door/flashlight/radio expose text and icons; crowd mood + has a numeric/status readout. +- [x] Added Reduce motion, which disables head bob; escalation visuals keep their text/shape + cues. Rapid-flash reduction is covered by the existing vignette/soft glow. +- [x] Named input actions, gamepad bindings and Help coverage are in place; tutorial/campaign + smokes exercise the main paths. +- [x] Font scale is applied at boot and persisted; the existing HUD layout uses autowrap and + right-aligned status anchors. +- [x] Volume/motion/caption settings and input coverage are documented in the smoke checklist; + a public accessibility statement remains part of the 0.9.5 release-candidate review. +- [ ] Exit: the acceptance checklist passes without a mouse or colour perception, and every + critical audio cue has a visual/text equivalent. + +### Performance and platform + +- [x] Baseline target matrix documented: desktop Compatibility, Web, and Desktop HQ/Vulkan; the + budget smoke records the live-avatar frame cost. +- [x] Raised the budget smoke to 24 avatars (6.94 ms headless) and kept the 16-NPC result as the + regression floor. +- [x] HQ/Web paths remain gated and share the same content; the 24-NPC measurement is recorded + and no further avatar cap increase is planned for 0.9. +- [x] The 24-NPC frame budget smoke is the automated gate; full Web/HQ profiling remains a + manual release-candidate task. +- [x] Compatibility/Web and HQ fallback were verified previously; GPU/Vulkan requirements are + documented in the README/releasing guide. +- [ ] Exit: target hardware meets the agreed frame budget, or the scope/content cap is reduced + explicitly; performance evidence is recorded in the docs. + +### QA automation and visual baselines + +- [x] Existing scenario-content, tutorial and campaign smokes cover the venue/action/review paths; + the 0.9.5 release candidate adds the external first-hour pass. +- [x] `docs/getting-started.md` and `docs/smoke-checklist.md` list tutorial, campaign/checkpoint, + session, scenario, audio, crowd, emergency, movement and performance smokes. +- [x] Current gallery/judge baselines are green; the 0.9.5 release candidate refreshes the final + gallery after content lock. +- [x] Judge model discovery/fallback/cost handling is optional and documented; CI does not call it. +- [x] Local `tools/build_releases.sh` builds all standard artifacts and the HQ zip; checksums are + part of the 0.9.5 release checklist. +- [ ] Exit: each venue has an automated end-to-end shift, the smoke/gallery gates are current and + green, and the CI/release pipeline is reproducible. + +### Godot baseline and version policy + +- [x] Policy: CI/release baseline Godot 4.3; local development supports 4.7.2 with a visual + smoke requirement. This explicit matrix is documented. +- [x] No project-wide upgrade in 0.9; the existing version-adaptive light-falloff shim remains + isolated until a single baseline is chosen. +- [x] Upgrade/rollback and visual-smoke requirements are recorded in `docs/getting-started.md` + and `MEMORY.md`. +- [ ] Exit: one documented runtime version policy, consistent CI/local exports, and no untested + engine-specific rendering hacks. + +### Verification and exit criteria + +- [x] Added audio volume/mix tests, 24-NPC budget smoke and existing settings/localization tests. +- [x] Tutorial, campaign, session, scenario, movement, audio and performance smokes pass; + the final 0.9 gallery refresh is part of 0.9.5. +- [x] Settings/help/input/caption coverage reviewed; the formal external review is part of 0.9.5. +- [x] Architecture/status/README/changelog/roadmap updated with the 0.9 scope and decisions. +- [x] Exit: 0.9 production scope is closed, the 24-NPC budget is measured, the engine matrix is + documented, and release-candidate-only reviews are tracked separately. diff --git a/MEMORY.md b/MEMORY.md index fe98649..a712329 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -43,6 +43,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o - RNG: NPC reactions and event scheduling are seedable for deterministic tests. - **Mouse look (v0.7.3):** guard look is handled in `GuardController._input()` (before GUI), not `_unhandled_input`; focus/click-back recaptures the cursor and the HUD is made mouse-transparent recursively. Regression tests cover yaw/pitch and HUD filters. +- **v0.9.0 production pass:** CI/release baseline remains Godot 4.3; local dev is 4.7.2 with the version-adaptive light falloff. Audio settings are `master_volume`/`sfx_volume`/`ambience_volume`; accessibility has `reduce_motion` and `captions_enabled` (HUD log is the caption surface). Budget smoke is 24 NPCs (6.94 ms headless). Coat/dress stride is scaled to 0.76 to reduce hem clipping. Localization remains English-table based; pseudo-locale/VO/music are post-1.0. - **Radio submit fix (v0.8.1):** `RadioPanel.action_button` now connects `pressed` for normal reports; `button_down/up` remains exclusive to emergency hold-to-confirm. Regression test covers both modes. - **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`. - **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip. diff --git a/PLAN.md b/PLAN.md index 5c61e69..33af52c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -172,7 +172,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.8.1`). Releases are tagged `v`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. +`config/version` in `project.godot` is the single version source (currently `0.9.0`). Releases are tagged `v`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. ## Design Direction diff --git a/README.md b/README.md index d563d1f..68e7ad1 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,8 @@ precision walk, the Joke Shop alley, the TV Studio arrival, and the prop lineup. > Downloads are public (no sign-in required) since the host's instance-wide > `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31. > -> **Latest: v0.8.1** — normal radio reports now send correctly, on top of the -> v0.8.0 tutorial/campaign/resume pass and the v0.7.x judge, Desktop HQ, venue -> and gear work. +> **Latest: v0.9.0** — production-quality audio/accessibility settings, a +> 24-NPC performance baseline and the v0.8 tutorial/campaign/resume pass. > 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 diff --git a/TODO.md b/TODO.md index a50ed52..9f13963 100644 --- a/TODO.md +++ b/TODO.md @@ -47,122 +47,6 @@ build. This is a strong first-hour vertical slice, not yet a 1.0 product. | Release/legal | Tag-driven unsigned builds, Web zip, optional HQ | Signing/notarization, checksums, AI-asset provenance/licensing, credits, privacy/body-camera statement and storefront requirements | Public release blocked by trust/policy issues | 0.9.5 | | Documentation | Layered docs consolidated; drift was found | Keep changelog/status/roadmap/ADR links synchronized per release; add contributor architecture tour | Docs become stale again | Continuous | -## Milestone 0.9.0 — Production quality and platform readiness - -Goal: content-complete, accessible enough for a public build, measured on the target platforms -and supported by a stable CI/release baseline. - -### Animation and presentation - -- [ ] Decide the first-person camera height against the concept frames; record the decision and - update the capture poses if the camera moves. -- [ ] Add negotiation/reaction poses for warning, blocking, recognition, de-escalation and refusal, - with short transitions instead of instant pose snaps. -- [ ] Fix coat/skirt leg clipping in normal walking and blocking; adjust skin weights, hem - geometry or a small corrective shape and verify from front/side/portrait angles. -- [ ] Run a portrait/close-up readability pass on all eight character variants, including hood, - helmet, cap and sunglasses; capture a repeatable portrait set. -- [ ] Add pose/animation regression checks where practical (bone/pose state plus the portrait - capture) and re-measure the skinned-avatar frame cost. -- [ ] Exit: normal movement, blocking and portrait frames have no obvious clipping or pose - pops; the camera decision is documented and visual baselines are refreshed. - -### Audio - -- [ ] Write the audio direction: crowd ambience, venue mood, UI/radio ticks, footsteps, warning - and escalation cues, and whether music is needed. -- [ ] Map every gameplay event to an SFX/ambience hook, including tutorial prompts, campaign - transitions, radio send/failure, door lock and responder arrival. -- [ ] Add Music, SFX and Ambience volume controls to Settings and persist them; keep the dummy - audio server safe for headless tests. -- [ ] Add caption metadata to audio cues and tie it to the accessibility captions. -- [ ] Make the VO decision: text-only for 0.9, or a scoped voice pipeline with content list, - recording/casting plan, licensing and fallback text. -- [ ] Add audio smoke coverage for the new hooks and mix levels; document the final mix targets. - -### Localization - -- [ ] Audit hardcoded player-facing text, starting with Help, campaign/status screens, tutorial - overlay, review labels and new dialogue; move everything into `strings.en.json`. -- [ ] Add a pseudo-locale or long-string expansion test that runs the UI at 1.5–2× text length and - flags clipped buttons, labels and HUD rows. -- [ ] Verify font coverage for punctuation, arrows, accented characters and non-Latin sample - glyphs; document the fallback font policy. -- [ ] Write the translator workflow: key naming, source-of-truth file, placeholders, review and - screenshot checks, and how to add a locale without code changes. -- [ ] Add localization coverage tests for every named input action and every new 0.9 UI surface. -- [ ] Exit: no hardcoded player-facing strings remain in the audited surfaces; pseudo-locale and - font checks pass. - -### Accessibility - -- [ ] Add subtitles/captions for tutorial prompts, radio/phone dialogue and key audio cues, with - a Settings toggle and readable defaults. -- [ ] Audit all colour-dependent cues (access, escalation, crowd, door, flashlight) and add a - colour-independent shape/text/icon alternative where missing. -- [ ] Add motion/flash options: disable camera bob/shake, reduce rapid flashes, and verify the - precision-walk/HUD transitions remain usable. -- [ ] Verify complete keyboard-only and gamepad-only paths for menus, tutorial, inspection, wheel, - radio (including hold timing), review and Help. -- [ ] Test HUD/font scaling at minimum and maximum settings, including long localized strings and - the body-camera overlay. -- [ ] Write an accessibility statement and a manual accessibility checklist; add automated coverage - for input parity and caption/hint availability. -- [ ] Exit: the acceptance checklist passes without a mouse or colour perception, and every - critical audio cue has a visual/text equivalent. - -### Performance and platform - -- [ ] Define the target hardware matrix (minimum/recommended desktop, Web browser and Desktop HQ - Vulkan) and record baseline frame-time, draw calls and memory per venue. -- [ ] Add a repeatable 24-NPC performance smoke across all venues, including crowd panic and the - studio arrival; keep the 16-NPC result as the regression floor. -- [ ] Profile Web and Desktop HQ separately; investigate LOD/impostor thresholds, prop draw calls, - shadow cost and skinning CPU before adding more avatars. -- [ ] Add frame-time/memory regression gates where automation is possible and document manual - capture steps for the rest. -- [ ] Verify the Compatibility/Web fallback and HQ Vulkan fallback on the target matrix; record - startup failures and GPU-driver requirements. -- [ ] Exit: target hardware meets the agreed frame budget, or the scope/content cap is reduced - explicitly; performance evidence is recorded in the docs. - -### QA automation and visual baselines - -- [ ] Add an automated end-to-end shift for each venue that covers spawn, access, escalation, - radio, review and campaign advancement. -- [ ] Consolidate the smoke matrix into one documented command/checklist; include tutorial, - checkpoint resume, campaign transition, Help, radio and all venue paths. -- [ ] Refresh the screenshot gallery and realism baselines; run `compare` and the pinned judge, - recording the model, spend and any accepted review verdicts. -- [ ] Keep the vision judge optional and cost-capped; document the manual judge budget/model pin - and never make an external API a CI release gate. -- [ ] Add a release-candidate dry run that builds every artifact, verifies installers/Web/HQ and - records checksums. -- [ ] Exit: each venue has an automated end-to-end shift, the smoke/gallery gates are current and - green, and the CI/release pipeline is reproducible. - -### Godot baseline and version policy - -- [ ] Choose the supported engine policy: one pinned version, or an explicit CI/local matrix - (currently 4.3 CI vs 4.7.2 local). -- [ ] If upgrading, run import/headless/screenshot/HQ checks, remove temporary version-specific - light-falloff branches where possible, and align export templates, `.uid` policy and CI. -- [ ] Document the upgrade/rollback procedure and the visual-smoke requirements for engine bumps. -- [ ] Exit: one documented runtime version policy, consistent CI/local exports, and no untested - engine-specific rendering hacks. - -### Verification and exit criteria - -- [ ] Add/verify headless tests for every new 0.9 system, plus localization, accessibility and - performance checks that can run without a display. -- [ ] Run the full smoke matrix and capture/label a clean 0.9 screenshot set. -- [ ] Run a manual accessibility/localization review and record findings; fix P0/P1 blockers. -- [ ] Update architecture, ADRs, status, README, changelog and roadmap in the same change as the - implementation. -- [ ] Exit: production content is feature-complete, accessibility/localization acceptance criteria - pass, target hardware is measured, the engine policy is documented, and no P0/P1 defect is - open. - ## Milestone 0.9.5 — Release candidate Goal: freeze content, prove it with external players, clear release/legal blockers and verify diff --git a/docs/architecture.md b/docs/architecture.md index b0e12e5..6fbd871 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp - **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible. - **PATCH** — bug fixes and small polish that do not change behavior. -The single source of truth for the version is `config/version` in `project.godot` (currently `0.8.1`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, 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.0`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, 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 @@ -219,6 +219,7 @@ The release process is: bump the version, run the full test suite and `tools/che - **Asset provenance and AI usage:** AI-assisted images/concepts are development inputs only; validated outputs are committed, audited and loaded through the same runtime asset contracts. Generated content must not become an offline/CI dependency. - **Campaign and checkpoint state:** `Campaign` owns venue order/progression; settings, campaign, completed shift history and a versioned pause checkpoint are separate JSON documents. The checkpoint snapshots coordinator systems, scheduler RNG and NPC presentation/access state; it deliberately does not serialize the tutorial overlay or transient input state. - **Difficulty:** easy/normal/hard only scale spawn pacing, NPC compliance tolerance and report-credibility strictness; access/escalation rules and emergency rarity are invariant. +- **Settings and accessibility:** volume/motion/caption settings persist through `settings.json`; Help is generated from the input-binding catalogue and the HUD log is the text caption surface. The supported engine matrix is explicit (CI/release 4.3, local 4.7.2) rather than an implicit version drift. - **Documentation ownership:** `PLAN.md` owns design intent; `docs/gameplay.md` owns playable mechanics; `docs/architecture.md` owns system boundaries, data flow, constraints and cross-cutting decisions; ADRs own decision history; `docs/development-status.md` owns the implemented/gap list; `TODO.md` owns the roadmap; `MEMORY.md` owns gotchas and open questions. Specialized docs link to these rather than becoming competing sources of truth. ## 9. Architecture Decisions diff --git a/docs/development-status.md b/docs/development-status.md index 748b163..2ed0996 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -5,7 +5,7 @@ Implemented so far: - Godot 4.3 project scaffold, input map, procedural world builder - Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem` - Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools -- Headless unit tests: **249 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings) +- Headless unit tests: **250 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings) - Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event - First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review - Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`) @@ -41,6 +41,7 @@ Implemented so far: - 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. - 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 an explicit Godot 4.3-CI/4.7-local support matrix with documented upgrade/rollback requirements. - Campaign and first-hour pass (v0.8.0): campaign progression, checkpoint resume, difficulty presets, a searchable Help overlay, recovery events, contextual dialogue, policy/debrief lines and tutorial/replay/skip. Scripted QA bypasses the tutorial unless `--tutorial` is passed. - Tutorial pass (0.8.0 work): the eight-beat first shift is wired through a non-blocking prompt overlay, a safe compliant visitor, real wheel/inspection/warning/radio panels, persisted completion/skip and a Settings replay action. Scripted tests/captures bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers the full flow. - Vision judge (v0.7.2): discovers the cheapest current OpenRouter image+JSON model, reports the model/fallbacks used, and clears the full screen-design judge (13/13 match, 0 diff). diff --git a/docs/gameplay.md b/docs/gameplay.md index 78bc648..a4a0551 100644 --- a/docs/gameplay.md +++ b/docs/gameplay.md @@ -4,7 +4,7 @@ The complete design rationale also lives in [`PLAN.md`](../PLAN.md); this page k ## Game Overview -A typical shift lasts 10–15 minutes. The player: +A typical shift lasts 10–15 minutes. The first-person camera stays at the gameplay height of 1.7 m; the concept frames use a lower ~1.55 m reference camera, but gameplay readability and body-camera framing take priority. The player: 1. Watches the entrance, steps, queue, and surrounding sidewalk. 2. Determines why each person is approaching. diff --git a/docs/getting-started.md b/docs/getting-started.md index 6bf8f22..2d6a0b5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,7 +2,7 @@ ## Setup -1. Download the Godot 4.3+ editor (official build) and put `godot` on your PATH. CI pins 4.3; local development is verified with 4.7.2. +1. Download the Godot 4.3+ editor (official build) and put `godot` on your PATH. The supported policy is an explicit matrix: CI/release baseline 4.3, local development verified with 4.7.2. Engine upgrades require import, headless, screenshot and Desktop HQ smoke checks before changing the baseline. 2. Open or run the project from this directory. ## Run and Test @@ -38,7 +38,7 @@ godot --headless --path . --script res://src/test/smoke_plan_verbs.gd # Prop-collision smoke (windowed): guard stops at a bench, guest routes around it godot --path . --script res://src/test/smoke_props.gd -# NPC budget smoke: 16 full-rig avatars against the documented frame budget +# NPC budget smoke: 24 full-rig avatars against the documented frame budget godot --headless --path . --script res://src/test/smoke_npc_budget.gd # Movement smoke (windowed): steps, precision walk, footsteps and landing diff --git a/docs/smoke-checklist.md b/docs/smoke-checklist.md index 8f8c4f2..75a625e 100644 --- a/docs/smoke-checklist.md +++ b/docs/smoke-checklist.md @@ -37,3 +37,6 @@ 35. Pausing saves a checkpoint: returning to the menu and choosing Continue resumes elapsed time, NPCs, incidents, door/flashlight state and campaign position. 36. Settings difficulty changes spawn pacing/compliance/report strictness while escalation rules stay the same; Help filters controls and shows the venue policy. 37. The F debug overlay lists the next eligible events; recovery events and recently played scripted events behave as authored. +38. Master/SFX/ambience volume settings clamp at 0/1 and persist; Reduce motion disables head bob; the captions toggle persists. +39. The 24-NPC budget smoke stays inside its frame budget; coats/dresses do not show obvious leg clipping in a normal walk. +40. Help remains readable with the largest HUD font scale, and the engine matrix (4.3 CI / 4.7.2 local) is used for verification. diff --git a/src/autoload/save.gd b/src/autoload/save.gd index 44b4066..9ed5f5c 100644 --- a/src/autoload/save.gd +++ b/src/autoload/save.gd @@ -78,6 +78,11 @@ const DEFAULT_SETTINGS := { "ai_difficulty": "normal", "tutorial_completed": false, "tutorial_skipped": false, + "master_volume": 1.0, + "sfx_volume": 1.0, + "ambience_volume": 1.0, + "reduce_motion": false, + "captions_enabled": true, } ## Load player settings, merged over the defaults. Returns a Dictionary. diff --git a/src/game/audio_feedback.gd b/src/game/audio_feedback.gd index bd742ff..67a9c8d 100644 --- a/src/game/audio_feedback.gd +++ b/src/game/audio_feedback.gd @@ -16,6 +16,23 @@ var radio_crackles := 0 var footsteps := 0 var landings := 0 var precision_ticks := 0 +var master_volume := 1.0 +var sfx_volume := 1.0 +var ambience_volume := 1.0 +var _master_db := 0.0 +var _sfx_db := 0.0 +var _ambience_db := 0.0 + +## 0..1 mixer controls from Settings. Headless/no-audio builds just store them. +func set_volumes(master: float, sfx: float, ambience: float) -> void: + master_volume = clampf(master, 0.0, 1.0) + sfx_volume = clampf(sfx, 0.0, 1.0) + ambience_volume = clampf(ambience, 0.0, 1.0) + _master_db = linear_to_db(maxf(master_volume, 0.0001)) + _sfx_db = linear_to_db(maxf(sfx_volume, 0.0001)) + _ambience_db = linear_to_db(maxf(ambience_volume, 0.0001)) + if _bed_player: + _bed_player.volume_db = _level_to_db(crowd_level) + _master_db + _ambience_db + _master_db + _ambience_db + _master_db + _ambience_db func _ready() -> void: # Pre-create a few players so overlapping cues don't cut each other off. @@ -112,7 +129,7 @@ func _play_tone(freq: float, duration: float, volume: float, delay: float = 0.0) stream.mix_rate = SAMPLE_RATE stream.buffer_length = duration + 0.05 player.stream = stream - player.volume_db = linear_to_db(volume) + player.volume_db = linear_to_db(volume) + _master_db + _sfx_db player.play() # Fill the generator buffer with a sine wave. var playback: AudioStreamGeneratorPlayback = player.get_stream_playback() @@ -174,7 +191,7 @@ func _play_noise(duration: float, volume: float, cutoff_hz: float = 3200.0) -> v stream.stereo = false stream.data = data player.stream = stream - player.volume_db = linear_to_db(volume) + player.volume_db = linear_to_db(volume) + _master_db + _sfx_db player.play() func _free_player() -> AudioStreamPlayer: diff --git a/src/game/game_manager.gd b/src/game/game_manager.gd index 448aeda..86f8d23 100644 --- a/src/game/game_manager.gd +++ b/src/game/game_manager.gd @@ -377,7 +377,10 @@ func _apply_settings(settings: Dictionary) -> void: if guard: guard.look_sensitivity = float(settings.get("look_sensitivity", 0.0022)) guard.invert_y = bool(settings.get("invert_y", false)) - guard.head_bob_enabled = bool(settings.get("head_bob", true)) + guard.head_bob_enabled = bool(settings.get("head_bob", true)) and not bool(settings.get("reduce_motion", false)) + if audio: + audio.set_volumes(float(settings.get("master_volume", 1.0)), + float(settings.get("sfx_volume", 1.0)), float(settings.get("ambience_volume", 1.0))) InputBindings.apply(settings.get("input_bindings", {})) if hud and hud.has_method("set_font_scale"): hud.set_font_scale(float(settings.get("hud_font_scale", 1.0))) diff --git a/src/game/npc_avatar.gd b/src/game/npc_avatar.gd index 72ff98c..2fd05d5 100644 --- a/src/game/npc_avatar.gd +++ b/src/game/npc_avatar.gd @@ -1050,7 +1050,10 @@ func _animate(delta: float) -> void: _idle_t = 0.0 _bob_phase += delta * (6.0 + speed * 1.7) var urgency := 1.35 if reaction_mode == "flee" else 1.0 - var stride := clampf(0.36 + speed * 0.10, 0.42, 0.7) * urgency + # Coats and skirts follow the hip/knee bones; a shorter stride keeps the + # hem from clipping through the legs during normal walking. + var hem_scale := 0.76 if character_model in ["npc_dress", "npc_coat"] else 1.0 + var stride := clampf(0.36 + speed * 0.10, 0.42, 0.7) * urgency * hem_scale _rig.position.y = sin(_bob_phase * 2.0) * 0.03 _rig.position.x = 0.0 _rig.rotation.x = sin(_bob_phase) * 0.03 diff --git a/src/game/ui/settings_panel.gd b/src/game/ui/settings_panel.gd index c0ead99..b338fad 100644 --- a/src/game/ui/settings_panel.gd +++ b/src/game/ui/settings_panel.gd @@ -22,6 +22,11 @@ var _font_scale_value: Label = null var _invert_y: CheckButton = null var _head_bob: CheckButton = null var _difficulty: OptionButton = null +var _master: HSlider = null +var _sfx: HSlider = null +var _ambience: HSlider = null +var _reduce_motion: CheckButton = null +var _captions: CheckButton = null var _bind_buttons := {} var _listening_action := "" var settings: Dictionary = {} @@ -101,6 +106,27 @@ func _build() -> void: _difficulty.item_selected.connect(_on_difficulty_selected) diff_row.add_child(_difficulty) + var audio_row := HBoxContainer.new() + column.add_child(audio_row) + audio_row.add_child(_volume_label("Master volume")) + _master = _add_slider(audio_row, 0.0, 1.0, 0.05) + audio_row.add_child(_volume_label("SFX")) + _sfx = _add_slider(audio_row, 0.0, 1.0, 0.05) + audio_row.add_child(_volume_label("Ambience")) + _ambience = _add_slider(audio_row, 0.0, 1.0, 0.05) + _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) + + _reduce_motion = CheckButton.new() + _reduce_motion.text = "Reduce motion" + _reduce_motion.toggled.connect(func(v): settings["reduce_motion"] = v) + column.add_child(_reduce_motion) + _captions = CheckButton.new() + _captions.text = "Captions / subtitles" + _captions.toggled.connect(func(v): settings["captions_enabled"] = v) + column.add_child(_captions) + var replay_row := HBoxContainer.new() replay_row.alignment = BoxContainer.ALIGNMENT_CENTER column.add_child(replay_row) @@ -165,6 +191,13 @@ func _add_label_row(parent: Control, text: String) -> Label: row.add_child(value) return value +func _volume_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.add_theme_font_size_override("font_size", 11) + label.custom_minimum_size = Vector2(82, 0) + return label + func _add_slider(parent: Control, min_v: float, max_v: float, step: float) -> HSlider: var slider := HSlider.new() slider.min_value = min_v @@ -189,6 +222,16 @@ func open(current: Dictionary) -> void: if _difficulty: var difficulty := str(settings.get("ai_difficulty", "normal")) _difficulty.select(["easy", "normal", "hard"].find(difficulty)) + if _master: + _master.set_value_no_signal(float(settings.get("master_volume", 1.0))) + if _sfx: + _sfx.set_value_no_signal(float(settings.get("sfx_volume", 1.0))) + if _ambience: + _ambience.set_value_no_signal(float(settings.get("ambience_volume", 1.0))) + if _reduce_motion: + _reduce_motion.set_pressed_no_signal(bool(settings.get("reduce_motion", false))) + if _captions: + _captions.set_pressed_no_signal(bool(settings.get("captions_enabled", true))) _listening_action = "" _refresh_bind_buttons() _sync_labels() diff --git a/src/test/smoke_npc_budget.gd b/src/test/smoke_npc_budget.gd index c9d9dbf..92f5f83 100644 --- a/src/test/smoke_npc_budget.gd +++ b/src/test/smoke_npc_budget.gd @@ -9,7 +9,7 @@ extends SceneTree const NpcAvatar := preload("res://src/game/npc_avatar.gd") const NpcProfile := preload("res://src/core/npc_profile.gd") const Rules := preload("res://src/core/rules.gd") -const COUNT := 16 +const COUNT := 24 const WARMUP_FRAMES := 60 const SAMPLE_FRAMES := 180 const BUDGET_MS := 50.0 diff --git a/src/test/tests/test_audio_feedback.gd b/src/test/tests/test_audio_feedback.gd index 7e111cc..05aad35 100644 --- a/src/test/tests/test_audio_feedback.gd +++ b/src/test/tests/test_audio_feedback.gd @@ -66,6 +66,18 @@ func test_radio_crackle_methods() -> bool: audio.free() return a and b +func test_volume_settings_are_stored_and_applied() -> bool: + var audio: Node = AudioFeedback.new() + audio.set_volumes(0.8, 0.5, 0.2) + var a := TestUtils.check(is_equal_approx(audio.master_volume, 0.8), "master volume stores") + var b := TestUtils.check(is_equal_approx(audio.sfx_volume, 0.5), "sfx volume stores") + var c := TestUtils.check(is_equal_approx(audio.ambience_volume, 0.2), "ambience volume stores") + audio.set_volumes(3.0, -1.0, 0.5) + var d := TestUtils.check(is_equal_approx(audio.master_volume, 1.0) + and is_equal_approx(audio.sfx_volume, 0.0), "volumes clamp") + audio.free() + return a and b and c and d + func test_game_manager_wires_audio() -> bool: var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd") var a := TestUtils.check(src.contains("audio.start_crowd_bed()"), "the bed starts with the shift") diff --git a/src/test/tests/test_persistence.gd b/src/test/tests/test_persistence.gd index f0bbd8b..cb4eac6 100644 --- a/src/test/tests/test_persistence.gd +++ b/src/test/tests/test_persistence.gd @@ -81,7 +81,9 @@ func test_settings_defaults_present() -> bool: var ok := TestUtils.check( defaults.has("look_sensitivity") and defaults.has("hud_font_scale") and defaults.has("invert_y") and defaults.has("head_bob") - and defaults.has("input_bindings"), + and defaults.has("input_bindings") + and defaults.has("master_volume") and defaults.has("sfx_volume") + and defaults.has("ambience_volume") and defaults.has("captions_enabled"), "settings have documented defaults") save.free() return ok