extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_enemy_bomber.gd # Forgejo #114: the enemy bomber - the artillery enemy, the one that lobs an # area shell. # # It was a siege piece that crept at 20px/s and threw a shell taking five # seconds to cross the gap, which read as a thing that was stuck rather than # a thing that was dangerous: by the time a shell landed the fleet had # wandered off, and the tower was still approaching. # # Covers: # A. it closes on a target at a speed worth calling flying, and faces # where it is going # B. its bombs travel fast enough to arrive while the target is roughly # where they were aimed # C. the bomb damages a player vessel it lands on # D. and a BUILDING it lands on - it used to hit only player_ships, which # was already out of step with #101 # E. it bombs a structure with no fleet in sight, through the shared # targeting rather than asking Fleet directly # F. its hull is its own, not a copy of another enemy's const MIN_FLYING_SPEED: float = 90.0 const MIN_SHELL_SPEED: float = 350.0 var game_state: Node var main: Node var fleet: Node var field: Node var bomber: Node2D var target_ship: Node2D var stage: int = 0 var elapsed: float = 0.0 var start_distance: float = 0.0 var building_id: int = 0 var hp_before: int = 0 func _initialize() -> void: # GameState is an autoload: a bare GameState. does not # compile in a SceneTree script, so the node is fetched by name instead. game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.transporters.clear() game_state.reset_progression() game_state.reset_waves() game_state.intro_active = false main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") field = main.get_node("BuildingField") func _fail(message: String) -> bool: print("TEST_ENEMY_BOMBER: FAIL (%s)" % message) quit(1) return true func _spawn_bomber(at: Vector2) -> Node2D: var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate() enemy.enemy_id = "artillery" enemy.position = at main.add_child(enemy) return enemy func _shells() -> Array: var out: Array = [] for shell in get_nodes_in_group("artillery_shells"): if is_instance_valid(shell) and not shell.is_queued_for_deletion(): out.append(shell) return out 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) for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"): if is_instance_valid(hazard): hazard.queue_free() # --- F. its own silhouette ------------------------------- var definitions: Script = load("res://enemies/EnemyDefinitions.gd") var bomber_def: Resource = definitions.get_definition("artillery") for other in definitions.all(): if other.enemy_id == "artillery": continue if other.hull_points == bomber_def.hull_points: return _fail("the bomber shares its hull with the %s" % other.enemy_id) if bomber_def.speed < MIN_FLYING_SPEED: return _fail("the bomber's top speed is %.0f - that is still creeping" \ % bomber_def.speed) print("bomber: %d hull points of its own, %.0fpx/s top speed" \ % [bomber_def.hull_points.size() / 2, bomber_def.speed]) fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR])) target_ship = fleet.ships[0] target_ship.move_target = target_ship.position bomber = _spawn_bomber(Vector2(700.0, 0.0)) start_distance = bomber.global_position.distance_to(target_ship.global_position) elapsed = 0.0 stage = 1 return false 1: # --- A. it flies, and faces where it is going ------------ if elapsed < 1.0: return false if not is_instance_valid(bomber): return _fail("the bomber is gone before it could be measured") var closed: float = start_distance \ - bomber.global_position.distance_to(target_ship.global_position) var speed: float = closed / elapsed if speed < MIN_FLYING_SPEED: return _fail("the bomber closed at %.0fpx/s - #114 asks for an aircraft" % speed) var heading: Vector2 = target_ship.global_position - bomber.global_position var drift: float = absf(angle_difference(bomber.rotation, heading.angle() + PI / 2.0)) if drift > PI / 3.0: return _fail("it flew %.0f degrees away from where it was pointing" \ % rad_to_deg(drift)) print("closed %.0fpx in %.1fs (%.0fpx/s), nose within %.0f degrees" \ % [closed, elapsed, speed, rad_to_deg(drift)]) # --- B/C. the bomb arrives, and it hurts ----------------- var component: Node = bomber.get_component("artillery") if component.shell_speed < MIN_SHELL_SPEED: return _fail("its bombs travel at %.0f - too slow to arrive" % component.shell_speed) component.cooldown_left = 0.0 elapsed = 0.0 stage = 2 return false 2: if _shells().is_empty(): if elapsed > 6.0: return _fail("the bomber never dropped a bomb") return false print("bomb away, travelling at %.0fpx/s" % _shells()[0].shell_speed) elapsed = 0.0 stage = 3 return false 3: if elapsed < 4.0: return false if is_instance_valid(target_ship) and not target_ship.is_queued_for_deletion(): return _fail("a bomb landed on a stationary vessel and it survived") print("the vessel it was aimed at is gone") # --- D/E. and it bombs what the player has built --------- for hazard in get_nodes_in_group("enemies"): if is_instance_valid(hazard): hazard.queue_free() # The fleet is parked far away, so nothing here is about ships. fleet.spawn_ships(1, Vector2(-9000.0, 0.0), PackedStringArray([GameState.VESSEL_INTERCEPTOR])) fleet.ships[0].move_target = fleet.ships[0].position field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2.ZERO) building_id = int(game_state.buildings[-1].id) elapsed = 0.0 stage = 4 return false 4: if elapsed < 0.3: return false hp_before = game_state.building_hp(building_id) bomber = _spawn_bomber(Vector2(600.0, 0.0)) bomber.speed = 0.0 bomber.get_component("artillery").cooldown_left = 0.0 elapsed = 0.0 stage = 5 return false 5: if elapsed < 5.0: return false var now: int = game_state.building_hp(building_id) if now >= hp_before: return _fail("the bomber shelled a crafter with no fleet in sight and did nothing") print("with the fleet 9000px away it bombed the crafter: %d -> %d hp" % [hp_before, now]) print("TEST_ENEMY_BOMBER: PASS") quit(0) return true return false