## #122: Monologue display — client tests (Sprint 14) ## Covers queue management, priority logic, stagger, colour palette, BBCode output, ## no-overwrite contract (P0 #477), and per-slot fade lifecycle. ## ## API per Tyre architecture review: ## show_monologue(text, duration, priority=2, is_urgent=false) ## GameState.lattice_profile selects colour palette class_name TestMonologueDisplay extends GdUnitTestSuite # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- func _make_display() -> Node: if not ResourceLoader.exists("res://ui/monologue_display.tscn"): push_warning("TestMonologueDisplay: scene not found — tests skipped") return null var node = load("res://ui/monologue_display.tscn").instantiate() add_child(node) return node func _label_text(d: Node) -> String: var slot_node: Node = d._visible[0].node return (slot_node.get_child(0) as RichTextLabel).text # --------------------------------------------------------------------------- # Initial state # --------------------------------------------------------------------------- func test_nothing_visible_on_init() -> void: var d = _make_display() if d == null: return assert_int(d._visible.size()).is_equal(0) assert_int(d._queue.size()).is_equal(0) d.queue_free() func test_stagger_timer_zero_on_init() -> void: var d = _make_display() if d == null: return assert_float(d._next_fade_in_msec).is_equal(0.0) d.queue_free() # --------------------------------------------------------------------------- # Single-line display # --------------------------------------------------------------------------- func test_single_line_goes_to_visible() -> void: var d = _make_display() if d == null: return d.show_monologue("One.", 5.0) assert_int(d._visible.size()).is_equal(1) assert_int(d._queue.size()).is_equal(0) d.queue_free() func test_show_monologue_sets_stagger_timer() -> void: var d = _make_display() if d == null: return var before := float(Time.get_ticks_msec()) d.show_monologue("Stagger.", 5.0) assert_float(d._next_fade_in_msec).is_greater(before) d.queue_free() # --------------------------------------------------------------------------- # MAX_VISIBLE = 3 simultaneous lines # --------------------------------------------------------------------------- func test_three_lines_all_visible() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) assert_int(d._visible.size()).is_equal(3) assert_int(d._queue.size()).is_equal(0) d.queue_free() func test_fourth_line_queues_when_slots_full() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) d.show_monologue("D", 5.0) assert_int(d._visible.size()).is_equal(3) assert_int(d._queue.size()).is_equal(1) d.queue_free() # --------------------------------------------------------------------------- # No-overwrite contract (P0, #477) # --------------------------------------------------------------------------- func test_new_line_does_not_replace_first_visible_line() -> void: var d = _make_display() if d == null: return d.show_monologue("First line.", 10.0) var first_text := _label_text(d) # Force stagger active; second call must queue, not display d._next_fade_in_msec = float(Time.get_ticks_msec()) + 10000.0 d.show_monologue("Second line.", 5.0) assert_that(_label_text(d)).is_equal(first_text) d.queue_free() func test_two_visible_lines_coexist_without_overwriting() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("Alpha", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("Beta", 10.0) assert_int(d._visible.size()).is_equal(2) d.queue_free() # --------------------------------------------------------------------------- # Priority queue # --------------------------------------------------------------------------- func test_queue_sorted_highest_priority_first() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) d.show_monologue("low", 5.0, 1) d.show_monologue("high", 5.0, 4) d.show_monologue("normal", 5.0, 2) assert_int(d._queue[0].priority).is_equal(4) d.queue_free() func test_queue_drop_replaces_lowest_when_full() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) for i in range(d.MAX_QUEUE): d.show_monologue("low_%d" % i, 5.0, 1) d.show_monologue("critical!", 5.0, 9) assert_int(d._queue.size()).is_equal(d.MAX_QUEUE) var has_critical := false for e in d._queue: if e.priority == 9: has_critical = true assert_that(has_critical).is_true() d.queue_free() func test_queue_ignores_lower_priority_when_full() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) for i in range(d.MAX_QUEUE): d.show_monologue("hi_%d" % i, 5.0, 5) d.show_monologue("noise", 1.0, 1) assert_int(d._queue.size()).is_equal(d.MAX_QUEUE) for e in d._queue: assert_int(e.priority).is_equal(5) d.queue_free() func test_queue_never_exceeds_max_depth() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) for i in range(d.MAX_QUEUE + 20): d.show_monologue("flood_%d" % i, 1.0, 2) assert_int(d._queue.size()).is_less_or_equal(d.MAX_QUEUE) d.queue_free() # --------------------------------------------------------------------------- # Expire and drain # --------------------------------------------------------------------------- func test_expired_slot_removed_from_visible() -> void: var d = _make_display() if d == null: return d.show_monologue("Expires.", 1.0) d._visible[0].expire_timer = -0.1 d._process(0.0) assert_int(d._visible.size()).is_equal(0) d.queue_free() func test_queue_drains_when_slot_opens() -> void: var d = _make_display() if d == null: return d._next_fade_in_msec = 0.0; d.show_monologue("A", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("B", 10.0) d._next_fade_in_msec = 0.0; d.show_monologue("C", 10.0) d.show_monologue("Queued.", 5.0) d._visible[0].expire_timer = -0.1 d._next_fade_in_msec = 0.0 # stagger elapsed d._process(0.0) assert_int(d._queue.size()).is_equal(0) assert_int(d._visible.size()).is_equal(3) d.queue_free() # --------------------------------------------------------------------------- # Stagger # --------------------------------------------------------------------------- func test_second_call_within_stagger_period_queues() -> void: var d = _make_display() if d == null: return d.show_monologue("First.", 5.0) # _next_fade_in_msec is now ~150ms in the future d.show_monologue("Second.", 5.0) assert_int(d._visible.size()).is_equal(1) assert_int(d._queue.size()).is_equal(1) d.queue_free() func test_stagger_elapsed_allows_second_visible() -> void: var d = _make_display() if d == null: return d.show_monologue("First.", 5.0) d._next_fade_in_msec = 0.0 # manually expire stagger d.show_monologue("Second.", 5.0) assert_int(d._visible.size()).is_equal(2) assert_int(d._queue.size()).is_equal(0) d.queue_free() # --------------------------------------------------------------------------- # BBCode output # --------------------------------------------------------------------------- func test_text_wrapped_in_italic_bbcode() -> void: var d = _make_display() if d == null: return d.show_monologue("Italic line.", 5.0) var txt := _label_text(d) assert_that(txt).contains("[i]") assert_that(txt).contains("[/i]") assert_that(txt).contains("Italic line.") d.queue_free() func test_text_has_color_bbcode() -> void: var d = _make_display() if d == null: return d.show_monologue("Coloured.", 5.0) var txt := _label_text(d) assert_that(txt).contains("[color=#") assert_that(txt).contains("[/color]") d.queue_free() # --------------------------------------------------------------------------- # Lattice colour palette # --------------------------------------------------------------------------- func test_augmented_colour_differs_from_baseline() -> void: var d = _make_display() if d == null: return GameState.lattice_profile = "lattice_augmented" d.show_monologue("Detective.", 5.0) var aug_txt := _label_text(d) d._visible[0].expire_timer = -0.1; d._process(0.0) d._next_fade_in_msec = 0.0 GameState.lattice_profile = "lattice_baseline" d.show_monologue("Smuggler.", 5.0) var base_txt := _label_text(d) assert_that(aug_txt).is_not_equal(base_txt) GameState.lattice_profile = "lattice_baseline" d.queue_free() func test_urgent_colour_differs_from_standard() -> void: var d = _make_display() if d == null: return GameState.lattice_profile = "lattice_baseline" d.show_monologue("Normal.", 5.0, 2, false) var std_txt := _label_text(d) d._visible[0].expire_timer = -0.1; d._process(0.0) d._next_fade_in_msec = 0.0 d.show_monologue("Urgent!", 5.0, 3, true) var urg_txt := _label_text(d) assert_that(std_txt).is_not_equal(urg_txt) d.queue_free() func test_unknown_profile_falls_back_without_crash() -> void: var d = _make_display() if d == null: return GameState.lattice_profile = "lattice_hypothetical_tier_x" d.show_monologue("Future proof.", 5.0) var txt := _label_text(d) assert_that(txt).contains("[color=#") # fallback colour applied, no crash GameState.lattice_profile = "lattice_baseline" d.queue_free() # --------------------------------------------------------------------------- # Slot lifecycle # --------------------------------------------------------------------------- func test_visible_slot_has_tween() -> void: var d = _make_display() if d == null: return d.show_monologue("Has tween.", 5.0) assert_that(d._visible[0].tween).is_not_null() d.queue_free() func test_visible_slot_stores_priority() -> void: var d = _make_display() if d == null: return d.show_monologue("Priority 7.", 5.0, 7) assert_int(d._visible[0].priority).is_equal(7) d.queue_free() func test_expire_timer_decrements_in_process() -> void: var d = _make_display() if d == null: return d.show_monologue("Timer.", 10.0) d._process(1.5) assert_float(d._visible[0].expire_timer).is_less(10.0) d.queue_free()