215 lines
7.4 KiB
GDScript
215 lines
7.4 KiB
GDScript
extends Projectile
|
|
class_name ExplosiveProjectile
|
|
|
|
var explosion_radius: float = 2.5
|
|
var explosion_knockback: float = 20.0
|
|
var can_self_damage: bool = false
|
|
var falloff_curve: float = 1.0 # 1.0 is linear, 2.0 is quadratic (steep drop)
|
|
var explosion_pitch: float = 1.0 # Audio pitch for explosion
|
|
var flight_sound: AudioStreamPlayer3D
|
|
var smoke_sys: GPUParticles3D
|
|
var jet_sys: GPUParticles3D
|
|
|
|
static var _smoke_tex: GradientTexture2D
|
|
|
|
static func get_smoke_texture() -> GradientTexture2D:
|
|
if _smoke_tex: return _smoke_tex
|
|
_smoke_tex = GradientTexture2D.new()
|
|
_smoke_tex.fill = GradientTexture2D.FILL_RADIAL
|
|
_smoke_tex.fill_from = Vector2(0.5, 0.5)
|
|
_smoke_tex.fill_to = Vector2(1.0, 1.0)
|
|
|
|
var g = Gradient.new()
|
|
g.add_point(0.0, Color(1, 1, 1, 1))
|
|
g.add_point(0.5, Color(1, 1, 1, 0.5))
|
|
g.add_point(1.0, Color(1, 1, 1, 0))
|
|
_smoke_tex.gradient = g
|
|
_smoke_tex.width = 64
|
|
_smoke_tex.height = 64
|
|
return _smoke_tex
|
|
|
|
func _ready() -> void:
|
|
super._ready()
|
|
_setup_effects()
|
|
|
|
func destroy() -> void:
|
|
if is_instance_valid(smoke_sys):
|
|
smoke_sys.emitting = false
|
|
var p = smoke_sys.get_parent()
|
|
if p:
|
|
var trans = smoke_sys.global_transform
|
|
p.remove_child(smoke_sys)
|
|
get_tree().current_scene.add_child(smoke_sys)
|
|
smoke_sys.global_transform = trans
|
|
var timer = smoke_sys.get_tree().create_timer(smoke_sys.lifetime)
|
|
timer.timeout.connect(smoke_sys.queue_free)
|
|
|
|
if is_instance_valid(jet_sys):
|
|
jet_sys.emitting = false
|
|
var p = jet_sys.get_parent()
|
|
if p:
|
|
var trans = jet_sys.global_transform
|
|
p.remove_child(jet_sys)
|
|
get_tree().current_scene.add_child(jet_sys)
|
|
jet_sys.global_transform = trans
|
|
var timer = jet_sys.get_tree().create_timer(jet_sys.lifetime)
|
|
timer.timeout.connect(jet_sys.queue_free)
|
|
|
|
super.destroy()
|
|
|
|
func _setup_effects() -> void:
|
|
# Audio
|
|
flight_sound = AudioStreamPlayer3D.new()
|
|
AudioManager.configure_player(flight_sound, "rocket_flight")
|
|
flight_sound.autoplay = true
|
|
# Localized: audible nearby, falls off with distance instead of droning
|
|
# in every player's ear regardless of where the projectile is.
|
|
flight_sound.unit_size = 6.0
|
|
flight_sound.max_db = 0.0
|
|
flight_sound.max_distance = 45.0
|
|
flight_sound.add_to_group("acoustic_occluded") # muffles behind walls
|
|
add_child(flight_sound)
|
|
flight_sound.play()
|
|
|
|
# Jet Fire Particles
|
|
jet_sys = GPUParticles3D.new()
|
|
var jet_mat = ParticleProcessMaterial.new()
|
|
jet_mat.direction = Vector3(0, 0, 1) # Backwards (since model looks at -Z)
|
|
jet_mat.spread = 10.0
|
|
jet_mat.initial_velocity_min = 1.0
|
|
jet_mat.initial_velocity_max = 2.0
|
|
jet_mat.gravity = Vector3.ZERO
|
|
|
|
var jet_mesh = BoxMesh.new()
|
|
jet_mesh.size = Vector3(0.05, 0.05, 0.05)
|
|
var mat = StandardMaterial3D.new()
|
|
mat.albedo_color = Color(1.0, 0.6, 0.0)
|
|
mat.emission_enabled = true
|
|
mat.emission = Color(1.0, 0.5, 0.0)
|
|
jet_mesh.material = mat
|
|
jet_sys.draw_pass_1 = jet_mesh
|
|
jet_sys.process_material = jet_mat
|
|
jet_sys.amount = 16
|
|
jet_sys.lifetime = 0.2
|
|
jet_sys.position = Vector3(0, 0, 0.1)
|
|
add_child(jet_sys)
|
|
|
|
# Smoke Trail
|
|
smoke_sys = GPUParticles3D.new()
|
|
var smoke_mat = ParticleProcessMaterial.new()
|
|
smoke_mat.direction = Vector3(0, 0, 1)
|
|
smoke_mat.spread = 5.0
|
|
smoke_mat.initial_velocity_min = 0.0
|
|
smoke_mat.initial_velocity_max = 0.5
|
|
smoke_mat.gravity = Vector3(0, 0.5, 0)
|
|
|
|
var grad = Gradient.new()
|
|
grad.add_point(0.0, Color(0.5, 0.5, 0.5, 0.8))
|
|
grad.add_point(1.0, Color(0.8, 0.8, 0.8, 0.0))
|
|
var grad_tex = GradientTexture1D.new()
|
|
grad_tex.gradient = grad
|
|
smoke_mat.color_ramp = grad_tex
|
|
|
|
var curve = Curve.new()
|
|
curve.add_point(Vector2(0.0, 1.0))
|
|
curve.add_point(Vector2(1.0, 3.0))
|
|
var curve_tex = CurveTexture.new()
|
|
curve_tex.curve = curve
|
|
smoke_mat.scale_curve = curve_tex
|
|
|
|
var smoke_mesh = QuadMesh.new()
|
|
smoke_mesh.size = Vector2(0.2, 0.2)
|
|
var s_mat = StandardMaterial3D.new()
|
|
s_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
s_mat.vertex_color_use_as_albedo = true
|
|
s_mat.albedo_texture = get_smoke_texture()
|
|
s_mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
|
s_mat.billboard_keep_scale = true
|
|
smoke_mesh.material = s_mat
|
|
smoke_sys.draw_pass_1 = smoke_mesh
|
|
smoke_sys.process_material = smoke_mat
|
|
smoke_sys.amount = 32
|
|
smoke_sys.lifetime = 1.5
|
|
smoke_sys.position = Vector3(0, 0, 0.15)
|
|
add_child(smoke_sys)
|
|
|
|
func _on_hit(result: Dictionary) -> void:
|
|
if not result.collider.has_method("take_damage"):
|
|
if result.collider is StaticBody3D or result.collider is CSGShape3D:
|
|
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
|
|
elif owner_player and owner_player.multiplayer.is_server():
|
|
result.collider.take_damage(damage, result.position, owner_player)
|
|
_explode(result.position)
|
|
destroy()
|
|
|
|
func _explode(pos: Vector3) -> void:
|
|
var space_state = get_world_3d().direct_space_state
|
|
|
|
# We use a sphere shape query to find all physics bodies in radius
|
|
var sphere = SphereShape3D.new()
|
|
sphere.radius = explosion_radius
|
|
|
|
var params = PhysicsShapeQueryParameters3D.new()
|
|
params.shape = sphere
|
|
params.transform = Transform3D(Basis(), pos)
|
|
# Find bodies and areas
|
|
params.collide_with_bodies = true
|
|
|
|
var results = space_state.intersect_shape(params)
|
|
|
|
# We want to only damage each object once (intersect_shape can return multiple shapes of the same object)
|
|
var processed_rids = {}
|
|
|
|
for res in results:
|
|
var col = res.collider
|
|
if not col or processed_rids.has(res.rid):
|
|
continue
|
|
processed_rids[res.rid] = true
|
|
|
|
# Calculate distance
|
|
var target_pos = col.global_position
|
|
# If it's a character, maybe aim at their center (offset y by 1.0)
|
|
if col is CharacterBody3D or col.has_method("take_damage"):
|
|
if col.has_node("CollisionShape3D"):
|
|
target_pos = col.get_node("CollisionShape3D").global_position
|
|
|
|
var dist = pos.distance_to(target_pos)
|
|
if dist > explosion_radius:
|
|
continue
|
|
|
|
# Normalised distance (0 = epicenter, 1 = edge)
|
|
var ndist = dist / explosion_radius
|
|
var falloff = clampf(pow(ndist, falloff_curve), 0.0, 1.0)
|
|
var damage_intensity = lerpf(1.0, 0.33, falloff)
|
|
var knockback_intensity = 1.0 - falloff
|
|
|
|
var final_damage = damage * damage_intensity
|
|
var final_knockback = explosion_knockback * knockback_intensity
|
|
|
|
# Knockback direction: from explosion pos to target
|
|
var dir = (target_pos - pos).normalized()
|
|
# Add a tiny bit of upward bias to knockback so it lifts them
|
|
dir = (dir + Vector3(0, 0.5, 0)).normalized()
|
|
|
|
# Apply knockback if it has server_apply_impulse (for rocket jumping)
|
|
if col.has_method("server_apply_impulse") and col == owner_player:
|
|
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
|
col.server_apply_impulse.rpc_id(1, dir * final_knockback)
|
|
else:
|
|
col.apply_impulse(dir * final_knockback)
|
|
|
|
# Apply damage (and knockback for other players via take_damage)
|
|
if col.has_method("take_damage"):
|
|
if col == owner_player and not can_self_damage:
|
|
# Skip self damage (we already applied knockback above)
|
|
pass
|
|
elif owner_player:
|
|
col.take_damage(final_damage, target_pos, owner_player, dir * final_knockback)
|
|
|
|
# Broadcast Visual Explosion Effect to other peers
|
|
if owner_player and owner_player.is_multiplayer_authority() and owner_player.has_method("server_play_explosion") and owner_player.multiplayer.has_multiplayer_peer() and not(owner_player.multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
|
owner_player.server_play_explosion.rpc_id(1, pos, explosion_radius)
|
|
|
|
# Local visual + audio: shared cel-shaded burst (also used by remote replay)
|
|
ExplosionVFX.spawn(get_tree(), pos, explosion_radius, explosion_pitch)
|