extends SceneTree ## Real physics contacts + blast regression + original sprite playback, no public API. const Grenade = preload("res://scripts/grenade.gd") const Explosion = preload("res://scripts/grenade_explosion.gd") var checks := 0 var failures := 0 var detonations := 0 var impact_time := 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 body(world: Node3D, at: Vector3, size: Vector3, layer: int = 1) -> StaticBody3D: var result := StaticBody3D.new() result.position = at result.collision_layer = layer result.collision_mask = 16 var shape := CollisionShape3D.new() var box := BoxShape3D.new() box.size = size shape.shape = box result.add_child(shape) world.add_child(result) return result func projectile(world: Node3D, at: Vector3, speed: Vector3) -> RigidBody3D: var grenade := Grenade.new() grenade.position = at world.add_child(grenade) grenade.linear_velocity = speed grenade.detonated.connect(func(): detonations += 1 impact_time = Time.get_ticks_msec()) return grenade func contact_case(label: String, at: Vector3, size: Vector3, layer: int, start: Vector3, speed: Vector3) -> void: var world := Node3D.new() root.add_child(world) var obstacle: Node3D if layer == 8: var area := Area3D.new() area.position = at area.collision_layer = 8 area.collision_mask = 0 var shape := CollisionShape3D.new() var box := BoxShape3D.new() box.size = size shape.shape = box area.add_child(shape) world.add_child(area) obstacle = area else: obstacle = body(world, at, size, layer) if layer == 4: obstacle.set_script(load("res://tools/grenade_test_target.gd")) obstacle.add_to_group("live_zombies") await physics_frame detonations = 0 var started := Time.get_ticks_msec() var grenade := projectile(world, start, speed) for i in range(120): await physics_frame if not is_instance_valid(grenade): break check(not is_instance_valid(grenade) and detonations == 1, label + ": esplode una sola volta al contatto") check(impact_time - started < 1500 and detonations == 1, label + ": nessuna attesa della vecchia miccia") if layer == 4: check(obstacle.get("damage_taken") > 0, "nemico colpito: riceve il danno esplosivo") world.queue_free() await process_frame func run_test() -> void: await contact_case("pavimento", Vector3(0, -0.1, 0), Vector3(20, 0.2, 20), 1, Vector3(0, 1.2, 0), Vector3(0, -5, 0)) await contact_case("scrivania", Vector3(0, 0.7, -2), Vector3(2, 1.4, 1), 1, Vector3(0, 1.2, 0), Vector3(0, 0, -12)) await contact_case("parete sottile / CCD", Vector3(0, 1.2, -2), Vector3(4, 4, 0.05), 1, Vector3(0, 1.2, 0), Vector3(0, 0, -80)) await contact_case("nemico", Vector3(0, 0.9, -2), Vector3(0.7, 1.8, 0.7), 4, Vector3(0, 1.2, 0), Vector3(0, 0, -12)) await contact_case("vetro Area3D / attraversamento rapido", Vector3(0, 1.2, -2), Vector3(4, 4, 0.03), 8, Vector3(0, 1.2, 0), Vector3(0, 0, -80)) var world := Node3D.new() root.add_child(world) body(world, Vector3(0, -0.1, 0), Vector3(30, 0.2, 30)) var player = load("res://scripts/player.gd").new() player.position = Vector3(0, 0.1, 4) world.add_child(player) player.hp = 10000 Input.mouse_mode = Input.MOUSE_MODE_VISIBLE await physics_frame var grenade: RigidBody3D = player.throw_grenade() var started := Time.get_ticks_msec() check(grenade != null and player.throw_grenade() == null and is_equal_approx(player.grenade_cooldown, 15), "lancio e cooldown 15s invariati") check(grenade.contact_monitor and grenade.max_contacts_reported > 0 and grenade.continuous_cd and grenade.physics_material_override.bounce == 0, "contatti abilitati, CCD e nessun rimbalzo") var start := grenade.global_position await create_timer(0.15).timeout check(is_instance_valid(grenade) and grenade.position.distance_to(start) > 1 and grenade.linear_velocity.y < 3, "ignora il tiratore; moto e gravità invariati") # Dispose flight before isolating blast LOS/range (no reliance on random bounces). if is_instance_valid(grenade): grenade.queue_free() await process_frame var targets: Array[Node3D] = [] for pos in [Vector3(0, 0, -2), Vector3(2, 0, 0), Vector3(0, 0, -7)]: var target = load("res://tools/grenade_test_target.gd").new() target.position = pos world.add_child(target) targets.append(target) body(world, Vector3(1, 1.5, 0), Vector3(0.2, 3, 2)) await physics_frame grenade = projectile(world, Vector3(0, 0.3, 0), Vector3.ZERO) grenade.freeze = true grenade.thrower = player detonations = 0 grenade._request_detonation(grenade.global_position) grenade._request_detonation(grenade.global_position) await process_frame await process_frame check(detonations == 1, "richieste simultanee non duplicano esplosione e danni") check(targets[0].damage_taken > 0 and targets[1].damage_taken == 0 and targets[2].damage_taken == 0, "danno, occlusione delle pareti e raggio 5m invariati") check(player.hp < 10000, "il tiratore vicino subisce ancora danni") # No fuse: an airborne grenade is not allowed to explode after four seconds. grenade = projectile(world, Vector3(0, 8, 0), Vector3.ZERO) grenade.freeze = true detonations = 0 await create_timer(4.2).timeout check(is_instance_valid(grenade) and not grenade.exploded and detonations == 0, "nessuna detonazione in aria dopo 4s") grenade.age = Grenade.MAX_LIFETIME await physics_frame await process_frame check(not is_instance_valid(grenade) and detonations == 0, "proiettile perso: cleanup senza esplosione") var effect := Explosion.new() world.add_child(effect) effect.set_process(false) check(effect.ATLAS.get_size() == Vector2(340, 204) and effect.material.blend_mode == BaseMaterial3D.BLEND_MODE_ADD and effect.material.billboard_mode == BaseMaterial3D.BILLBOARD_ENABLED, "sprite originale: atlas con gutter, blending additivo, billboard") effect._process(14.1 / Explosion.FPS) check(effect.frame_index == 14 and effect.fireball.visible and is_equal_approx(effect.material.uv1_offset.y, 138.0 / 204.0), "15 fotogrammi a 15 FPS: ultimo frame e UV corretti") effect._process(0.1) check(not effect.fireball.visible, "animazione one-shot: sparisce dopo un secondo") # Wait for the real countdown; never reset or accelerate the player's timer. await create_timer(player.grenade_cooldown + 0.15).timeout check(Time.get_ticks_msec() - started >= 15000 and player.grenade_cooldown == 0 and player.throw_grenade() != null, "nuovo lancio disponibile dopo il cooldown") var main = load("res://scripts/main.gd").new() check(main.zombies_for_wave(1) == 6 and main.zombies_for_wave(11) == 26, "regressione crescita ondate oltre 24 zombie") main.free() world.queue_free() await process_frame print(checks, " controlli superati: ", checks - failures, "; falliti: ", failures) quit(1 if failures else 0)