spaceship/tests/test_dialog_pause.gd
Loïc Wisniewski 1e89780bd7 #128: a dialog stops the game until it is acknowledged
While a line is on the banner the whole tree is paused, which freezes the
fleet, the enemies and GameState's own production and wave clocks, and stops
input reaching any node but the banner. So a press while a dialog is up
acknowledges it and does nothing else - it cannot also steer the fleet or
open a menu on the way through.

The pause is owned by NarrationBanner rather than by its callers: the thing
that must not get out of step is "a line is showing" and "the game is
stopped", and the banner is the only code that knows the first. It releases
on the last line, on clear(), and on leaving the tree - a scene swapped out
mid-line must not leave the game stopped with nothing left to start it.

Two consequences worth naming. A beat's deadline in the prologue is now game
time, so it can no longer run out from under an unread line; it remains the
way past a beat the player will not finish. And "the prologue leaves you
movement and nothing else" was applied by _process on the first frame, which
no longer comes - the prologue opens ON a dialog - so BuildController and
WorldMap apply it at _ready too.
2026-08-28 14:40:46 +02:00

208 lines
7.5 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 ----
_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")
_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