feat(client): resolve OQ-07 — insert-off suppresses verb labels (#522)

Option (a): cursor shape still changes (body orients to targets), but
verb labels and interaction prompts are suppressed when insert_active
is false. Amends D-056 and D-057 with resolution note.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 12:05:36 +01:00
co-authored by Claude Opus 4.6
parent 6f9a53cb1f
commit 931399f248
5 changed files with 91 additions and 1 deletions
+23
View File
@@ -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:
@@ -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
+40
View File
@@ -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:
+15
View File
@@ -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
+3 -1
View File
@@ -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)*