extends SceneTree const Boss = preload("res://scripts/alien_grunt.gd") const Hornet = preload("res://scripts/boss_hornet.gd") const Main = preload("res://scripts/main.gd") var checks := 0 var failures := 0 var killed_events := 0 func _initialize() -> void: call_deferred("run_test") func check(ok: bool, label: String) -> void: checks += 1 if not ok: failures += 1 print("PASS: " if ok else "FAIL: ", label) func make_world() -> Node3D: var world = load("res://scenes/main.tscn").instantiate() root.add_child(world) world.player.hp = 100000 world.player.set_physics_process(false) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE return world func target_body(world: Node3D, at: Vector3, size: Vector3) -> StaticBody3D: var body := StaticBody3D.new() body.position = at var collider := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = size collider.shape = shape body.add_child(collider) world.add_child(body) return body func profile_snapshot() -> String: if not FileAccess.file_exists(Main.Profile.PATH): return "ABSENT" return FileAccess.get_sha256(Main.Profile.PATH) func run_test() -> void: var profile_before := profile_snapshot() check(not Main.is_boss_wave(0) and not Main.is_boss_wave(9) and Main.is_boss_wave(10) and not Main.is_boss_wave(11) and Main.is_boss_wave(20), "boss solo ogni dieci ondate") var a := Boss.stats_for_wave(10) var b := Boss.stats_for_wave(20) var c := Boss.stats_for_wave(30) check(a.rank == 1 and b.rank == 2 and c.rank == 3 and a.hp == 800 and b.hp == 1200 and c.hp == 1600, "grado e salute crescono a ogni comparsa") check(a.melee < b.melee and b.melee < c.melee and a.ranged < b.ranged and b.ranged < c.ranged and a.speed < b.speed and b.interval < a.interval, "danni, velocita' e cadenza crescono") var late := Boss.stats_for_wave(10000) check(late.speed <= 3.4 and late.interval >= 1.3, "mobilita' e cadenza hanno limiti giocabili") var world = make_world() await physics_frame await physics_frame check(world.zombies_for_wave(9) == 22 and world.zombies_for_wave(10) == 0 and world.zombies_for_wave(11) == 26, "ondata boss sostituisce gli zombie, non si somma") for i in range(16): var at: Vector3 = world._boss_spawn_position() check(at.is_finite() and at.distance_to(world.player.global_position) >= 5, "portale libero e distante: estrazione %d" % (i + 1)) # Real 9 -> 10 transition: finish all 22 ordinary enemies before the boss. world.wave = 8 world.start_game("Test", false) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE check(world.wave == 9 and world.boss == null and world.spawning, "ondata 9 ancora ordinaria") await create_timer(18.0).timeout check(world.alive == 22 and not world.spawning, "ondata 9 completa: 22 zombie, nessun boss") for zombie in get_nodes_in_group("live_zombies"): zombie.take_damage(1000000, zombie.global_position.y + 0.9) await create_timer(3.2).timeout var actor = world.boss var score_before_boss: int = world.score check(world.wave == 10 and world.alive == 1 and not world.spawning and get_nodes_in_group("live_zombies").size() == 1 and actor.is_in_group("bosses"), "ondata 10: esattamente un boss e nessuno zombie ordinario") check(actor.entering and not actor.model.visible and actor.collision_layer == 0, "preavviso teletrasporto: boss non ancora attivo") var hp_before: float = actor.hp actor.take_damage(100, actor.position.y + 1) world._check_wave() check(actor.hp == hp_before and not world.wave_pending, "invulnerabile durante arrivo; niente salto ondata") await create_timer(1.65).timeout actor.set_physics_process(false) check(not actor.entering and actor.model.visible and actor.collision_layer == 4, "teletrasporto materializza modello e hitbox") check(actor.anim.has_animation("walk") and actor.anim.has_animation("hornetattack") and actor.anim.has_animation("mattack2") and actor.anim.has_animation("diesimple"), "animazioni di combattimento e morte originali presenti") var surfaces := 0 var textured := true for mesh in actor.model.find_children("*", "MeshInstance3D", true, false): for i in mesh.mesh.get_surface_count(): var material = mesh.mesh.surface_get_material(i) textured = textured and material is StandardMaterial3D and material.albedo_texture != null surfaces += 1 check(textured and surfaces > 0, "modello originale con texture incorporate, scena nativa") # Stable open lane, actual hitscan and grenade blast (same live_zombies interface). actor.position = Vector3(0, 0.05, -3) world.player.position = Vector3(0, 0.1, 2) world.player.camera.look_at(actor.global_position + Vector3.UP * 1.2) await physics_frame await physics_frame hp_before = actor.hp world.player.shoot() check(actor.hp < hp_before, "fucile: il boss riceve i colpi tramite la normale hitbox") var grenade = load("res://scripts/grenade.gd").new() grenade.thrower = world.player grenade.position = actor.position + Vector3(0, 0.4, 1) world.add_child(grenade) grenade.freeze = true hp_before = actor.hp grenade._request_detonation(grenade.global_position) await process_frame await process_frame check(actor.hp < hp_before, "granata: il boss riceve il danno esplosivo") world.hud.update_boss(actor) check(world.hud.boss_panel.visible and world.hud.boss_health.value == actor.hp and world.hud.boss_health.max_value == actor.max_hp, "barra vita del boss segue i danni") # Real hornet flight towards player, with a separate obstruction regression. var player_hp: float = world.player.hp actor._fire_hornet() check(get_nodes_in_group("boss_projectiles").size() == 1, "attacco alieno crea un proiettile visibile") await create_timer(0.8).timeout check(world.player.hp < player_hp, "proiettile alieno colpisce e danneggia il giocatore") var wall := target_body(world, Vector3(0, 1.4, -0.5), Vector3(3, 3, 0.15)) await physics_frame await physics_frame player_hp = world.player.hp actor._fire_hornet() await create_timer(0.8).timeout check(world.player.hp == player_hp and get_nodes_in_group("boss_projectiles").is_empty(), "parete ferma i proiettili: niente colpi attraverso gli oggetti") wall.queue_free() await process_frame # Melee cannot reach through the existing desk. world.player.position = Vector3(-5, 0.1, -3.2) actor.position = Vector3(-5, 0.05, -4.8) await physics_frame await physics_frame player_hp = world.player.hp actor._attack() check(world.player.hp == player_hp, "corpo a corpo bloccato dalla scrivania") actor.position = Vector3(0, 0.05, -1.5) world.player.position = Vector3(0, 0.1, 0) await physics_frame await physics_frame actor._attack() check(world.player.hp < player_hp, "corpo a corpo infligge danno a distanza utile") # AI pathfinding actually moves around an office desk, without teleport tricks. var routes := [ [Vector3(-5, 0.05, -6.5), Vector3(-5, 0.1, 0)], [Vector3(5, 0.05, 1.5), Vector3(5, 0.1, 7.5)], [Vector3(-6, 0.05, 4), Vector3(-2, 0.1, 4)], [Vector3(6.8, 0.05, -6), Vector3(6.8, 0.1, -2)], ] for route in routes: world.player.position = route[1] actor.position = route[0] actor.cooldown = 100 actor.attack_lock = 0 actor._nav_started = false actor._repath = 0 actor.set_physics_process(true) var reached := false for i in range(650): await physics_frame if actor.position.distance_to(world.player.position) < 2.3: reached = true break actor.set_physics_process(false) check(reached, "boss aggira gli arredi: %s -> %s" % [route[0], route[1]]) # A queued burst must never fire after death. actor._start_burst() actor.killed.connect(func(_head): killed_events += 1) actor.take_damage(1000000, actor.global_position.y + 1) actor.take_damage(1000000, actor.global_position.y + 1) check(killed_events == 1 and world.alive == 0 and world.score == score_before_boss + actor.reward and world.wave_pending, "morte unica, ricompensa unica e completamento ondata boss") world.hud.update_boss(actor) check(not world.hud.boss_panel.visible and actor.collision_layer == 0 and not actor.is_in_group("live_zombies"), "morto: HUD nascosto, corpo non blocca e non conta vivo") await create_timer(0.8).timeout check(get_nodes_in_group("boss_projectiles").is_empty(), "raffica pendente annullata dalla morte") await create_timer(2.5).timeout check(world.wave == 11 and world.boss == null and world.alive > 0 and get_nodes_in_group("bosses").is_empty(), "10 -> 11: riprendono gli zombie normali una sola volta") world.queue_free() await process_frame # Native sandbox: no start menu/network/profile; stops after the selected boss. world = load("res://scenes/main.tscn").instantiate() world.boss_simulation = true world.simulation_wave = 20 root.add_child(world) world.player.hp = 100000 world.player.set_physics_process(false) await create_timer(1.8).timeout Input.mouse_mode = Input.MOUSE_MODE_VISIBLE actor = world.boss actor.set_physics_process(false) check(world.wave == 20 and actor.rank == 2 and actor.max_hp == 1200 and world.start_screen == null and world.leaderboard.disabled and world.leaderboard._pending.is_empty(), "simulazione diretta grado 2, senza menu/classifica") actor.take_damage(1000000, actor.position.y + 1) await create_timer(3.2).timeout check(world.simulation_finished and world.simulation_retry.visible and world.wave == 20 and world.alive == 0, "simulazione termina nella singola ondata; pulsante RIPROVA") world.player.invulnerable = true world.player.take_damage(1000000) check(world.player.hp > 0, "opzione invulnerabile per prove native") world.player.invulnerable = false world.player.take_damage(1000000) check(world.leaderboard._pending.is_empty() and not world.score_submitted and world.profile.best == 0, "morte in simulazione non invia punteggi e non aggiorna il record") check(profile_before == profile_snapshot(), "profilo persistente byte-identico dopo i test") world.queue_free() await process_frame print(checks - failures, " controlli superati, ", failures, " falliti (", checks, " totali)") quit(1 if failures else 0)