spaceship/tests/test_dialog_typing.gd
2026-09-02 13:43:56 +02:00

188 lines
7.3 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_dialog_typing.gd
# Forgejo #149: "les dialogues doivent arriver petit a petit, avec un VFX
# associe a des dialogues de science fiction" - dialogs should arrive bit by
# bit, with an effect that reads as science fiction.
#
# The failure worth guarding against is not "it does not type" - it is the
# one that comes with typing: a player clicking at their usual pace skips
# the second line entirely, having never seen a word of it. #127 exists to
# stop exactly that, so a press on a line still arriving has to finish it
# rather than dismiss it.
#
# Covers:
# A. a line arrives a character at a time rather than all at once
# B. the text itself is set whole and REVEALED - growing the string would
# re-wrap the line every frame and make words jump between rows
# C. a press on a line still arriving finishes it and does not dismiss it
# D. a press on a finished line dismisses it, as it always did
# E. it types while the game is stopped, being the one node still running
# F. the sci-fi part: the panel is drawn differently while decoding, and
# settles when the line is complete
# G. #175 - keyboard presses do not finish or dismiss a dialog; only a
# deliberate tap/click advances it
# H. #178 - revealed text is seated at the left edge rather than growing
# outward from the middle
var game_state: Node
var scene: Node
var narration: Control
var stage: int = 0
var elapsed: float = 0.0
var first_line: String = ""
var seen: Array = []
func _initialize() -> void:
# GameState is an autoload: a bare GameState.<instance member> does not
# compile in a SceneTree script, so the node is fetched by name instead.
game_state = root.get_node("GameState")
game_state.reset_new_game()
game_state.intro_active = false
game_state.reset_quests()
func _fail(message: String) -> bool:
print("TEST_DIALOG_TYPING: FAIL (%s)" % message)
quit(1)
return true
func _click() -> void:
var press: InputEventMouseButton = InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
narration._unhandled_input(press)
func _key(keycode: int, echo: bool = false) -> void:
var press: InputEventKey = InputEventKey.new()
press.keycode = keycode
press.pressed = true
press.echo = echo
narration._unhandled_input(press)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
narration = scene.get_node("UI/NarrationBanner")
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.3:
return false
first_line = "Convoy Kestrel, this is a long enough line that decoding it takes a moment."
narration.say(first_line, "TEST")
narration.say("And a second one behind it.", "TEST")
# --- A and B. it arrives, and the text is whole ----------
if not narration.is_typing():
return _fail("the line was complete the instant it was said")
if narration.label.text != first_line:
return _fail("the label holds a partial string - the line will re-wrap as it types")
if narration.label.visible_characters < 0:
return _fail("the whole line is visible already")
if narration.label.horizontal_alignment != HORIZONTAL_ALIGNMENT_LEFT:
return _fail("dialog text is not left-aligned - partial text will grow from the middle")
seen.append(narration.label.visible_characters)
# Hand-ticked, so the reveal is measured rather than raced. The
# banner processes while the tree is stopped (#128), which is E.
if not paused:
return _fail("a dialog did not stop the game")
for i in range(12):
narration._process(1.0 / 60.0)
seen.append(narration.label.visible_characters)
var rising: int = 0
for i in range(1, seen.size()):
if int(seen[i]) > int(seen[i - 1]):
rising += 1
if rising < 6:
return _fail("the reveal advanced on only %d of %d frames" % [rising, seen.size() - 1])
if int(seen[-1]) >= first_line.length():
return _fail("the whole line decoded within 12 frames - that is not arriving")
print("typed while the game was stopped: %d -> %d of %d characters over 12 frames" \
% [seen[0], seen[-1], first_line.length()])
# --- F. and it looks like it is working ------------------
# is_typing() is the flag _draw() branches on for the sweep, the
# write-head caret and the frame's charge, so its transition IS
# the effect changing. Checked rather than calling _draw() by
# hand, which Godot refuses outside a paint.
if not narration.is_typing():
return _fail("the test's own ticking finished the line")
narration.queue_redraw()
narration.finish_typing()
if narration.is_typing():
return _fail("finish_typing() left the line still arriving")
if narration.label.visible_characters != -1:
return _fail("a finished line is still hiding characters")
if narration.SWEEP_SECONDS <= 0.0 or narration.CHARS_PER_SECOND <= 0.0:
return _fail("the decode has no rate to run at")
print("decoding while arriving, settled when it lands (%.0f chars/s, %.1fs sweep)" \
% [narration.CHARS_PER_SECOND, narration.SWEEP_SECONDS])
# --- C. a press finishes rather than dismisses -----------
narration.say(first_line, "TEST")
# say() queues behind the current line, so start it cleanly.
narration.clear()
narration.say(first_line, "TEST")
narration.say("Second.", "TEST")
if not narration.is_typing():
return _fail("the fresh line was not arriving")
_click()
if narration.is_typing():
return _fail("a press did not finish a line that was still arriving")
if narration.current != first_line:
return _fail("a press on an arriving line dismissed it - the next line would be skipped unread")
print("a press on an arriving line finishes it, and does not move on")
# --- D. and the next press does move on ------------------
_click()
if narration.current == first_line:
return _fail("a press on a finished line did not dismiss it")
if narration.current != "Second.":
return _fail("dismissing landed on \"%s\"" % narration.current)
print("the press after that moves on to \"%s\"" % narration.current)
# --- G. #175: keys never advance a dialog ----------------
narration.clear()
narration.say(first_line, "TEST")
narration.say("Second.", "TEST")
if not narration.is_typing():
return _fail("the fresh line was not arriving")
_key(KEY_SHIFT)
_key(KEY_SPACE, true)
_key(KEY_SPACE)
_key(KEY_W)
_key(KEY_ENTER)
if not narration.is_typing() or narration.current != first_line:
return _fail("a keyboard press changed a dialog that was still arriving")
narration.finish_typing()
_key(KEY_W)
_key(KEY_ENTER)
if narration.current != first_line or narration.is_typing():
return _fail("a keyboard press dismissed a complete dialog")
_click()
if narration.current != "Second.":
return _fail("clicking after ignored keys did not reach the second line")
narration.finish_typing()
_click()
if narration.is_speaking():
return _fail("clicking the last complete line did not close the dialog")
if paused:
return _fail("clicking through every line left the game stopped")
print("space, W and enter leave dialogs untouched; clicks still finish and advance")
print("TEST_DIALOG_TYPING: PASS")
quit(0)
return true
return false