Add scene-authored FX library and bull ability effects
An Fx autoload + one_shot_effect base so every effect is an inspector-editable .tscn (the systems/fx/ authoring dir): blood/slam/spark/flash primitives and the bull's kick/dash/slam/roll + movement effects (bull_*.tscn), fired via Fx.spawn() / Fx.attach(). Plus a preview gallery + smoke test, and bull_impact_fx.gd for the coded, non-particle slam rings + kick flash-pop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
```gdscript
|
||||
# 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
|
||||
|
||||
1. **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.
|
||||
2. **Pick an effect** `.tscn` in `fx/` (`blood_burst`, `slam_dust`, `spark`, `hit_flash`) and open it.
|
||||
3. **Tune it in the inspector.** Everything lives on the `CPUParticles3D` node 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.
|
||||
4. **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 |
|
||||
@@ -0,0 +1,49 @@
|
||||
extends CPUParticles3D
|
||||
## One-shot blood spray. Add as a child of whatever bleeds, then call burst().
|
||||
|
||||
func _ready() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 0.07
|
||||
sphere.height = 0.14
|
||||
sphere.radial_segments = 4
|
||||
sphere.rings = 2
|
||||
sphere.material = mat
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
|
||||
ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(0.6, 0.5))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
mesh = sphere
|
||||
color_ramp = ramp
|
||||
scale_amount_curve = scale_curve
|
||||
one_shot = true
|
||||
explosiveness = 0.9
|
||||
amount = 25
|
||||
lifetime = 1.1
|
||||
randomness = 0.4
|
||||
local_coords = false
|
||||
spread = 80.0
|
||||
gravity = Vector3(0.0, -7.0, 0.0)
|
||||
initial_velocity_min = 4.0
|
||||
initial_velocity_max = 11.0
|
||||
scale_amount_min = 0.5
|
||||
scale_amount_max = 1.6
|
||||
emitting = false
|
||||
|
||||
|
||||
# Fire a burst at a world position, sprayed along dir (biased upward).
|
||||
func burst(at: Vector3, dir: Vector3) -> void:
|
||||
global_position = at
|
||||
direction = (dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||
restart()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cnhmhom4w5o3i
|
||||
@@ -0,0 +1,42 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_l68fm"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_okatq"]
|
||||
transparency = 1
|
||||
cull_mode = 2
|
||||
shading_mode = 0
|
||||
vertex_color_use_as_albedo = true
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_n7uw4"]
|
||||
material = SubResource("StandardMaterial3D_okatq")
|
||||
radius = 0.07
|
||||
height = 0.14
|
||||
radial_segments = 4
|
||||
rings = 2
|
||||
|
||||
[sub_resource type="Curve" id="Curve_a71nq"]
|
||||
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.6, 0.5), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||
point_count = 3
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_mhqc0"]
|
||||
colors = PackedColorArray(1, 0.02, 0.02, 1, 0.25, 0, 0, 0)
|
||||
|
||||
[node name="blood_burst" type="CPUParticles3D" unique_id=1553421910]
|
||||
emitting = false
|
||||
amount = 25
|
||||
lifetime = 1.1
|
||||
one_shot = true
|
||||
explosiveness = 0.9
|
||||
randomness = 0.4
|
||||
mesh = SubResource("SphereMesh_n7uw4")
|
||||
direction = Vector3(0, 1, 0)
|
||||
spread = 80.0
|
||||
gravity = Vector3(0, -7, 0)
|
||||
initial_velocity_min = 4.0
|
||||
initial_velocity_max = 11.0
|
||||
scale_amount_min = 0.5
|
||||
scale_amount_max = 1.6
|
||||
scale_amount_curve = SubResource("Curve_a71nq")
|
||||
color_ramp = SubResource("Gradient_mhqc0")
|
||||
script = ExtResource("1_l68fm")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -0,0 +1,33 @@
|
||||
extends Node3D
|
||||
## Live preview stage for the FX library. Cycles through every registered effect on a loop against
|
||||
## arena-ish lighting so Siim sees each one as it reads in-game. Open fx/effect_gallery.tscn and
|
||||
## 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",
|
||||
&"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
|
||||
# script headless (autoload globals aren't resolvable during an on-demand compile in a --script run).
|
||||
@onready var _fx: Node = get_node("/root/Fx")
|
||||
|
||||
var _index := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var timer := Timer.new()
|
||||
timer.wait_time = _CYCLE_SEC
|
||||
timer.autostart = true
|
||||
add_child(timer)
|
||||
timer.timeout.connect(_fire_next)
|
||||
_fire_next()
|
||||
|
||||
|
||||
func _fire_next() -> void:
|
||||
var effect := _EFFECTS[_index % _EFFECTS.size()]
|
||||
_index += 1
|
||||
_label.text = "%s (%d/%d)" % [effect, (_index - 1) % _EFFECTS.size() + 1, _EFFECTS.size()]
|
||||
_fx.call("spawn", effect, global_transform.translated_local(Vector3(0.0, 0.25, 0.0)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu2r8vtpfpwe7
|
||||
@@ -0,0 +1,32 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/fx/effect_gallery.gd" id="1_opqwa"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lt3bb"]
|
||||
albedo_color = Color(0.62, 0.53, 0.36, 1)
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_2rj6n"]
|
||||
material = SubResource("StandardMaterial3D_lt3bb")
|
||||
size = Vector2(12, 12)
|
||||
|
||||
[node name="EffectGallery" type="Node3D" unique_id=530454952]
|
||||
script = ExtResource("1_opqwa")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=1818153475]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.95105654, 0.309017, 0, -0.309017, 0.95105654, 0, 2, 4.5)
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=1218164313]
|
||||
transform = Transform3D(0.8660254, 0.38302222, -0.32139382, 0, 0.64278764, 0.76604444, 0.5, -0.66341394, 0.5566704, 0, 0, 0)
|
||||
|
||||
[node name="Ground" type="MeshInstance3D" parent="." unique_id=178415463]
|
||||
mesh = SubResource("PlaneMesh_2rj6n")
|
||||
|
||||
[node name="UI" type="CanvasLayer" parent="." unique_id=1848100773]
|
||||
|
||||
[node name="Label" type="Label" parent="UI" unique_id=1347149836]
|
||||
offset_left = 24.0
|
||||
offset_top = 20.0
|
||||
offset_right = 24.0
|
||||
offset_bottom = 20.0
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "FX gallery"
|
||||
@@ -0,0 +1,71 @@
|
||||
extends Node
|
||||
## Fx — the one place gameplay code fires a visual effect. Code names WHICH effect and WHERE;
|
||||
## the .tscn owns HOW it looks (colour, curves, emission — all inspector-editable, no recompile).
|
||||
## Autoload singleton. Usage: Fx.spawn(&"blood_burst", hit_transform)
|
||||
##
|
||||
## Scenes are loaded lazily (not preloaded) so this autoload compiles and boots even before the
|
||||
## 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 = {}
|
||||
|
||||
|
||||
# Spawn a named effect at `xform`, parented to `parent` (or the current scene). Returns the
|
||||
# instance, or null if the effect is unknown or its scene is missing.
|
||||
func spawn(effect: StringName, xform: Transform3D, parent: Node = null) -> Node:
|
||||
var scene := _scene(effect)
|
||||
if scene == null:
|
||||
return null
|
||||
var host := parent if parent != null else _default_host()
|
||||
if host == null:
|
||||
push_warning("Fx.spawn(%s): no parent and no current scene to attach to" % effect)
|
||||
return null
|
||||
var inst := scene.instantiate() as Node3D
|
||||
host.add_child(inst)
|
||||
inst.global_transform = xform
|
||||
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]
|
||||
if not _PATHS.has(effect):
|
||||
push_warning("Fx.spawn: unknown effect '%s' (add it to fx/fx.gd)" % effect)
|
||||
return null
|
||||
var scene := load(_PATHS[effect]) as PackedScene
|
||||
if scene == null:
|
||||
push_warning("Fx.spawn: scene missing for '%s' at %s" % [effect, _PATHS[effect]])
|
||||
return null
|
||||
_cache[effect] = scene
|
||||
return scene
|
||||
|
||||
|
||||
func _default_host() -> Node:
|
||||
var tree := get_tree()
|
||||
return tree.current_scene if tree != null else null
|
||||
@@ -0,0 +1 @@
|
||||
uid://b223163l8yasu
|
||||
@@ -0,0 +1,41 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_6wb3q"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n65j7"]
|
||||
transparency = 1
|
||||
cull_mode = 2
|
||||
shading_mode = 0
|
||||
vertex_color_use_as_albedo = true
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_ih2se"]
|
||||
material = SubResource("StandardMaterial3D_n65j7")
|
||||
radius = 0.05
|
||||
height = 0.1
|
||||
radial_segments = 4
|
||||
rings = 2
|
||||
|
||||
[sub_resource type="Curve" id="Curve_kfjqk"]
|
||||
_data = [Vector2(0, 0.2), 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="Gradient_8o2om"]
|
||||
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
|
||||
|
||||
[node name="hit_flash" type="CPUParticles3D" unique_id=454164430]
|
||||
emitting = false
|
||||
amount = 6
|
||||
lifetime = 0.18
|
||||
one_shot = true
|
||||
explosiveness = 1.0
|
||||
randomness = 0.2
|
||||
mesh = SubResource("SphereMesh_ih2se")
|
||||
spread = 180.0
|
||||
gravity = Vector3(0, 0, 0)
|
||||
initial_velocity_min = 0.5
|
||||
initial_velocity_max = 1.5
|
||||
scale_amount_min = 0.8
|
||||
scale_amount_max = 1.4
|
||||
scale_amount_curve = SubResource("Curve_kfjqk")
|
||||
color_ramp = SubResource("Gradient_8o2om")
|
||||
script = ExtResource("1_6wb3q")
|
||||
@@ -0,0 +1,33 @@
|
||||
extends CPUParticles3D
|
||||
## Root helper for a one-shot effect scene: fires this emitter (and any CPUParticles3D children)
|
||||
## once when spawned, then frees the whole scene after the longest-lived particle has died. This
|
||||
## is what lets an effect be nothing but a tunable CPUParticles3D scene — no per-effect code, so
|
||||
## Siim owns the look entirely in the inspector. Fired by the Fx autoload; see fx/README.md.
|
||||
|
||||
const _FREE_MARGIN := 0.5
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var ttl := _arm(self)
|
||||
for emitter: CPUParticles3D in _child_emitters():
|
||||
ttl = maxf(ttl, _arm(emitter))
|
||||
get_tree().create_timer(ttl + _FREE_MARGIN).timeout.connect(queue_free)
|
||||
|
||||
|
||||
func _arm(p: CPUParticles3D) -> float:
|
||||
p.one_shot = true
|
||||
p.emitting = true
|
||||
return p.lifetime
|
||||
|
||||
|
||||
func _child_emitters() -> Array[CPUParticles3D]:
|
||||
var out: Array[CPUParticles3D] = []
|
||||
_gather(self, out)
|
||||
return out
|
||||
|
||||
|
||||
func _gather(node: Node, out: Array[CPUParticles3D]) -> void:
|
||||
for child: Node in node.get_children():
|
||||
if child is CPUParticles3D:
|
||||
out.append(child)
|
||||
_gather(child, out)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkoudbnrkfiaw
|
||||
@@ -0,0 +1,44 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_88uyl"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1tr47"]
|
||||
transparency = 1
|
||||
cull_mode = 2
|
||||
shading_mode = 0
|
||||
vertex_color_use_as_albedo = true
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_bw8nk"]
|
||||
material = SubResource("StandardMaterial3D_1tr47")
|
||||
radius = 0.16
|
||||
height = 0.32
|
||||
radial_segments = 4
|
||||
rings = 2
|
||||
|
||||
[sub_resource type="Curve" id="Curve_xgm72"]
|
||||
_data = [Vector2(0, 0.4), 0.0, 0.0, 0, 0, Vector2(0.35, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||
point_count = 3
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_l8m5g"]
|
||||
colors = PackedColorArray(0.72, 0.6, 0.4, 0.9, 0.55, 0.45, 0.3, 0)
|
||||
|
||||
[node name="slam_dust" type="CPUParticles3D" unique_id=82833428]
|
||||
emitting = false
|
||||
amount = 36
|
||||
lifetime = 0.9
|
||||
one_shot = true
|
||||
explosiveness = 1.0
|
||||
randomness = 0.5
|
||||
mesh = SubResource("SphereMesh_bw8nk")
|
||||
emission_shape = 1
|
||||
emission_sphere_radius = 0.5
|
||||
direction = Vector3(0, 1, 0)
|
||||
spread = 75.0
|
||||
gravity = Vector3(0, -6, 0)
|
||||
initial_velocity_min = 2.5
|
||||
initial_velocity_max = 6.5
|
||||
scale_amount_min = 0.6
|
||||
scale_amount_max = 1.4
|
||||
scale_amount_curve = SubResource("Curve_xgm72")
|
||||
color_ramp = SubResource("Gradient_l8m5g")
|
||||
script = ExtResource("1_88uyl")
|
||||
@@ -0,0 +1,40 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_kk3a7"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tjifq"]
|
||||
transparency = 1
|
||||
cull_mode = 2
|
||||
shading_mode = 0
|
||||
vertex_color_use_as_albedo = true
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_pv2r6"]
|
||||
material = SubResource("StandardMaterial3D_tjifq")
|
||||
radius = 0.03
|
||||
height = 0.06
|
||||
radial_segments = 4
|
||||
rings = 2
|
||||
|
||||
[sub_resource type="Curve" id="Curve_l33bb"]
|
||||
_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="Gradient_pnkyn"]
|
||||
colors = PackedColorArray(1, 0.95, 0.6, 1, 1, 0.55, 0.1, 0)
|
||||
|
||||
[node name="spark" type="CPUParticles3D" unique_id=69954641]
|
||||
emitting = false
|
||||
amount = 14
|
||||
lifetime = 0.35
|
||||
one_shot = true
|
||||
explosiveness = 1.0
|
||||
randomness = 0.6
|
||||
mesh = SubResource("SphereMesh_pv2r6")
|
||||
direction = Vector3(0, 1, 0)
|
||||
gravity = Vector3(0, -9, 0)
|
||||
initial_velocity_min = 3.0
|
||||
initial_velocity_max = 7.0
|
||||
scale_amount_min = 0.4
|
||||
scale_amount_curve = SubResource("Curve_l33bb")
|
||||
color_ramp = SubResource("Gradient_pnkyn")
|
||||
script = ExtResource("1_kk3a7")
|
||||
Reference in New Issue
Block a user