188 lines
7.3 KiB
GDScript
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
|