extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_capture_points.gd # Verifies: # 1. PlanetSetupHost restores an already-captured point immediately on # (re-)composition, instead of restarting its fill # 2. a ship parked in range with no enemies nearby fills a point to # CAPTURED in about capture_time seconds, and GameState reflects it # (captured_point_count, is_planet_conquered - which is what # BuildController's extractor gate reads, see test_planet_conquest_gate) # 3. an enemy within enemy_clear_radius blocks further progress # (state == BLOCKED) without erasing what was already filled # 4. moving out of range drains progress gradually, not instantly, and # never below what a completed point already holds # 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, which GDScript resolves at # compile time. preload()ing it from inside _initialize() defers that # until after the engine has finished booting, same as every plain load() # call elsewhere in this file. var CAPTURE_POINT_SCRIPT: Script var PLANET_SETUP_SCRIPT: Script var game_state: Node var main: Node var fleet: Node var point: Node var enemy: Node var planet_id: String = "test_capture_planet" var stage: int = 0 var elapsed: float = 0.0 var progress_before_block: float = 0.0 var progress_before_drain: float = 0.0 func _initialize() -> void: CAPTURE_POINT_SCRIPT = load("res://CapturePoint.gd") PLANET_SETUP_SCRIPT = load("res://PlanetSetup.gd") game_state = root.get_node("GameState") game_state.planet_capture.clear() # --- 1. restoration: an already-captured point stays captured ----- var restore_planet_id: String = "test_capture_restore_planet" var fake_setup: Resource = PLANET_SETUP_SCRIPT.new() fake_setup.setup_id = "test_restore" fake_setup.surface_profile_id = "rocky" fake_setup.capture_points = PackedVector2Array([Vector2(0, 0), Vector2(500, 0)]) game_state.planet_id = restore_planet_id var host1: Node = load("res://PlanetSetupHost.tscn").instantiate() host1.apply_setup(fake_setup) game_state.mark_point_captured(restore_planet_id, 0) # simulates a completed prior visit var host2: Node = load("res://PlanetSetupHost.tscn").instantiate() host2.apply_setup(fake_setup) var restored: Node = null var fresh: Node = null for child in host2.get_children(): if child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 0: restored = child elif child.get_script() == CAPTURE_POINT_SCRIPT and child.point_index == 1: fresh = child if restored == null or restored.state != CAPTURE_POINT_SCRIPT.State.CAPTURED or restored.progress < 0.999: return _fail("re-landing did not restore an already-captured point") if fresh == null or fresh.state == CAPTURE_POINT_SCRIPT.State.CAPTURED: return _fail("an uncaptured point came back already CAPTURED") # --- 2-4. capture mechanics, in a live scene with a real fleet ----- game_state.planet_capture.clear() game_state.planet_id = planet_id game_state.register_capture_points(planet_id, 1) game_state.buildings.clear() game_state.extractors.clear() game_state.rocks = 200 main = load("res://Main.tscn").instantiate() root.add_child(main) fleet = main.get_node("Fleet") func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.1: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) for a in get_nodes_in_group("asteroids"): a.queue_free() point = load("res://CapturePoint.tscn").instantiate() point.point_index = 0 point.capture_time = 1.0 point.capture_radius = 200.0 point.enemy_clear_radius = 300.0 point.drain_rate = 0.5 point.position = Vector2(4000, 0) # well clear of spawn-time activity main.add_child(point) var far_away: Vector2 = point.position + Vector2(2500, 0) for ship in fleet.ships: ship.position = far_away fleet.move_to(far_away) elapsed = 0.0 stage = 1 return false 1: # Out of range: no progress at all. if elapsed < 0.3: return false if point.progress > 0.001 or point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL: return _fail("progress rose while no ship was in capture_radius") for ship in fleet.ships: ship.position = point.position fleet.move_to(point.position) elapsed = 0.0 stage = 2 return false 2: # Partway through capture_time: rising, not yet complete. if elapsed < point.capture_time * 0.4: return false if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURING or point.progress <= 0.05: return _fail("point did not start filling with a ship in range (state=%d progress=%.2f)" % [ point.state, point.progress ]) # An enemy shows up within enemy_clear_radius: must block further # fill without erasing what's already there. A bare "enemies" # group member (not a real Enemy.tscn) is deliberate: CapturePoint # only cares that something is in the group and in range, and # this avoids a real chaser/weapon exchange destroying the fleet # ships this test also needs alive in later stages. enemy = Node2D.new() enemy.add_to_group("enemies") enemy.position = point.position + Vector2(50, 0) main.add_child(enemy) progress_before_block = point.progress elapsed = 0.0 stage = 3 return false 3: if elapsed < 0.5: return false if point.state != CAPTURE_POINT_SCRIPT.State.BLOCKED: return _fail("point did not report BLOCKED with an enemy nearby (state=%d)" % point.state) if point.progress > progress_before_block + 0.01: return _fail("progress rose while an enemy was inside enemy_clear_radius") # Enemy leaves and the ship backs off out of range: progress # should drain, but gradually - not snap to zero. enemy.queue_free() var far_away: Vector2 = point.position + Vector2(2500, 0) for ship in fleet.ships: ship.position = far_away fleet.move_to(far_away) progress_before_drain = point.progress elapsed = 0.0 stage = 4 return false 4: if elapsed < 0.3: return false if point.progress >= progress_before_drain - 0.001: return _fail("progress did not drain while out of range") if point.progress <= 0.001: return _fail("progress dropped to zero instantly instead of draining gradually") if point.state != CAPTURE_POINT_SCRIPT.State.NEUTRAL: return _fail("point state did not return to NEUTRAL while draining out of range") # Move back in and let it finish this time. for ship in fleet.ships: ship.position = point.position fleet.move_to(point.position) elapsed = 0.0 stage = 5 return false 5: # Draining first means completion now takes longer than a bare # capture_time - give it a generous margin. if elapsed < point.capture_time * 2.0 + 0.5: return false if point.state != CAPTURE_POINT_SCRIPT.State.CAPTURED: return _fail("point never reached CAPTURED (state=%d progress=%.2f after %.2fs)" % [ point.state, point.progress, elapsed ]) if game_state.captured_point_count(planet_id) != 1: return _fail("GameState.captured_point_count did not record the capture") # is_planet_conquered is exactly what BuildController's extractor # gate reads (see test_planet_conquest_gate.gd) - not re-tested # through the build UI here to avoid duplicating that coverage. if not game_state.is_planet_conquered(planet_id): return _fail("single-point planet was not marked conquered") print("TEST_CAPTURE_POINTS: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_CAPTURE_POINTS: FAIL (%s)" % message) quit(1) return true