spaceship/tests/test_planet_setups.gd
Loïc Wisniewski 2f161621c3 #84: move 215 root files into a folder hierarchy
Everything lived in one flat directory, so a diff gave no clue what area it
touched and finding the file you wanted meant knowing its exact name. Twelve
folders now, grouped by what a thing IS rather than what type it is - a
vessel's definition, its component scenes and its upgrade resources sit
together, because that is the set you read when you change one of them.

  buildings  core  controllers  enemies  logistics  planets
  resources  shaders  surface  ui  vessels  vfx  world

332 files moved and every res:// reference rewritten - 306 files touched,
including the autoload and main-scene paths in project.godot. Nothing renamed:
this is purely where files sit, so the diff is a pile of pure moves and no
behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at
the root, where Godot and the export preset expect them.

Verified three ways, because a suite alone would not have caught all of it:
the full 117 pass, the web export builds clean, and the game renders.

The export is why that matters. The FIRST export after the move failed with
"Attempt to open script 'res://GameState.gd'" - the old path - while
project.godot on disk plainly said res://core/GameState.gd. That is this
project's documented Android FUSE mtime trap: Godot served a cached copy of a
file whose content had changed. A `touch` fixed it. Nothing in the test suite
would have found that, because the suite runs from a copy on a native
filesystem - the one place the bug cannot happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00

155 lines
6.4 KiB
GDScript

extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_planet_setups.gd
# Verifies:
# 1. setup_for_planet is deterministic per planet_id and only returns
# setups matching the requested surface profile
# 2. dropping a new .tres into res://PlanetSetups/ makes it appear via
# PlanetSetups.rescan() with no other code change
# 3. landing on a rocky/gas planet composes a PlanetSetupHost carrying the
# resolved setup, with one marker per capture point
const SCRATCH_SETUP_PATH: String = "res://PlanetSetups/_test_scratch_setup.tres"
# Resolved via load() inside _initialize(), NOT a top-level `const ...
# preload(...)` - a top-level preload in a script-mode ENTRY point (-s
# this_file.gd) evaluates during this file's own early compilation, which
# can happen before autoloads are registered, and CapturePoint.gd
# references GameState in a function body (resolved at compile time). See
# test_capture_points.gd for the full explanation.
var PLANET_SETUP_SCRIPT: Script
var CAPTURE_POINT_SCRIPT: Script
var game_state: Node
var rocky_setup: Resource
var stage: int = 0
var elapsed: float = 0.0
var pending_surface: Node
func _initialize() -> void:
PLANET_SETUP_SCRIPT = load("res://surface/PlanetSetup.gd")
CAPTURE_POINT_SCRIPT = load("res://surface/CapturePoint.gd")
# --- 1. determinism and profile filtering ---
rocky_setup = PlanetSetups.setup_for_planet("chunk:3:7", "rocky")
if rocky_setup == null:
return _fail("no rocky setup resolved for a rocky planet id")
if rocky_setup.surface_profile_id != "rocky":
return _fail("resolved setup does not match the requested profile")
var rocky_setup_again: Resource = PlanetSetups.setup_for_planet("chunk:3:7", "rocky")
if rocky_setup != rocky_setup_again:
return _fail("setup_for_planet was not deterministic for the same planet id")
var gas_setup: Resource = PlanetSetups.setup_for_planet("chunk:3:7", "gas")
if gas_setup == null:
return _fail("no gas setup resolved for the same planet id under the gas profile")
if gas_setup.surface_profile_id != "gas":
return _fail("gas request returned a mismatched-profile setup")
for setup in PlanetSetups.setups_for_profile("rocky"):
if setup.surface_profile_id != "rocky":
return _fail("setups_for_profile('rocky') leaked a non-rocky setup")
# --- 2. dropping a new .tres is picked up by rescan() ---
# Checked by MEMBERSHIP of this test's own scratch id, not by counting the
# pool before and after. res://PlanetSetups/ is shared, the suite runs
# several tests at once, and two of them write scratch setups there - so a
# count taken before and a count taken after are not measuring the same
# population, and this failed intermittently while the behaviour it tests
# was perfectly fine. Membership of a uniquely-named file cannot be
# perturbed by anyone else.
if _pool_contains("_test_scratch_setup"):
return _fail("a leftover scratch setup was already in the pool before this test wrote one")
var scratch: Resource = PLANET_SETUP_SCRIPT.new()
scratch.setup_id = "_test_scratch_setup"
scratch.display_name = "Scratch"
scratch.surface_profile_id = "rocky"
scratch.capture_points = PackedVector2Array([Vector2(100, 100)])
var save_err: int = ResourceSaver.save(scratch, SCRATCH_SETUP_PATH)
if save_err != OK:
return _fail("could not save the scratch setup for the pickup check (err %d)" % save_err)
PlanetSetups.rescan()
var picked_up: bool = _pool_contains("_test_scratch_setup")
DirAccess.remove_absolute(ProjectSettings.globalize_path(SCRATCH_SETUP_PATH))
var import_file: String = SCRATCH_SETUP_PATH + ".import"
if FileAccess.file_exists(import_file):
DirAccess.remove_absolute(ProjectSettings.globalize_path(import_file))
PlanetSetups.rescan()
if not picked_up:
return _fail("registry did not pick up a newly saved setup")
if _pool_contains("_test_scratch_setup"):
return _fail("registry still offers the scratch setup after its file was deleted")
# --- 3. landing composes a PlanetSetupHost with the resolved setup ---
# Deferred to _process(): a scene instantiated and added to the tree
# inside _initialize() has not had its _ready() chain flushed yet (this
# runs before the engine's first frame), so children added during
# PlanetSurfaceController._ready() are not observable until a frame has
# actually processed - see test_gas_system.gd for the same pattern.
game_state = root.get_node("GameState")
game_state.planet_name = "Setup Test"
game_state.planet_id = "chunk:3:7"
game_state.planet_definition_id = "rocky"
pending_surface = load("res://surface/PlanetSurface.tscn").instantiate()
root.add_child(pending_surface)
func _process(delta: float) -> bool:
elapsed += delta
match stage:
0:
if elapsed < 0.1:
return false
var host: Node = pending_surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost")
if host == null:
pending_surface.free()
return _fail("landing did not compose a PlanetSetupHost")
if host.setup != rocky_setup:
pending_surface.free()
return _fail("host did not receive the deterministically resolved setup")
var point_count: int = 0
for child in host.get_children():
if child.get_script() == CAPTURE_POINT_SCRIPT:
point_count += 1
if point_count != rocky_setup.capture_points.size():
pending_surface.free()
return _fail("host placed %d capture points for %d authored ones" % [
point_count, rocky_setup.capture_points.size()
])
pending_surface.free()
game_state.planet_definition_id = "gas_giant"
pending_surface = load("res://surface/PlanetSurface.tscn").instantiate()
root.add_child(pending_surface)
elapsed = 0.0
stage = 1
return false
1:
if elapsed < 0.1:
return false
var gas_host: Node = pending_surface.get_node_or_null("SurfaceFeatures/PlanetSetupHost")
if gas_host == null or gas_host.setup.surface_profile_id != "gas":
pending_surface.free()
return _fail("gas landing did not compose a matching-profile setup host")
pending_surface.free()
print("TEST_PLANET_SETUPS: PASS")
quit(0)
return true
return false
func _pool_contains(setup_id: String) -> bool:
for setup in PlanetSetups.setups_for_profile("rocky"):
if String(setup.setup_id) == setup_id:
return true
return false
func _fail(message: String) -> bool:
print("TEST_PLANET_SETUPS: FAIL (%s)" % message)
if FileAccess.file_exists(SCRATCH_SETUP_PATH):
DirAccess.remove_absolute(ProjectSettings.globalize_path(SCRATCH_SETUP_PATH))
quit(1)
return true