extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_chaser_rams.gd # Forgejo #82: a chaser is destroyed by the impact when it reaches a player # vessel. It used to pass straight through the wreck untouched and line up # another pass, which made a ramming enemy strictly better than any other. # # Covers: # A. the chaser dies on contact, and dies the same way it dies to gunfire - # through take_damage, so the explosion and the destroyed signal happen # B. the vessel it hit still dies too. Trading one for one is the point; # the chaser must not become harmless # C. an enemy NOT marked dies_on_contact survives the same collision, so # this is a per-enemy decision and not a blanket rule about touching # D. a SHIELDED vessel survives, and still destroys the chaser var game_state: Node var main: Node var fleet: Node var stage: int = 0 var elapsed: float = 0.0 var chaser: Node2D var sniper: Node2D var victim: Node2D var destroyed_fired: bool = false func _initialize() -> void: game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() game_state.vessel_levels["unlock_shield"] = 1 main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main fleet = main.get_node("Fleet") 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) enemy.speed = 0.0 return enemy 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() fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR])) var definitions: Script = load("res://enemies/EnemyDefinitions.gd") if not bool(definitions.get_definition("chaser").dies_on_contact): return _fail("the chaser is not marked dies_on_contact") if bool(definitions.get_definition("sniper").dies_on_contact): return _fail("the sniper is marked dies_on_contact, so case C proves nothing") # Held as an instance, not read back off fleet.ships: the # wiped-out-fleet safety net rebuilds a vessel within a frame, so # "the fleet is empty" is never true for long enough to observe. victim = fleet.ships[0] chaser = _spawn_enemy("chaser", victim.global_position) chaser.destroyed.connect(func(): destroyed_fired = true) stage = 1 elapsed = 0.0 return false 1: if elapsed < 0.5: return false # --- A/B. one for one ------------------------------------------ if is_instance_valid(chaser) and not chaser.is_queued_for_deletion(): return _fail("the chaser survived ramming a vessel") if not destroyed_fired: return _fail("the chaser vanished without firing `destroyed` - it was not routed through take_damage") if is_instance_valid(victim) and not victim.is_queued_for_deletion(): return _fail("the rammed vessel survived, so the chaser cost nothing to stop") print("chaser and vessel destroyed each other, through the normal death path") # --- C. not a blanket rule ------------------------------------- fleet.spawn_ships(1, Vector2(3000.0, 0.0), PackedStringArray([GameState.VESSEL_INTERCEPTOR])) sniper = _spawn_enemy("sniper", fleet.ships[0].global_position) stage = 2 elapsed = 0.0 return false 2: if elapsed < 0.5: return false if not is_instance_valid(sniper) or sniper.is_queued_for_deletion(): return _fail("a sniper died on contact too - the rule is meant to be per-enemy") print("a sniper survives the same collision") sniper.queue_free() # --- D. a shield saves the vessel, not the chaser -------------- fleet.spawn_ships(1, Vector2(-3000.0, 0.0), PackedStringArray([GameState.VESSEL_SHIELD])) stage = 3 elapsed = 0.0 return false 3: if elapsed < 0.3: return false victim = fleet.ships[0] var shielded: Node2D = victim if not shielded.has_shield(): return _fail("the shield vessel spawned with no shield, so this case proves nothing") chaser = _spawn_enemy("chaser", shielded.global_position) stage = 4 elapsed = 0.0 return false 4: if elapsed < 0.5: return false if is_instance_valid(chaser) and not chaser.is_queued_for_deletion(): return _fail("the chaser survived ramming a shielded vessel") if not is_instance_valid(victim) or victim.is_queued_for_deletion(): return _fail("the shielded vessel died anyway") print("a shielded vessel absorbs the ram and the chaser still dies") print("TEST_CHASER_RAMS: PASS") quit(0) return true return false func _fail(message: String) -> bool: print("TEST_CHASER_RAMS: FAIL (%s)" % message) quit(1) return true