Files

224 lines
9.8 KiB
GDScript

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)