Teach where research data comes from, and say so when it runs out

The scanner quest moves ahead of the gas burner and gains the hunt it exists
for: a research lab world, which pays several times what an ordinary planet
does and is the only reliable source of research data. Finding one is what
the scanner is FOR, so the conquest record now remembers which kind of world
was taken and a quest can ask for that kind.

Bombers get a pair of quests after the transporters. That pulled the ship lab
up with them - a vessel is unlocked AT the lab, so asking for a bomber before
one exists would have been unfinishable, the same trap second_hull fell into.

And both research screens now explain a shortfall instead of hiding it. The
skill tree refused a doctrine in silence; the ship lab said "already queued
or complete", which is not why it refused and sends the player to look at the
wrong thing. Both open an explainer naming what was wanted, what is held, and
where data actually comes from.

#192's rule - a tool is taught immediately before the quest that needs it -
now points at the scanner's FIRST use rather than its only one, with the
world survey still required to follow it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-06 16:24:09 +02:00
commit 8462099068
8 changed files with 327 additions and 26 deletions

View file

@ -0,0 +1,132 @@
extends SceneTree
# A player who is short of research data is told where it comes from.
#
# It is the one currency with a non-obvious source: ore and gas are visibly
# mined, but data comes off conquered worlds and nothing said so at the point
# it mattered. The skill tree refused the node in SILENCE, and the ship lab
# refused with "Research is already queued or complete" - which is not why it
# refused, and sends the player looking at the wrong thing.
#
# Covers:
# A. a doctrine that costs data the player has not got opens the explainer,
# naming what was wanted and what they hold
# B. the same from the ship lab, instead of blaming the queue
# C. with the data in hand, the research goes through and no popup appears
var game_state: Node
var scene: Node
var controller: Node
var popup: Control
var elapsed: float = 0.0
var stage: int = 0
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
game_state.reset_new_game()
game_state.intro_active = false
game_state.reset_quests()
game_state.rocks = 5000
game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 5000)
game_state.bank_inventory.set_amount(GameState.RESOURCE_ENERGY, 5000)
game_state.research_points = 0
game_state.unlocked = ["shipyards"]
scene = load("res://world/Main.tscn").instantiate()
root.add_child(scene)
current_scene = scene
controller = scene.get_node("BuildController")
popup = scene.get_node("UI/ResearchPointsMenu")
func _costly_doctrine() -> String:
for node in GameState.SKILL_TREE:
if int(node.get("research_points", 0)) > 0:
return String(node.id)
return ""
func _process(delta: float) -> bool:
elapsed += delta
if elapsed < 0.4:
return false
match stage:
0:
var doctrine: String = _costly_doctrine()
if doctrine == "":
return _fail("no doctrine costs research data, so this proves nothing")
# Everything but the data: the prerequisite met and the ore paid,
# so data is unambiguously the thing that is missing.
game_state.unlocked = ["shipyards"]
var requires: String = String(game_state.skill_node(doctrine).get("requires", ""))
if requires != "" and not game_state.is_unlocked(requires):
game_state.unlocked.append(requires)
game_state.research_points = 0
var mothership_id: int = game_state.add_building(
GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
controller.skill_menu.building_id = mothership_id
if popup.visible:
return _fail("the explainer was open before anything was refused")
controller._on_unlock_requested(doctrine)
if not popup.visible:
return _fail("a doctrine costing data was refused with no explanation")
if game_state.is_unlocked(doctrine):
return _fail("the doctrine was researched with no data at all")
var said: String = popup.have_label.text
if not said.contains("YOU HAVE 0"):
return _fail("the explainer does not say what the player holds: '%s'" % said)
if not popup.detail_label.text.to_lower().contains("scanner"):
return _fail("the explainer never mentions how to find a lab world")
print("skill tree: refused and explained - '%s'" % said)
popup.close_menu()
# --- C. with the data, it goes through, quietly ---------------
game_state.research_points = 20
controller._on_unlock_requested(doctrine)
if not game_state.is_unlocked(doctrine):
return _fail("the doctrine was refused with data in hand")
if popup.visible:
return _fail("the explainer opened on a research that succeeded")
print("with data in hand it goes through, and says nothing")
elapsed = 0.0
stage = 1
return false
1:
# --- B. the lab explains too, rather than blaming the queue ---
game_state.research_points = 0
var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB,
Vector2(600.0, 0.0))
controller.lab_building_id = lab_id
controller.lab_vessel = GameState.VESSEL_INTERCEPTOR
controller.lab_choice = 0
var options: Array = controller._lab_options()
if options.is_empty():
return _fail("the lab offers nothing, so this proves nothing")
# Pick an option that actually costs data.
for i in range(options.size()):
if game_state.research_point_cost(String(options[i].id)) > 0:
controller.lab_choice = i
break
if game_state.research_point_cost(String(options[controller.lab_choice].id)) <= 0:
return _fail("no lab option costs research data, so this proves nothing")
controller._on_lab_buy()
if not popup.visible:
return _fail("the lab refused for want of data with no explanation")
if controller.lab_menu.status.text.contains("already queued"):
return _fail("the lab still blames the queue: '%s'" % controller.lab_menu.status.text)
print("ship lab: refused and explained, not blamed on the queue")
print("TEST_RESEARCH_POINTS_EXPLAINER: PASS")
quit(0)
return true
return false
func _fail(message: String) -> bool:
print("TEST_RESEARCH_POINTS_EXPLAINER: FAIL (%s)" % message)
quit(1)
return true