spaceship/tests/test_planet_scanner.gd
Loïc Wisniewski df4d5e35f9 Find the planet setups in an exported build
The registry scanned res://PlanetSetups for names ending ".tres". An export
does not ship those: text resources are converted to binary and the pack
carries "<name>.tres.remap", which ResourceLoader follows when asked for the
original path. So the scan found nothing in the web build, every planet drew
no setup, and since #211 the setup IS the garrison - every world in the
deployed game was deserted, with nothing logged to say why. Confirmed by
reading index.pck, which holds rocky_default.tres.remap and no .tres.

The suite runs from source, where the bug cannot occur, so the name
resolution is its own function and tested against the exact strings the pack
contains.

Also: the planning centre listed transporters and repair drones but
set_vessel_target() still refused anything not fleet-eligible, so both rows
had buttons that did nothing - the worst shape a refusal can take. Listing
and setting read the same rule now, and a standing order is only offered to
a crafter that can actually take it.

And two quests close the chain: the tentacle world, which the scanner will
not offer until the story hands it over, and the Warden itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-07 13:55:41 +02:00

214 lines
9.6 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()
# The survey alone is not enough any more: the tentacle world is offered
# only once the chain sends the player after it, so a player who has
# charted four kinds early cannot stumble into the endgame.
if menu.get_node("BossButton").visible:
return _fail("the boss signature appeared on the survey alone, before the quest that names it")
state.quest_index = QUESTS.index_of("tentacles")
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 once its quest was live")
print("the boss signature waits for BOTH the four-world survey and its own quest")
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")
# #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