Detonate rifle grenades on impact with original Half-Life fireball
This commit is contained in:
+153
-48
@@ -1,70 +1,175 @@
|
||||
extends SceneTree
|
||||
var detonation_time := -1
|
||||
## 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 run_test() -> void:
|
||||
|
||||
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 room := Node3D.new()
|
||||
room.set_script(load("res://scripts/room.gd"))
|
||||
world.add_child(room)
|
||||
var player := CharacterBody3D.new()
|
||||
player.set_script(load("res://scripts/player.gd"))
|
||||
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()
|
||||
assert(grenade != null)
|
||||
assert(player.throw_grenade() == null)
|
||||
assert(is_equal_approx(player.grenade_cooldown, 15))
|
||||
assert(grenade.physics_material_override.bounce > 0)
|
||||
grenade.detonated.connect(func(): detonation_time = Time.get_ticks_msec())
|
||||
var started := Time.get_ticks_msec()
|
||||
var start := grenade.position
|
||||
await create_timer(0.5).timeout
|
||||
assert(grenade.position.distance_to(start) > 1)
|
||||
assert(grenade.linear_velocity.y < 3)
|
||||
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 := Node3D.new()
|
||||
target.set_script(load("res://tools/grenade_test_target.gd"))
|
||||
var target = load("res://tools/grenade_test_target.gd").new()
|
||||
target.position = pos
|
||||
world.add_child(target)
|
||||
targets.append(target)
|
||||
var wall := StaticBody3D.new()
|
||||
wall.position = Vector3(1, 1.5, 0)
|
||||
var shape := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(0.2, 3, 2)
|
||||
shape.shape = box
|
||||
wall.add_child(shape)
|
||||
world.add_child(wall)
|
||||
# Isolate explosion coverage from the projectile's random rotation/bounces.
|
||||
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.global_position = Vector3(0, 0.3, 0)
|
||||
await create_timer(3.15).timeout
|
||||
assert(is_instance_valid(grenade) and not grenade.exploded)
|
||||
await create_timer(0.6).timeout
|
||||
assert(detonation_time >= 0)
|
||||
var seconds := (detonation_time - started) / 1000.0
|
||||
assert(seconds >= 3.8 and seconds <= 4.3)
|
||||
assert(targets[0].damage_taken > 0)
|
||||
assert(targets[1].damage_taken == 0) # behind a wall
|
||||
assert(targets[2].damage_taken == 0) # beyond blast radius
|
||||
assert(player.hp < 10000)
|
||||
assert(player.throw_grenade() == null)
|
||||
# Wait until the real cooldown expires, without changing the game timer.
|
||||
await create_timer(player.grenade_cooldown + 0.1).timeout
|
||||
assert(player.grenade_cooldown == 0)
|
||||
assert(player.throw_grenade() != null)
|
||||
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()
|
||||
assert(main.zombies_for_wave(1) == 6)
|
||||
assert(main.zombies_for_wave(2) == 8)
|
||||
assert(main.zombies_for_wave(11) == 26)
|
||||
check(main.zombies_for_wave(1) == 6 and main.zombies_for_wave(11) == 26,
|
||||
"regressione crescita ondate oltre 24 zombie")
|
||||
main.free()
|
||||
print("PASS: thrown physics/gravity, 4s fuse (", seconds, "), 15s cooldown, blast LOS/range/self-damage, wave growth beyond 24")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
print(checks, " controlli superati: ", checks - failures, "; falliti: ", failures)
|
||||
quit(1 if failures else 0)
|
||||
|
||||
Reference in New Issue
Block a user