a4d18e4928
The bull's kick/dash/slam/roll and movement particles were ~112 lines of hand-built CPUParticles3D in player.gd, bypassing the Fx system entirely (Fx.spawn had zero callers) — so Siim couldn't author them in the inspector. - Add six inspector-editable effect scenes (bull_slam/kick/dash one-shots + bull_hoof_dust/charge_trail/roll_sparks continuous); wire player.gd to Fx.spawn() / new Fx.attach(); register them + the gallery + smoke test. - Extract the coded, non-particle slam rings + kick flash into a new bull_impact_fx.gd component (bull_legs.gd pattern). - Drop 12 now-obsolete DP dust look-params (the .tscn owns the look now). - Trim god-file comment noise: rewrite an AI-placeholder barrel block to typed code, drop dead commented-out bounce/topple code, fix stale post-reorg file paths (Assets/… → art/…, PascalCase scene names → snake_case). - Document the Siim workflow in systems/fx/README.md; update CLAUDE.md. player.gd 1307 → 944 lines. Full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
111 lines
3.9 KiB
GDScript
111 lines
3.9 KiB
GDScript
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
|