Detonate rifle grenades on impact with original Half-Life fireball
This commit is contained in:
+46
-19
@@ -1,6 +1,8 @@
|
||||
extends RigidBody3D
|
||||
signal detonated
|
||||
const FUSE_SECONDS := 4.0
|
||||
# No timed fuse: the rifle grenade detonates on its first physical contact.
|
||||
const MAX_LIFETIME := 15.0
|
||||
const Explosion = preload("res://scripts/grenade_explosion.gd")
|
||||
const BLAST_RADIUS := 5.0
|
||||
const MAX_DAMAGE := 160.0
|
||||
const Audio = preload("res://scripts/hl_audio.gd")
|
||||
@@ -8,18 +10,24 @@ const Blood = preload("res://scripts/blood_effects.gd")
|
||||
var age := 0.0
|
||||
var exploded := false
|
||||
var thrower: CharacterBody3D
|
||||
var previous_position := Vector3.ZERO
|
||||
var impact_position := Vector3.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
name = "Grenade"
|
||||
add_to_group("grenades")
|
||||
previous_position = global_position
|
||||
collision_layer = 16
|
||||
collision_mask = 1 | 4
|
||||
mass = 0.4
|
||||
continuous_cd = true
|
||||
contact_monitor = true
|
||||
max_contacts_reported = 8
|
||||
body_entered.connect(_on_body_entered)
|
||||
linear_damp = 0.15
|
||||
angular_damp = 0.6
|
||||
var physics := PhysicsMaterial.new()
|
||||
physics.bounce = 0.45
|
||||
physics.bounce = 0.0
|
||||
physics.friction = 0.65
|
||||
physics_material_override = physics
|
||||
var collider := CollisionShape3D.new()
|
||||
@@ -39,9 +47,36 @@ func _ready() -> void:
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
age += delta
|
||||
if age >= FUSE_SECONDS and not exploded:
|
||||
exploded = true
|
||||
call_deferred("_explode")
|
||||
# Window panes are Area3D on layer 8, not PhysicsBody3D. A swept ray catches
|
||||
# those too, even if a fast grenade crosses the whole pane in one tick.
|
||||
if not exploded and not previous_position.is_equal_approx(global_position):
|
||||
var query := PhysicsRayQueryParameters3D.create(previous_position, global_position, 8)
|
||||
query.collide_with_areas = true
|
||||
query.collide_with_bodies = false
|
||||
var hit := get_world_3d().direct_space_state.intersect_ray(query)
|
||||
if not hit.is_empty():
|
||||
_request_detonation(hit.position + hit.normal * 0.13)
|
||||
previous_position = global_position
|
||||
# Safety cleanup only: a projectile lost outside the map must not explode in air.
|
||||
if age >= MAX_LIFETIME and not exploded:
|
||||
queue_free()
|
||||
|
||||
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
|
||||
if not exploded and state.get_contact_count() > 0:
|
||||
# Save the physics position before deferring scene/shape changes. The sphere
|
||||
# centre stays on the free side of the surface, also for damage LOS rays.
|
||||
_request_detonation(state.transform.origin)
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if body != thrower:
|
||||
_request_detonation(global_position)
|
||||
|
||||
func _request_detonation(point: Vector3) -> void:
|
||||
if exploded:
|
||||
return
|
||||
exploded = true
|
||||
impact_position = point
|
||||
call_deferred("_explode")
|
||||
|
||||
func _explode() -> void:
|
||||
freeze = true
|
||||
@@ -49,7 +84,7 @@ func _explode() -> void:
|
||||
collision_mask = 0
|
||||
hide()
|
||||
var world := get_parent() as Node3D
|
||||
var point := global_position
|
||||
var point := impact_position
|
||||
var targets := get_tree().get_nodes_in_group("live_zombies")
|
||||
if is_instance_valid(thrower) and not thrower.death_started:
|
||||
targets.append(thrower)
|
||||
@@ -74,20 +109,13 @@ func _explode() -> void:
|
||||
queue_free()
|
||||
|
||||
func _effect(world: Node3D, point: Vector3) -> void:
|
||||
var effect := Node3D.new()
|
||||
effect.name = "GrenadeExplosion"
|
||||
var effect := Explosion.new()
|
||||
world.add_child(effect)
|
||||
effect.global_position = point
|
||||
Audio.play(effect, "weapons/explode%d.wav" % randi_range(3, 5), true, -7)
|
||||
var light := OmniLight3D.new()
|
||||
light.light_color = Color(1, 0.45, 0.08)
|
||||
light.light_energy = 8
|
||||
light.omni_range = 10
|
||||
effect.add_child(light)
|
||||
var tween := effect.create_tween()
|
||||
tween.tween_property(light, "light_energy", 0.0, 0.3)
|
||||
# Secondary embers only: the original fireball is now the main visual.
|
||||
var particles := CPUParticles3D.new()
|
||||
particles.amount = 60
|
||||
particles.amount = 18
|
||||
particles.lifetime = 0.85
|
||||
particles.one_shot = true
|
||||
particles.explosiveness = 1
|
||||
@@ -97,8 +125,8 @@ func _effect(world: Node3D, point: Vector3) -> void:
|
||||
particles.initial_velocity_max = 6
|
||||
particles.gravity = Vector3(0, -3, 0)
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = 0.06
|
||||
mesh.height = 0.12
|
||||
mesh.radius = 0.018
|
||||
mesh.height = 0.036
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = Color(1, 0.3, 0.05)
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
@@ -110,4 +138,3 @@ func _effect(world: Node3D, point: Vector3) -> void:
|
||||
particles.color_ramp = colors
|
||||
effect.add_child(particles)
|
||||
particles.emitting = true
|
||||
world.get_tree().create_timer(4).timeout.connect(effect.queue_free)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
extends Node3D
|
||||
## Original Half-Life fireball, 15 frames at 15 FPS, additive camera-facing quad.
|
||||
## A PNG atlas avoids the native GoldSrc importer in Web exports.
|
||||
const ATLAS = preload("res://assets/effects/explosion/zerogxplode_atlas.png")
|
||||
const FRAME_COUNT := 15
|
||||
const FPS := 15.0
|
||||
const DURATION := FRAME_COUNT / FPS
|
||||
const SIZE := 3.2
|
||||
var elapsed := 0.0
|
||||
var frame_index := 0
|
||||
var fireball: MeshInstance3D
|
||||
var material: StandardMaterial3D
|
||||
|
||||
func _ready() -> void:
|
||||
name = "GrenadeExplosion"
|
||||
material = StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
material.albedo_texture = ATLAS
|
||||
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
|
||||
material.texture_repeat = false
|
||||
# Two-pixel black gutters prevent adjacent frames bleeding under filtering.
|
||||
material.uv1_scale = Vector3(64.0 / 340.0, 64.0 / 204.0, 1)
|
||||
fireball = MeshInstance3D.new()
|
||||
fireball.name = "Fireball"
|
||||
fireball.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(SIZE, SIZE)
|
||||
quad.material = material
|
||||
fireball.mesh = quad
|
||||
# Raise the fireball, but leave the damage origin at the physical impact.
|
||||
fireball.position.y = 0.8
|
||||
add_child(fireball)
|
||||
_set_frame(0)
|
||||
var light := OmniLight3D.new()
|
||||
light.name = "Flash"
|
||||
light.light_color = Color(1, 0.45, 0.08)
|
||||
light.light_energy = 8
|
||||
light.omni_range = 10
|
||||
light.shadow_enabled = false
|
||||
add_child(light)
|
||||
create_tween().tween_property(light, "light_energy", 0.0, 0.3)
|
||||
# Keep the node alive for the original explosion sound to finish.
|
||||
get_tree().create_timer(4).timeout.connect(queue_free)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
elapsed += delta
|
||||
if elapsed >= DURATION:
|
||||
fireball.hide()
|
||||
set_process(false)
|
||||
return
|
||||
_set_frame(mini(int(elapsed * FPS), FRAME_COUNT - 1))
|
||||
|
||||
func _set_frame(index: int) -> void:
|
||||
frame_index = index
|
||||
material.uv1_offset = Vector3(
|
||||
(float(index % 5) * 68 + 2) / 340.0,
|
||||
(float(index / 5) * 68 + 2) / 204.0, 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d030pl0ug1qcs
|
||||
Reference in New Issue
Block a user