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
+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