spaceship/tests/test_planet_scanner.gd
Loïc Wisniewski 8462099068 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
2026-09-06 16:24:09 +02:00

206 lines
9.1 KiB
GDScript

extends SceneTree
# Focused coverage for #192-#196: the scanner progression, its scene-owned
# menu, the one hostile region and the ambient enemies that inhabit it.
const QUESTS = preload("res://core/Quests.gd")
class FakeFleet:
extends Node2D
var ships: Array = [Node2D.new()]
func get_centroid() -> Vector2:
return position
func _initialize() -> void:
_run.call_deferred()
func _run() -> void:
var state: Node = root.get_node("GameState")
state.reset_new_game()
state.intro_active = false
state.reset_quests(false)
# #194: exact, finite hostile geography instead of an endless rare roll.
WorldGen.dev_planets_enabled = false
var boss_count: int = 0
for x in range(-40, 41):
for y in range(-40, 41):
var chunk := Vector2i(x, y)
if WorldGen.has_planet_at_chunk(chunk) \
and String(WorldGen.planet_data_at_chunk(chunk).definition_id) \
== PlanetDefinitions.BOSS_ID:
boss_count += 1
if boss_count != 1:
return _fail("normal generation contains %d boss planets, expected exactly one" % boss_count)
if WorldGen.biome_at_chunk(WorldGen.OCTOPUS_BIOME_CENTER) \
!= WorldGen.Biome.OCTOPUS:
return _fail("the unique boss is not inside the octopus biome")
if WorldGen.octopus_biome_chunks().size() < 5:
return _fail("the octopus biome is not a multi-chunk region")
for chunk in WorldGen.octopus_biome_chunks():
if WorldGen.biome_at_chunk(chunk) != WorldGen.Biome.OCTOPUS:
return _fail("declared octopus chunk %s has another biome" % chunk)
if chunk != WorldGen.OCTOPUS_BIOME_CENTER \
and WorldGen.has_planet_at_chunk(chunk):
return _fail("the hostile region contains an extra planet at %s" % chunk)
print("one finite nine-chunk octopus biome contains the one normal boss planet")
# #193/#195: research gate, real generated scan results and boss gating.
if state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
return _fail("planet scanner is available without research")
state.unlocked.append("coordination")
state.bank_inventory.deposit(GameState.RESOURCE_ORE, 50)
state.bank_inventory.deposit(GameState.RESOURCE_GAS, 15)
state.research_points = 1
if not state.unlock("deep_space_scanning") \
or not state.is_building_unlocked(GameState.BUILDING_PLANET_SCANNER):
return _fail("Deep-Space Scanning did not unlock its building")
state.add_building(GameState.BUILDING_PLANET_SCANNER, Vector2.ZERO)
var explored_before_scan: Dictionary = state.explored_chunks.duplicate()
var rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
Vector2.ZERO)
if rocky.is_empty() or String(rocky.definition_id) != PlanetDefinitions.ROCKY_ID:
return _fail("the scanner did not find a real rocky planet")
if explored_before_scan.has(Vector2i(rocky.chunk)):
return _fail("the scanner test's first target was already explored")
if not state.explored_chunks.has(Vector2i(rocky.chunk)):
return _fail("a scan result was not revealed on the persistent map")
if String(state.active_scanner_target().planet_id) != String(rocky.planet_id):
return _fail("the revealed planet was not retained as navigation target")
if not _is_nearest_unconquered(state, rocky, PlanetDefinitions.ROCKY_ID):
return _fail("the scanner did not select the physically closest rocky planet")
# A conquered nearest match must be skipped on the next scan.
state.planet_capture[String(rocky.planet_id)] = {
"captured": [], "conquered": true,
}
var next_rocky: Dictionary = state.scan_for_planet(PlanetDefinitions.ROCKY_ID,
Vector2.ZERO)
if next_rocky.is_empty() or String(next_rocky.planet_id) == String(rocky.planet_id) \
or state.is_planet_conquered(String(next_rocky.planet_id)):
return _fail("scanner reused the nearest conquered rocky planet")
print("scanner found the nearest unvisited world and skipped it after conquest")
if PlanetDefinitions.BOSS_ID in state.scanner_available_planet_types():
return _fail("boss signature is available before the four-world survey")
var menu: Control = load("res://ui/menus/ScannerMenu.tscn").instantiate()
root.add_child(menu)
menu.open_menu()
if menu.get_node("BossButton").visible:
return _fail("scanner menu shows the boss before the survey")
for definition_id in GameState.SCANNER_SURVEY_TYPES:
state.note_planet_visited("scanner:test:%s" % definition_id, definition_id)
menu.refresh()
if not menu.get_node("BossButton").visible \
or not menu.get_node("BossLabel").visible:
return _fail("the fifth scanner option did not appear after four planet types")
var boss: Dictionary = state.scan_for_planet(PlanetDefinitions.BOSS_ID,
Vector2.ZERO)
if boss.is_empty() or Vector2i(boss.chunk) != WorldGen.OCTOPUS_BIOME_CENTER:
return _fail("boss scan did not resolve the unique hostile region")
var map_content: Node2D = load("res://world/WorldMapContent.gd").new()
var marker: Dictionary = map_content.scanner_target_marker()
if marker.is_empty() or String(marker.planet_id) != String(boss.planet_id) \
or Vector2(marker.position).distance_to(
Vector2(boss.world_position) * map_content.WORLD_TO_LOCAL) > 0.01:
return _fail("the active scan has no matching destination point on the map")
map_content.free()
print("researched scanner reveals generated destinations; the fifth option waits for 4/4")
# #192: the scanner is taught immediately before the quest that needs it,
# and before every later one that does.
#
# That used to mean the world survey, which was the only thing it served.
# The scanner now also finds research lab worlds - the only reliable
# source of research data - and THAT is taught first, so the adjacency
# moved with it. The rule is unchanged in substance: a tool is taught
# where it is first wanted, not filed away several quests early.
var scanner_quest_at: int = -1
var lab_world_at: int = -1
var worlds_at: int = -1
for i in range(QUESTS.count()):
match String(QUESTS.at(i).id):
"planet_scanner": scanner_quest_at = i
"lab_world": lab_world_at = i
"worlds": worlds_at = i
if scanner_quest_at < 0:
return _fail("nothing in the chain teaches the scanner")
if lab_world_at != scanner_quest_at + 1:
return _fail("the scanner is not taught directly before the lab-world hunt that needs it")
if worlds_at < scanner_quest_at:
return _fail("the world survey comes before the scanner that serves it")
if int(QUESTS.at(worlds_at).goal.count) != 4:
return _fail("world survey does not require four planet types")
print("the quest chain teaches the scanner immediately before the four-world survey")
# #196: entering/leaving the biome owns a bounded roaming population.
var host := Node2D.new()
root.add_child(host)
var fake_fleet := FakeFleet.new()
fake_fleet.name = "Fleet"
fake_fleet.position = Vector2.ZERO
fake_fleet.add_to_group("player_fleet")
host.add_child(fake_fleet)
menu.close_menu()
var guidance: Control = load("res://ui/ScannerGuidance.tscn").instantiate()
root.add_child(guidance)
guidance._process(0.0)
if not guidance.visible or not guidance.get_node("TargetLabel").text.contains(
String(boss.name).to_upper()):
return _fail("the flight HUD did not point toward the scanned destination")
var indicator: Control = guidance.get_node("Indicator")
var indicator_center: Vector2 = indicator.position + indicator.size * 0.5
var viewport_size: Vector2 = guidance.get_viewport_rect().size
if indicator_center.x < 20.0 or indicator_center.y < 20.0 \
or indicator_center.x > viewport_size.x - 20.0 \
or indicator_center.y > viewport_size.y - 20.0:
return _fail("scanner direction indicator was placed outside the viewport")
guidance.queue_free()
fake_fleet.position = WorldGen.chunk_origin(WorldGen.OCTOPUS_BIOME_CENTER) \
+ Vector2(WorldGen.CHUNK_SIZE * 0.5, WorldGen.CHUNK_SIZE * 0.5)
var field: Node2D = load("res://world/OctopusField.gd").new()
field.name = "OctopusField"
host.add_child(field)
field._process(0.1)
if field.active.size() != 1:
return _fail("entering the octopus biome spawned %d roamers" % field.active.size())
var roamer: Node = field.active[0]
if roamer.max_hp < 7 or not roamer.is_in_group("roaming_octopuses"):
return _fail("ambient octopus lacks its durable roaming composition")
fake_fleet.position = Vector2.ZERO
field._process(0.1)
if not field.active.is_empty():
return _fail("roaming octopuses followed the fleet outside their biome")
print("octopus biome spawns bounded durable roamers and retires them at its edge")
print("TEST_PLANET_SCANNER: PASS")
quit(0)
func _fail(message: String) -> void:
print("FAIL: %s" % message)
quit(1)
func _is_nearest_unconquered(state: Node, result: Dictionary,
definition_id: String) -> bool:
var origin := Vector2.ZERO
var result_distance: float = origin.distance_to(Vector2(result.world_position))
var radius: int = int(result.distance_chunks) + 2
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var chunk := Vector2i(x, y)
if not WorldGen.has_planet_at_chunk(chunk):
continue
var data: Dictionary = WorldGen.planet_data_at_chunk(chunk)
if String(data.definition_id) != definition_id \
or state.is_planet_conquered(String(data.planet_id)):
continue
var world_position: Vector2 = WorldGen.chunk_origin(chunk) \
+ Vector2(data.local_pos)
if origin.distance_to(world_position) < result_distance - 0.01:
return false
return true