spaceship/tests/test_planet_setups.gd
Loïc Wisniewski 6e13b6397c Take planets by killing what defends them
#211 conquest is the fight now: a planet registers the garrison its setup
authors, each defender's own death counts against it, and the last one takes
the world. The capture points - stand in a ring, wait out a timer, once per
ring - are gone, along with the editor palette that placed them.
#212 conquest opens a dialog on the surface naming the research points
recovered; the award travels with the signal, after it is banked.
#209 the prologue's charted sectors go with the breach, so the map the
survivors keep is of where they now are - and "chart 30 sectors" is no
longer complete before they have flown anywhere.
#218 enemy variety is gated on the FLEET, not on wave strength: a starting
fleet meets chasers only at any wave number, and each vessel past three
opens one more kind.

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

154 lines
6.3 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, placing one enemy per authored spawn
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 the scripts loaded here
# reference GameState in function bodies (resolved at compile time). See
# test_planet_garrison.gd for the full explanation.
var PLANET_SETUP_SCRIPT: Script
var ENEMY_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")
ENEMY_SCRIPT = load("res://enemies/Enemy.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"
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 enemy_count: int = 0
for child in host.get_children():
if child.get_script() == ENEMY_SCRIPT:
enemy_count += 1
if enemy_count != rocky_setup.enemy_spawns.size():
pending_surface.free()
return _fail("host placed %d enemies for %d authored spawns" % [
enemy_count, rocky_setup.enemy_spawns.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