#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
625 lines
28 KiB
GDScript
625 lines
28 KiB
GDScript
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
|
|
# 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
|
|
# R. #145 - every structure quest is actually finishable: building the
|
|
# thing it asks for, the way the game builds it, completes it
|
|
# S. #157 - nothing costing more than a single hold is asked for before
|
|
# the chain has grown the fleet or taught banking
|
|
# 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
|
|
# is announced down there, not on the way back up, and the objective is
|
|
# reachable from a button there as well
|
|
# K. landing is recorded, both the planet and the kind of world it was
|
|
# 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
|
|
|
|
const QUESTS = preload("res://core/Quests.gd")
|
|
|
|
var game_state: Node
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
var spoke: bool = false
|
|
var scene: Node
|
|
var panel: Control
|
|
var quest_button: Control
|
|
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])
|
|
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])
|
|
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")
|
|
|
|
# #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")
|
|
|
|
# --- 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")
|
|
|
|
# --- 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])
|
|
|
|
# --- 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])
|
|
|
|
# --- 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])
|
|
|
|
# --- 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")
|
|
|
|
# --- 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")
|
|
|
|
# --- 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)
|
|
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
|
|
panel = scene.get_node("UI/QuestPanel")
|
|
quest_button = scene.get_node("UI/QuestButton")
|
|
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 lives behind a button next to the map's,
|
|
# not on the HUD - it used to sit on top of the dev 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 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")
|
|
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")
|
|
var quest: Dictionary = game_state.active_quest()
|
|
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 dev button; panel: \"%s\" - %s (%d/%d)" \
|
|
% [panel.quest_label.text, panel.objective_label.text,
|
|
panel.progress.x, panel.progress.y])
|
|
panel.close()
|
|
|
|
# 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
|
|
# #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)" \
|
|
% game_state.active_quest().get("id", "<none>"))
|
|
print("5 ore done; the chain asks for the pile before the mothership")
|
|
while narration.dismiss():
|
|
pass
|
|
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)
|
|
|
|
# --- 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")
|
|
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")
|
|
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
|
|
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()
|
|
|
|
# --- 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
|