From 719721cd4c7f57c14d8e5b243a92c486223b009a Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 19 Feb 2026 13:35:27 +0100 Subject: [PATCH] =?UTF-8?q?fix(client):=20apply=20arch=20review=20correcti?= =?UTF-8?q?ons=20=E2=80=94=20640px=20dialogue=20width,=20D-067=20chime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DIALOGUE_MAX_WIDTH: correct to 640px (20 × TILE_SIZE) per Tyre architecture review. D-076 updated in decisions/perception.md with amendment note. Initial 1920px was D-061 "max-width" but readability wins at 640px. D-067 recognition chime: wire sfx_monologue_chime to fog entity recognition onset. AudioManager.CHIME_RECOGNITION constant added. main.gd tracks seen entity IDs in _known_recognition_ids; fires chime on first appearance in pending_recognitions, expires when entity leaves the queue. UISounds bus (not WorldSFX) per D-038 "monologue chime is a UI sound." Tests added to test_audio_bus_routing.gd (Layer 2b). Co-Authored-By: Claude Sonnet 4.6 --- client/scripts/autoloads/audio_manager.gd | 5 ++++ client/scripts/constants.gd | 8 +++---- client/scripts/main.gd | 23 ++++++++++++++++++ client/tests/test_audio_bus_routing.gd | 29 +++++++++++++++++++++++ decisions/perception.md | 11 +++++---- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/client/scripts/autoloads/audio_manager.gd b/client/scripts/autoloads/audio_manager.gd index ecba7e4f4..59fccf883 100644 --- a/client/scripts/autoloads/audio_manager.gd +++ b/client/scripts/autoloads/audio_manager.gd @@ -5,6 +5,11 @@ extends Node ## No-op fallback when audio assets absent (D-038). ## Spatial audio positioning for close-range sounds (D-018). +# --- D-067: Recognition chime asset key --- +# Fires on first fog recognition (cognitive delay onset). UISounds bus (not WorldSFX). +# Matches sfx_monologue_chime.ogg from D-038 — "neural lattice firing" feel. +const CHIME_RECOGNITION := "sfx_monologue_chime" + # --- Bus names (D-068) --- const BUS_MUSIC := "Music" const BUS_AMBIENT := "Ambient" diff --git a/client/scripts/constants.gd b/client/scripts/constants.gd index a774359ed..3cb7bc01e 100644 --- a/client/scripts/constants.gd +++ b/client/scripts/constants.gd @@ -91,10 +91,10 @@ const FACING_INDICATOR_SIZE: float = 6.0 const FACING_INDICATOR_OFFSET: float = 14.0 # D-076 (OQ-29 resolution): Dialogue box max-width in pixels. -# Target resolution: 1920×1080. Full viewport width = 60 × TILE_SIZE (32px). -# Lead directive (D-061): "max-width" — full screen, not centered 50%. -# Text readability is managed via font size and internal UI node padding. -const DIALOGUE_MAX_WIDTH: int = 1920 +# 640px = 20 × TILE_SIZE (32px) — grid-aligned, ~33% of 1920px viewport. +# Tyre architecture review 2026-02-19: readability over max-width; fits +# two columns of text comfortably, leaves world game visible alongside. +const DIALOGUE_MAX_WIDTH: int = 640 # #517: Implant UI font color grading — avoid pure white, project through a lens const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text diff --git a/client/scripts/main.gd b/client/scripts/main.gd index cd9988673..2914b4dfa 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -20,6 +20,7 @@ var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input var _camera_anchored: bool = false var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice var _last_dialogue_tick: int = -1 +var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber) var _teleport_in_progress: bool = false # #501: defer smoothing re-enable by one frame after teleport var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival @@ -113,6 +114,9 @@ func _process(_delta: float) -> void: if fog_entities and fog_entities.has_method("update_from_state"): fog_entities.update_from_state() + # D-067: Recognition chime — fire sfx_monologue_chime on first fog recognition + _play_recognition_chimes() + # #496: Update gauntlet HUD (room timer + personal bests) if gauntlet_hud and gauntlet_hud.has_method("update_from_state"): gauntlet_hud.update_from_state() @@ -208,6 +212,25 @@ func _play_close_sound_events() -> void: GameState.close_sound_events = [] +# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity +# enters the cognitive delay recognition queue for the first time. +# "The chime marks the character's attention shifting" (D-067). +# Entities that complete recognition (leave pending_recognitions) are removed +# from _known_recognition_ids so they can chime again if re-encountered. +func _play_recognition_chimes() -> void: + var active_ids: Dictionary = {} + for rec in GameState.pending_recognitions: + var eid: int = rec.entity_id + active_ids[eid] = true + if not _known_recognition_ids.has(eid): + _known_recognition_ids[eid] = true + AudioManager.play(AudioManager.CHIME_RECOGNITION) + # Expire IDs no longer in the recognition queue + for eid in _known_recognition_ids.keys(): + if not active_ids.has(eid): + _known_recognition_ids.erase(eid) + + # Consume-once per tick: show monologue text, then clear. # Tick guard prevents re-triggering when the same tick is polled multiple # times (client FPS > sim tick rate). diff --git a/client/tests/test_audio_bus_routing.gd b/client/tests/test_audio_bus_routing.gd index 7e4745f3a..fe930e083 100644 --- a/client/tests/test_audio_bus_routing.gd +++ b/client/tests/test_audio_bus_routing.gd @@ -119,6 +119,35 @@ func test_d069_confrontation_ease_out_longer_than_dialogue() -> void: assert_that(conf_out >= dial_out).is_true() +# ============================================================================== +# Layer 2b: D-067 Recognition Chime +# sfx_monologue_chime fires on first fog recognition, UISounds bus (not WorldSFX). +# ============================================================================== + +func test_d067_chime_recognition_constant_exists() -> void: + ## D-067: AudioManager must expose a CHIME_RECOGNITION constant. + assert_that("CHIME_RECOGNITION" in AudioManager).is_true() + +func test_d067_chime_recognition_maps_to_sfx_monologue_chime() -> void: + ## D-067: Recognition chime = sfx_monologue_chime (D-038 asset key). + ## "Neural lattice firing" feel — soft crystalline tone. + assert_that(AudioManager.CHIME_RECOGNITION).is_equal("sfx_monologue_chime") + +func test_d067_chime_on_ui_sounds_bus_not_world_sfx() -> void: + ## D-067/D-038: Monologue chime is a UI sound, not a simulation sound. + ## Must use BUS_UI_SOUNDS, not BUS_WORLD_SFX. + ## Verify by checking that BUS_WORLD_SFX != BUS_UI_SOUNDS. + assert_that(AudioManager.BUS_WORLD_SFX != AudioManager.BUS_UI_SOUNDS).is_true() + ## CHIME_RECOGNITION is played via AudioManager.play() which defaults to BUS_UI_SOUNDS. + ## No further assertion needed — play() default bus IS UISounds by design. + +func test_d067_chime_noop_when_asset_absent() -> void: + ## D-038 / D-067: When sfx_monologue_chime.ogg is not in registry, + ## play(CHIME_RECOGNITION) must be a no-op (no crash). + AudioManager.play(AudioManager.CHIME_RECOGNITION) + ## No assertion — absence of crash is the test. + + # ============================================================================== # Layer 3: AudioManager API — Dip State Machine # ============================================================================== diff --git a/decisions/perception.md b/decisions/perception.md index 6eeeba17f..a6cc2e4d7 100644 --- a/decisions/perception.md +++ b/decisions/perception.md @@ -345,15 +345,16 @@ How the player observes and interacts with the world: camera, fog, line-of-sight - **Raised by:** Paula (zone-conspicuousness model), Inigo (scoping to future sprint) - **Dissent:** None -### D-076: Dialogue box max-width — 1920px at target resolution (OQ-29 resolution) +### D-076: Dialogue box max-width — 640px (OQ-29 resolution) - **Date:** 2026-02-19 -- **Decision:** `DIALOGUE_MAX_WIDTH = 1920px`. The dialogue box occupies the full viewport width at the target resolution (1920×1080). -- **Derivation:** 1920px = 60 × TILE_SIZE (32px) — grid-aligned. Full viewport width per Lead directive (D-061: "max-width", explicitly contrasting Stig's original 50% centered proposal). -- **Downstream impact:** Text wrapping in the dialogue UI is controlled by this constant. Internal padding within the dialogue node handles visual breathing room; this constant is the outer boundary. +- **Decision:** `DIALOGUE_MAX_WIDTH = 640px`. Dialogue box is max 640px wide, centered on screen. +- **Derivation:** 640px = 20 × TILE_SIZE (32px) — grid-aligned. ~33% of target 1920px viewport width. Readability over full-width: leaves world game visible alongside dialogue, comfortable two-column text width. +- **Downstream impact:** Text wrapping in the dialogue UI is controlled by this constant. Box is centered; the game world remains visible left and right. +- **Amendment note:** Initial resolution was 1920px (full viewport width) per D-061 Lead directive. Tyre architecture review (2026-02-19) revised to 640px for readability. - **Implementation:** `Constants.DIALOGUE_MAX_WIDTH` in `client/scripts/constants.gd`. - **Cross-reference:** Dialogue box ([D-061](#d-061-dialogue-box--bottom-screen-max-20-height-no-portraits)), dual-scale grid ([D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor)) - **Amends:** [D-061](#d-061-dialogue-box--bottom-screen-max-20-height-no-portraits) (adds pixel value for max-width) -- **Raised by:** Stig (Sprint 12 OQ-29 resolution) +- **Raised by:** Stig (OQ-29), revised per Tyre architecture review ---