diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd index 78866ba2f..5b852d539 100644 --- a/client/scripts/autoloads/game_state.gd +++ b/client/scripts/autoloads/game_state.gd @@ -40,6 +40,15 @@ var dialogue_active: bool = false var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag +# OQ-07 (#522): Insert active state — false suppresses verb labels (z-layer 6). +# Cursor shape changes still fire when false (D-056 option a). +# v0.1: always true (both detective and smuggler have inserts). Server may send false in future. +var insert_active: bool = true + +# #507: RNG seed for replay determinism — populated from snapshot "rng_seed" field. +# Null in v0.1 (server does not yet send this field; protocol change required). +var rng_seed: Variant = null + # v7 fields (#431, D-059/D-060) var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}] @@ -132,6 +141,20 @@ func apply_snapshot(snapshot: Dictionary) -> void: else: room_id = null + # OQ-07 (#522): insert_active — defaults true (v0.1 always has insert). + # Server may send false for characters without an insert in future sprints. + if snapshot.has("insert_active") and snapshot.insert_active is bool: + insert_active = snapshot.insert_active + else: + insert_active = true + + # #507: rng_seed — server sends current RNG seed for replay determinism. + # Field: "rng_seed" (u64 as integer). Null if server does not include it. + if snapshot.has("rng_seed"): + rng_seed = snapshot.rng_seed + else: + rng_seed = null + # v2: visible_tiles with visibility sectors # Derives visible_positions when not explicitly provided (real server mode) if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0: diff --git a/client/scripts/rendering/cursor_renderer.gd b/client/scripts/rendering/cursor_renderer.gd index a68036ff9..690f05d46 100644 --- a/client/scripts/rendering/cursor_renderer.gd +++ b/client/scripts/rendering/cursor_renderer.gd @@ -11,6 +11,9 @@ enum State { DEFAULT, ENTITY_HOVER, OBJECT_HOVER, WEAPON_AIM } var current_state: State = State.DEFAULT var hovered_entity_id: int = -1 var weapon_mode_active: bool = false +# OQ-07 (#522): when false, verb labels are suppressed (should_show_interactions → false). +# Cursor shape transitions still fire — the character's body still orients to targets. +var insert_active: bool = true signal state_changed(new_state: State) signal hovered_entity_changed(entity_id: int) @@ -305,8 +308,15 @@ func get_z_layer() -> int: func should_show_interactions() -> bool: + # OQ-07: insert off suppresses verb labels even though cursor shape still changes + if not insert_active: + return false return not weapon_mode_active or _shift_held +func set_insert_active(active: bool) -> void: + insert_active = active + + func get_interaction_range() -> int: return 2 # D-056: ~2 sim tiles diff --git a/client/tests/test_cursor_states.gd b/client/tests/test_cursor_states.gd index 2b5358127..726be1b72 100644 --- a/client/tests/test_cursor_states.gd +++ b/client/tests/test_cursor_states.gd @@ -305,6 +305,46 @@ func test_cursor_changes_require_los() -> void: cursor.queue_free() +# -- OQ-07: Insert-off behavior (D-056 amendment, #522) ---------------------- +# Option (a): cursor shape still changes, verb labels suppressed. + +func test_insert_off_cursor_still_changes_shape() -> void: + # OQ-07 option (a): cursor state machine still fires when insert is off. + # The character's body orients toward targets even without insert data. + var cursor = _make_cursor_or_skip() + if cursor == null: + return + if cursor.has_method("set_insert_active") and cursor.has_method("set_hover_target"): + cursor.set_insert_active(false) + cursor.set_hover_target({"entity_id": 2, "kind": "Npc", "relationship": "Unknown"}) + assert_that(str(cursor.get_state())).is_equal("EntityHover") + cursor.queue_free() + + +func test_insert_off_suppresses_interactions() -> void: + # OQ-07 option (a): with insert off, should_show_interactions() returns false. + # Verb labels (z-layer 6) are suppressed — no actionable insert data. + var cursor = _make_cursor_or_skip() + if cursor == null: + return + if cursor.has_method("set_insert_active") and cursor.has_method("should_show_interactions"): + cursor.set_insert_active(false) + assert_that(cursor.should_show_interactions()).is_false() + cursor.queue_free() + + +func test_insert_on_restores_interaction_display() -> void: + # Re-enabling insert allows interactions to show again. + var cursor = _make_cursor_or_skip() + if cursor == null: + return + if cursor.has_method("set_insert_active") and cursor.has_method("should_show_interactions"): + cursor.set_insert_active(false) + cursor.set_insert_active(true) + assert_that(cursor.should_show_interactions()).is_true() + cursor.queue_free() + + # -- Interaction range (D-056) ------------------------------------------------ func test_click_interaction_range() -> void: diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd index dfa569d0c..2fd9152cc 100644 --- a/client/ui/interaction_prompt.gd +++ b/client/ui/interaction_prompt.gd @@ -13,6 +13,8 @@ extends PanelContainer var _is_showing: bool = false var _active_tween: Tween = null var _current_target_id: int = -1 +# OQ-07 (#522): when false, prompt is suppressed (z-layer 6 insert overlay only) +var _insert_active: bool = true const FADE_IN: float = 0.15 const FADE_OUT: float = 0.15 @@ -23,6 +25,11 @@ func _ready() -> void: _is_showing = false func _process(_delta: float) -> void: + # OQ-07: insert off means no verb labels (z-layer 6 insert overlay suppressed) + if not _insert_active: + if _is_showing: + _hide_prompt() + return var interactions: Array = GameState.nearby_interactions if interactions.size() > 0: _show_prompt(interactions[0]) @@ -69,6 +76,14 @@ func _hide_prompt() -> void: func get_interaction_target() -> int: return _current_target_id +## OQ-07 (#522): insert off hides prompt (diegetic: no insert data on z-layer 6). +## Cursor shape changes still fire on cursor_renderer.gd. +func set_insert_active(active: bool) -> void: + _insert_active = active + if not active and _is_showing: + _hide_prompt() + + ## Returns the selected verb kind (v0.1: first verb on nearest, v0.2: radial selection). func get_selected_verb() -> String: var interactions: Array = GameState.nearby_interactions diff --git a/decisions/perception.md b/decisions/perception.md index 965e4b907..10af01f29 100644 --- a/decisions/perception.md +++ b/decisions/perception.md @@ -212,6 +212,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight - **Source:** Control & Interaction Workshop (2026-02-13) - **Raised by:** Araminta (visual spec), Stig (UX rules + diegetic test), Ozzie (weapon suppression) - **Dissent:** None. +- **OQ-07 resolution (2026-02-19, #522):** Insert-off behavior is **option (a): cursor shape still changes, verb labels suppressed.** The cursor state machine fires normally (entity hover → bracket shape, object hover → X-shape) — the character's body physically orients toward targets as a subconscious/spatial response. But the insert does not process these into actionable data: `should_show_interactions()` returns false when `insert_active == false`, and interaction labels (z-layer 6) are hidden via `set_insert_active(false)` on `InteractionList` and `InteractionPrompt`. `GameState.insert_active` is the source of truth (defaults true in v0.1; wired from snapshot field `insert_active`). Rationale: diegetically consistent — the body reacts to proximity; the insert reacts to commands. ### D-057: Entity interaction — vertical list, insert-styled - **Date:** 2026-02-13 @@ -224,6 +225,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight - **Source:** Control & Interaction Workshop (2026-02-13) - **Raised by:** Stig (vertical list structure + diegetic test), Araminta (insert aesthetic), Dudley (two-phase verb computation), Nigel (character-archetype verb sets). Lead resolved: Stig's structure, Araminta's styling. - **Dissent:** Araminta argued for spoke radial (geometry transformation signals qualitative knowledge change — new spoke growing). Lead rejected: items moving under cursor when knowledge changes is a moving goalpost (bad UX while aiming at an option). +- **OQ-07 resolution (2026-02-19, #522):** When `insert_active == false`, the interaction list hides completely (`set_insert_active(false)` → `_hide()`). Cursor shape changes still occur per D-056 OQ-07 — the list suppression is independent of cursor state. `GameState.insert_active` drives this at runtime, wired via `main.gd` on each snapshot. ### D-058: World menu — radial, 4 spokes - **Date:** 2026-02-13 @@ -338,4 +340,4 @@ How the player observes and interacts with the world: camera, fog, line-of-sight --- -*29 decisions. Last updated: 2026-02-16* +*29 decisions. Last updated: 2026-02-19 (OQ-07 resolved: D-056/D-057 amendment)*