#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_quest_chain.gd
|
|
|
|
|
# Forgejo #129 (#131, #132, #133): the quest chain.
|
|
|
|
|
#
|
|
|
|
|
# The chain is a campaign, so most of what can go wrong with it is not a
|
|
|
|
|
# crash - it is a quest that cannot be finished, one that finishes itself,
|
|
|
|
|
# or one that asks for something the player has no way to reach yet. Those
|
|
|
|
|
# are what is checked here, against the real catalogue rather than a fixture:
|
|
|
|
|
# a chain that passes with test data and strands the player is exactly the
|
|
|
|
|
# failure this is for.
|
|
|
|
|
#
|
|
|
|
|
# Covers:
|
|
|
|
|
# A. the model: a fresh game is on the first quest, and every entry in the
|
|
|
|
|
# catalogue is well formed and reachable
|
|
|
|
|
# B. no quest counts as done before it is done, and every goal kind is
|
|
|
|
|
# answerable
|
|
|
|
|
# C. progress LATCHES - collecting and then spending does not un-complete
|
|
|
|
|
# D. a quest is announced before it can complete, so an already-satisfied
|
|
|
|
|
# goal still gets read out
|
|
|
|
|
# E. the whole chain can be walked, and the ore quest really is finished by
|
|
|
|
|
# collecting ore
|
|
|
|
|
# F. #133 - the mothership quest's answer is that the World Ship is out of
|
|
|
|
|
# reach, and exploration is what follows
|
|
|
|
|
# G. research points only come from conquest, so no quest may ask for a
|
|
|
|
|
# doctrine that costs them before the conquest quest
|
|
|
|
|
# H. quests do not run during the prologue
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
# P. #142 - enough rocks are asked for before the first structure is
|
|
|
|
|
# Q. #141 - the first structure that needs a doctrine has that doctrine as
|
|
|
|
|
# a quest of its own, so research is taught rather than assumed
|
#145, #144, #143: a quest that could not be finished, and two boxes that overhung
#145 - the extractor quest could never be completed. Extractors are placed
onto a planet from space and live in GameState.extractors, with their own
position, stock and id; they are not in `buildings`. So the goal asked
has_building_of_type("extractor"), which is false however many the player
builds. Not the death the reporter guessed at - the quest was simply asking
a question that had no yes.
structure_exists() is the question anything wanting "does the player have
one of these" should ask, and the quest model asks it now. The regression
test builds every structure quest's target THE WAY THE GAME BUILDS IT and
watches the goal flip, rather than knowing which array each one lands in -
the knowing is what got this wrong in the first place.
#144 - the build menu's status line is anchored to the top of the panel and
its grid to the panel's centre, so the gap between the two is half the panel
height. It was a written-down constant, correct only at 1280 tall; at 1080
the grid started four pixels ABOVE the status line and drew through it.
Nothing about that geometry is written against a screen size any more. The
band is measured from the header that is actually there, the pitch closes up
to fit it (never below a row's own height, which is what makes overlap
impossible rather than unlikely), and the column count rises until the rows
fit - so a short window gets a wide grid instead of one running off the
bottom. Ten structures now lay out as 5 columns at 900px tall, 4 at 1080, 3
at 1280 and 2 at 1600, clearing the header at every one, which is what the
test walks.
#143 - a hangar caption is "Gas Bomber\n2 available": two lines before
autowrap and three after it in a narrow column, at a size fixed in the
scene. Labels do not clip by default, so the third line was drawn over the
icon of the row below. The size is fitted to the box the layout works out,
and the box clips - the first keeps it legible, the second keeps it correct
however the vessel names grow.
Its box also overhung by exactly HANGAR_LABEL_GAP: the layout subtracted one
gap where it needed two, above the caption and below it. Two pixels, but two
pixels inside the next row's icon.
2026-08-29 12:51:09 +02:00
|
|
|
# R. #145 - every structure quest is actually finishable: building the
|
|
|
|
|
# thing it asks for, the way the game builds it, completes it
|
#157, #158, #159: pay from the hold you flew in, and nothing is exempt from coming down
#157 was already satisfied - second_hull is quest 7 and extractor is quest
11 - so this pins the property rather than adding a quest: nothing costing
more than a single hold may be asked for before the chain has grown the
fleet or taught banking. It fails if either is ever moved behind it.
#158 - a building's local costs may now be paid out of the fleet's hold.
#12 made production cost resources physically DELIVERED rather than money
out of the bank, and that stands: the bank is still not raided here. The
hold is simply the other place resources physically are, and making the
player park, open the transfer menu and move cargo across by hand was
ceremony rather than a decision.
But only WHERE THE FLEET IS, and that qualifier is the whole design. The
first draft had no range on it and test_manual_supply_route caught it
immediately: a crafter three thousand units away reached into the hold and
spent ore the player had just withdrawn to carry somewhere else - which is
exactly the complaint that got automatic banking removed in #24, arriving
from the other direction. The range is Building.interact_radius, the same
distance that decides whether the transfer button appears, so the rule is
not a hidden one: if you could hand the cargo over, the building can take
it.
#159 - the mothership floored at 1hp, so a battered one sat there reading as
dead and never exploded. That exemption existed to prevent a soft-lock, and
the soft-lock turns out not to exist: the bank is GameState.bank_inventory
and the hangar is stored_vessels, both of which outlive any building; the
mothership is the one structure research never gates; it costs exactly what
a single interceptor's hold carries; and ore comes from asteroids, which do
not run out. So the way back is always mine, fly, rebuild - and that is what
test_building_health now asserts, in place of the exemption that used to
stand in for it.
2026-08-29 15:51:34 +02:00
|
|
|
# S. #157 - nothing costing more than a single hold is asked for before
|
|
|
|
|
# the chain has grown the fleet or taught banking
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
# I. lines survive having nobody to say them: a quest finished with no
|
|
|
|
|
# banner alive is read out by the next one that is
|
|
|
|
|
# J. a planet surface carries the chain too - a quest finished down there
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
# is announced down there, not on the way back up, and the objective is
|
|
|
|
|
# reachable from a button there as well
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
# K. landing is recorded, both the planet and the kind of world it was
|
#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
|
|
|
# L. #167 - a TEACHING quest handed out with its goal already met is
|
|
|
|
|
# credited without a word, while a STORY quest in the same position is
|
|
|
|
|
# still read out
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
|
|
|
|
|
const QUESTS = preload("res://core/Quests.gd")
|
|
|
|
|
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
var spoke: bool = false
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
var scene: Node
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
var panel: Control
|
|
|
|
|
var quest_button: Control
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
var narration: Control
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.intro_active = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
|
|
|
print("TEST_QUEST_CHAIN: FAIL (%s)" % message)
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Which skill-tree nodes cost research points, which only conquest supplies.
|
|
|
|
|
func _costs_research_points(id: String) -> bool:
|
|
|
|
|
return int(game_state.skill_node(id).get("research_points", 0)) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _catalogue_cases() -> bool:
|
|
|
|
|
# --- A. the catalogue is well formed ---------------------------------
|
|
|
|
|
if QUESTS.count() < 8:
|
|
|
|
|
return _fail("a chain of %d is not a campaign" % QUESTS.count())
|
|
|
|
|
var seen: Dictionary = {}
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var quest: Dictionary = QUESTS.at(i)
|
|
|
|
|
for field in ["id", "title", "objective", "goal", "brief", "done"]:
|
|
|
|
|
if not quest.has(field):
|
|
|
|
|
return _fail("quest %d has no %s" % [i, field])
|
|
|
|
|
if field != "goal" and String(quest[field]).strip_edges() == "":
|
|
|
|
|
return _fail("quest %s has an empty %s" % [quest.id, field])
|
|
|
|
|
if seen.has(quest.id):
|
|
|
|
|
return _fail("two quests share the id %s" % quest.id)
|
|
|
|
|
seen[quest.id] = true
|
|
|
|
|
# Every goal must be one this build can actually answer. An unknown
|
|
|
|
|
# kind reads as never-done and would strand the player there.
|
|
|
|
|
var progress: Vector2i = game_state.quest_progress(quest)
|
|
|
|
|
if progress.y <= 0:
|
|
|
|
|
return _fail("quest %s has a goal of zero, so it is already done" % quest.id)
|
|
|
|
|
# --- B. and nothing is complete on a fresh game ------------------
|
|
|
|
|
if game_state.quest_goal_met(quest):
|
|
|
|
|
return _fail("quest %s counts as done on a brand new game" % quest.id)
|
|
|
|
|
# The things a goal names must exist.
|
|
|
|
|
var goal: Dictionary = quest.goal
|
|
|
|
|
match String(goal.kind):
|
|
|
|
|
QUESTS.GOAL_BUILDING:
|
|
|
|
|
if game_state.building_definition(String(goal.building)) == null:
|
|
|
|
|
return _fail("quest %s asks for a building that does not exist: %s" \
|
|
|
|
|
% [quest.id, goal.building])
|
|
|
|
|
QUESTS.GOAL_FLEET_VESSEL, QUESTS.GOAL_VESSEL_UNLOCKED:
|
|
|
|
|
if game_state.vessel_definition(String(goal.vessel)) == null:
|
|
|
|
|
return _fail("quest %s asks for a vessel that does not exist: %s" \
|
|
|
|
|
% [quest.id, goal.vessel])
|
|
|
|
|
QUESTS.GOAL_RESOURCE:
|
|
|
|
|
if not (String(goal.resource) in game_state.resource_types()):
|
|
|
|
|
return _fail("quest %s asks for a resource that does not exist: %s" \
|
|
|
|
|
% [quest.id, goal.resource])
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
QUESTS.GOAL_RESEARCH:
|
|
|
|
|
if game_state.skill_node(String(goal.node)).is_empty():
|
|
|
|
|
return _fail("quest %s asks to research a doctrine that does not exist: %s" \
|
|
|
|
|
% [quest.id, goal.node])
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
print("catalogue: %d quests, all well formed and none already done" % QUESTS.count())
|
|
|
|
|
|
|
|
|
|
# --- F. the hinge ----------------------------------------------------
|
|
|
|
|
var mothership: Dictionary = QUESTS.by_id("mothership")
|
|
|
|
|
if mothership.is_empty():
|
|
|
|
|
return _fail("there is no mothership quest - #129 turns on it")
|
|
|
|
|
var answer: String = String(mothership.done).to_lower()
|
|
|
|
|
if not ("world ship" in answer):
|
|
|
|
|
return _fail("the mothership quest's answer never mentions the World Ship")
|
|
|
|
|
if not ("further" in answer or "far" in answer or "carry" in answer):
|
|
|
|
|
return _fail("the mothership quest does not say the World Ship is out of reach")
|
|
|
|
|
# And what follows it is a search.
|
|
|
|
|
var after: Dictionary = QUESTS.at(QUESTS.CHAIN.find(mothership) + 1)
|
|
|
|
|
if String(after.goal.kind) != QUESTS.GOAL_EXPLORE:
|
|
|
|
|
return _fail("the quest after the mothership is %s, not exploration" % after.id)
|
|
|
|
|
print("the mothership answers with distance, and exploration follows it")
|
|
|
|
|
|
2026-09-03 00:12:08 +02:00
|
|
|
# #185: the final wide search has a propulsion lesson immediately before
|
|
|
|
|
# it. The goal uses the stable vessel id while the player-facing copy uses
|
|
|
|
|
# the renamed Energy Propulser name.
|
|
|
|
|
var trail: Dictionary = QUESTS.by_id("trail")
|
|
|
|
|
var trail_at: int = QUESTS.CHAIN.find(trail)
|
|
|
|
|
if trail_at <= 0:
|
|
|
|
|
return _fail("the Chart 150 quest is missing or opens the chain")
|
|
|
|
|
var before_trail: Dictionary = QUESTS.at(trail_at - 1)
|
|
|
|
|
if String(before_trail.id) != "propulser" \
|
|
|
|
|
or String(before_trail.goal.kind) != QUESTS.GOAL_FLEET_VESSEL \
|
|
|
|
|
or String(before_trail.goal.vessel) != GameState.VESSEL_DISTORDER:
|
|
|
|
|
return _fail("Chart 150 is preceded by %s, not the Energy Propulser vessel quest" \
|
|
|
|
|
% before_trail.get("id", "nothing"))
|
|
|
|
|
if not "Energy Propulser" in String(before_trail.objective):
|
|
|
|
|
return _fail("the propulsion quest does not name the Energy Propulser to the player")
|
|
|
|
|
print("the Energy Propulser vessel quest leads directly into Chart 150")
|
|
|
|
|
|
#145, #144, #143: a quest that could not be finished, and two boxes that overhung
#145 - the extractor quest could never be completed. Extractors are placed
onto a planet from space and live in GameState.extractors, with their own
position, stock and id; they are not in `buildings`. So the goal asked
has_building_of_type("extractor"), which is false however many the player
builds. Not the death the reporter guessed at - the quest was simply asking
a question that had no yes.
structure_exists() is the question anything wanting "does the player have
one of these" should ask, and the quest model asks it now. The regression
test builds every structure quest's target THE WAY THE GAME BUILDS IT and
watches the goal flip, rather than knowing which array each one lands in -
the knowing is what got this wrong in the first place.
#144 - the build menu's status line is anchored to the top of the panel and
its grid to the panel's centre, so the gap between the two is half the panel
height. It was a written-down constant, correct only at 1280 tall; at 1080
the grid started four pixels ABOVE the status line and drew through it.
Nothing about that geometry is written against a screen size any more. The
band is measured from the header that is actually there, the pitch closes up
to fit it (never below a row's own height, which is what makes overlap
impossible rather than unlikely), and the column count rises until the rows
fit - so a short window gets a wide grid instead of one running off the
bottom. Ten structures now lay out as 5 columns at 900px tall, 4 at 1080, 3
at 1280 and 2 at 1600, clearing the header at every one, which is what the
test walks.
#143 - a hangar caption is "Gas Bomber\n2 available": two lines before
autowrap and three after it in a narrow column, at a size fixed in the
scene. Labels do not clip by default, so the third line was drawn over the
icon of the row below. The size is fitted to the box the layout works out,
and the box clips - the first keeps it legible, the second keeps it correct
however the vessel names grow.
Its box also overhung by exactly HANGAR_LABEL_GAP: the layout subtracted one
gap where it needed two, above the caption and below it. Two pixels, but two
pixels inside the next row's icon.
2026-08-29 12:51:09 +02:00
|
|
|
# --- R. #145: every structure quest can actually be finished ---------
|
|
|
|
|
# The extractor quest could not be. Extractors are placed onto a planet
|
|
|
|
|
# from space and live in their own array, not in `buildings`, so the
|
|
|
|
|
# question the goal asked ("is there a building of this type") was one
|
|
|
|
|
# that could never be answered yes however many were built.
|
|
|
|
|
#
|
|
|
|
|
# Checked by BUILDING each one the way the game builds it and watching
|
|
|
|
|
# the goal flip, rather than by knowing which array it lands in - the
|
|
|
|
|
# knowing is what got this wrong in the first place.
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var build_quest: Dictionary = QUESTS.at(i)
|
|
|
|
|
if String(build_quest.goal.kind) != QUESTS.GOAL_BUILDING:
|
|
|
|
|
continue
|
|
|
|
|
var type: String = String(build_quest.goal.building)
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
if game_state.quest_goal_met(build_quest):
|
|
|
|
|
return _fail("quest %s counts as done with nothing built at all" % build_quest.id)
|
|
|
|
|
if type == GameState.BUILDING_EXTRACTOR:
|
|
|
|
|
game_state.add_extractor("Quest Test", Vector2.ZERO, GameState.RESOURCE_ORE,
|
|
|
|
|
"quest:costs")
|
|
|
|
|
else:
|
|
|
|
|
game_state.add_building(type, Vector2.ZERO)
|
|
|
|
|
if not game_state.quest_goal_met(build_quest):
|
|
|
|
|
return _fail("building a %s does not finish quest %s - it cannot be completed at all" \
|
|
|
|
|
% [type, build_quest.id])
|
|
|
|
|
game_state.buildings.clear()
|
|
|
|
|
game_state.extractors.clear()
|
|
|
|
|
print("every structure quest is finished by building the thing it asks for")
|
|
|
|
|
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
# --- P. #142: enough rocks before anything is asked to be built ------
|
|
|
|
|
# The first structure the chain asks for used to arrive straight after a
|
|
|
|
|
# five-ore errand, costing four times that, with nothing in between
|
|
|
|
|
# saying so.
|
|
|
|
|
var first_build: int = -1
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
if String(QUESTS.at(i).goal.kind) == QUESTS.GOAL_BUILDING:
|
|
|
|
|
first_build = i
|
|
|
|
|
break
|
|
|
|
|
if first_build <= 0:
|
|
|
|
|
return _fail("the chain opens on a build quest, with nothing gathered first")
|
|
|
|
|
var build_type: String = String(QUESTS.at(first_build).goal.building)
|
|
|
|
|
var build_price: int = int(game_state.building_costs(build_type).get(GameState.RESOURCE_ORE, 0))
|
|
|
|
|
var gathered: int = 0
|
|
|
|
|
for i in range(first_build):
|
|
|
|
|
var goal_before: Dictionary = QUESTS.at(i).goal
|
|
|
|
|
if String(goal_before.kind) == QUESTS.GOAL_RESOURCE \
|
|
|
|
|
and String(goal_before.resource) == GameState.RESOURCE_ORE:
|
|
|
|
|
gathered = maxi(gathered, int(goal_before.amount))
|
|
|
|
|
if gathered < build_price:
|
|
|
|
|
return _fail("the chain asks for a %s costing %d ore, having only ever asked for %d" \
|
|
|
|
|
% [build_type, build_price, gathered])
|
|
|
|
|
# ...and the pile has to FIT. Before the first structure exists there is
|
|
|
|
|
# nowhere to bank anything, so the fleet's own hold is the only store
|
|
|
|
|
# there is - and a pile larger than it would be a quest that cannot be
|
|
|
|
|
# finished at all. It currently fits exactly, with no slack, so this is
|
|
|
|
|
# the assertion that says so out loud rather than leaving it to be
|
|
|
|
|
# discovered by a player.
|
|
|
|
|
var starting: PackedStringArray = PackedStringArray()
|
|
|
|
|
for i in range(GameState.STARTING_SHIP_COUNT):
|
|
|
|
|
starting.append(GameState.VESSEL_INTERCEPTOR)
|
|
|
|
|
var hold: int = game_state.fleet_cargo_capacity(starting)
|
|
|
|
|
if gathered > hold:
|
|
|
|
|
return _fail("the chain asks for %d ore before anything can bank it, and the starting fleet holds %d" \
|
|
|
|
|
% [gathered, hold])
|
|
|
|
|
print("%d ore is asked for before the %s that costs %d, and the opening hold carries %d" \
|
|
|
|
|
% [gathered, build_type, build_price, hold])
|
|
|
|
|
|
#157, #158, #159: pay from the hold you flew in, and nothing is exempt from coming down
#157 was already satisfied - second_hull is quest 7 and extractor is quest
11 - so this pins the property rather than adding a quest: nothing costing
more than a single hold may be asked for before the chain has grown the
fleet or taught banking. It fails if either is ever moved behind it.
#158 - a building's local costs may now be paid out of the fleet's hold.
#12 made production cost resources physically DELIVERED rather than money
out of the bank, and that stands: the bank is still not raided here. The
hold is simply the other place resources physically are, and making the
player park, open the transfer menu and move cargo across by hand was
ceremony rather than a decision.
But only WHERE THE FLEET IS, and that qualifier is the whole design. The
first draft had no range on it and test_manual_supply_route caught it
immediately: a crafter three thousand units away reached into the hold and
spent ore the player had just withdrawn to carry somewhere else - which is
exactly the complaint that got automatic banking removed in #24, arriving
from the other direction. The range is Building.interact_radius, the same
distance that decides whether the transfer button appears, so the rule is
not a hidden one: if you could hand the cargo over, the building can take
it.
#159 - the mothership floored at 1hp, so a battered one sat there reading as
dead and never exploded. That exemption existed to prevent a soft-lock, and
the soft-lock turns out not to exist: the bank is GameState.bank_inventory
and the hangar is stored_vessels, both of which outlive any building; the
mothership is the one structure research never gates; it costs exactly what
a single interceptor's hold carries; and ore comes from asteroids, which do
not run out. So the way back is always mine, fly, rebuild - and that is what
test_building_health now asserts, in place of the exemption that used to
stand in for it.
2026-08-29 15:51:34 +02:00
|
|
|
# --- S. #157: a second hull before anything one hull cannot pay for --
|
|
|
|
|
# A structure costing more than a single hold cannot be bought in one
|
|
|
|
|
# trip, so the chain has to have grown the fleet - or taught banking -
|
|
|
|
|
# before it asks for one. Both are in front of the extractor today; this
|
|
|
|
|
# fails if either is ever moved behind it.
|
|
|
|
|
var one_hold: int = game_state.fleet_cargo_capacity(
|
|
|
|
|
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
|
|
|
|
var grew_fleet: int = -1
|
|
|
|
|
var taught_banking: int = -1
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var goal_s: Dictionary = QUESTS.at(i).goal
|
|
|
|
|
if String(goal_s.kind) == QUESTS.GOAL_FLEET_VESSEL and int(goal_s.count) > 1:
|
|
|
|
|
grew_fleet = i if grew_fleet < 0 else grew_fleet
|
|
|
|
|
if String(goal_s.kind) == QUESTS.GOAL_BANKED:
|
|
|
|
|
taught_banking = i if taught_banking < 0 else taught_banking
|
|
|
|
|
if String(goal_s.kind) != QUESTS.GOAL_BUILDING:
|
|
|
|
|
continue
|
|
|
|
|
var price: int = int(game_state.building_costs(String(goal_s.building)).get(
|
|
|
|
|
GameState.RESOURCE_ORE, 0))
|
|
|
|
|
if price <= one_hold:
|
|
|
|
|
continue
|
|
|
|
|
if grew_fleet < 0 and taught_banking < 0:
|
|
|
|
|
return _fail("quest %s wants a %s at %d ore, more than the %d one hull carries, and the chain has neither grown the fleet nor taught banking yet" \
|
|
|
|
|
% [QUESTS.at(i).id, goal_s.building, price, one_hold])
|
|
|
|
|
if grew_fleet < 0:
|
|
|
|
|
return _fail("no quest ever asks for a second hull")
|
|
|
|
|
print("a second hull at %d and banking at %d, both before anything costing more than one %d-unit hold" \
|
|
|
|
|
% [grew_fleet, taught_banking, one_hold])
|
|
|
|
|
|
Teach the crews and the berths the game now asks the player to keep
#216 and #219 turned two things into player work without saying so anywhere:
transporters are built and assigned by hand, and a berth in the fleet is a
structure that has to be supplied. Five quests now teach both.
For the hub: build a transporter at a crafter, then assign it. Two steps,
because they happen in different places and one quest covering both strands
whoever misses the second in front of a hub that does nothing. The closing
line warns that they wear out, since the alternative is finding out when a
route quietly stops.
For the fleet: research Berth Authority, build an office, PUT ORE IN IT, and
only then crew the berth. The stocking step comes before the vessel on
purpose - the other way to learn it is to watch a vessel you paid for be
destroyed.
And a campaign-breaking bug of my own: #219 moved capacity onto the office,
so "fly two vessels at once" - sixth in the chain, six quests before
anything that could hold a second - could not be finished at all. It sits
after the office now, and a new rule in the chain test refuses any quest
that asks for more hulls than the player can yet hold. Verified by putting
the bug back: it fails, naming both steps.
Also: the conquest quest still said "take every capture point", which #211
deleted from the game.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-06 14:43:35 +02:00
|
|
|
# --- T. a fleet the player cannot yet hold ---------------------------
|
|
|
|
|
# #219 moved fleet capacity onto the Fleet Office, and the chain went on
|
|
|
|
|
# asking for a second hull six quests before anything that could hold one
|
|
|
|
|
# - an unfinishable quest that stalls the whole campaign. Rule S already
|
|
|
|
|
# says a second hull comes before expensive structures; this says it
|
|
|
|
|
# comes AFTER the one that makes it possible at all.
|
|
|
|
|
var capacity_at: int = -1
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var goal_t: Dictionary = QUESTS.at(i).goal
|
|
|
|
|
if String(goal_t.kind) == QUESTS.GOAL_BUILDING \
|
|
|
|
|
and String(goal_t.building) == GameState.BUILDING_FLEET_OFFICE:
|
|
|
|
|
capacity_at = i
|
|
|
|
|
break
|
|
|
|
|
if capacity_at < 0:
|
|
|
|
|
return _fail("no quest ever builds a Fleet Office, so the fleet can never grow past %d" \
|
|
|
|
|
% GameState.BASE_FLEET_SIZE)
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var goal_v: Dictionary = QUESTS.at(i).goal
|
|
|
|
|
if String(goal_v.kind) != QUESTS.GOAL_FLEET_VESSEL:
|
|
|
|
|
continue
|
|
|
|
|
if int(goal_v.get("count", 1)) <= GameState.BASE_FLEET_SIZE:
|
|
|
|
|
continue
|
|
|
|
|
if i < capacity_at:
|
|
|
|
|
return _fail("quest %s wants %d vessels at step %d, but the fleet cannot hold more than %d until step %d builds a Fleet Office" \
|
|
|
|
|
% [QUESTS.at(i).id, int(goal_v.count), i, GameState.BASE_FLEET_SIZE, capacity_at])
|
|
|
|
|
print("the fleet is grown at %d, before any quest asks for more than %d vessels" \
|
|
|
|
|
% [capacity_at, GameState.BASE_FLEET_SIZE])
|
|
|
|
|
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
# --- Q. #141: research is taught before it is assumed ----------------
|
|
|
|
|
# The first structure in the chain that needs a doctrine must have that
|
|
|
|
|
# doctrine as a quest of its own first. Only the FIRST one: once the
|
|
|
|
|
# player has been into the tree, a mention in a brief is enough, and
|
|
|
|
|
# demanding a research quest per building would double the chain.
|
|
|
|
|
var taught: Dictionary = {}
|
|
|
|
|
var checked: bool = false
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
var goal_here: Dictionary = QUESTS.at(i).goal
|
|
|
|
|
if String(goal_here.kind) == QUESTS.GOAL_RESEARCH:
|
|
|
|
|
taught[String(goal_here.node)] = true
|
|
|
|
|
continue
|
|
|
|
|
if checked or String(goal_here.kind) != QUESTS.GOAL_BUILDING:
|
|
|
|
|
continue
|
|
|
|
|
var gate: String = String(game_state.research_node_for_building(String(goal_here.building)))
|
|
|
|
|
if gate == "":
|
|
|
|
|
continue
|
|
|
|
|
checked = true
|
|
|
|
|
if not taught.has(gate):
|
|
|
|
|
return _fail("the chain asks for a %s, gated behind \"%s\", without ever sending anyone to research it" \
|
|
|
|
|
% [goal_here.building, gate])
|
|
|
|
|
print("the first gated structure (%s) has its doctrine \"%s\" as a quest of its own" \
|
|
|
|
|
% [goal_here.building, gate])
|
|
|
|
|
if not checked:
|
|
|
|
|
return _fail("no quest asks for a structure that needs research at all")
|
|
|
|
|
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
# --- G. nothing asks for points before conquest ----------------------
|
|
|
|
|
var conquest_at: int = -1
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
if String(QUESTS.at(i).goal.kind) == QUESTS.GOAL_CONQUERED:
|
|
|
|
|
conquest_at = i
|
|
|
|
|
break
|
|
|
|
|
if conquest_at < 0:
|
|
|
|
|
return _fail("no quest ever conquers a planet, so research points are unreachable")
|
|
|
|
|
for i in range(conquest_at):
|
|
|
|
|
var quest: Dictionary = QUESTS.at(i)
|
|
|
|
|
if String(quest.goal.kind) != QUESTS.GOAL_BUILDING:
|
|
|
|
|
continue
|
|
|
|
|
var research: String = game_state.research_for_building(String(quest.goal.building))
|
|
|
|
|
if research != "" and _costs_research_points(research):
|
|
|
|
|
return _fail("quest %s needs %s, which costs research points, before any planet is taken" \
|
|
|
|
|
% [quest.id, research])
|
|
|
|
|
print("no quest needs research points before the conquest quest at %d" % conquest_at)
|
|
|
|
|
|
|
|
|
|
# --- H. not during the prologue --------------------------------------
|
|
|
|
|
game_state.reset_new_game()
|
|
|
|
|
if not game_state.intro_active:
|
|
|
|
|
return _fail("a new game is not in the prologue")
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_announced or not game_state.quest_messages.is_empty():
|
|
|
|
|
return _fail("a quest spoke up during the prologue")
|
|
|
|
|
game_state.intro_active = false
|
|
|
|
|
print("the chain holds its tongue while the prologue runs")
|
|
|
|
|
|
|
|
|
|
# --- D. announced before it can complete -----------------------------
|
|
|
|
|
game_state.bank_inventory.deposit(GameState.RESOURCE_ORE, 500)
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index != 0:
|
|
|
|
|
return _fail("the first quest completed before it was ever announced")
|
|
|
|
|
if game_state.quest_messages.is_empty():
|
|
|
|
|
return _fail("the first quest was not announced")
|
|
|
|
|
var brief: String = String(game_state.take_quest_messages()[0].line)
|
|
|
|
|
if brief != String(QUESTS.at(0).brief):
|
|
|
|
|
return _fail("the first quest announced something other than its own brief")
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index != 1:
|
|
|
|
|
return _fail("a satisfied quest did not complete on the frame after it was announced")
|
|
|
|
|
print("a quest is read out first, then completed: \"%s\"" % brief.substr(0, 48))
|
|
|
|
|
|
|
|
|
|
# --- C. and it stays complete ----------------------------------------
|
|
|
|
|
game_state.bank_inventory.withdraw(GameState.RESOURCE_ORE, 500)
|
|
|
|
|
if game_state.available_resource(GameState.RESOURCE_ORE) != 0:
|
|
|
|
|
return _fail("the test failed to spend the ore it just banked")
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index < 1:
|
|
|
|
|
return _fail("spending the ore put the chain back on the ore quest")
|
|
|
|
|
print("spending the ore did not un-complete the quest that asked for it")
|
|
|
|
|
|
|
|
|
|
# --- E. the whole chain walks ----------------------------------------
|
|
|
|
|
var guard: int = 0
|
|
|
|
|
while not game_state.quest_chain_complete() and guard < QUESTS.count() * 4:
|
|
|
|
|
guard += 1
|
|
|
|
|
game_state.complete_active_quest()
|
|
|
|
|
if not game_state.quest_chain_complete():
|
|
|
|
|
return _fail("the chain would not walk to its end")
|
|
|
|
|
if not game_state.active_quest().is_empty():
|
|
|
|
|
return _fail("there is still an active quest after the chain ended")
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
print("the chain walks to the end and stops there")
|
#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
|
|
|
|
|
|
|
|
# --- L. #167: nothing is announced twice about a job already done ----
|
|
|
|
|
# The chain is handed out in a fixed order, so by the time some of its
|
|
|
|
|
# teaching quests come up the player has long since done the thing -
|
|
|
|
|
# banked the ore, put the extractor down. Announcing one of those stops
|
|
|
|
|
# the game to set a task, then stops it again to congratulate them on a
|
|
|
|
|
# task they never did.
|
|
|
|
|
#
|
|
|
|
|
# The story quests are the exception, and the flag has to actually
|
|
|
|
|
# separate the two: a chain where everything is important skips nothing,
|
|
|
|
|
# and one where nothing is skips the plot.
|
|
|
|
|
var important: int = 0
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
if QUESTS.is_important(QUESTS.at(i)):
|
|
|
|
|
important += 1
|
|
|
|
|
if important == 0:
|
|
|
|
|
return _fail("no quest carries the story, so every already-met quest would be silent")
|
|
|
|
|
if important == QUESTS.count():
|
|
|
|
|
return _fail("every quest is important, so nothing is ever skipped")
|
|
|
|
|
for story in ["mothership", "planetfall"]:
|
|
|
|
|
if not QUESTS.is_important(QUESTS.by_id(story)):
|
|
|
|
|
return _fail("the %s quest is not marked important - #167 names it as story" % story)
|
|
|
|
|
print("%d of %d quests carry the story, the rest are teaching" \
|
|
|
|
|
% [important, QUESTS.count()])
|
|
|
|
|
|
|
|
|
|
game_state.reset_new_game()
|
|
|
|
|
game_state.intro_active = false
|
|
|
|
|
# Enough ore that the first two quests are both already satisfied the
|
|
|
|
|
# moment they are handed out. One is story, one is teaching.
|
|
|
|
|
game_state.bank_inventory.deposit(GameState.RESOURCE_ORE, 500)
|
|
|
|
|
if not QUESTS.is_important(QUESTS.at(0)) or QUESTS.is_important(QUESTS.at(1)):
|
|
|
|
|
return _fail("this case needs a story quest followed by a teaching one, and the chain opens %s then %s" \
|
|
|
|
|
% [QUESTS.at(0).id, QUESTS.at(1).id])
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index != 0 or game_state.quest_messages.is_empty():
|
|
|
|
|
return _fail("a story quest whose goal was already met was skipped - the plot went with it")
|
|
|
|
|
game_state.take_quest_messages()
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index != 1:
|
|
|
|
|
return _fail("the story quest did not close on the frame after it was announced")
|
|
|
|
|
game_state.take_quest_messages()
|
|
|
|
|
|
|
|
|
|
var skipped: String = String(QUESTS.at(1).id)
|
|
|
|
|
game_state._advance_quests()
|
|
|
|
|
if game_state.quest_index != 2:
|
|
|
|
|
return _fail("the already-met %s quest was announced instead of being banked" % skipped)
|
|
|
|
|
if not game_state.quest_messages.is_empty():
|
|
|
|
|
return _fail("the skipped %s quest still said \"%s\"" \
|
|
|
|
|
% [skipped, game_state.quest_messages[0].line])
|
|
|
|
|
# ...and it was CREDITED, not dropped. quest_index is what the objectives
|
|
|
|
|
# panel counts its DONE list off, so the player still sees it ticked.
|
|
|
|
|
if String(game_state.quest_at(game_state.quest_index - 1).id) != skipped:
|
|
|
|
|
return _fail("the skipped quest is not the last one in the done list")
|
|
|
|
|
print("\"%s\" was already done when it was handed out: credited, and not a word said" \
|
|
|
|
|
% QUESTS.by_id(skipped).title)
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
if _catalogue_cases():
|
|
|
|
|
return true
|
|
|
|
|
# --- E (second half). the ore quest, really collected --------
|
|
|
|
|
game_state.reset_new_game()
|
|
|
|
|
game_state.intro_active = false
|
|
|
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
current_scene = scene
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
panel = scene.get_node("UI/QuestPanel")
|
|
|
|
|
quest_button = scene.get_node("UI/QuestButton")
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
narration = scene.get_node("UI/NarrationBanner")
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
# The opening brief is up, which has stopped the game (#128).
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("the first quest never reached the banner")
|
|
|
|
|
if not paused:
|
|
|
|
|
return _fail("a quest line did not stop the game")
|
|
|
|
|
print("the first quest arrives as a dialog and stops the game")
|
|
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
|
|
|
|
if paused:
|
|
|
|
|
return _fail("acknowledging every line left the game stopped")
|
|
|
|
|
|
|
|
|
|
# --- the HUD ------------------------------------------------
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 2
|
|
|
|
|
return false
|
|
|
|
|
2:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
# #140: the objective lives behind a button next to the map's,
|
2026-09-03 20:05:33 +02:00
|
|
|
# not on the HUD - it used to sit on top of the dev button.
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
if panel.visible:
|
|
|
|
|
return _fail("the objective panel is open before anyone pressed anything")
|
|
|
|
|
if not quest_button.visible:
|
|
|
|
|
return _fail("there is no quest button to press")
|
|
|
|
|
var map_button: Control = scene.get_node("UI/MapButton")
|
|
|
|
|
if quest_button.get_rect().intersects(map_button.get_rect()):
|
|
|
|
|
return _fail("the quest button is drawn on top of the map button")
|
2026-09-03 20:05:33 +02:00
|
|
|
var dev_button: Control = scene.get_node("UI/DevMenuButton")
|
|
|
|
|
if quest_button.get_rect().intersects(dev_button.get_rect()):
|
|
|
|
|
return _fail("the quest button is drawn on top of the dev button")
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
if absf(quest_button.get_rect().get_center().y - map_button.get_rect().get_center().y) > 1.0:
|
|
|
|
|
return _fail("the quest button does not sit beside the map button")
|
|
|
|
|
panel.open()
|
|
|
|
|
if not panel.visible:
|
|
|
|
|
return _fail("pressing the quest button opened nothing")
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
var quest: Dictionary = game_state.active_quest()
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
if panel.quest_label.text != String(quest.title):
|
|
|
|
|
return _fail("the panel shows \"%s\", the quest is \"%s\"" \
|
|
|
|
|
% [panel.quest_label.text, quest.title])
|
|
|
|
|
if panel.objective_label.text != String(quest.objective):
|
|
|
|
|
return _fail("the panel does not show the objective")
|
|
|
|
|
if panel.progress != Vector2i(0, 5):
|
|
|
|
|
return _fail("the panel reads %s, expected 0 of 5" % panel.progress)
|
2026-09-03 20:05:33 +02:00
|
|
|
print("quest button beside the map, clear of the dev button; panel: \"%s\" - %s (%d/%d)" \
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
% [panel.quest_label.text, panel.objective_label.text,
|
|
|
|
|
panel.progress.x, panel.progress.y])
|
|
|
|
|
panel.close()
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
|
|
|
|
|
# Collect what the quest asked for, the way the game does.
|
|
|
|
|
game_state.collect_cargo(5)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 3
|
|
|
|
|
return false
|
|
|
|
|
3:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
# #142: five ore finishes the first errand and starts the
|
|
|
|
|
# stockpile, rather than jumping straight to a structure costing
|
|
|
|
|
# four times what was just asked for.
|
|
|
|
|
if String(game_state.active_quest().get("id", "")) != "stockpile":
|
|
|
|
|
return _fail("collecting 5 ore did not finish the first ore quest (on %s)" \
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
% game_state.active_quest().get("id", "<none>"))
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
print("5 ore done; the chain asks for the pile before the mothership")
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
#141, #142: the chain teaches research, and asks for the pile before the build
Two gaps between the first quests, both of the same kind: a step the chain
assumed rather than asked for.
#142 - "collect 5 ore" was followed straight by "build the mothership",
which costs twenty. The first thing the chain ever asked for was four times
what it had just taught, with nothing in between saying so. A stockpile
quest sits between them now.
#141 - the crafter quest's brief said "spend ore on the Shipyards doctrine,
then put a ship crafter down", which is two unrelated things off one line:
open a menu the player has never opened, and place a structure. Researching
Shipyards is its own quest now, and it is the one that says where the
research tree lives. Every later quest that needs a doctrine still only
mentions it in passing - once is enough.
That needed a goal kind for researching a doctrine, which the model did not
have.
Both are pinned as properties rather than as positions in a list. The chain
must ask for at least as much ore as the first structure it wants costs; and
the first structure that is gated behind a doctrine must have that doctrine
as a quest of its own. Only the first - demanding one per building would
double the chain for no teaching.
One thing that fell out of writing those: the pile is exactly the starting
fleet's hold. An interceptor carries 20 and the mothership costs 20, and
before the mothership exists there is nowhere to bank anything, so the hold
is the only store there is. It fits with no slack at all, so there is now an
assertion saying so - a pile larger than the hold would be a quest that
cannot be finished, and nothing else would have caught it.
2026-08-28 15:47:38 +02:00
|
|
|
game_state.collect_cargo(15)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 31
|
|
|
|
|
return false
|
|
|
|
|
31:
|
|
|
|
|
# A quest completing puts its line up, and the next quest's brief
|
|
|
|
|
# behind it - and each of those stops the tree (#128), which is
|
|
|
|
|
# what stops the goal below from ever being checked. Clicked
|
|
|
|
|
# through the way a player does, rather than waited out.
|
|
|
|
|
if narration.is_speaking():
|
|
|
|
|
# Seen here rather than asserted after the fact: by the time
|
|
|
|
|
# the chain has moved on, the lines it said are gone.
|
|
|
|
|
spoke = true
|
|
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
return false
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if String(game_state.active_quest().get("id", "")) != "mothership":
|
|
|
|
|
return _fail("collecting the full 20 ore did not finish the stockpile (on %s, ore=%d, hold cap=%d)" \
|
|
|
|
|
% [game_state.active_quest().get("id", "<none>"),
|
|
|
|
|
game_state.available_resource(GameState.RESOURCE_ORE),
|
|
|
|
|
game_state.fleet_inventory.total_capacity])
|
|
|
|
|
if not spoke:
|
|
|
|
|
return _fail("finishing a quest said nothing")
|
|
|
|
|
print("20 ore collected: the chain moved on to \"%s\"" \
|
|
|
|
|
% game_state.active_quest().title)
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
|
|
|
|
|
# --- I. a line said with no banner alive --------------------
|
|
|
|
|
scene.free()
|
|
|
|
|
scene = null
|
|
|
|
|
game_state.complete_active_quest()
|
|
|
|
|
if game_state.quest_messages.is_empty():
|
|
|
|
|
return _fail("a quest finished with no banner and dropped its line")
|
|
|
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
current_scene = scene
|
|
|
|
|
narration = scene.get_node("UI/NarrationBanner")
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 4
|
|
|
|
|
return false
|
|
|
|
|
4:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("the line queued with no banner alive was never said")
|
|
|
|
|
print("a quest finished off-scene is read out by the next banner that exists")
|
|
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# --- J. and the same on a planet surface ---------------------
|
|
|
|
|
scene.free()
|
|
|
|
|
game_state.planet_name = "Quest Test"
|
|
|
|
|
game_state.planet_id = "quest:test"
|
|
|
|
|
game_state.planet_definition_id = "rocky"
|
|
|
|
|
scene = load("res://surface/PlanetSurface.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
current_scene = scene
|
|
|
|
|
narration = scene.get_node_or_null("UI/NarrationBanner")
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
panel = scene.get_node_or_null("UI/QuestPanel")
|
|
|
|
|
quest_button = scene.get_node_or_null("UI/QuestButton")
|
|
|
|
|
if narration == null or panel == null or quest_button == null:
|
|
|
|
|
return _fail("a planet surface has no banner and no objective panel")
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
game_state.complete_active_quest()
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 5
|
|
|
|
|
return false
|
|
|
|
|
5:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("a quest finished on a surface said nothing down there")
|
|
|
|
|
print("a quest finished on a surface is announced on the surface")
|
|
|
|
|
# The tracker is an ordinary HUD node, so it is frozen along with
|
|
|
|
|
# everything else behind a dialog (#128) and comes up as the line
|
|
|
|
|
# closes. Acknowledged first, then looked at.
|
|
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 6
|
|
|
|
|
return false
|
|
|
|
|
6:
|
|
|
|
|
if elapsed < 0.3:
|
|
|
|
|
return false
|
#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on
top of the dev wave button - and there is no free strip of that edge to move
it to, because everything down it is already something. So the objective
stops living on the HUD: a quest button beside the map's, and a panel when
you press it.
The panel shows the current objective, its progress, and the road behind it.
Deliberately NOT what comes next - the chain is meant to open the game up a
step at a time (#133), and a list of everything still to come is that
discovery given away on the first press.
It owns its own button, the way WorldMap owns the map button rather than
being wired through BuildController, and it hides that button during the
prologue for the same reason and in the same two places (at _ready as well
as in _process, since #128 means a dialog stops the tree before the first
_process frame ever comes).
Both scenes carry it. On a planet surface there is no map button to sit
beside, so it goes under the planet's name.
The test now checks the button is beside the map's and clear of the wave
button by rectangle rather than by eye, which is the bug the ticket reported.
2026-08-28 15:37:13 +02:00
|
|
|
if not quest_button.visible:
|
|
|
|
|
return _fail("the objective button is missing on a planet surface")
|
|
|
|
|
panel.open()
|
|
|
|
|
if not panel.visible or panel.quest_label.text.strip_edges() == "":
|
|
|
|
|
return _fail("the objective panel says nothing on a planet surface")
|
|
|
|
|
print("the surface carries the objective too: \"%s\"" % panel.quest_label.text)
|
|
|
|
|
panel.close()
|
#129 (#131, #132, #133): a quest chain, and a World Ship out of reach
Sixteen quests that walk a new game through the things it contains: ore,
the mothership, exploration, a yard and a second hull, planetfall and
conquest, extractors, gas, freight, power, the lab, a new vessel type, an
outpost, and a survey.
The chain turns on the second quest. The fleet builds the mothership to
call the World Ship and hears nothing - and the array is not at fault. The
breach put them further out than that signal will carry, so every quest
after it is a search that has to be flown rather than transmitted.
Three pieces:
Quests.gd the chain, as data and nothing else. Two rules it obeys:
nothing may ask for something unreachable - research points
come only from conquering a planet, so every doctrine that
costs them sits after the conquest quest - and a quest names
one goal, with the research it implies said in the brief
rather than split into an errand of its own.
GameState how far along the chain the player is, and the answer to each
goal kind. Progress latches for free: the chain is walked one
quest at a time, so collecting five ore and spending it cannot
un-complete the quest that asked for five - by then nothing is
asking. It lives here so a quest finished on a planet surface
is still finished after take-off.
the HUD QuestTracker, the standing objective, in the wave alert's
idiom; QuestDirector, which reads the lines out.
Lines are QUEUED by GameState and drained by the director rather than
emitted at a banner, because quests advance in the autoload, which runs on
a planet surface too - where a line emitted straight at a banner would be
lost. Both scenes now carry a banner, a tracker and a director, so a quest
finished underground is announced underground.
The chain is armed by reset_new_game(), the same gate the prologue uses -
the only door into a real game - so instantiating a scene is not by itself
a reason to start briefing somebody.
2026-08-28 14:41:03 +02:00
|
|
|
|
|
|
|
|
# --- K. landing is recorded ----------------------------------
|
|
|
|
|
game_state.reset_quests()
|
|
|
|
|
game_state.note_planet_visited("quest:one", "rocky")
|
|
|
|
|
game_state.note_planet_visited("quest:one", "rocky")
|
|
|
|
|
game_state.note_planet_visited("quest:two", "gas_giant")
|
|
|
|
|
if game_state.visited_planets.size() != 2:
|
|
|
|
|
return _fail("landing twice on one planet counted as two worlds")
|
|
|
|
|
if game_state.visited_planet_kinds.size() != 2:
|
|
|
|
|
return _fail("two kinds of world were not recorded as two kinds")
|
|
|
|
|
var kinds: Dictionary = {"kind": QUESTS.GOAL_PLANET_KINDS, "count": 3}
|
|
|
|
|
if game_state.quest_goal_met({"goal": kinds}):
|
|
|
|
|
return _fail("a three-world survey passed on two worlds")
|
|
|
|
|
game_state.note_planet_visited("quest:three", "research_lab")
|
|
|
|
|
if not game_state.quest_goal_met({"goal": kinds}):
|
|
|
|
|
return _fail("a three-world survey did not pass on three kinds of world")
|
|
|
|
|
print("landings recorded: %d planets across %d kinds of world" \
|
|
|
|
|
% [game_state.visited_planets.size(), game_state.visited_planet_kinds.size()])
|
|
|
|
|
|
|
|
|
|
print("TEST_QUEST_CHAIN: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|