extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_asteroid_mining.gd # Asteroids used to be pure hazard - the only thing one could ever do was cost # you a ship. They can now be shot apart for the ore inside, which is the one # resource reachable in open space without conquering anything. # # Covers: # A. an asteroid survives a hit it should survive, so "destructible" does # not mean "evaporates on contact with anything" # B. enough fire destroys it and leaves ore behind # C. the ore is fetchable - flying a ship over it loads the fleet's hold, # which is the whole point of dropping it # D. ramming destroys the asteroid but yields NOTHING. Without that split a # shield vessel could farm asteroids by flying into them # E. dropped ore expires if nobody collects it, so a field that gets shot up # and abandoned cannot leak nodes for the rest of the session; a # hand-placed surface rock still lives forever var game_state: Node var main: Node var fleet: Node var asteroid: Node2D var stage: int = 0 var elapsed: float = 0.0 var hold_before: int = 0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") func _spawn_asteroid(at: Vector2) -> Node2D: var rock: Node2D = load("res://world/Asteroid.tscn").instantiate() rock.position = at main.add_child(rock) return rock func _clear_sky() -> void: for node in get_nodes_in_group("asteroids") + get_nodes_in_group("rocks") \ + get_nodes_in_group("enemies") + get_nodes_in_group("player_projectiles"): if is_instance_valid(node): node.free() func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false main.get_node("EnemySpawner").set_process(false) main.get_node("AsteroidField").set_process(false) _clear_sky() fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR])) # --- A/B. it takes real damage, then breaks ------------------- asteroid = _spawn_asteroid(Vector2(4000.0, 0.0)) var hit_points: int = int(asteroid.hp) if hit_points < 2: return _fail("an asteroid dies to a single point of damage, so there is nothing to survive") for i in range(hit_points - 1): asteroid.take_damage(1) if not is_instance_valid(asteroid) or asteroid.is_queued_for_deletion(): return _fail("the asteroid broke after %d of its %d hit points" % [hit_points - 1, hit_points]) print("asteroid survived %d of %d hit points" % [hit_points - 1, hit_points]) asteroid.take_damage(1) if is_instance_valid(asteroid) and not asteroid.is_queued_for_deletion(): return _fail("the asteroid survived its full %d hit points" % hit_points) stage = 1 elapsed = 0.0 return false 1: # The drop is deferred - it is created from inside a signal, where # the physics server refuses a direct add_child. if elapsed < 0.2: return false var dropped: Array = get_nodes_in_group("rocks") if dropped.is_empty(): return _fail("destroying an asteroid left no ore behind") print("destroyed asteroid dropped %d ore node(s)" % dropped.size()) # --- C. the ore is fetchable --------------------------------- hold_before = game_state.fleet_resource(GameState.RESOURCE_ORE) var ship: Node2D = fleet.ships[0] # Placed just clear of the ship rather than on top of it, so the # rock's own drift has to bring it in - that pull is how a player # actually collects one. dropped[0].position = ship.global_position + Vector2(80.0, 0.0) stage = 2 elapsed = 0.0 return false 2: if elapsed < 1.5: return false var gained: int = game_state.fleet_resource(GameState.RESOURCE_ORE) - hold_before if gained <= 0: return _fail("flying over the dropped ore put nothing in the hold") print("dropped ore collected into the fleet hold (+%d)" % gained) # --- D. ramming yields nothing ------------------------------- _clear_sky() hold_before = game_state.fleet_resource(GameState.RESOURCE_ORE) asteroid = _spawn_asteroid(fleet.ships[0].global_position) stage = 3 elapsed = 0.0 return false 3: if elapsed < 0.5: return false if is_instance_valid(asteroid) and not asteroid.is_queued_for_deletion(): return _fail("an asteroid dropped onto a ship did not shatter against it") if not get_nodes_in_group("rocks").is_empty(): return _fail("ramming an asteroid yielded ore - a shield vessel could farm them that way") if game_state.fleet_resource(GameState.RESOURCE_ORE) != hold_before: return _fail("ramming an asteroid changed the hold") print("ramming shattered the asteroid and yielded nothing") # --- E. abandoned ore expires, authored ore does not ---------- _clear_sky() var abandoned: Node2D = load("res://surface/Rock.tscn").instantiate() abandoned.build_shape(1) abandoned.lifetime = 0.4 # Far from every ship, or it would be collected rather than expire. abandoned.position = fleet.get_centroid() + Vector2(9000.0, 0.0) main.add_child(abandoned) var authored: Node2D = load("res://surface/Rock.tscn").instantiate() authored.build_shape(2) authored.position = fleet.get_centroid() + Vector2(9600.0, 0.0) main.add_child(authored) if authored.lifetime != 0.0: return _fail("a plain rock has a lifetime of %.1f - surface rocks must live forever" % authored.lifetime) stage = 4 elapsed = 0.0 return false 4: if elapsed < 1.0: return false var survivors: Array = get_nodes_in_group("rocks") var names: Array = [] for rock in survivors: names.append(rock.name) if survivors.size() != 1: return _fail("expected only the authored rock to remain, found %d: %s" % [survivors.size(), str(names)]) print("abandoned ore expired; the rock with no lifetime is still there") print("TEST_ASTEROID_MINING: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_ASTEROID_MINING: FAIL (%s)" % message) quit(1) return true