141 lines
4.7 KiB
GDScript
141 lines
4.7 KiB
GDScript
extends RigidBody3D
|
|
signal detonated
|
|
# 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 := 320.0
|
|
const Audio = preload("res://scripts/hl_audio.gd")
|
|
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.0
|
|
physics.friction = 0.65
|
|
physics_material_override = physics
|
|
var collider := CollisionShape3D.new()
|
|
var sphere := SphereShape3D.new()
|
|
sphere.radius = 0.12
|
|
collider.shape = sphere
|
|
add_child(collider)
|
|
var model: Node3D = load("res://assets/weapons/grenade.scn").instantiate()
|
|
add_child(model)
|
|
for area in model.find_children("*", "Area3D", true, false):
|
|
area.collision_layer = 0
|
|
area.collision_mask = 0
|
|
for mesh in model.find_children("*", "MeshInstance3D", true, false):
|
|
mesh.layers = 2
|
|
if is_instance_valid(thrower):
|
|
add_collision_exception_with(thrower)
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
age += delta
|
|
# 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
|
|
collision_layer = 0
|
|
collision_mask = 0
|
|
hide()
|
|
var world := get_parent() as Node3D
|
|
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)
|
|
for target in targets:
|
|
if not is_instance_valid(target):
|
|
continue
|
|
var center: Vector3 = target.global_position + Vector3.UP * 0.9
|
|
var distance := point.distance_to(center)
|
|
if distance > BLAST_RADIUS:
|
|
continue
|
|
var query := PhysicsRayQueryParameters3D.create(point, center, 1)
|
|
if not get_world_3d().direct_space_state.intersect_ray(query).is_empty():
|
|
continue
|
|
var damage := MAX_DAMAGE * (1.0 - distance / BLAST_RADIUS)
|
|
if target == thrower:
|
|
target.take_damage(damage)
|
|
else:
|
|
Blood.impact(world, center, (center - point).normalized())
|
|
target.take_damage(damage, center.y)
|
|
_effect(world, point)
|
|
detonated.emit()
|
|
queue_free()
|
|
|
|
func _effect(world: Node3D, point: Vector3) -> void:
|
|
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)
|
|
# Secondary embers only: the original fireball is now the main visual.
|
|
var particles := CPUParticles3D.new()
|
|
particles.amount = 18
|
|
particles.lifetime = 0.85
|
|
particles.one_shot = true
|
|
particles.explosiveness = 1
|
|
particles.direction = Vector3.UP
|
|
particles.spread = 180
|
|
particles.initial_velocity_min = 2
|
|
particles.initial_velocity_max = 6
|
|
particles.gravity = Vector3(0, -3, 0)
|
|
var mesh := SphereMesh.new()
|
|
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
|
|
mesh.material = material
|
|
particles.mesh = mesh
|
|
var colors := Gradient.new()
|
|
colors.set_color(0, Color(1, 0.65, 0.1, 1))
|
|
colors.set_color(1, Color(0.15, 0.12, 0.1, 0))
|
|
particles.color_ramp = colors
|
|
effect.add_child(particles)
|
|
particles.emitting = true
|