Move bull ability FX into the authorable systems/fx/ library
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled

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
This commit is contained in:
2026-09-08 14:35:25 +03:00
parent c742c4ae0e
commit a4d18e4928
18 changed files with 580 additions and 462 deletions
+2 -13
View File
@@ -46,21 +46,10 @@ func _register_all() -> void:
# (The dash ability bypasses this.)
_reg_f("Bull", "bull_gore_arc", 75.0, 10.0, 180.0, 5.0)
# ── Dust speed thresholds ─────────────────────────────────────────────────
# The dust LOOK (colour, size, velocity) lives in systems/fx/bull_hoof_dust.tscn now; these
# two only decide when walk vs charge dust kicks in by ground speed.
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
# ── Dust ──────────────────────────────────────────────────────────────────
_reg_f("Dust", "dust_lifetime", 0.9, 0.1, 4.0)
_reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0)
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
_reg_f("Dust", "walk_scale_min", 0.20, 0.05, 3.0)
_reg_f("Dust", "walk_scale_max", 0.40, 0.05, 3.0)
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
_reg_f("Dust", "charge_scale_min", 0.35, 0.05, 6.0)
_reg_f("Dust", "charge_scale_max", 0.70, 0.05, 6.0)
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
# ── Tail ──────────────────────────────────────────────────────────────────
_reg_f("Tail", "tail_gravity", 9.8, 0.0, 30.0)
_reg_f("Tail", "tail_damping", 0.08, 0.0, 0.5, 0.01)
+32 -3
View File
@@ -6,8 +6,9 @@ change to how a hit *feels* is a `.tscn` Siim edits in the inspector — no code
```gdscript
# anywhere in gameplay code
Fx.spawn(&"blood_burst", global_transform) # at a node
Fx.spawn(&"blood_burst", global_transform) # one-shot: fire and forget (self-frees)
Fx.spawn(&"slam_dust", Transform3D(Basis(), point)) # at a world point
var trail := Fx.attach(&"bull_charge_trail", node) # continuous: caller keeps it + toggles .emitting
```
## Siim's workflow
@@ -25,6 +26,33 @@ Fx.spawn(&"slam_dust", Transform3D(Basis(), point)) # at a world point
New effect? Add a `fx/<name>.tscn` (root = `CPUParticles3D` with `one_shot_effect.gd` attached — copy
an existing one), then register its name → path in `fx/fx.gd`.
## Bull ability effects
The bull's moves are wired to effect scenes here, so **you can retune every one in the inspector** —
open the `.tscn`, drag its colour ramp / curves / emission, save. No code.
| Ability | Effect scene(s) | Kind |
|---|---|---|
| Ground slam | `bull_slam.tscn` (dust) | one-shot burst |
| Back-kick | `bull_kick.tscn` — dust **+** its `stones` and `sparks` child emitters | one-shot burst |
| Dash / roll launch | `bull_dash.tscn` | one-shot burst |
| Hoof dust (walking / sprinting) | `bull_hoof_dust.tscn` | continuous |
| Charge trail (the blue streak) | `bull_charge_trail.tscn` | continuous |
| Roll sparks | `bull_roll_sparks.tscn` | continuous |
**Two kinds, edited the same way.** *One-shot* effects fire once and clean themselves up — they show
up in the gallery, so preview and tune them there. *Continuous* effects follow the bull and switch on
and off with what it's doing (dust while moving, the trail during a roll); the gallery can't show
those, so preview one by opening its `.tscn` and ticking **Emitting** on in the inspector's viewport.
Two things stay in code and aren't yours to tune here: the slam's expanding shockwave **rings** and the
kick's white **flash-pop** — those are stretched flat meshes, not particles (`actors/player/bull_impact_fx.gd`).
The dust's *walk-vs-sprint size* is also code (a fixed multiplier); its colour and shape are the `.tscn`.
**Composite effect** (like the kick): one scene, several `CPUParticles3D` — a root plus child emitters.
`one_shot_effect.gd` fires every emitter in the scene together, so you can build a layered hit
(dust + debris + sparks) as one file and tune each layer separately.
## House style (keep effects reading as one game)
**Palette** — pull from these, don't invent per-effect colours:
@@ -52,8 +80,9 @@ the inspector and safe on-device. Keep this constraint when adding effects.
| File | What it is |
|---|---|
| `fx.gd` | The `Fx` autoload — `spawn(name, xform, parent=null)`; name→path registry |
| `one_shot_effect.gd` | Root helper: fires the emitter(s) once, frees the scene when done |
| `fx.gd` | The `Fx` autoload — `spawn()` (one-shot) / `attach()` (continuous); name→path registry |
| `one_shot_effect.gd` | Root helper: fires every emitter in the scene once, frees it when done |
| `blood_burst.tscn` etc. | The effects themselves — **edit these** |
| `bull_*.tscn` | The bull's ability effects (see the table above) — **edit these** |
| `effect_gallery.tscn` | Preview stage (run it to review) |
| `_seed_placeholders.gd` | One-off generator that wrote the first-draft defaults; not used at runtime |
+42
View File
@@ -0,0 +1,42 @@
[gd_scene format=3]
; Bull charge trail — the blue energy streak that streams off the bull during a roll/dash.
; Continuous: the player instances it once under the bull, toggles `emitting`, and steers
; `direction` opposite the travel direction each frame. Tune the look here; see README.md.
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="mesh"]
material = SubResource("mat")
radius = 0.065
height = 0.13
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="curve"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.6, 0.8), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="grad"]
colors = PackedColorArray(0.3, 0.65, 1, 1, 0.3, 0.65, 1, 0)
[node name="bull_charge_trail" type="CPUParticles3D"]
emitting = false
amount = 18
lifetime = 0.3
local_coords = false
randomness = 0.4
mesh = SubResource("mesh")
direction = Vector3(0, 0.2, 1)
spread = 25.0
gravity = Vector3(0, 2, 0)
initial_velocity_min = 4.0
initial_velocity_max = 9.0
scale_amount_min = 0.4
scale_amount_max = 1.2
scale_amount_curve = SubResource("curve")
color_ramp = SubResource("grad")
+44
View File
@@ -0,0 +1,44 @@
[gd_scene format=3]
; Bull dash burst — the one-shot streak of speed shards left when a dash/roll launches. Fired
; via Fx.spawn(&"bull_dash", xform); the spawn transform aims local -Z along the dash direction,
; so the shards streak the right way. Tune the look here; see systems/fx/README.md.
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="oneshot"]
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="BoxMesh" id="mesh"]
material = SubResource("mat")
size = Vector3(0.045, 0.045, 0.3)
[sub_resource type="Curve" id="curve"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 2
[sub_resource type="Gradient" id="grad"]
colors = PackedColorArray(0.5, 0.82, 1, 1, 0.5, 0.82, 1, 0)
[node name="bull_dash" type="CPUParticles3D"]
emitting = false
amount = 18
lifetime = 0.22
one_shot = true
local_coords = false
explosiveness = 0.92
randomness = 0.15
mesh = SubResource("mesh")
direction = Vector3(0, 0, -1)
spread = 15.0
gravity = Vector3(0, 0, 0)
initial_velocity_min = 18.0
initial_velocity_max = 35.0
scale_amount_min = 0.8
scale_amount_max = 2.8
scale_amount_curve = SubResource("curve")
color_ramp = SubResource("grad")
script = ExtResource("oneshot")
+43
View File
@@ -0,0 +1,43 @@
[gd_scene format=3]
; Bull hoof dust — the little kick-up under each hoof while moving. Continuous (not one-shot):
; the player instances one per hoof, positions it, and toggles `emitting`. Charge sprint bumps
; scale/velocity in code. Tune the look here; see systems/fx/README.md.
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="mesh"]
material = SubResource("mat")
radius = 0.07
height = 0.14
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="curve"]
_data = [Vector2(0, 0.2), 0.0, 0.0, 0, 0, Vector2(0.4, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="grad"]
colors = PackedColorArray(0.94, 0.9, 0.83, 1, 0.94, 0.9, 0.83, 0)
[node name="bull_hoof_dust" type="CPUParticles3D"]
emitting = false
amount = 12
lifetime = 0.9
local_coords = false
explosiveness = 0.35
randomness = 0.6
mesh = SubResource("mesh")
direction = Vector3(0, 1, 0)
spread = 45.0
gravity = Vector3(0, -1.5, 0)
initial_velocity_min = 0.8
initial_velocity_max = 1.8
scale_amount_min = 0.2
scale_amount_max = 0.4
scale_amount_curve = SubResource("curve")
color_ramp = SubResource("grad")
+115
View File
@@ -0,0 +1,115 @@
[gd_scene format=3]
; Bull back-kick impact — the one-shot combined dust + stones + sparks thrown behind the kick.
; A composite effect: the root emitter is the dust; the "stones" and "sparks" child emitters fire
; with it (one_shot_effect.gd arms every CPUParticles3D in the scene). Fired via
; Fx.spawn(&"bull_kick", xform); the spawn transform aims local -Z along the kick-back direction.
; The bright flash-pop is a separate coded effect (bull_impact_fx.gd). Tune all three looks here.
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="oneshot"]
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="dust_mesh"]
material = SubResource("mat")
radius = 0.12
height = 0.24
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="dust_curve"]
_data = [Vector2(0, 0.3), 0.0, 0.0, 0, 0, Vector2(0.3, 1.5), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="dust_grad"]
colors = PackedColorArray(1, 0.78, 0.18, 0.95, 1, 0.45, 0.05, 0)
[sub_resource type="SphereMesh" id="stone_mesh"]
material = SubResource("mat")
radius = 0.055
height = 0.11
radial_segments = 3
rings = 1
[sub_resource type="Curve" id="stone_curve"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.6, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="stone_grad"]
colors = PackedColorArray(0.62, 0.58, 0.52, 1, 0.62, 0.58, 0.52, 0)
[sub_resource type="SphereMesh" id="spark_mesh"]
material = SubResource("mat")
radius = 0.04
height = 0.08
radial_segments = 3
rings = 1
[sub_resource type="Curve" id="spark_curve"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.5, 0.6), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="spark_grad"]
colors = PackedColorArray(1, 0.98, 0.6, 1, 1, 1, 1, 0)
[node name="bull_kick" type="CPUParticles3D"]
emitting = false
amount = 40
lifetime = 1.2
one_shot = true
local_coords = false
explosiveness = 0.8
randomness = 0.5
mesh = SubResource("dust_mesh")
direction = Vector3(0, 0.7, -1)
spread = 65.0
gravity = Vector3(0, -6, 0)
initial_velocity_min = 3.0
initial_velocity_max = 9.0
scale_amount_min = 0.5
scale_amount_max = 2.2
scale_amount_curve = SubResource("dust_curve")
color_ramp = SubResource("dust_grad")
script = ExtResource("oneshot")
[node name="stones" type="CPUParticles3D" parent="."]
emitting = false
amount = 14
lifetime = 0.65
one_shot = true
local_coords = false
explosiveness = 0.92
randomness = 0.6
mesh = SubResource("stone_mesh")
direction = Vector3(0, 0.35, -1)
spread = 45.0
gravity = Vector3(0, -9.8, 0)
initial_velocity_min = 5.0
initial_velocity_max = 14.0
scale_amount_min = 0.5
scale_amount_max = 1.8
scale_amount_curve = SubResource("stone_curve")
color_ramp = SubResource("stone_grad")
[node name="sparks" type="CPUParticles3D" parent="."]
emitting = false
amount = 22
lifetime = 0.28
one_shot = true
local_coords = false
explosiveness = 0.95
randomness = 0.4
mesh = SubResource("spark_mesh")
direction = Vector3(0, 0.5, -1)
spread = 30.0
gravity = Vector3(0, -14, 0)
initial_velocity_min = 8.0
initial_velocity_max = 18.0
scale_amount_min = 0.6
scale_amount_max = 2.0
scale_amount_curve = SubResource("spark_curve")
color_ramp = SubResource("spark_grad")
+42
View File
@@ -0,0 +1,42 @@
[gd_scene format=3]
; Bull roll sparks — the dirt/ember spray flung outward from the spinning roll ("Powerball").
; Continuous: the player instances it once under the bull and toggles `emitting` during a roll.
; World-space so the spray peels off into a trail. Tune the look here; see README.md.
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="mesh"]
material = SubResource("mat")
radius = 0.05
height = 0.1
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="curve"]
_data = [Vector2(0, 0.3), 0.0, 0.0, 0, 0, Vector2(0.3, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="grad"]
colors = PackedColorArray(1, 0.78, 0.32, 1, 0.7, 0.42, 0.14, 0)
[node name="bull_roll_sparks" type="CPUParticles3D"]
emitting = false
amount = 46
lifetime = 0.45
local_coords = false
randomness = 0.7
mesh = SubResource("mesh")
direction = Vector3(0, 1, 0)
spread = 180.0
gravity = Vector3(0, -6, 0)
initial_velocity_min = 3.0
initial_velocity_max = 8.0
scale_amount_min = 0.5
scale_amount_max = 1.3
scale_amount_curve = SubResource("curve")
color_ramp = SubResource("grad")
+47
View File
@@ -0,0 +1,47 @@
[gd_scene format=3]
; Bull ground-slam dust — the one-shot dirt burst thrown up when the slam lands. Fired via
; Fx.spawn(&"bull_slam", xform) at the impact point. The expanding shockwave RINGS are a
; separate coded effect (actors/player/bull_impact_fx.gd), not this. Tune the look here.
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="oneshot"]
[sub_resource type="StandardMaterial3D" id="mat"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="mesh"]
material = SubResource("mat")
radius = 0.09
height = 0.18
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="curve"]
_data = [Vector2(0, 0.25), 0.0, 0.0, 0, 0, Vector2(0.35, 1.6), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="grad"]
colors = PackedColorArray(0.88, 0.74, 0.46, 1, 0.88, 0.74, 0.46, 0)
[node name="bull_slam" type="CPUParticles3D"]
emitting = false
amount = 48
lifetime = 1.1
one_shot = true
local_coords = false
explosiveness = 0.85
randomness = 0.5
mesh = SubResource("mesh")
direction = Vector3(0, 1, 0)
spread = 85.0
gravity = Vector3(0, -7, 0)
initial_velocity_min = 3.5
initial_velocity_max = 10.0
scale_amount_min = 0.5
scale_amount_max = 2.0
scale_amount_curve = SubResource("curve")
color_ramp = SubResource("grad")
script = ExtResource("oneshot")
+4 -1
View File
@@ -4,7 +4,10 @@ extends Node3D
## press Play, then edit any effect .tscn in the inspector and re-run to compare. See fx/README.md.
const _CYCLE_SEC := 1.6
const _EFFECTS: Array[StringName] = [&"blood_burst", &"slam_dust", &"spark", &"hit_flash"]
const _EFFECTS: Array[StringName] = [
&"blood_burst", &"slam_dust", &"spark", &"hit_flash",
&"bull_slam", &"bull_kick", &"bull_dash",
]
@onready var _label: Label = $UI/Label
# Reached by path rather than the Fx global so the placeholder-seeder can compile and attach this
+20
View File
@@ -7,10 +7,18 @@ extends Node
## placeholder .tscn are seeded — see tools/seed_fx_placeholders.gd.
const _PATHS: Dictionary = {
# Generic hit effects.
&"blood_burst": "res://systems/fx/blood_burst.tscn",
&"slam_dust": "res://systems/fx/slam_dust.tscn",
&"spark": "res://systems/fx/spark.tscn",
&"hit_flash": "res://systems/fx/hit_flash.tscn",
# Bull ability effects — one-shots fired by spawn(), continuous ones held via attach().
&"bull_slam": "res://systems/fx/bull_slam.tscn",
&"bull_kick": "res://systems/fx/bull_kick.tscn",
&"bull_dash": "res://systems/fx/bull_dash.tscn",
&"bull_hoof_dust": "res://systems/fx/bull_hoof_dust.tscn",
&"bull_charge_trail": "res://systems/fx/bull_charge_trail.tscn",
&"bull_roll_sparks": "res://systems/fx/bull_roll_sparks.tscn",
}
var _cache: Dictionary = {}
@@ -32,6 +40,18 @@ func spawn(effect: StringName, xform: Transform3D, parent: Node = null) -> Node:
return inst
# Instance a CONTINUOUS effect as a child of `parent` and hand it back. Unlike spawn(), it neither
# positions nor auto-frees the effect — the caller keeps the reference to place it and toggle
# `emitting` (hoof dust that follows the bull, the charge trail). Returns null if unknown/missing.
func attach(effect: StringName, parent: Node) -> Node:
var scene := _scene(effect)
if scene == null or parent == null:
return null
var inst := scene.instantiate()
parent.add_child(inst)
return inst
func _scene(effect: StringName) -> PackedScene:
if _cache.has(effect):
return _cache[effect]