extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_resources_and_buildings.gd # Covers the resource/building loop end to end: # 1. flying a ship over a surface rock loads it into the fleet's hold # (banking it needs a trip to the mothership - see #14 and # tests/test_fleet_cargo.gd) # 2. buildings are paid for, persist in GameState, and respawn with the # space scene (they must survive the trip to a planet and back) # 3. a ship crafter adds a vessel to the fleet for rocks # 4. wiping out the fleet rebuilds one vessel at the mothership var stage: int = 0 var elapsed: float = 0.0 var scene: Node var fleet: Node var game_state: Node var rock: Area2D var mothership_pos: Vector2 = Vector2(900.0, -400.0) func _initialize() -> void: game_state = root.get_node("GameState") game_state.rocks = 0 game_state.fleet_cargo = 0 game_state.buildings.clear() game_state.has_return_position = false scene = load("res://surface/PlanetSurface.tscn").instantiate() root.add_child(scene) current_scene = scene fleet = scene.get_node("Fleet") func _process(delta: float) -> bool: elapsed += delta match stage: 0: # --- gather a rock ------------------------------------------- scene.get_node("SurfaceFeatures/ResourceProvider").set_process(false) rock = load("res://surface/Rock.tscn").instantiate() rock.build_shape(1) # Placed OUTSIDE the pickup shape but inside attract_radius, so # this also proves the magnetic pull actually reels it in rather # than the ship simply happening to overlap it on spawn. rock.position = fleet.ships[0].position + Vector2(150.0, 0.0) scene.add_child(rock) elapsed = 0.0 stage = 1 return false 1: if elapsed < 1.5: return false # Into the hold, not the bank: since #14 ore is cargo until the # fleet delivers it to the mothership. if game_state.fleet_cargo != 1: print("fleet_cargo=%d after flying a ship onto one" % game_state.fleet_cargo) print("TEST_RESOURCES_AND_BUILDINGS: FAIL (rock not gathered)") quit(1) return true print("gathered rock -> hold=%d bank=%d" % [game_state.fleet_cargo, game_state.rocks]) # Bank enough to build with, then move to the space scene. game_state.rocks = 60 change_scene_to_file("res://world/Main.tscn") elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.4: return false if current_scene == null or current_scene.name != "Main": return false scene = current_scene fleet = scene.get_node("Fleet") scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) # --- place both buildings ------------------------------------- var field: Node = scene.get_node("BuildingField") var before: int = game_state.rocks for ship in fleet.ships: ship.position = mothership_pos fleet.move_to(mothership_pos) field.place(GameState.BUILDING_MOTHERSHIP, mothership_pos) game_state.spend(int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE])) field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(1400.0, 300.0)) game_state.spend(int(game_state.building_costs(GameState.BUILDING_SHIP_CRAFTER)[GameState.RESOURCE_ORE])) var paid: bool = game_state.rocks == before - int(game_state.building_costs(GameState.BUILDING_MOTHERSHIP)[GameState.RESOURCE_ORE]) - int(game_state.building_costs(GameState.BUILDING_SHIP_CRAFTER)[GameState.RESOURCE_ORE]) var stored: bool = game_state.buildings.size() == 2 print("buildings=%d rocks %d->%d paid=%s" % [ game_state.buildings.size(), before, game_state.rocks, paid ]) if not (paid and stored): print("TEST_RESOURCES_AND_BUILDINGS: FAIL (placing/paying wrong)") quit(1) return true elapsed = 0.0 stage = 3 return false 3: if elapsed < 0.3: return false # --- crafter adds a ship -------------------------------------- # Crafting adds a FOURTH vessel below, one past the 3 slots a # new game has, so make room first - the subject here is the # crafter, not the fleet cap. game_state.unlocked.append_array(["wing", "squadron", "armada"]) fleet.spawn_ships(3, Vector2.ZERO) var before_ships: int = fleet.ships.size() var before_rocks: int = game_state.rocks game_state.spend(GameState.SHIP_COST) var added: bool = fleet.add_ship() print("craft: ships %d->%d rocks %d->%d" % [ before_ships, fleet.ships.size(), before_rocks, game_state.rocks ]) if not (added and fleet.ships.size() == before_ships + 1 and game_state.rocks == before_rocks - GameState.SHIP_COST): print("TEST_RESOURCES_AND_BUILDINGS: FAIL (crafting a ship)") quit(1) return true # --- command centre: drag a formation slot --------------------- var editor: Control = scene.get_node("UI/FormationMenu/Editor") var menu: Control = scene.get_node("UI/FormationMenu") menu.visible = true editor.size = Vector2(520, 520) # The editor is driven from BuildController._unhandled_input, # which never fires for Controls that consume touches through # the GUI system first. The menu AND its full-screen backdrop # must both be IGNORE, or dragging a slot silently does nothing # on a real device - which is exactly what happened. for node in [menu, menu.get_node("Backdrop"), editor]: if node.mouse_filter != Control.MOUSE_FILTER_IGNORE: print("%s has mouse_filter=%d, needs IGNORE" % [node.name, node.mouse_filter]) print("TEST_RESOURCES_AND_BUILDINGS: FAIL (formation menu would swallow drags)") quit(1) return true var slot_before: Vector2 = game_state.formation[1] var down: InputEventScreenTouch = InputEventScreenTouch.new() down.index = 0 down.pressed = true down.position = editor.global_position + editor._slot_screen_pos(1) var grabbed: bool = editor.handle_touch(down) var move: InputEventScreenDrag = InputEventScreenDrag.new() move.index = 0 move.position = editor.global_position + editor._slot_screen_pos(1) + Vector2(40.0, 25.0) editor.handle_drag(move) var slot_after: Vector2 = game_state.formation[1] menu.visible = false print("formation slot 1: %s -> %s (grabbed=%s)" % [slot_before, slot_after, grabbed]) if not (grabbed and slot_after != slot_before): print("TEST_RESOURCES_AND_BUILDINGS: FAIL (formation slot did not drag)") quit(1) return true # Editing must re-seat the ships ALREADY in flight, not just # ones built later - otherwise the edit reads as doing nothing. var live_target: Vector2 = fleet.ships[1].move_target var expected_target: Vector2 = fleet.fleet_target + slot_after var live_ok: bool = live_target.distance_to(expected_target) < 0.01 print("live ship 1 target %s, expected %s (ok=%s)" % [ live_target, expected_target, live_ok ]) if not live_ok: print("TEST_RESOURCES_AND_BUILDINGS: FAIL (existing ships ignored the edit)") quit(1) return true # --- pivot the formation -------------------------------------- var before_pivot: Vector2 = game_state.formation[1] game_state.rotate_formation(PI / 2.0) fleet.apply_formation() var after_pivot: Vector2 = game_state.formation[1] var rotated_ok: bool = after_pivot.distance_to(before_pivot.rotated(PI / 2.0)) < 0.01 var length_kept: bool = abs(after_pivot.length() - before_pivot.length()) < 0.01 var ship_followed: bool = fleet.ships[1].move_target.distance_to(fleet.fleet_target + after_pivot) < 0.01 print("pivot: %s -> %s (rotated=%s shape_kept=%s ship_followed=%s)" % [ before_pivot, after_pivot, rotated_ok, length_kept, ship_followed ]) if not (rotated_ok and length_kept and ship_followed): print("TEST_RESOURCES_AND_BUILDINGS: FAIL (pivot)") quit(1) return true game_state.rotate_formation(-PI / 2.0) slot_after = game_state.formation[1] # A rebuilt fleet must adopt the edited layout, which is the # whole point of storing the formation in GameState. fleet.spawn_ships(2, Vector2.ZERO) var uses_layout: bool = fleet.ships[1].position.distance_to(slot_after) < 0.01 print("respawned ship 1 at %s, formation says %s (matches=%s)" % [ fleet.ships[1].position, slot_after, uses_layout ]) if not uses_layout: print("TEST_RESOURCES_AND_BUILDINGS: FAIL (fleet ignored the edited formation)") quit(1) return true # --- wipe the fleet ------------------------------------------- for ship in fleet.ships.duplicate(): ship.queue_free() fleet.ships.clear() fleet.formation_offsets.clear() elapsed = 0.0 stage = 4 return false 4: if elapsed < 0.5: return false # BuildController._process should have rebuilt one vessel, at # the mothership rather than the origin. var respawned: bool = fleet.ships.size() == 1 var at_mothership: bool = respawned and fleet.ships[0].position.distance_to(mothership_pos) < 120.0 print("after wipe: ships=%d at_mothership=%s (pos=%s expected~%s)" % [ fleet.ships.size(), at_mothership, fleet.ships[0].position if respawned else Vector2.ZERO, mothership_pos ]) if respawned and at_mothership: print("TEST_RESOURCES_AND_BUILDINGS: PASS") quit(0) else: print("TEST_RESOURCES_AND_BUILDINGS: FAIL (fleet not rebuilt at the mothership)") quit(1) return true return false