feat(client): #122 monologue display — multi-line, priority queue, 34 tests #49

Merged
jpmschweitzer merged 5 commits from client into main 2026-02-20 19:28:49 +01:00
Owner

Summary

  • #122 Monologue display — client: Full implementation of the internal monologue HUD display per D-016, D-032, D-055
  • Multi-line architecture: up to 3 simultaneous visible lines in a VBoxContainer, 5-deep priority queue with eviction
  • Character-specific text colours via GameState.lattice_profile (lattice_augmented = detective, lattice_baseline = smuggler)
  • Italic BBCode rendering, 0.15s stagger between fade-ins, standard/urgent opacity levels
  • main.gd passes priority + is_urgent from MonologueEvent; confrontation monologue uses priority 3 + urgent
  • Empty-text guard prevents ghost slots or queue pollution
  • Bottom-left positioning (420px wide, 80px above verb list)
  • 34 gdUnit4 tests: queue management, priority eviction, stagger timing, character colours, BBCode output, no-overwrite contract, empty-text guard, GameState integration (null/stale clearing, round-trip, non-dict rejection), D-049 canvas layer compliance

Files changed

  • client/ui/monologue_display.gd — rewritten: multi-line VBox, priority queue, lattice colour palette
  • client/ui/monologue_display.tscn — repositioned bottom-left, font size 13px
  • client/scripts/main.gd_consume_monologue() passes priority/is_urgent; confrontation uses priority 3
  • client/scripts/autoloads/game_state.gd — added lattice_profile field (parsed from snapshot)
  • client/tests/test_monologue_display.gd — 34 tests (new file)

Server dependency note

lattice_profile field ("lattice_augmented" or "lattice_baseline") should be added to ObserverSnapshot. Until then, defaults to "lattice_baseline" (smuggler palette) — no client error.

