extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_waves_attack_buildings.gd # Forgejo #101 (#97c): "Waves tries to attack built buildings." # # Before this every enemy component asked one question - which player SHIP # is nearest - so a structure was scenery they flew straight through. A # Building is also a plain Node2D, so there was nothing to collide with even # if one had tried. # # Covers: # A. an enemy with no ship anywhere near it goes for a building # B. a ship in its face still wins: the fleet must be able to defend a # structure by interposing itself, and an enemy that ignored the # vessels shooting it to reach a depot slightly closer reads as broken # C. a chaser that reaches a building damages it, and spends itself doing # so - the same bargain it makes ramming a vessel # D. an enemy parked on a structure keeps hurting it, rather than landing # one hit on entry and then sitting there harmlessly # E. an enemy projectile damages a structure # F. enough of it levels the building, and the node goes with the record var game_state: Node var main: Node var fleet: Node var field: Node var chaser: Node2D var sniper: Node2D var target_building: Node2D # Held separately: the node is freed when the building comes down, and the # last stage still has to ask GameState about the record it left behind. var target_id: int = 0 var stage: int = 0 var elapsed: float = 0.0 var hp_watermark: int = 0 var destroyed_fired: bool = false 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.rocks = 5000 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_WAVES_ATTACK_BUILDINGS: FAIL (%s)" % message) quit(1) return true func _spawn_enemy(enemy_id: String, at: Vector2) -> Node2D: var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate() enemy.enemy_id = enemy_id enemy.position = at main.add_child(enemy) return enemy func _building_of(type: String) -> Node2D: for b in field.spawned: if is_instance_valid(b) and not b.is_queued_for_deletion() and b.building_type == type: return b return null 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() # The fleet is parked far away for A, C and D. It comes back for B. 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(0.0, 0.0)) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.2: return false target_building = _building_of(GameState.BUILDING_SHIP_CRAFTER) if target_building == null: return _fail("no crafter was placed") target_id = int(target_building.building_id) # --- A. with no ship near, a building is the target ---------- var targets: Script = load("res://enemies/EnemyTargets.gd") var from: Vector2 = Vector2(600.0, 0.0) var picked: Node2D = targets.nearest_target(main, from) if picked != target_building: return _fail("an enemy 600px from a crafter and 9600px from the fleet picked %s" \ % ("nothing" if picked == null else str(picked.name))) print("with the fleet 9600px away, the crafter is the target") # --- B. a ship in its face still wins ------------------------ # Deliberately FURTHER from the enemy than the building is: the # rule is not "nearest thing", it is "the fleet you are fighting". var guard: Node2D = fleet.ships[0] guard.position = from + Vector2(0.0, 500.0) guard.move_target = guard.position var defended: Node2D = targets.nearest_target(main, from) if defended != guard: return _fail("a vessel 500px away lost priority to a building 600px away - the fleet cannot screen anything") guard.position = Vector2(-9000.0, 0.0) guard.move_target = guard.position print("a vessel inside %.0fpx outranks a nearer building" % targets.SHIP_PRIORITY_RANGE) # --- C. a chaser spends itself on the structure -------------- hp_watermark = game_state.building_hp(target_id) chaser = _spawn_enemy("chaser", target_building.global_position) chaser.destroyed.connect(func(): destroyed_fired = true) elapsed = 0.0 stage = 2 return false 2: if elapsed < 1.2: return false var now: int = game_state.building_hp(target_id) if now >= hp_watermark: return _fail("a chaser sat on the crafter and it took no damage") if is_instance_valid(chaser) and not chaser.is_queued_for_deletion(): return _fail("the chaser survived ramming a structure") if not destroyed_fired: return _fail("the chaser vanished without firing `destroyed`") print("a chaser took the crafter from %d to %d hp and died doing it" % [hp_watermark, now]) # --- D. a besieger keeps working ---------------------------- sniper = _spawn_enemy("sniper", target_building.global_position) sniper.speed = 0.0 hp_watermark = game_state.building_hp(target_id) elapsed = 0.0 stage = 3 return false 3: if elapsed < 2.5: return false var now: int = game_state.building_hp(target_id) var strikes: int = hp_watermark - now if not is_instance_valid(sniper): return _fail("the sniper died on contact - it is not marked dies_on_contact") if strikes <= sniper.definition.contact_damage: return _fail("%.1fs of siege did %d damage - that is one hit on entry, not a siege" \ % [elapsed, strikes]) print("%.1fs parked on the crafter cost it %d hp" % [elapsed, strikes]) sniper.queue_free() # --- E. a projectile hits a structure ------------------------ hp_watermark = game_state.building_hp(target_id) var projectile: Area2D = load("res://enemies/EnemyProjectile.tscn").instantiate() projectile.position = target_building.global_position + Vector2(0.0, -140.0) projectile.damage = 3 root.add_child(projectile) projectile.launch(Vector2(0.0, 1.0)) elapsed = 0.0 stage = 4 return false 4: if elapsed < 1.2: return false var now: int = game_state.building_hp(target_id) if now >= hp_watermark: return _fail("an enemy bolt passed straight through the crafter") print("an enemy bolt took %d hp off the crafter" % (hp_watermark - now)) # --- F. enough of it levels the place ------------------------ game_state.damage_building(target_id, game_state.building_hp(target_id) - 1) var last: Node2D = _spawn_enemy("chaser", target_building.global_position) last.speed = 0.0 elapsed = 0.0 stage = 5 return false 5: if elapsed < 1.2: return false if not game_state.find_building(target_id).is_empty(): return _fail("the crafter survived on its last hit point") if _building_of(GameState.BUILDING_SHIP_CRAFTER) != null: return _fail("the crafter node outlived the record") print("the crafter came down, and its node went with it") print("TEST_WAVES_ATTACK_BUILDINGS: PASS") quit(0) return true return false