#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:
Loïc Wisniewski 2026-08-28 14:40:46 +02:00
commit 1e89780bd7
7 changed files with 330 additions and 10 deletions

View file

@ -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: