extends Node ## Procedural (non-particle) bull impact FX: the expanding ground-slam shockwave rings and the ## kick flash-pop. These are tween-animated MeshInstance3D flashes, not CPUParticles3D, so they ## can't be authored as systems/fx/ scenes and live in code here. Created by player.gd; the ## transient meshes are parented into the level (the bull's parent) so they hold their world ## position while the bull keeps moving. var _player: Node3D var _ring_mesh: ArrayMesh = null func setup(player: Node3D) -> void: _player = player # Three nested shockwave rings racing outward from a ground slam, each fading as it grows. func slam_rings(origin: Vector3, max_r: float) -> void: if _ring_mesh == null: _ring_mesh = _make_ring_mesh() _launch_ring(origin, max_r, 0.00, 0.55, Color(1.0, 0.70, 0.10, 0.85)) _launch_ring(origin, max_r, 0.10, 0.52, Color(1.0, 0.50, 0.10, 0.65)) _launch_ring(origin, max_r, 0.20, 0.48, Color(0.9, 0.30, 0.10, 0.50)) # A quick bright flash-pop at the kick's contact point. func kick_flash(origin: Vector3) -> void: var mat := _flat_material(Color(1.0, 0.97, 0.40, 0.0)) var sphere := SphereMesh.new() sphere.radius = 1.0 sphere.height = 2.0 sphere.radial_segments = 8 sphere.rings = 4 sphere.material = mat var mi := MeshInstance3D.new() mi.mesh = sphere mi.material_override = mat mi.scale = Vector3(0.05, 0.05, 0.05) _host().add_child(mi) mi.global_position = origin var tween := create_tween() tween.set_parallel(true) tween.tween_property(mi, "scale", Vector3(0.7, 0.7, 0.7), 0.13)\ .set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT) var fade := func(a: float) -> void: mat.albedo_color.a = a tween.tween_method(fade, 0.92, 0.0, 0.14)\ .set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) tween.tween_callback(mi.queue_free).set_delay(0.16) func _launch_ring(origin: Vector3, max_r: float, delay: float, duration: float, col: Color) -> void: var mat := _flat_material(Color(col.r, col.g, col.b, 0.0)) var mi := MeshInstance3D.new() mi.mesh = _ring_mesh mi.material_override = mat mi.scale = Vector3(0.02, 1.0, 0.02) _host().add_child(mi) mi.global_position = origin var tween := create_tween() tween.set_parallel(true) tween.tween_property(mi, "scale", Vector3(max_r, 1.0, max_r), duration)\ .set_delay(delay).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT) var fade := func(a: float) -> void: mat.albedo_color.a = a tween.tween_method(fade, col.a, 0.0, duration)\ .set_delay(delay).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) tween.tween_callback(mi.queue_free).set_delay(delay + duration + 0.05) func _flat_material(color: Color) -> StandardMaterial3D: var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.albedo_color = color return mat # A flat annulus in the XZ plane (unit radius), scaled up at runtime into the shockwave ring. func _make_ring_mesh() -> ArrayMesh: const INNER_R: float = 0.82 const OUTER_R: float = 1.00 const SEGMENTS: int = 48 var verts := PackedVector3Array() var indices := PackedInt32Array() for i: int in SEGMENTS: var a0 := i / float(SEGMENTS) * TAU var a1 := (i + 1) / float(SEGMENTS) * TAU var vi := i * 4 verts.append(Vector3(cos(a0) * INNER_R, 0.0, sin(a0) * INNER_R)) verts.append(Vector3(cos(a0) * OUTER_R, 0.0, sin(a0) * OUTER_R)) verts.append(Vector3(cos(a1) * OUTER_R, 0.0, sin(a1) * OUTER_R)) verts.append(Vector3(cos(a1) * INNER_R, 0.0, sin(a1) * INNER_R)) indices.append_array([vi, vi + 2, vi + 1, vi, vi + 3, vi + 2]) var arrays: Array = [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts arrays[Mesh.ARRAY_INDEX] = indices var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) return mesh func _host() -> Node: if _player != null and _player.get_parent() != null: return _player.get_parent() return _player