extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_planet_identity_persists.gd # Regression test for: leaving a planet's surface and coming back # respawned Main.tscn's PlanetField from scratch, and since planet # name/position/shape were picked with the unseeded global RNG at spawn # time, the "same" planet (same chunk) would come back with a different # name, a different position within the chunk, and a different outline # shape every single time. Fixed by seeding planet identity purely from # the chunk's own coordinates (mirrors WorldGen's own deterministic noise # pattern), so this loads Main.tscn twice and checks the planet at the # same chunk is identical both times. var target_chunk: Vector2i var first_name: String var first_pos: Vector2 var first_shape: PackedVector2Array var stage: int = 0 var main_scene: Node var elapsed: float = 0.0 func _initialize() -> void: var found: bool = false for x in range(-15, 15): for y in range(-15, 15): var chunk: Vector2i = Vector2i(x, y) if WorldGen.has_planet_at_chunk(chunk): target_chunk = chunk found = true break if found: break if not found: print("FAIL: could not find any chunk with a planet to test with") quit(1) return _load_main() func _load_main() -> void: # GameState (an autoload) isn't accessible as a bare global identifier # from a script that extends SceneTree directly - fetch it via root. var game_state: Node = root.get_node("GameState") game_state.has_return_position = true game_state.surviving_ship_count = 5 game_state.return_world_position = WorldGen.chunk_origin(target_chunk) + Vector2.ONE * WorldGen.CHUNK_SIZE * 0.5 game_state.return_camera_zoom = 1.0 var packed: PackedScene = load("res://Main.tscn") main_scene = packed.instantiate() root.add_child(main_scene) main_scene.get_node("AsteroidField").set_process(false) main_scene.get_node("EnemySpawner").set_process(false) func _process(_delta: float) -> bool: match stage: 0: # Wait one tick for _ready() (and the forced synchronous # update_chunks() call inside it) to have actually run. stage = 1 return false 1: var planet_field: Node = main_scene.get_node("PlanetField") if not planet_field.populated_chunks.has(target_chunk): print("FAIL: planet never spawned in target chunk on first load") quit(1) return true var planet: Node2D = planet_field.populated_chunks[target_chunk][0] first_name = planet.planet_name first_pos = planet.position first_shape = planet.shape_points.duplicate() main_scene.queue_free() stage = 2 return false 2: # Give queue_free() a moment to actually clear the old tree # before loading a fresh Main.tscn on top of it. elapsed += 1.0 if elapsed < 3.0: return false _load_main() elapsed = 0.0 stage = 3 return false 3: stage = 4 return false 4: var planet_field: Node = main_scene.get_node("PlanetField") if not planet_field.populated_chunks.has(target_chunk): print("FAIL: planet never spawned in target chunk on second load") quit(1) return true var planet: Node2D = planet_field.populated_chunks[target_chunk][0] var name_ok: bool = planet.planet_name == first_name var pos_ok: bool = planet.position.distance_to(first_pos) < 0.01 var shape_ok: bool = planet.shape_points.size() == first_shape.size() if shape_ok: for i in range(first_shape.size()): if planet.shape_points[i].distance_to(first_shape[i]) > 0.01: shape_ok = false break print("name: %s vs %s (ok=%s)" % [first_name, planet.planet_name, name_ok]) print("pos: %s vs %s (ok=%s)" % [first_pos, planet.position, pos_ok]) print("shape_ok=%s" % shape_ok) if name_ok and pos_ok and shape_ok: print("TEST_PLANET_IDENTITY_PERSISTS: PASS") quit(0) else: print("TEST_PLANET_IDENTITY_PERSISTS: FAIL") quit(1) return true return false