refactor(ui): monologue display — multi-line architecture per Tyre review (#122)

Rewrites the monologue display system per Tyre architecture review (Sprint 14):

Rendering:
- Up to 3 simultaneous visible lines (VBoxContainer, dynamic node creation)
- Lines created programmatically as MarginContainer > RichTextLabel per slot
- Percentage-based anchors: 5% left, 75–98% vertical (25% area from bottom), 50% max width
- Z-layer 7 in UILayer (CanvasLayer 20, D-049)

Queue:
- 5-entry priority queue; highest priority drains first
- On overflow: incoming line replaces lowest-priority queued entry if it outranks it
- Lower/equal priority incoming lines silently dropped when queue full

API:
  show_monologue(text, duration, priority=2, is_urgent=false)
- Replaces old (text, duration, character_type) signature
- main.gd passes priority and is_urgent from MonologueEvent fields
- Confrontation monologue: priority=3, is_urgent=true (D-063)

Colour:
- Reads GameState.lattice_profile at render time (D-032)
- lattice_augmented (detective): standard #d0d4e0 / urgent #e0e8f8
- lattice_baseline (smuggler): standard #d8d0c4 / urgent #f0e4d4
- Fallback for unknown profiles; no crash

Stagger: 0.15s between consecutive fade-ins (spec §5.4)
Opacity: standard 0.85, urgent 1.0; bloom deferred

GameState: adds lattice_profile field, parsed from snapshot

Tests: 27 gdUnit4 test cases — queue order, priority drop, overflow, stagger,
no-overwrite (P0 #477), BBCode output, palette selection, slot lifecycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 18:39:49 +01:00
co-authored by Claude Sonnet 4.6
parent 4365871846
commit 1765e2922f
5 changed files with 406 additions and 277 deletions
+10 -1
View File
@@ -23,7 +23,12 @@ var player_entity_id: int = 1
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
# v5 fields (#414)
var current_monologue: Variant = null # {id, text, duration_seconds} or null
var current_monologue: Variant = null # {id, text, duration_seconds, priority, is_urgent} or null
# #122 (D-032): Character lattice profile — selects monologue text colour palette.
# "lattice_augmented" = detective, "lattice_baseline" = smuggler.
# Server sends this field as part of the player's capability snapshot.
var lattice_profile: String = "lattice_baseline"
# v6 fields (#449, D-053, D-065)
var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
@@ -138,6 +143,10 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
current_monologue = null
# #122: lattice_profile — character insert capability level for monologue colour
if snapshot.has("lattice_profile") and snapshot.lattice_profile is String:
lattice_profile = snapshot.lattice_profile
# v6: player_stance (#449, D-053)
if snapshot.has("player_stance") and snapshot.player_stance is String:
player_stance = snapshot.player_stance
+4 -17
View File
@@ -291,7 +291,8 @@ func _consume_monologue() -> void:
monologue_display.show_monologue(
mono.get("text", ""),
mono.get("duration_seconds", 5.0),
_get_active_character_type()
mono.get("priority", 2),
mono.get("is_urgent", false)
)
# #502: Amber flash on room reset
var mono_id: String = mono.get("id", "")
@@ -300,21 +301,6 @@ func _consume_monologue() -> void:
GameState.current_monologue = null
# Derive the active character type from the player entity kind data.
# Server populates kind.data.character_type ("detective" or "smuggler") per D-032.
# Returns "" when the field is absent (display falls back to neutral colour).
func _get_active_character_type() -> String:
for entity in GameState.visible_entities:
if not entity is Dictionary:
continue
var kind = entity.get("kind", {})
if not kind is Dictionary or kind.get("variant") != "Player":
continue
var data = kind.get("data", {})
if data is Dictionary:
return data.get("character_type", "")
return ""
# Consume-once per tick with ID tracking: show dialogue, then clear.
# Tick guard + is_dialogue_active check prevent re-triggering.
@@ -351,9 +337,10 @@ func _on_dialogue_option_selected(response_id: String, text: String) -> void:
# D-063: Handle confrontation beat monologue → show on monologue display (layer 7)
# Confrontation lines are high-priority (3) and urgent — full opacity, elevated colour.
func _on_confrontation_monologue(text: String, duration: float) -> void:
if monologue_display:
monologue_display.show_monologue(text, duration, _get_active_character_type())
monologue_display.show_monologue(text, duration, 3, true)
# D-064: Handle walk-away → send WalkAway{npc_id} to server