#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
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
const QUESTS = preload("res://core/Quests.gd")
|
|
|
|
|
|
|
|
|
|
var game_state: Node
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
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])
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
# --- 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")
|
|
|
|
|
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,
|
|
|
|
|
# not on the HUD - it used to sit on top of the wave button.
|
|
|
|
|
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")
|
|
|
|
|
var wave_button: Control = scene.get_node("UI/DevWaveButton")
|
|
|
|
|
if quest_button.get_rect().intersects(wave_button.get_rect()):
|
|
|
|
|
return _fail("the quest button is drawn on top of the wave button")
|
|
|
|
|
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)
|
|
|
|
|
print("quest button beside the map, clear of the wave button; panel: \"%s\" - %s (%d/%d)" \
|
|
|
|
|
% [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
|
|
|
|
|
if String(game_state.active_quest().get("id", "")) != "mothership":
|
|
|
|
|
return _fail("collecting 5 ore did not finish the ore quest (on %s)" \
|
|
|
|
|
% game_state.active_quest().get("id", "<none>"))
|
|
|
|
|
if not narration.is_speaking():
|
|
|
|
|
return _fail("finishing a quest said nothing")
|
|
|
|
|
print("5 ore collected: the chain moved on to \"%s\"" \
|
|
|
|
|
% game_state.active_quest().title)
|
|
|
|
|
while narration.dismiss():
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# --- 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
|