- 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>
254 lines
7.3 KiB
GDScript
254 lines
7.3 KiB
GDScript
## #122: Monologue display — client tests (Sprint 14)
|
|
## Tests queue management, character colour, italic BBCode, and no-overwrite contract.
|
|
## Spec: Sprint 14 briefing (D-016, D-032, D-055, P0 #477).
|
|
##
|
|
## Approach: instantiate the scene, drive show_monologue() directly, inspect internal
|
|
## state and label text. Fade timing is tested by manipulating _fade_timer and calling
|
|
## _process() rather than awaiting real time — keeps the suite fast and deterministic.
|
|
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 scene = load("res://ui/monologue_display.tscn")
|
|
var node = scene.instantiate()
|
|
add_child(node)
|
|
# _ready() fires here; panel.modulate.a = 0, _displaying = false
|
|
return node
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Colour mapping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_detective_colour_is_cool_blue() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
var c: Color = d._color_for_character("detective")
|
|
# Must not be the default/neutral colour
|
|
assert_that(c).is_not_equal(d.COLOR_DEFAULT)
|
|
# Blue channel dominant
|
|
assert_float(c.b).is_greater(c.r)
|
|
d.queue_free()
|
|
|
|
|
|
func test_smuggler_colour_is_warm_amber() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
var c: Color = d._color_for_character("smuggler")
|
|
assert_that(c).is_not_equal(d.COLOR_DEFAULT)
|
|
# Red channel dominant (amber)
|
|
assert_float(c.r).is_greater(c.b)
|
|
d.queue_free()
|
|
|
|
|
|
func test_unknown_character_returns_default_colour() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
assert_that(d._color_for_character("")).is_equal(d.COLOR_DEFAULT)
|
|
assert_that(d._color_for_character("merchant")).is_equal(d.COLOR_DEFAULT)
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BBCode output — italic + colour tags
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_show_monologue_wraps_text_in_italic_bbcode() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Test line.", 5.0, "")
|
|
var txt: String = d.text_label.text
|
|
assert_that(txt).contains("[i]")
|
|
assert_that(txt).contains("[/i]")
|
|
assert_that(txt).contains("Test line.")
|
|
d.queue_free()
|
|
|
|
|
|
func test_show_monologue_includes_color_tag() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Colour test.", 5.0, "detective")
|
|
var txt: String = d.text_label.text
|
|
assert_that(txt).contains("[color=#")
|
|
assert_that(txt).contains("[/color]")
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# No-overwrite contract (P0, #477)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_second_call_does_not_overwrite_active_display() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("First line.", 10.0, "")
|
|
assert_that(d._displaying).is_true()
|
|
var first_text: String = d.text_label.text
|
|
|
|
d.show_monologue("Second line.", 5.0, "")
|
|
# Text label must still show the first line
|
|
assert_that(d.text_label.text).is_equal(first_text)
|
|
d.queue_free()
|
|
|
|
|
|
func test_second_call_while_active_is_queued() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("First.", 10.0, "")
|
|
d.show_monologue("Second.", 5.0, "")
|
|
assert_int(d._queue.size()).is_equal(1)
|
|
assert_that(d._queue[0].text).is_equal("Second.")
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Queue management
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_queue_drains_after_fade_complete() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Line A.", 1.0, "")
|
|
d.show_monologue("Line B.", 2.0, "")
|
|
|
|
# Simulate fade completing on line A
|
|
d._displaying = false
|
|
d._on_fade_complete()
|
|
|
|
assert_that(d._displaying).is_true()
|
|
assert_that(d.text_label.text).contains("Line B.")
|
|
assert_int(d._queue.size()).is_equal(0)
|
|
d.queue_free()
|
|
|
|
|
|
func test_queue_empty_after_fade_complete_does_nothing() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Only line.", 1.0, "")
|
|
d._displaying = false
|
|
|
|
# No items queued — should not crash and _displaying stays false
|
|
d._on_fade_complete()
|
|
assert_that(d._displaying).is_false()
|
|
d.queue_free()
|
|
|
|
|
|
func test_queue_preserves_character_type() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("First.", 5.0, "detective")
|
|
d.show_monologue("Second.", 3.0, "smuggler")
|
|
|
|
assert_that(d._queue[0].character_type).is_equal("smuggler")
|
|
d.queue_free()
|
|
|
|
|
|
func test_multiple_queued_items_drain_in_order() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("A", 1.0, "")
|
|
d.show_monologue("B", 1.0, "")
|
|
d.show_monologue("C", 1.0, "")
|
|
|
|
d._displaying = false
|
|
d._on_fade_complete() # should display B
|
|
assert_that(d.text_label.text).contains("B")
|
|
|
|
d._displaying = false
|
|
d._on_fade_complete() # should display C
|
|
assert_that(d.text_label.text).contains("C")
|
|
|
|
d._displaying = false
|
|
d._on_fade_complete() # queue empty — nothing new
|
|
assert_that(d._displaying).is_false()
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Queue depth cap
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_queue_does_not_exceed_max_depth() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
# First call starts displaying immediately (not queued)
|
|
d.show_monologue("Active.", 99.0, "")
|
|
|
|
# Fill queue to max
|
|
for i in range(d.MAX_QUEUE_DEPTH + 5):
|
|
d.show_monologue("Overflow %d" % i, 1.0, "")
|
|
|
|
assert_int(d._queue.size()).is_less_or_equal(d.MAX_QUEUE_DEPTH)
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fade timer integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_process_triggers_fade_after_duration() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Timer test.", 2.0, "")
|
|
assert_that(d._displaying).is_true()
|
|
|
|
# Drive timer past duration without awaiting real time
|
|
d._fade_timer = 2.1
|
|
d._process(0.0)
|
|
|
|
# _displaying should now be false (fade_out called)
|
|
assert_that(d._displaying).is_false()
|
|
d.queue_free()
|
|
|
|
|
|
func test_process_does_not_fade_before_duration() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
d.show_monologue("Still showing.", 5.0, "")
|
|
d._fade_timer = 2.0
|
|
d._process(0.0)
|
|
assert_that(d._displaying).is_true()
|
|
d.queue_free()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Initial state
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func test_panel_starts_invisible() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
assert_float(d.text_panel.modulate.a).is_equal(0.0)
|
|
d.queue_free()
|
|
|
|
|
|
func test_not_displaying_on_init() -> void:
|
|
var d = _make_display()
|
|
if d == null:
|
|
return
|
|
assert_that(d._displaying).is_false()
|
|
d.queue_free()
|