Add scaling Alien Grunt boss waves and native simulation; publish Web release
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user