2026-08-15 21:22:10 +02:00
|
|
|
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")
|
2026-08-15 22:47:44 +02:00
|
|
|
if point.z_index >= 0:
|
|
|
|
|
return _fail("capture point does not draw behind the fleet/enemies (z_index=%d)" % point.z_index)
|
2026-08-15 21:22:10 +02:00
|
|
|
|
|
|
|
|
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
|