Test plan

  • Queue: FIFO drain, depth cap at 5, priority eviction
  • No-overwrite: mid-display lines not replaced (P0 #477)
  • Character colours: detective cool blue, smuggler warm amber, unknown fallback
  • Italic BBCode wrapping on all lines
  • Empty text guard: no ghost slots, no queue pollution, stagger untouched
  • GameState: null monologue = no display, round-trip preservation, non-dict rejection
  • D-049: monologue on CanvasLayer 20 (CANVAS_UI)
## Summary - **#122 Monologue display — client**: Full implementation of the internal monologue HUD display per D-016, D-032, D-055 - Multi-line architecture: up to 3 simultaneous visible lines in a VBoxContainer, 5-deep priority queue with eviction - Character-specific text colours via `GameState.lattice_profile` (`lattice_augmented` = detective, `lattice_baseline` = smuggler) - Italic BBCode rendering, 0.15s stagger between fade-ins, standard/urgent opacity levels - `main.gd` passes `priority` + `is_urgent` from MonologueEvent; confrontation monologue uses priority 3 + urgent - Empty-text guard prevents ghost slots or queue pollution - Bottom-left positioning (420px wide, 80px above verb list) - **34 gdUnit4 tests**: queue management, priority eviction, stagger timing, character colours, BBCode output, no-overwrite contract, empty-text guard, GameState integration (null/stale clearing, round-trip, non-dict rejection), D-049 canvas layer compliance ## Files changed - `client/ui/monologue_display.gd` — rewritten: multi-line VBox, priority queue, lattice colour palette - `client/ui/monologue_display.tscn` — repositioned bottom-left, font size 13px - `client/scripts/main.gd` — `_consume_monologue()` passes priority/is_urgent; confrontation uses priority 3 - `client/scripts/autoloads/game_state.gd` — added `lattice_profile` field (parsed from snapshot) - `client/tests/test_monologue_display.gd` — 34 tests (new file) ## Server dependency note `lattice_profile` field ("lattice_augmented" or "lattice_baseline") should be added to ObserverSnapshot. Until then, defaults to "lattice_baseline" (smuggler palette) — no client error. ## Test plan - [x] Queue: FIFO drain, depth cap at 5, priority eviction - [x] No-overwrite: mid-display lines not replaced (P0 #477) - [x] Character colours: detective cool blue, smuggler warm amber, unknown fallback - [x] Italic BBCode wrapping on all lines - [x] Empty text guard: no ghost slots, no queue pollution, stagger untouched - [x] GameState: null monologue = no display, round-trip preservation, non-dict rejection - [x] D-049: monologue on CanvasLayer 20 (CANVAS_UI)
jpmschweitzer added 4 commits 2026-02-20 18:53:05 +01:00
- Queue management: enqueue on mid-display arrival, drain in order, MAX_QUEUE_DEPTH=8 cap, no-overwrite contract (P0 #477)
- Character colours: detective=#c8e0ff (cool blue), smuggler=#f0c870 (warm amber), fallback to neutral insert text
- Typography: italic via BBCode [i] tags; font size 13px (smaller than dialogue)
- Positioning: bottom-left of viewport, 420×120px, 80px bottom margin above verb list
- main.gd: _get_active_character_type() reads player entity kind.data.character_type; passed to show_monologue() and _on_confrontation_monologue()
- gdUnit4 tests: queue order, depth cap, no-overwrite, BBCode output, colour mapping, fade timer integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
show_monologue() now returns early when text.is_empty(), preventing:
- ghost visible slots with blank labels
- stagger timer advancing on empty calls
- empty strings queuing when slots are full

Tests: add test_show_monologue_with_empty_text_does_not_set_displaying and
two companion cases (stagger timer unchanged, no enqueue when full) anticipating
Hoshe's QA additions to the test suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7 tests: snapshot round-trip, null/stale clearing, non-dict rejection,
duration/id preservation, canvas layer constant. before_test/after_test
lifecycle hooks prevent state bleed between test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR Review: client -> main (type: code)

Hoshe (Code Quality): REQUEST_CHANGES

Well-structured queue/priority/stagger architecture with solid test coverage. Several issues found.

# File Severity Issue
1 monologue_display.gd:~155 warning BBCode injection — server text interpolated raw into [i][color=...] without escaping [/]
2 monologue_display.gd:125 warning sort_custom() runs unconditionally including on the silent-drop path
3 monologue_display.tscn warning No clip_contents on container — wrapped long lines could overflow into dialogue box
4 main.gd:~195 warning _on_confrontation_monologue bypasses tick guard — no rate limiting
5 test_monologue_display.gd:~460 suggestion D-049 compliance test only checks constant, not scene tree parenting
6 monologue_display.gd suggestion No minimum duration clamp — very short durations expire during fade-in
7 test_monologue_display.gd:~20 suggestion _label_text helper has no bounds check on empty _visible

Tyre (Architecture): REQUEST_CHANGES

Core architecture is sound. Z-layer, lattice palette, and D-032 compliance correct. Two architectural concerns.

# File Severity Issue
1 monologue_display.gd:96-107 warning _build_line_node reads GameState.lattice_profile directly — hard autoload coupling violates D-020 renderer contract. Pass as parameter instead.
2 monologue_display.gd:126-141 warning Silent drop of equal-priority lines — strict > in eviction means default priority-2 lines get lost when queue is full. Should be >= or FIFO tiebreak.
3 test_monologue_display.gd:~460 suggestion D-049 compliance test doesn't verify actual scene tree attachment
4 monologue_display.gd:104-107 suggestion BBCode text not sanitized — future content pipeline risk

Verdict: CHANGES REQUESTED

## PR Review: client -> main (type: code) ### Hoshe (Code Quality): REQUEST_CHANGES Well-structured queue/priority/stagger architecture with solid test coverage. Several issues found. | # | File | Severity | Issue | |---|------|----------|-------| | 1 | `monologue_display.gd:~155` | warning | BBCode injection — server text interpolated raw into `[i][color=...]` without escaping `[`/`]` | | 2 | `monologue_display.gd:125` | warning | `sort_custom()` runs unconditionally including on the silent-drop path | | 3 | `monologue_display.tscn` | warning | No `clip_contents` on container — wrapped long lines could overflow into dialogue box | | 4 | `main.gd:~195` | warning | `_on_confrontation_monologue` bypasses tick guard — no rate limiting | | 5 | `test_monologue_display.gd:~460` | suggestion | D-049 compliance test only checks constant, not scene tree parenting | | 6 | `monologue_display.gd` | suggestion | No minimum duration clamp — very short durations expire during fade-in | | 7 | `test_monologue_display.gd:~20` | suggestion | `_label_text` helper has no bounds check on empty `_visible` | ### Tyre (Architecture): REQUEST_CHANGES Core architecture is sound. Z-layer, lattice palette, and D-032 compliance correct. Two architectural concerns. | # | File | Severity | Issue | |---|------|----------|-------| | 1 | `monologue_display.gd:96-107` | warning | `_build_line_node` reads `GameState.lattice_profile` directly — hard autoload coupling violates D-020 renderer contract. Pass as parameter instead. | | 2 | `monologue_display.gd:126-141` | warning | Silent drop of equal-priority lines — strict `>` in eviction means default priority-2 lines get lost when queue is full. Should be `>=` or FIFO tiebreak. | | 3 | `test_monologue_display.gd:~460` | suggestion | D-049 compliance test doesn't verify actual scene tree attachment | | 4 | `monologue_display.gd:104-107` | suggestion | BBCode text not sanitized — future content pipeline risk | ### Verdict: CHANGES REQUESTED
jpmschweitzer added 1 commit 2026-02-20 19:08:53 +01:00
Warnings fixed:
- BBCode injection (line 153): escape [ → [lb] in server text before interpolation
- sort_custom on silent-drop (line 125): sort now only runs on actual insertion/replacement
- clip_contents: add clip_contents=true to MonologueDisplay Control (overflow guard)
- confrontation tick guard (main.gd): _last_confrontation_tick deduplicates same-tick signals
- GameState decoupling: show_monologue() reads lattice_profile once and passes it through
  _show_line() → _build_line_node(); renderer no longer reaches into autoload (D-020)
- Equal-priority eviction: >= tiebreak (was >); FIFO for equal-priority queue overflow

Suggestions fixed:
- Minimum duration clamp: maxf(duration, FADE_IN_SEC + 0.1) — line survives own fade-in
- _label_text bounds check: guard against empty _visible before indexing [0]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpmschweitzer merged commit c10b896562 into main 2026-02-20 19:28:49 +01:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jpmschweitzer/settled-reach#49