#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>
2026-08-23 20:59:12 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_intro_scene.gd
|
|
|
|
|
# Forgejo #108 (#122, #123, #124): the prologue.
|
|
|
|
|
#
|
|
|
|
|
# The director drives the REAL scene rather than being a scene of its own, so
|
|
|
|
|
# everything below is checked against the real fleet, the real enemies and
|
|
|
|
|
# the real asteroid field.
|
|
|
|
|
#
|
|
|
|
|
# Covers:
|
|
|
|
|
# A. it runs on a NEW game and not on a resumed one
|
|
|
|
|
# B. "no action button available, only movements" - every pressable
|
|
|
|
|
# affordance is gone, and the joystick is not
|
|
|
|
|
# C. the convoy: bigger than the player's own ceiling, and mostly holds
|
|
|
|
|
# D. the guide speaks, and the banner never shows a line nobody set
|
|
|
|
|
# E. the ambush is easy: chasers only, and clearing them advances the beat
|
|
|
|
|
# F. the belt is dense, and it is across the lane rather than beside it
|
|
|
|
|
# G. the breach takes the wheel: player input stops moving the fleet
|
|
|
|
|
# H. the vortex leaves exactly one vessel, and it is the default interceptor
|
|
|
|
|
# I. the handover: the flag clears, the buttons come back, the spawner and
|
|
|
|
|
# the asteroid field are running again, and the fleet is a new game's
|
|
|
|
|
# J. skipping lands in the same state as playing it through
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
# K. #125 - no beat ends before its own floor, and a beat whose point is
|
|
|
|
|
# the guide talking does not end mid-sentence
|
|
|
|
|
# L. #125 - the convoy is spread out, with its holds surrounded by escorts
|
|
|
|
|
# M. #125 - the breach is a growing black hole with a lens over it, not a
|
|
|
|
|
# few lines turning around a point
|
2026-08-24 20:57:56 +02:00
|
|
|
# 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
|
2026-08-28 14:40:46 +02:00
|
|
|
# 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
|
2026-08-28 15:37:11 +02:00
|
|
|
# R. #139 - no belt rock is laid down inside the visible screen
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# S. #170 - the prologue opens on the PICTURE: the convoy is flying and the
|
|
|
|
|
# game is running for a few seconds before the guide says anything
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var scene: Node
|
|
|
|
|
var director: Node
|
|
|
|
|
var fleet: Node
|
|
|
|
|
var narration: Control
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
var convoy_size: int = 0
|
|
|
|
|
var skipped_props: Array = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
game_state.rocks = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
|
|
|
print("TEST_INTRO_SCENE: FAIL (%s)" % message)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _load_scene() -> void:
|
|
|
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
current_scene = scene
|
|
|
|
|
fleet = scene.get_node("Fleet")
|
|
|
|
|
director = scene.get_node_or_null("IntroDirector")
|
|
|
|
|
narration = scene.get_node("UI/NarrationBanner")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _drop_scene() -> void:
|
|
|
|
|
scene.queue_free()
|
|
|
|
|
scene = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _hidden(path: String) -> bool:
|
|
|
|
|
var node: Control = scene.get_node_or_null(path)
|
|
|
|
|
return node == null or not node.visible
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
# --- A. a new game gets one ------------------------------
|
|
|
|
|
if not game_state.intro_active:
|
|
|
|
|
return _fail("reset_new_game() did not arm the prologue")
|
|
|
|
|
_load_scene()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.4:
|
|
|
|
|
return false
|
|
|
|
|
if director == null:
|
|
|
|
|
return _fail("no IntroDirector in the scene")
|
|
|
|
|
if not director.running:
|
|
|
|
|
return _fail("the director did not start on a new game")
|
|
|
|
|
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
# --- S. #170: it opens on the picture, not the caption ---
|
|
|
|
|
# The scene is a third of a second old here, well inside the
|
|
|
|
|
# silence, so the convoy should be flying and nothing should be
|
|
|
|
|
# on the banner.
|
|
|
|
|
if narration.is_speaking():
|
|
|
|
|
return _fail("the guide was already talking %.2fs in - #170 asks for a few seconds first" \
|
|
|
|
|
% director.beat_time)
|
|
|
|
|
if paused:
|
|
|
|
|
return _fail("the prologue opened stopped, with nothing on the banner to stop it")
|
|
|
|
|
# Hand-ticked from here so the silence is MEASURED rather than
|
|
|
|
|
# waited out, and so the tree's own clock cannot race it.
|
|
|
|
|
director.set_process(false)
|
|
|
|
|
var ticks: int = 0
|
|
|
|
|
while not narration.is_speaking() and ticks < 1200:
|
|
|
|
|
director._process(1.0 / 60.0)
|
|
|
|
|
ticks += 1
|
|
|
|
|
if narration.is_speaking():
|
|
|
|
|
break
|
|
|
|
|
if director.beat_time > director.OPENING_SILENCE + 0.05:
|
|
|
|
|
return _fail("the clock passed the %.1fs silence at %.2fs and the guide still had not spoken" \
|
|
|
|
|
% [director.OPENING_SILENCE, director.beat_time])
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("the guide never spoke at all")
|
|
|
|
|
if director.beat_time < director.OPENING_SILENCE:
|
|
|
|
|
return _fail("the guide spoke at %.2fs, inside the %.1fs silence" \
|
|
|
|
|
% [director.beat_time, director.OPENING_SILENCE])
|
|
|
|
|
print("the prologue flies for %.1fs before the guide calls in" % director.beat_time)
|
|
|
|
|
director.set_process(true)
|
|
|
|
|
|
2026-08-24 20:57:56 +02:00
|
|
|
# --- N. a dialog waits for the reader --------------------
|
|
|
|
|
var first_line: String = narration.label.text
|
|
|
|
|
if first_line.strip_edges() == "":
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
return _fail("the guide spoke with an empty banner")
|
|
|
|
|
# The beat clock as the line went up. Nothing may move it on while
|
|
|
|
|
# the line is unread (#128) - the tree is stopped, and this test
|
|
|
|
|
# stops hand-ticking the director for exactly that reason.
|
|
|
|
|
var clock_at_line: float = director.beat_time
|
2026-08-24 20:57:56 +02:00
|
|
|
# Ten seconds of clock, with nothing pressed. A timer-driven
|
|
|
|
|
# banner would have moved on several times over.
|
|
|
|
|
for i in range(600):
|
|
|
|
|
narration._process(1.0 / 60.0)
|
|
|
|
|
if narration.label.text != first_line:
|
|
|
|
|
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")
|
2026-08-28 14:40:46 +02:00
|
|
|
# --- 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")
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
if absf(director.beat_time - clock_at_line) > 0.01:
|
2026-08-28 14:40:46 +02:00
|
|
|
return _fail("the director's clock ran on %.2fs behind an unread line" \
|
#166-#173: plainer words, a point to shoot at, and a catalog that says when it is broken
Eight tickets. The last one is the interesting one.
#166 - PLAINER DIALOG. Every spoken line rewritten: the nine prologue lines and
all twenty quest brief/done pairs. Mostly splitting em-dash subclauses into
sentences and dropping the literary register ("hulls" -> "ships", "That is our
hands back" -> "That frees us for other work"). Same meaning, same voice.
#167 - NOTHING ANNOUNCED TWICE. Quests carry an `important` flag; seven of the
twenty have it, and they are the search for the World Ship. A quest handed out
with its goal ALREADY met is now credited in silence unless it is one of those
- no brief, no answer, both signals still fired and the index still moved, so
it appears ticked in the objectives panel. The chain used to stop the game to
set a task the player had already done, then stop it again to congratulate
them on it.
#168 - THE KEYBOARD CAN ANSWER A DIALOG. The rule the ticket asks for (still
arriving, show it all; already whole, move on) was already the rule for a
click since #149, but a key press reached the banner and did nothing - and
this game is played with WASD and a mouse. Touch, click and key now go through
one door. Modifiers alone and auto-repeat echoes are not presses.
#169 - A LOADING SCREEN. A press raises a cover, and the load happens after it
has been on screen for a frame, so the wait is something the player is
watching. Deliberately still a blocking load: load_threaded_request() comes
back FAILED for these scenes, reporting text-parse errors that load() does not
produce, because the global class names and preloads they reach each other
through are not resolvable off the loading thread. A progress bar is not worth
a menu that sometimes refuses to start the game, and the web build ships with
thread support off anyway. Further presses while loading are ignored - on the
web a button fires on PRESS, so an impatient double tap costs nothing to make
and used to build the world twice.
#170 - THE PICTURE BEFORE THE CAPTION. The prologue's first line was up before
the scene had finished arriving, so the game opened STOPPED (#128) over a
convoy nobody had seen yet. It flies for three seconds first.
#171 - THE CONTROLS FOLLOW WHAT IS BEING HELD. is_touchscreen_available() asks
whether a touch COULD arrive, and in a browser that is yes on any laptop whose
panel reports touch support - so the web build put thumb sticks over a screen
being played with a mouse, and switched mouse aiming off on the machine it
exists for. The reading is now the last input actually used, it lives in
GameState so the sticks and the guns cannot disagree, and it is two-way:
nothing can strand a tablet, because the gesture that needs the controls is
the gesture that brings them back. A touch suppresses mouse evidence for a
second, so the click Godot manufactures out of a tap is not mistaken for a
mouse.
#172 - AIM AT A PLACE. Every gun was handed the same direction, so a fleet
firing at something threw a sheet of parallel bolts past either side of it and
only the middle hull could hit - worse the wider the formation. Guns now take
a POINT and each hull works out its own heading to it, so the fleet's fire
converges. The mouse already had a place and was flattening it into a bearing;
a pushed stick names the point at the guns' reach along it, so touch loses
nothing. The assist (#89) still bends a shot onto an enemy, now per hull and
around that hull's own heading. The aim guide stops at the point and marks it.
#173 - AND THE ONE THAT EXPLAINS ITSELF. "In the web export version the fleet
can not store any resources" is not a web bug. It is the third time the Godot
editor's .tres writer has quietly dropped a property it thought was
default-valued: InterceptorVesselDefinition losing `capabilities` leaves
cargo_capacity at 20 and takes away the "cargo" claim that
fleet_cargo_capacity() actually gates on, so the hold is zero and nothing can
be mined or carried. That is the symptom, word for word.
test_definitions_intact caught it in the suite. What it could not do is catch
it in a BUILD - an export is taken from the working tree at whatever moment
the editor last touched it, which is exactly when the damage is done and
exactly where nobody is running tests. The checks move to core/CatalogCheck.gd
and now run at startup in every build as well as in the test, from the same
function. On the web push_error lands in the on-page log web_shell.html
mirrors, which is the only error a phone can read.
The tree is intact today, so a fresh export should restore the hold. The point
of the change is that the next time it is not, the build says so.
Full suite green at 154/154.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139MmxRAJSUe1j77kftAWv3
2026-08-30 14:04:46 +02:00
|
|
|
% absf(director.beat_time - clock_at_line))
|
|
|
|
|
print("the line stopped the game: beat clock held at %.2fs" % director.beat_time)
|
2026-08-28 14:40:46 +02:00
|
|
|
|
2026-08-24 20:57:56 +02:00
|
|
|
var click: InputEventMouseButton = InputEventMouseButton.new()
|
|
|
|
|
click.button_index = MOUSE_BUTTON_LEFT
|
|
|
|
|
click.pressed = true
|
|
|
|
|
narration._unhandled_input(click)
|
|
|
|
|
if narration.label.text == first_line and narration.is_speaking():
|
|
|
|
|
return _fail("a click did not move the dialog on")
|
|
|
|
|
print("line held for 10s untouched, then a click moved it on")
|
|
|
|
|
|
|
|
|
|
# --- O. and it says who is talking -----------------------
|
|
|
|
|
if narration.speaker.strip_edges() == "":
|
|
|
|
|
return _fail("the dialog names no speaker")
|
|
|
|
|
if narration.speaker_label.text != narration.speaker:
|
|
|
|
|
return _fail("the speaker label does not show the speaker")
|
|
|
|
|
# It sits on the top border, at the left - above the text, and
|
|
|
|
|
# left of centre.
|
|
|
|
|
if narration.speaker_label.position.y >= narration.label.position.y:
|
|
|
|
|
return _fail("the speaker sits below the text, not on the top border")
|
|
|
|
|
if narration.speaker_label.position.x > narration.size.x * 0.5:
|
|
|
|
|
return _fail("the speaker is not at the left of the border")
|
|
|
|
|
print("speaker \"%s\" on the top-left border" % narration.speaker)
|
|
|
|
|
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
# --- K. nothing ends before its floor --------------------
|
|
|
|
|
# The floors are what #125 bought: every beat had a condition
|
|
|
|
|
# that could be satisfied while the guide was still talking.
|
|
|
|
|
for b in range(director.Beat.AFTERMATH + 1):
|
|
|
|
|
if director._floor_time() <= 0.0 and b != director.Beat.RUN:
|
|
|
|
|
return _fail("beat %d has no minimum length" % b)
|
|
|
|
|
if director.beat != director.Beat.BRIEF:
|
|
|
|
|
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")
|
2026-08-28 14:40:46 +02:00
|
|
|
# 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.
|
2026-08-24 20:57:56 +02:00
|
|
|
if director._deadline() <= director._floor_time():
|
2026-08-28 14:40:46 +02:00
|
|
|
return _fail("the opening beat has no deadline past its floor - a stalled player would be stuck")
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
print("opening beat still running at %.1fs, guide still talking" % director.beat_time)
|
|
|
|
|
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
# --- C. the convoy --------------------------------------
|
|
|
|
|
convoy_size = fleet.ships.size()
|
|
|
|
|
if convoy_size <= GameState.BASE_FLEET_SIZE:
|
|
|
|
|
return _fail("the convoy is %d vessels, no more than a new game's %d slots" \
|
|
|
|
|
% [convoy_size, GameState.BASE_FLEET_SIZE])
|
|
|
|
|
var holds: int = 0
|
|
|
|
|
var escorts: int = 0
|
|
|
|
|
for ship in fleet.ships:
|
|
|
|
|
if String(ship.vessel_type) == GameState.VESSEL_INTERCEPTOR:
|
|
|
|
|
escorts += 1
|
|
|
|
|
else:
|
|
|
|
|
holds += 1
|
|
|
|
|
if holds <= escorts:
|
|
|
|
|
return _fail("the convoy is %d holds to %d escorts - it should be mostly cargo" \
|
|
|
|
|
% [holds, escorts])
|
|
|
|
|
if escorts <= 0:
|
|
|
|
|
return _fail("the convoy has no escorts at all")
|
|
|
|
|
print("convoy: %d vessels, %d holds and %d escorts" % [convoy_size, holds, escorts])
|
|
|
|
|
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
# --- L. spread out, and the holds are on the inside -------
|
|
|
|
|
var spread: float = 0.0
|
|
|
|
|
var closest: float = 1.0e9
|
|
|
|
|
for a in fleet.ships:
|
|
|
|
|
for b in fleet.ships:
|
|
|
|
|
if a == b:
|
|
|
|
|
continue
|
|
|
|
|
spread = maxf(spread, a.position.distance_to(b.position))
|
|
|
|
|
closest = minf(closest, a.position.distance_to(b.position))
|
|
|
|
|
if spread < 400.0:
|
|
|
|
|
return _fail("the whole convoy fits in %.0fpx - that is a wedge, not a convoy" % spread)
|
|
|
|
|
if closest < 80.0:
|
|
|
|
|
return _fail("two convoy vessels are %.0fpx apart" % closest)
|
|
|
|
|
# Every hold must be further in than the furthest-out escort is,
|
|
|
|
|
# which is what "surrounded by the default vessels" means.
|
|
|
|
|
var escort_reach: float = 0.0
|
|
|
|
|
var hold_reach: float = 0.0
|
|
|
|
|
for ship in fleet.ships:
|
|
|
|
|
var from_centre: float = ship.position.length()
|
|
|
|
|
if String(ship.vessel_type) == GameState.VESSEL_INTERCEPTOR:
|
|
|
|
|
escort_reach = maxf(escort_reach, from_centre)
|
|
|
|
|
else:
|
|
|
|
|
hold_reach = maxf(hold_reach, from_centre)
|
|
|
|
|
if hold_reach >= escort_reach:
|
|
|
|
|
return _fail("a hold sits %.0fpx out, past the escorts at %.0f - they are not screened" \
|
|
|
|
|
% [hold_reach, escort_reach])
|
|
|
|
|
print("convoy %.0fpx across, holds within %.0f and escorts out at %.0f" \
|
|
|
|
|
% [spread, hold_reach, escort_reach])
|
|
|
|
|
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
# --- B. movement and nothing else -----------------------
|
|
|
|
|
for pressable in ["UI/BuildButton", "UI/MapButton", "UI/BuildActionButton",
|
|
|
|
|
"UI/BuildingActionButton", "UI/EnterAtmosphereButton",
|
|
|
|
|
"UI/TransferButton", "UI/HangarButton", "UI/LaserButton",
|
|
|
|
|
"UI/GasBombButton"]:
|
|
|
|
|
if not _hidden(pressable):
|
|
|
|
|
return _fail("%s is offered during the prologue" % pressable)
|
|
|
|
|
var joystick: Control = scene.get_node("UI/Joystick")
|
|
|
|
|
if not joystick.visible:
|
|
|
|
|
return _fail("the joystick is hidden - the player cannot even fly")
|
|
|
|
|
print("every action button is away; the joystick is not")
|
|
|
|
|
|
|
|
|
|
# --- D. the guide speaks --------------------------------
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("the prologue opened without a word from the guide")
|
|
|
|
|
if narration.label.text.strip_edges() == "":
|
|
|
|
|
return _fail("the banner is up with nothing on it")
|
|
|
|
|
print("guide: \"%s\"" % narration.label.text)
|
|
|
|
|
|
|
|
|
|
# --- E. the ambush --------------------------------------
|
|
|
|
|
director._begin(director.Beat.AMBUSH)
|
|
|
|
|
if director.ambush.is_empty():
|
|
|
|
|
return _fail("the ambush beat spawned nothing")
|
|
|
|
|
for enemy in director.ambush:
|
|
|
|
|
if String(enemy.enemy_id) != "chaser":
|
|
|
|
|
return _fail("the ambush includes a %s - it is meant to be easy" % enemy.enemy_id)
|
|
|
|
|
if director._beat_complete():
|
|
|
|
|
return _fail("the ambush counts as cleared while its raiders are alive")
|
|
|
|
|
for enemy in director.ambush:
|
|
|
|
|
enemy.queue_free()
|
2026-08-28 14:40:46 +02:00
|
|
|
# #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")
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 2
|
|
|
|
|
return false
|
|
|
|
|
2:
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
# Past the ambush beat's own floor (#125): clearing it is what
|
|
|
|
|
# ENDS it, but no beat may end before its floor, so watching for
|
|
|
|
|
# the advance means waiting that long.
|
|
|
|
|
if elapsed < director._floor_time() + 0.5:
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
return false
|
|
|
|
|
# The director advances itself, so the evidence that clearing the
|
|
|
|
|
# ambush ended the beat is that it has MOVED ON - asking
|
|
|
|
|
# _beat_complete() again would be asking about a beat it left.
|
|
|
|
|
if director.beat <= director.Beat.AMBUSH:
|
|
|
|
|
return _fail("killing every raider did not end the ambush beat")
|
|
|
|
|
print("%d raiders, all chasers, and clearing them ends the beat" % director.AMBUSH_COUNT)
|
|
|
|
|
|
|
|
|
|
# --- F. the belt ----------------------------------------
|
|
|
|
|
director._begin(director.Beat.GAUNTLET)
|
|
|
|
|
# Only the rocks still standing: the convoy flies into them, and
|
|
|
|
|
# a rock it has already broken is not evidence about the belt.
|
|
|
|
|
var standing: Array = []
|
|
|
|
|
for rock in director.rocks:
|
|
|
|
|
if is_instance_valid(rock) and not rock.is_queued_for_deletion():
|
|
|
|
|
standing.append(rock)
|
|
|
|
|
if standing.size() < 40:
|
|
|
|
|
return _fail("the \"pretty dense\" belt is %d rocks" % standing.size())
|
|
|
|
|
var ahead: int = 0
|
|
|
|
|
for rock in standing:
|
|
|
|
|
if rock.global_position.y < fleet.get_centroid().y:
|
|
|
|
|
ahead += 1
|
|
|
|
|
if ahead < standing.size() / 2:
|
|
|
|
|
return _fail("only %d of %d rocks are on the lane ahead" \
|
|
|
|
|
% [ahead, standing.size()])
|
|
|
|
|
print("belt: %d rocks, %d of them across the lane ahead" % [standing.size(), ahead])
|
|
|
|
|
|
2026-08-28 15:37:11 +02:00
|
|
|
# --- R. #139: and none of it appears on screen ------------
|
|
|
|
|
# The belt used to be laid out with its own arithmetic, which put
|
|
|
|
|
# its near edge within a screen of the fleet - so rocks popped
|
|
|
|
|
# into existence in view. It goes through the asteroid field's
|
|
|
|
|
# own keep-out now, the same one #126 gave the rest of the game.
|
|
|
|
|
var field: Node2D = scene.get_node("AsteroidField")
|
|
|
|
|
var keep_out: Rect2 = field.keep_out_rect(fleet.get_centroid())
|
|
|
|
|
var visible_rect: Rect2 = ViewUtils.get_visible_world_rect(field)
|
|
|
|
|
if visible_rect.size.x > 1.0 and not keep_out.encloses(visible_rect):
|
|
|
|
|
return _fail("the belt's keep-out %s does not cover the screen %s" \
|
|
|
|
|
% [keep_out, visible_rect])
|
|
|
|
|
var nearest: float = 1.0e9
|
|
|
|
|
for rock in standing:
|
|
|
|
|
if keep_out.has_point(rock.global_position):
|
|
|
|
|
return _fail("a belt rock was laid down at %s, inside the view %s" \
|
|
|
|
|
% [rock.global_position, keep_out])
|
|
|
|
|
nearest = minf(nearest, absf(rock.global_position.y - fleet.get_centroid().y))
|
|
|
|
|
print("no belt rock inside the keep-out %.0fx%.0f; nearest is %.0f ahead" \
|
|
|
|
|
% [keep_out.size.x, keep_out.size.y, nearest])
|
|
|
|
|
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
# --- G. the breach takes the wheel ----------------------
|
|
|
|
|
director._begin(director.Beat.BREACH)
|
|
|
|
|
if not game_state.intro_locks_movement:
|
|
|
|
|
return _fail("the breach beat left the player in control")
|
|
|
|
|
# GameController.gd is the script on Main itself, not a child.
|
|
|
|
|
var before: Vector2 = fleet.fleet_target
|
|
|
|
|
scene._process(0.1)
|
|
|
|
|
if fleet.fleet_target != before:
|
|
|
|
|
return _fail("player steering still moved the fleet during the breach")
|
|
|
|
|
print("the breach has the wheel: steering input moves nothing")
|
|
|
|
|
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
# --- M. the breach is a black hole ----------------------
|
|
|
|
|
var hole: Node2D = director.breach
|
|
|
|
|
if hole == null or not is_instance_valid(hole):
|
|
|
|
|
return _fail("the breach beat opened no black hole")
|
|
|
|
|
if hole.horizon_radius() > 1.0:
|
|
|
|
|
return _fail("the hole was already open the moment the beat began")
|
|
|
|
|
director._drive_breach(0.5)
|
|
|
|
|
var half_open: float = hole.horizon_radius()
|
|
|
|
|
director._drive_breach(1.0)
|
|
|
|
|
if hole.horizon_radius() <= half_open:
|
|
|
|
|
return _fail("the hole does not grow - %.1f at half, %.1f at full" \
|
|
|
|
|
% [half_open, hole.horizon_radius()])
|
|
|
|
|
if hole.disc_radius() <= hole.horizon_radius():
|
|
|
|
|
return _fail("the accretion disc is inside the horizon")
|
|
|
|
|
var lens: ColorRect = scene.get_node("UI/BreachOverlay")
|
|
|
|
|
if not lens.visible:
|
|
|
|
|
return _fail("the screen-space lens never came up")
|
|
|
|
|
if lens.material == null or lens.material.shader == null:
|
|
|
|
|
return _fail("the lens has no shader on it")
|
|
|
|
|
print("breach: horizon %.0f -> %.0f, disc %.0f, lens up" \
|
|
|
|
|
% [half_open, hole.horizon_radius(), hole.disc_radius()])
|
|
|
|
|
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
# --- H. the vortex --------------------------------------
|
|
|
|
|
director._begin(director.Beat.VORTEX)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 3
|
|
|
|
|
return false
|
|
|
|
|
3:
|
|
|
|
|
# Left to the director's own clock rather than ticked by hand:
|
|
|
|
|
# the vortex is a timed animation and driving it twice would not
|
|
|
|
|
# be the thing that ships.
|
|
|
|
|
if elapsed < 4.2:
|
|
|
|
|
return false
|
|
|
|
|
var alive: int = 0
|
|
|
|
|
for ship in fleet.ships:
|
|
|
|
|
if is_instance_valid(ship) and not ship.is_queued_for_deletion():
|
|
|
|
|
alive += 1
|
|
|
|
|
if alive != 1:
|
|
|
|
|
return _fail("the vortex left %d vessels, expected 1" % alive)
|
|
|
|
|
if String(fleet.ships[0].vessel_type) != GameState.VESSEL_INTERCEPTOR:
|
|
|
|
|
return _fail("the survivor is a %s, not the default interceptor" \
|
|
|
|
|
% fleet.ships[0].vessel_type)
|
|
|
|
|
print("the vortex took %d vessels and left one interceptor" % (convoy_size - 1))
|
|
|
|
|
|
|
|
|
|
# --- I. the handover ------------------------------------
|
|
|
|
|
director.finish()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 4
|
|
|
|
|
return false
|
|
|
|
|
4:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if game_state.intro_active or game_state.intro_locks_movement:
|
|
|
|
|
return _fail("the prologue finished without clearing its own flags")
|
|
|
|
|
if game_state.intro_fleet_size != 0:
|
|
|
|
|
return _fail("the convoy's fleet-size override outlived the prologue")
|
|
|
|
|
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE:
|
|
|
|
|
return _fail("the fleet still holds %d slots after the prologue" \
|
|
|
|
|
% game_state.max_fleet_size())
|
|
|
|
|
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
|
|
|
|
|
return _fail("the game starts with %d vessels, expected %d" \
|
|
|
|
|
% [fleet.ships.size(), GameState.STARTING_SHIP_COUNT])
|
|
|
|
|
if not scene.get_node("EnemySpawner").is_processing():
|
|
|
|
|
return _fail("waves are still switched off after the prologue")
|
|
|
|
|
if not scene.get_node("AsteroidField").is_processing():
|
|
|
|
|
return _fail("the asteroid field is still switched off after the prologue")
|
|
|
|
|
scene.get_node("BuildController")._process(0.1)
|
|
|
|
|
if _hidden("UI/BuildButton"):
|
|
|
|
|
return _fail("the build button never came back")
|
2026-08-28 14:40:46 +02:00
|
|
|
# #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)
|
#125: slow the prologue down, and give the breach a real black hole
Five things, all of them "this happens too fast or looks too thin".
PACING. Every beat had a completion test that could be satisfied while the
guide was still mid-sentence, which is why everything landed seconds after
the thing before it. Beats now have a FLOOR as well as a deadline - a
minimum length - and a beat whose point is the guide talking will not end
while a line is still on screen. One rule in one place rather than tuning
each condition. The run is 3400px instead of 1500, which at cruise was about
four seconds, and a held line is 6.5s instead of 4.5.
THE CONVOY. Escorts at the four corners of a box, holds packed in the middle
of it, roughly three times the spread of the fleet's own wedge - this is
freight under guard, not a fighting formation. CONVOY and CONVOY_FORMATION
pair index-for-index, which is all it takes for spawn_ships to seat the guns
on the outside, and the test pins that every hold sits further in than the
furthest-out escort.
THE DIALOG. Cut corners, brackets holding the short edges open, a tick strip
along the bottom and a caret in the corner - geometry rather than art, so it
scales with the banner and costs nothing.
THE BREACH. It was a few lines turning around a point. It is now two halves
driven by one number: DimensionalBreach, a growing black hole out in the
world built on BlackHoleVisual's idiom - empty core, counter-rotating arc
segments hotter toward the centre - with infalling streamers it does not
have, because a planet's disc is steady and this one has to say "now"; and
breach.gdshader over the top, bending the ACTUAL SCENE around it. Ships,
rocks and the starfield all get dragged in, which is most of what makes a
hole in space read as a hole rather than as a decal drawn on one.
Inside the horizon the shader samples nothing at all - the same decision
BlackHoleVisual makes, for the same reason: a black hole with something
visible in the middle stops being one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:25:10 +02:00
|
|
|
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:
|
|
|
|
|
return _fail("the black hole outlived the prologue")
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
print("handover: flags clear, 1 interceptor, waves and rocks running, buttons back")
|
|
|
|
|
_drop_scene()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 5
|
|
|
|
|
return false
|
|
|
|
|
5:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
# --- A (second half). a resumed game gets no prologue ----
|
|
|
|
|
game_state.intro_active = false
|
|
|
|
|
_load_scene()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 6
|
|
|
|
|
return false
|
|
|
|
|
6:
|
|
|
|
|
if elapsed < 0.4:
|
|
|
|
|
return false
|
|
|
|
|
if director != null and is_instance_valid(director) and director.running:
|
|
|
|
|
return _fail("the prologue ran again on a game that is past it")
|
|
|
|
|
if fleet.ships.size() > GameState.BASE_FLEET_SIZE:
|
|
|
|
|
return _fail("a resumed game opened with a convoy")
|
|
|
|
|
print("a game already past the prologue does not replay it")
|
|
|
|
|
_drop_scene()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 7
|
|
|
|
|
return false
|
|
|
|
|
7:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
# --- J. skipping lands where playing it through does -----
|
|
|
|
|
game_state.reset_new_game()
|
|
|
|
|
_load_scene()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 8
|
|
|
|
|
return false
|
|
|
|
|
8:
|
|
|
|
|
if elapsed < 0.4:
|
|
|
|
|
return false
|
|
|
|
|
# Skipped mid-prologue, with props on the board, so the check
|
|
|
|
|
# below is about clearing them rather than about an empty stage.
|
|
|
|
|
director._begin(director.Beat.AMBUSH)
|
|
|
|
|
director._begin(director.Beat.GAUNTLET)
|
|
|
|
|
skipped_props = director.ambush + director.rocks
|
|
|
|
|
if skipped_props.size() < 40:
|
|
|
|
|
return _fail("nothing was on the board to skip past")
|
|
|
|
|
director.skip()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 9
|
|
|
|
|
return false
|
|
|
|
|
9:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if game_state.intro_active or game_state.intro_locks_movement:
|
|
|
|
|
return _fail("skipping left the prologue's flags set")
|
|
|
|
|
if game_state.max_fleet_size() != GameState.BASE_FLEET_SIZE:
|
|
|
|
|
return _fail("skipping left the convoy's fleet size behind")
|
|
|
|
|
if fleet.ships.size() != GameState.STARTING_SHIP_COUNT:
|
|
|
|
|
return _fail("skipping left %d vessels flying" % fleet.ships.size())
|
|
|
|
|
for prop in skipped_props:
|
|
|
|
|
if is_instance_valid(prop) and not prop.is_queued_for_deletion():
|
|
|
|
|
return _fail("skipping left the prologue's own props on the board")
|
|
|
|
|
print("skipping clears %d props and lands where playing it through does" \
|
|
|
|
|
% skipped_props.size())
|
|
|
|
|
|
Implement #160, #161, #162, #163, #164 and #165
#162 - a single tap bought a research node outright. One physical tap arrives
TWICE: Godot emulates a mouse button from every touch AND a touch from every
mouse button, both project settings are on, and the menu's dispatcher routes
InputEventScreenTouch and InputEventMouseButton alike to the tree. The second
arrival found the node already selected and took itself for the confirming
tap. Measured, not reasoned about - one tap produced two picks and two
confirms.
What is ignored is specifically the TWIN: same frame, same place, arriving
through the other input kind. Deduping on frame and position alone also
swallowed two genuine taps in one frame, which the test caught.
#164 - the build menu predicted a caption's height from the COLUMN WIDTH:
wide column, assume one line. Right for most names and wrong for the long
ones, so "Spacial Workshop" wrapped onto the price beneath it. Captions are
now measured against the font that draws them, and prices sized from
PriceTag.wanted_height(), which existed for exactly this and was not being
asked. Prices are set before anything is measured, because the old order laid
out first and priced afterwards.
The ship lab: its buy button is gone and a second tap on an option buys it,
the same rule as the research tree; the explanation drops into the room that
freed; research points move up into the resource table, where everything else
being spent already is; the second row of options is placed from the first
row's measured bottom rather than a constant; and the vessel strip is spaced
from the panel's own width, so the eighth vessel is no longer half off the
right edge.
#160 - the quest panel was drawn BEFORE the sticks and the pivot pair, so
they sat on top of it. Moved after them, under the narration banner. Its title
rule ran straight through the close button; the header band is now as tall as
that button.
#161 - the prologue widens the formation into a freight convoy by writing over
GameState.formation, and nothing put it back. The player flew the convoy's
600-unit spread for the rest of the game - a shape nobody chose, which reads
as the fleet having drifted apart.
#163 - a quest to station a vessel at the outpost, after the one that asks for
the outpost. Building one and crewing it are different acts, and an empty
outpost holds nothing.
#165 - the gas bomb's blast and a vessel arriving fast both damage a
structure. The blast used to stop politely at anything the player owned, which
made a bomb dropped on a besieged mothership a free way to sweep the attackers
off it.
Collisions are caught on ENTRY, not on the 0.7s strike tick: a vessel is only
moving fast for the instant it arrives, and a ship sent in at 900 did no
damage at all until this moved. Docking is free - the fleet sits on its own
mothership constantly to bank ore - and garrison vessels are exempt outright,
because they launch from their outpost and re-dock with it at speed. The suite
caught that one: the ram rule was destroying the garrison as it launched.
Suite 153/153. Four tests changed because what they pinned moved: the lab's
buy button, the intro's formation, the research tree's tap delivery, and the
build menu's own preview.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:27:44 +02:00
|
|
|
# #161: the prologue widens the formation into a freight convoy
|
|
|
|
|
# by writing over game_state.formation. Finishing has to put the
|
|
|
|
|
# fleet's own wedge back, or the player flies the convoy's
|
|
|
|
|
# 600-unit spread for the rest of the game - a shape nobody chose,
|
|
|
|
|
# which reads as the fleet having drifted apart.
|
|
|
|
|
for i in range(mini(game_state.formation.size(),
|
|
|
|
|
game_state.DEFAULT_FORMATION.size())):
|
|
|
|
|
if not game_state.formation[i].is_equal_approx(game_state.DEFAULT_FORMATION[i]):
|
|
|
|
|
print("TEST_INTRO_SCENE: FAIL (slot %d is %s after the prologue, expected the default %s)" \
|
|
|
|
|
% [i, game_state.formation[i], game_state.DEFAULT_FORMATION[i]])
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
print("formation restored to the fleet's own wedge")
|
|
|
|
|
|
#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>
2026-08-23 20:59:12 +02:00
|
|
|
print("TEST_INTRO_SCENE: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|