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)