#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.
This commit is contained in:
parent
902d6922e7
commit
1e89780bd7
7 changed files with 330 additions and 10 deletions
208
tests/test_dialog_pause.gd
Normal file
208
tests/test_dialog_pause.gd
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
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
|
||||
1
tests/test_dialog_pause.gd.uid
Normal file
1
tests/test_dialog_pause.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://clgtbnc7pfxbd
|
||||
|
|
@ -29,6 +29,9 @@ extends SceneTree
|
|||
# N. #127 - a dialog stays up until it is dismissed: no timer closes it,
|
||||
# a click does, and the click is eaten rather than also steering
|
||||
# O. #127 - every dialog names its speaker
|
||||
# P. #128 - the prologue opens STOPPED behind its first line, and
|
||||
# acknowledging it is what starts the world
|
||||
# Q. #133 - the handover runs straight into the first quest
|
||||
|
||||
var game_state: Node
|
||||
var scene: Node
|
||||
|
|
@ -105,6 +108,14 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the line changed on its own after 10s - #127 asks for a click")
|
||||
if not narration.visible:
|
||||
return _fail("the banner closed itself")
|
||||
# --- P. and the game is STOPPED behind it (#128) ---------
|
||||
if not paused:
|
||||
return _fail("the prologue opened on a line with the game still running")
|
||||
if director.beat_time > 0.01:
|
||||
return _fail("the director's clock ran on %.2fs behind an unread line" \
|
||||
% director.beat_time)
|
||||
print("the prologue opens stopped: beat clock at %.2fs" % director.beat_time)
|
||||
|
||||
var click: InputEventMouseButton = InputEventMouseButton.new()
|
||||
click.button_index = MOUSE_BUTTON_LEFT
|
||||
click.pressed = true
|
||||
|
|
@ -136,10 +147,13 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the opening beat was over before the first line finished")
|
||||
if not narration.is_speaking():
|
||||
return _fail("the opening beat is silent, so waiting for it proves nothing")
|
||||
# A player who never clicks must not be stuck: the beat's own
|
||||
# deadline is the way out.
|
||||
# Every beat has room to run past its floor before its deadline,
|
||||
# so a player who will not finish a beat's task is carried past
|
||||
# it. Since #128 that deadline is game time and a dialog stops
|
||||
# the clock, so it is a way past an unfinished BEAT, never past
|
||||
# an unacknowledged line.
|
||||
if director._deadline() <= director._floor_time():
|
||||
return _fail("the opening beat has no deadline past its floor - a silent player would stall")
|
||||
return _fail("the opening beat has no deadline past its floor - a stalled player would be stuck")
|
||||
print("opening beat still running at %.1fs, guide still talking" % director.beat_time)
|
||||
|
||||
# --- C. the convoy --------------------------------------
|
||||
|
|
@ -220,6 +234,13 @@ func _process(delta: float) -> bool:
|
|||
return _fail("the ambush counts as cleared while its raiders are alive")
|
||||
for enemy in director.ambush:
|
||||
enemy.queue_free()
|
||||
# #128: the beat announced itself, and an unacknowledged line
|
||||
# stops the tree - including the director, whose advance is what
|
||||
# stage 2 is waiting for. Acknowledged here, the way a player
|
||||
# would, so the clock below is running.
|
||||
narration.clear()
|
||||
if paused:
|
||||
return _fail("clearing the last line left the game stopped")
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
|
|
@ -335,8 +356,16 @@ func _process(delta: float) -> bool:
|
|||
scene.get_node("BuildController")._process(0.1)
|
||||
if _hidden("UI/BuildButton"):
|
||||
return _fail("the build button never came back")
|
||||
if narration.is_speaking():
|
||||
return _fail("the guide is still talking after the handover")
|
||||
# #133: the guide has nothing more to say - but the quest chain
|
||||
# starts exactly here, so what is on the banner now is the first
|
||||
# objective rather than any leftover of the prologue.
|
||||
var quest: Dictionary = game_state.active_quest()
|
||||
if quest.is_empty():
|
||||
return _fail("the handover left no first quest")
|
||||
if narration.is_speaking() and narration.current != String(quest.brief):
|
||||
return _fail("the banner is still on \"%s\" after the handover" \
|
||||
% narration.current)
|
||||
print("the prologue hands over to the first quest: \"%s\"" % quest.title)
|
||||
if scene.get_node("UI/BreachOverlay").visible:
|
||||
return _fail("the breach lens is still over the screen after the handover")
|
||||
if is_instance_valid(director) and director.breach != null:
|
||||
|
|
|
|||
Loading…
Reference in a new issue