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
FX — the effect library
Gameplay code fires a named effect; the scene decides how it looks. That split is the whole
point: a programmer writes Fx.spawn(&"blood_burst", hit_transform) once, and from then on every
change to how a hit feels is a .tscn Siim edits in the inspector — no code, no recompile.
# anywhere in gameplay code
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
- Open the gallery. Run
fx/effect_gallery.tscn. It loops through every effect on a sand-lit stage with the effect's name on screen, so you see each one the way it reads in play. - Pick an effect
.tscninfx/(blood_burst,slam_dust,spark,hit_flash) and open it. - Tune it in the inspector. Everything lives on the
CPUParticles3Dnode and its material:- Colour → the node's Color Ramp (start colour → faded/transparent end).
- Shape of the burst over time → Scale Amount Curve.
- Amount / Lifetime / Explosiveness / Spread / Velocity / Gravity → the node's emission group.
- The viewport previews particles live as you drag — no play needed.
- Save. That's the whole edit. Re-run the gallery to compare.
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:
| Role | Start | End (fades to alpha 0) |
|---|---|---|
| Blood | #FF0505 |
#400000 |
| Dust | #B89966 (tan) |
#8C7326 |
| Ember | #FFF299 → #FF8C1A |
transparent |
| Flash | #FFFFFF |
transparent |
Timing budgets — hits read best short: strike/flash effects ≤ 0.4 s, dust/settle ≤ 1.1 s.
A one-shot effect frees itself lifetime + 0.5 s after spawn (one_shot_effect.gd), so keep
lifetime honest.
PS1 look — materials are unshaded, alpha-blended, vertex-colour (colour comes from the ramp), cull disabled. Prefer few, chunky, low-poly billboarded quads over dense fine particles; the game runs the PS1 filter over everything, so subtlety is lost anyway.
Mobile — effects are CPUParticles3D, never GPUParticles3D. The web/GLES3/ANGLE target
compiles particle shaders badly (see the notes in player.gd); CPU particles are just as tunable in
the inspector and safe on-device. Keep this constraint when adding effects.
Files
| File | What it is |
|---|---|
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 |