172 lines
6.8 KiB
GDScript
172 lines
6.8 KiB
GDScript
extends Object
|
|
class_name ExplosionVFX
|
|
|
|
## Cel-shaded anime explosion, shared by local projectiles and the remote
|
|
## replay RPC. Flat unshaded colors stepped through hot stages (white core ->
|
|
## yellow -> orange -> smoke), an expanding ink shockwave ring, and radiating
|
|
## star spikes — no gradients, no realistic fire, all comic-book punch.
|
|
|
|
const CORE_STAGES := [
|
|
Color(1.0, 1.0, 0.95), # white-hot flash
|
|
Color(1.0, 0.85, 0.2), # yellow
|
|
Color(1.0, 0.45, 0.1), # orange
|
|
]
|
|
const INK := Color(0.09, 0.08, 0.1)
|
|
const SMOKE := Color(0.82, 0.8, 0.85)
|
|
|
|
|
|
static func _flat_mat(color: Color, emissive: float = 0.0) -> StandardMaterial3D:
|
|
var m := StandardMaterial3D.new()
|
|
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
m.albedo_color = color
|
|
if emissive > 0.0:
|
|
m.emission_enabled = true
|
|
m.emission = color
|
|
m.emission_energy_multiplier = emissive
|
|
return m
|
|
|
|
|
|
static func spawn(scene_tree: SceneTree, pos: Vector3, radius: float,
|
|
pitch_scale: float = 1.0) -> void:
|
|
var root := scene_tree.current_scene
|
|
if root == null:
|
|
return
|
|
|
|
# ── Core fireball: stepped color pops (each stage is a hard swap) ─────
|
|
var core := MeshInstance3D.new()
|
|
var core_mesh := SphereMesh.new()
|
|
core_mesh.radius = 1.0
|
|
core_mesh.height = 2.0
|
|
core_mesh.radial_segments = 16
|
|
core_mesh.rings = 8
|
|
core.mesh = core_mesh
|
|
var core_mat := _flat_mat(CORE_STAGES[0], 2.0)
|
|
core.material_override = core_mat
|
|
root.add_child(core)
|
|
core.global_position = pos
|
|
core.scale = Vector3.ONE * radius * 0.35
|
|
|
|
var t := core.create_tween()
|
|
t.tween_property(core, "scale", Vector3.ONE * radius * 0.9, 0.07) \
|
|
.set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
|
|
t.tween_callback(func(): core_mat.albedo_color = CORE_STAGES[1]; core_mat.emission = CORE_STAGES[1])
|
|
t.tween_property(core, "scale", Vector3.ONE * radius * 1.05, 0.08)
|
|
t.tween_callback(func(): core_mat.albedo_color = CORE_STAGES[2]; core_mat.emission = CORE_STAGES[2])
|
|
t.tween_property(core, "scale", Vector3.ONE * radius * 1.15, 0.08)
|
|
# collapse to nothing (anime explosions vanish, they don't fade)
|
|
t.tween_property(core, "scale", Vector3.ONE * 0.01, 0.12) \
|
|
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN)
|
|
t.tween_callback(core.queue_free)
|
|
|
|
# ── Ink shockwave ring: flat torus expanding outward ──────────────────
|
|
var ring := MeshInstance3D.new()
|
|
var ring_mesh := TorusMesh.new()
|
|
ring_mesh.inner_radius = 0.92
|
|
ring_mesh.outer_radius = 1.0
|
|
ring_mesh.rings = 32
|
|
ring_mesh.ring_segments = 6
|
|
ring.mesh = ring_mesh
|
|
ring.material_override = _flat_mat(INK)
|
|
root.add_child(ring)
|
|
ring.global_position = pos
|
|
ring.scale = Vector3.ONE * radius * 0.3
|
|
|
|
var rt := ring.create_tween()
|
|
rt.tween_property(ring, "scale", Vector3(radius * 2.2, radius * 0.5, radius * 2.2), 0.28) \
|
|
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
|
rt.parallel().tween_property(ring, "transparency", 1.0, 0.28) \
|
|
.set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN)
|
|
rt.tween_callback(ring.queue_free)
|
|
|
|
# ── Star spikes: stretched octahedra radiating out (impact flash) ─────
|
|
var spike_count := 6
|
|
for i in spike_count:
|
|
var spike := MeshInstance3D.new()
|
|
var spike_mesh := PrismMesh.new()
|
|
spike_mesh.size = Vector3(0.14, 1.0, 0.14)
|
|
spike.mesh = spike_mesh
|
|
spike.material_override = _flat_mat(Color(1.0, 0.95, 0.6), 1.5)
|
|
root.add_child(spike)
|
|
var dir := Vector3(randf_range(-1, 1), randf_range(-0.3, 1), randf_range(-1, 1)).normalized()
|
|
spike.global_position = pos + dir * radius * 0.3
|
|
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.98 else Vector3.RIGHT
|
|
spike.look_at(pos + dir * 2.0, up)
|
|
spike.rotate_object_local(Vector3.RIGHT, PI / 2.0)
|
|
spike.scale = Vector3(radius * 0.25, radius * 0.9, radius * 0.25)
|
|
|
|
var st := spike.create_tween()
|
|
st.tween_property(spike, "global_position", pos + dir * radius * 1.5, 0.16) \
|
|
.set_trans(Tween.TRANS_QUART).set_ease(Tween.EASE_OUT)
|
|
st.parallel().tween_property(spike, "scale", Vector3(0.02, radius * 0.3, 0.02), 0.16)
|
|
st.tween_callback(spike.queue_free)
|
|
|
|
# ── Smoke puffs: flat gray spheres that pop out then shrink away ──────
|
|
var puff_count := 5
|
|
for i in puff_count:
|
|
var puff := MeshInstance3D.new()
|
|
var puff_mesh := SphereMesh.new()
|
|
puff_mesh.radius = 1.0
|
|
puff_mesh.height = 2.0
|
|
puff_mesh.radial_segments = 10
|
|
puff_mesh.rings = 5
|
|
puff.mesh = puff_mesh
|
|
var shade := randf_range(0.75, 0.95)
|
|
puff.material_override = _flat_mat(Color(shade, shade, shade * 1.05))
|
|
root.add_child(puff)
|
|
var dir := Vector3(randf_range(-1, 1), randf_range(0.2, 1), randf_range(-1, 1)).normalized()
|
|
puff.global_position = pos + dir * radius * 0.4
|
|
puff.scale = Vector3.ONE * radius * randf_range(0.2, 0.35)
|
|
|
|
var pt := puff.create_tween()
|
|
pt.tween_property(puff, "global_position", pos + dir * radius * randf_range(0.9, 1.3), 0.3) \
|
|
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
|
pt.parallel().tween_property(puff, "scale", Vector3.ONE * radius * randf_range(0.4, 0.55), 0.3)
|
|
pt.tween_property(puff, "scale", Vector3.ONE * 0.01, 0.25) \
|
|
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN)
|
|
pt.tween_callback(puff.queue_free)
|
|
|
|
# ── Audio: explosion samples with variation ───────────────────────────
|
|
var am = scene_tree.root.get_node_or_null("AudioManager")
|
|
if am and am.has_sound("explosion"):
|
|
var size_db := clampf((radius - 2.5) * 0.42, -2.5, 2.5)
|
|
am.play_3d("explosion", pos, size_db, clampf(pitch_scale, 0.72, 1.15))
|
|
|
|
|
|
## Comic star-flash at a weapon muzzle: 4 flat spikes + hot core, parented to
|
|
## the weapon so it tracks view sway, gone in ~70 ms. Pairs with the existing
|
|
## OmniLight pop the weapons already do.
|
|
static func muzzle_flash(parent: Node3D, local_pos: Vector3) -> void:
|
|
if parent == null or not parent.is_inside_tree():
|
|
return
|
|
var flash := Node3D.new()
|
|
parent.add_child(flash)
|
|
flash.position = local_pos
|
|
flash.rotation.z = randf() * TAU
|
|
|
|
var core := MeshInstance3D.new()
|
|
var core_mesh := SphereMesh.new()
|
|
core_mesh.radius = 0.035
|
|
core_mesh.height = 0.07
|
|
core_mesh.radial_segments = 8
|
|
core_mesh.rings = 4
|
|
core.mesh = core_mesh
|
|
core.material_override = _flat_mat(Color(1.0, 0.98, 0.85), 3.0)
|
|
flash.add_child(core)
|
|
|
|
for i in 4:
|
|
var spike := MeshInstance3D.new()
|
|
var spike_mesh := PrismMesh.new()
|
|
spike_mesh.size = Vector3(0.025, 0.11, 0.012)
|
|
spike.mesh = spike_mesh
|
|
spike.material_override = _flat_mat(Color(1.0, 0.82, 0.25), 2.0)
|
|
spike.position = Vector3(0, 0, 0)
|
|
spike.rotation.z = TAU * float(i) / 4.0 + randf_range(-0.2, 0.2)
|
|
spike.translate_object_local(Vector3(0, 0.07, 0))
|
|
flash.add_child(spike)
|
|
|
|
var t := flash.create_tween()
|
|
t.tween_property(flash, "scale", Vector3.ONE * 1.4, 0.04) \
|
|
.set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
|
|
t.tween_property(flash, "scale", Vector3.ONE * 0.01, 0.03)
|
|
t.tween_callback(flash.queue_free)
|