2026-09-04 15:56:07 +02:00
|
|
|
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)
|
2026-09-04 21:46:24 +02:00
|
|
|
var explored_before_scan: Dictionary = state.explored_chunks.duplicate()
|
2026-09-04 15:56:07 +02:00
|
|
|
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")
|
2026-09-04 21:46:24 +02:00
|
|
|
if explored_before_scan.has(Vector2i(rocky.chunk)):
|
|
|
|
|
return _fail("the scanner test's first target was already explored")
|
2026-09-04 15:56:07 +02:00
|
|
|
if not state.explored_chunks.has(Vector2i(rocky.chunk)):
|
|
|
|
|
return _fail("a scan result was not revealed on the persistent map")
|
2026-09-04 21:46:24 +02:00
|
|
|
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")
|
2026-09-04 15:56:07 +02:00
|
|
|
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")
|
2026-09-04 21:46:24 +02:00
|
|
|
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()
|
2026-09-04 15:56:07 +02:00
|
|
|
print("researched scanner reveals generated destinations; the fifth option waits for 4/4")
|
|
|
|
|
|
|
|
|
|
# #192: the scanner is taught immediately before the widened survey.
|
|
|
|
|
var scanner_quest_at: int = -1
|
|
|
|
|
var worlds_at: int = -1
|
|
|
|
|
for i in range(QUESTS.count()):
|
|
|
|
|
if String(QUESTS.at(i).id) == "planet_scanner":
|
|
|
|
|
scanner_quest_at = i
|
|
|
|
|
if String(QUESTS.at(i).id) == "worlds":
|
|
|
|
|
worlds_at = i
|
|
|
|
|
if scanner_quest_at < 0 or worlds_at != scanner_quest_at + 1:
|
|
|
|
|
return _fail("scanner quest is not directly before the world survey")
|
|
|
|
|
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"
|
2026-09-04 21:46:24 +02:00
|
|
|
fake_fleet.position = Vector2.ZERO
|
2026-09-04 15:56:07 +02:00
|
|
|
fake_fleet.add_to_group("player_fleet")
|
|
|
|
|
host.add_child(fake_fleet)
|
2026-09-04 21:46:24 +02:00
|
|
|
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)
|
2026-09-04 15:56:07 +02:00
|
|
|
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)
|
2026-09-04 21:46:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|