#108: the prologue - the convoy, the ambush, and the breach

Landed as one piece across #122, #123 and #124: the frame is useless without
a script, and the script has nowhere to run without the frame.

The director drives the REAL scene rather than being a scene of its own. It
wants the real fleet, the real enemies and the real asteroid field, and it
makes "then the game starts" cost nothing - the director stops steering,
clears the flag, and what is left running is the ordinary game.

The script is a list of beats, each with a deadline as well as its own
completion test, so a beat whose condition is never met - a player who
refuses to fly at the ambush - runs out rather than wedging the prologue.
skip() ends the whole thing at any point and lands in exactly the state
playing it through does, so the game has one way to start rather than two
that have to be kept in step. The test pins that equivalence.

"No action button available, only movements" is enforced where the buttons
are decided, not by the director reaching in: the ability controllers set
their own visibility every frame and would have put them straight back, and
the map button belongs to the map. The joystick, the pivot and the fader
stay.

The convoy is ten hulls where a new game's fleet is one, so it needs an
explicit fleet-size override - a plain number rather than a fake doctrine,
because nothing about the player's own progression should be bent to fit a
cutscene. It is carriers rather than transporters: transporters belong to
hubs and are not fleet-eligible, so a convoy of them would be silently
filtered down to its escorts.

The ambush is chasers and only chasers. "Easy enough" is the requirement,
and a sniper against a convoy the player cannot yet fight properly is
neither easy nor a good first impression.

The breach takes the wheel through one check in GameController, covering the
stick, the keyboard and the released-again path together - three separate
gates would be three chances for one of them to keep steering. The shake is
written to camera.offset, never to its position, because GameController owns
where the camera is and would smooth a shake away.

The vessel that survives is chosen as the interceptor rather than whichever
happened to be first in the list, and the prologue ends on exactly a new
game's opening position.

Also: PlanetAtmosphereController no longer spawns the opening fleet while
the prologue is arming, and the tests that are about what the GAME starts
from now say so by turning the prologue off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Loïc Wisniewski 2026-08-23 20:59:12 +02:00
commit 209169206b
14 changed files with 885 additions and 9 deletions

View file

@ -24,6 +24,24 @@ var surviving_vessel_types: PackedStringArray = PackedStringArray()
# whenever the composition changes. GameState cannot see scene nodes, and the
# census the Planning Center works from has to include the fleet.
var fleet_vessel_types: PackedStringArray = PackedStringArray()
# #122: the prologue is running. Set by reset_new_game() so it plays once at
# the start of a NEW game and never on a resumed one, and cleared by the
# director when the prologue hands over.
#
# It is a flag rather than a separate scene because the prologue DIRECTS the
# real game: the real fleet, the real enemies, the real asteroid field. That
# is also what makes "then the game starts" cost nothing - the director stops
# steering and there is nothing left to load.
var intro_active: bool = false
# #124: the breach takes the wheel. Movement input is ignored while this is
# set, so the fleet goes where the script sends it rather than fighting the
# player for the last few seconds of the prologue.
var intro_locks_movement: bool = false
# #123: the prologue's convoy is bigger than any fleet the player can field
# yet - it is not their fleet, it is the one they are about to lose. An
# explicit override rather than a fake doctrine, so nothing about the
# player's own progression has to be bent to fit a cutscene.
var intro_fleet_size: int = 0
var return_world_position: Vector2 = Vector2.ZERO
var return_camera_zoom: float = 1.0
@ -566,6 +584,8 @@ func research_for_building(type: String) -> String:
func max_fleet_size() -> int:
if intro_fleet_size > 0:
return mini(intro_fleet_size, DEFAULT_FORMATION.size())
var n: int = BASE_FLEET_SIZE
for id in unlocked:
n += int(skill_node(id).get("fleet_bonus", 0))
@ -582,6 +602,9 @@ func reset_progression() -> void:
# process would start from wherever the last one left off instead of a
# fresh game.
func reset_new_game() -> void:
intro_active = true
intro_locks_movement = false
intro_fleet_size = 0
has_return_position = false
surviving_ship_count = STARTING_SHIP_COUNT
surviving_vessel_types = PackedStringArray()