spaceship/tests/test_dialog_pause.gd
Loïc Wisniewski b721760803 #149, #153, #154: dialogs arrive, sit on top, and give the keyboard back
#149 - a line is typed out at about three and a half words a second, with a
scan sweep running down the panel and faint rules across it while it
decodes. The frame carries the charge while it works and settles when the
line lands; the caret is a solid write-head during the decode and only
starts blinking once it is waiting on the reader.

Two implementation notes worth keeping. The full text is set and then
REVEALED with visible_characters rather than the string being grown - a
growing string re-wraps on nearly every frame, so words jump between rows as
they are typed. And a press on a line still arriving FINISHES it instead of
dismissing it: without that, a player clicking at their usual pace skips the
second line having never seen a word of it, which is the exact thing #127
exists to prevent.

#153 - "after a dialog popup the input system is lost on keypress". Nothing
was lost; it was never delivered. A dialog stops the tree (#128), a paused
node receives no input, so a key RELEASED while the line was up never
reached the controller - keys_down kept it and the fleet flew on in that
direction once the game restarted.

NOTIFICATION_UNPAUSED arrives whatever a node's process mode is, which makes
it the one moment the question can be asked, and the physical keyboard is
the authority on what is actually held. That fixes anything that stops the
tree, not only dialogs, and the test checks it that way.

#154 - the banner was the ninth child of the UI layer and every panel came
after it, so all twelve of them drew over the top. It is the last child now,
in both scenes. Input order was never the problem and does not change: the
UI layer already sees unhandled input before the controllers do.
2026-08-29 14:39:07 +02:00

217 lines
8 KiB
GDScript

extends SceneTree
# Headless test: run with
# godot --headless -s res://tests/test_dialog_pause.gd
# Forgejo #128: "When a dialog appears, no command should work and the game
# physics should be paused as long as the message is not acknowledged."
#
# Two claims, and they are checked separately because they can fail
# separately: the world must STOP, and the controls must not ANSWER. Presses
# are pushed through the viewport rather than handed to a handler directly -
# calling a handler by hand would prove nothing about whether the engine
# would have delivered the event at all, which is the whole question here.
#
# What a press does while a dialog is up is ACKNOWLEDGE it (#127) and
# nothing else. So "no command works" is not "the press vanishes": it is
# "the press reached the dialog and reached nothing else", and that is what
# is asserted below.
#
# Covers:
# A. a line goes up and the tree is paused
# B. the world is frozen: the fleet holds still under a movement order
# C. GameState's own clock is frozen too - the wave clock does not run
# D. no command works: a press on a HUD button, delivered the way a finger
# delivers one, acknowledges the dialog and does not fire the button
# E. queued lines hold the pause until the LAST one is acknowledged
# F. after the last one, the world runs and the same press does fire
# G. leaving the tree with a line up does not leave the game stopped with
# nothing left that could start it again
var game_state: Node
var scene: Node
var fleet: Node2D
var narration: Control
var button: Control
var build_menu: Control
var presses: int = 0
var stage: int = 0
var elapsed: float = 0.0
var frozen_at: Vector2 = Vector2.ZERO
var frozen_wave_timer: float = 0.0
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()
# Not the prologue. The prologue has its own dialog coverage
# (test_intro_scene); this is about dialogs in the ordinary game, where
# there are commands to block in the first place.
game_state.intro_active = false
# And no quest chain. Quest briefings are dialogs too (#132), and this
# test is about what a dialog DOES - it needs to be the one deciding
# when one appears.
game_state.reset_quests()
game_state.rocks = 500
func _fail(message: String) -> bool:
print("TEST_DIALOG_PAUSE: FAIL (%s)" % message)
quit(1)
return true
func _on_pressed() -> void:
presses += 1
# A press delivered the way the player delivers one: through the viewport, at
# the button's own screen rect, so the engine's routing decides whether it
# arrives rather than the test deciding for it.
func _press_button() -> void:
var at: Vector2 = button.get_global_rect().get_center()
for is_down in [true, false]:
var event: InputEventMouseButton = InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = is_down
event.position = at
event.global_position = at
# in_local_coords, deliberately. The other form runs the position
# through the window's screen transform, and headless has no window -
# the event then lands nowhere and the button looks broken.
root.push_input(event, true)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
fleet = scene.get_node("Fleet")
narration = scene.get_node("UI/NarrationBanner")
button = scene.get_node("UI/BuildButton")
build_menu = scene.get_node("UI/BuildMenu")
button.pressed.connect(_on_pressed)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.4:
return false
if paused:
return _fail("the game was already stopped before any dialog")
# The button answers when nothing is in the way. Without this
# baseline, D below would prove only that the button was
# unreachable all along.
presses = 0
_press_button()
elapsed = 0.0
stage = 2
return false
2:
if elapsed < 0.2:
return false
if presses == 0:
return _fail("the build button does not answer a press even with no dialog up")
print("baseline: a press on the build button fires it")
# It opened the build menu, which is modal and would block every
# later press for a reason that has nothing to do with #128.
build_menu.visible = false
# --- A. a line stops the game ----------------------------
narration.say("Hold position.", "TEST")
narration.say("And stand by.", "TEST")
if not paused:
return _fail("a dialog went up and the game kept running")
print("dialog up: the tree is paused")
frozen_at = fleet.get_centroid()
frozen_wave_timer = game_state.wave_timer
# Ordered somewhere it is nowhere near, so even a little drift
# would show up. steer_direction persists, so this order stands
# for the whole frozen second below.
fleet.move_in_direction(Vector2.RIGHT)
presses = 0
elapsed = 0.0
stage = 3
return false
3:
# --- B and C. a whole second of frozen world -------------
if elapsed < 1.0:
return false
var moved: float = fleet.get_centroid().distance_to(frozen_at)
if moved > 0.5:
return _fail("the fleet drifted %.1fpx while a dialog was up" % moved)
print("fleet held still for 1s under a movement order (%.2fpx)" % moved)
var ran: float = absf(game_state.wave_timer - frozen_wave_timer)
if ran > 0.001:
return _fail("the wave clock ran on %.2fs while a dialog was up" % ran)
print("the wave clock did not advance")
# --- D. the press acknowledges, and does nothing else ----
# #149: a line arrives a character at a time, and a press on one
# still arriving finishes it rather than dismissing it. The
# banner types while the game is stopped - it is the one node
# still running - so a second of frozen world is more than
# enough for this line to have finished on its own. Finished
# explicitly here so the press below is unambiguously the
# acknowledging one; the typing itself is test_dialog_typing's.
narration.finish_typing()
_press_button()
if presses != 0:
return _fail("the build button fired while a dialog was up")
if build_menu.visible:
return _fail("the build menu opened from a press meant for the dialog")
print("a press while the dialog was up did not reach the button")
# --- E. the queued line keeps the game stopped -----------
if not narration.is_speaking():
return _fail("one press acknowledged both queued lines")
if not paused:
return _fail("the game restarted with a line still on screen")
print("a second queued line keeps the game stopped")
narration.finish_typing()
_press_button()
if presses != 0:
return _fail("the build button fired on the acknowledging press")
if narration.is_speaking():
return _fail("the banner is still speaking after the last line")
if paused:
return _fail("the last line was acknowledged and the game is still stopped")
print("last line acknowledged: the game is running again")
frozen_at = fleet.get_centroid()
presses = 0
elapsed = 0.0
stage = 4
return false
4:
# --- F. and the world answers again ----------------------
if elapsed < 0.4:
return false
if fleet.get_centroid().distance_to(frozen_at) < 1.0:
return _fail("the fleet is still frozen after the dialog closed")
_press_button()
if presses == 0:
return _fail("the build button stayed dead after the dialog closed")
build_menu.visible = false
print("the world moves again: the fleet flies and the button fires")
# --- G. torn down mid-line ------------------------------
narration.say("Interrupted.", "TEST")
if not paused:
return _fail("the dialog did not stop the game the second time")
scene.free()
scene = null
if paused:
return _fail("the scene was torn down mid-dialog and left the game stopped")
print("a scene torn down mid-dialog leaves the game running")
print("TEST_DIALOG_PAUSE: PASS")
quit(0)
return true
return false