From c0a022fed43b903e52fa215cf48d1ec26ff6abb1 Mon Sep 17 00:00:00 2001 From: Richard Aasa Date: Tue, 8 Sep 2026 14:56:01 +0300 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj --- actors/player/bull_impact_fx.gd | 110 +++++++++++++++ actors/player/bull_impact_fx.gd.uid | 1 + systems/fx/README.md | 88 ++++++++++++ systems/fx/blood_burst.gd | 49 +++++++ systems/fx/blood_burst.gd.uid | 1 + systems/fx/blood_burst.tscn | 42 ++++++ systems/fx/bull_charge_trail.tscn | 42 ++++++ systems/fx/bull_dash.tscn | 44 ++++++ systems/fx/bull_hoof_dust.tscn | 43 ++++++ systems/fx/bull_kick.tscn | 115 ++++++++++++++++ systems/fx/bull_roll_sparks.tscn | 42 ++++++ systems/fx/bull_slam.tscn | 47 +++++++ systems/fx/effect_gallery.gd | 33 +++++ systems/fx/effect_gallery.gd.uid | 1 + systems/fx/effect_gallery.tscn | 32 +++++ systems/fx/fx.gd | 71 ++++++++++ systems/fx/fx.gd.uid | 1 + systems/fx/hit_flash.tscn | 41 ++++++ systems/fx/one_shot_effect.gd | 33 +++++ systems/fx/one_shot_effect.gd.uid | 1 + systems/fx/slam_dust.tscn | 44 ++++++ systems/fx/spark.tscn | 40 ++++++ tests/fx_gallery_test.gd | 51 +++++++ tests/fx_gallery_test.gd.uid | 1 + tools/seed_fx_placeholders.gd | 200 ++++++++++++++++++++++++++++ tools/seed_fx_placeholders.gd.uid | 1 + 26 files changed, 1174 insertions(+) create mode 100644 actors/player/bull_impact_fx.gd create mode 100644 actors/player/bull_impact_fx.gd.uid create mode 100644 systems/fx/README.md create mode 100644 systems/fx/blood_burst.gd create mode 100644 systems/fx/blood_burst.gd.uid create mode 100644 systems/fx/blood_burst.tscn create mode 100644 systems/fx/bull_charge_trail.tscn create mode 100644 systems/fx/bull_dash.tscn create mode 100644 systems/fx/bull_hoof_dust.tscn create mode 100644 systems/fx/bull_kick.tscn create mode 100644 systems/fx/bull_roll_sparks.tscn create mode 100644 systems/fx/bull_slam.tscn create mode 100644 systems/fx/effect_gallery.gd create mode 100644 systems/fx/effect_gallery.gd.uid create mode 100644 systems/fx/effect_gallery.tscn create mode 100644 systems/fx/fx.gd create mode 100644 systems/fx/fx.gd.uid create mode 100644 systems/fx/hit_flash.tscn create mode 100644 systems/fx/one_shot_effect.gd create mode 100644 systems/fx/one_shot_effect.gd.uid create mode 100644 systems/fx/slam_dust.tscn create mode 100644 systems/fx/spark.tscn create mode 100644 tests/fx_gallery_test.gd create mode 100644 tests/fx_gallery_test.gd.uid create mode 100644 tools/seed_fx_placeholders.gd create mode 100644 tools/seed_fx_placeholders.gd.uid diff --git a/actors/player/bull_impact_fx.gd b/actors/player/bull_impact_fx.gd new file mode 100644 index 0000000..c3a1a67 --- /dev/null +++ b/actors/player/bull_impact_fx.gd @@ -0,0 +1,110 @@ +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 diff --git a/actors/player/bull_impact_fx.gd.uid b/actors/player/bull_impact_fx.gd.uid new file mode 100644 index 0000000..3d7fb21 --- /dev/null +++ b/actors/player/bull_impact_fx.gd.uid @@ -0,0 +1 @@ +uid://sf5etma4kjt0 diff --git a/systems/fx/README.md b/systems/fx/README.md new file mode 100644 index 0000000..604a094 --- /dev/null +++ b/systems/fx/README.md @@ -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/.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 | diff --git a/systems/fx/blood_burst.gd b/systems/fx/blood_burst.gd new file mode 100644 index 0000000..7f34529 --- /dev/null +++ b/systems/fx/blood_burst.gd @@ -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() diff --git a/systems/fx/blood_burst.gd.uid b/systems/fx/blood_burst.gd.uid new file mode 100644 index 0000000..581dd00 --- /dev/null +++ b/systems/fx/blood_burst.gd.uid @@ -0,0 +1 @@ +uid://cnhmhom4w5o3i diff --git a/systems/fx/blood_burst.tscn b/systems/fx/blood_burst.tscn new file mode 100644 index 0000000..ed9bb16 --- /dev/null +++ b/systems/fx/blood_burst.tscn @@ -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") diff --git a/systems/fx/bull_charge_trail.tscn b/systems/fx/bull_charge_trail.tscn new file mode 100644 index 0000000..83a7100 --- /dev/null +++ b/systems/fx/bull_charge_trail.tscn @@ -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") diff --git a/systems/fx/bull_dash.tscn b/systems/fx/bull_dash.tscn new file mode 100644 index 0000000..5fc004e --- /dev/null +++ b/systems/fx/bull_dash.tscn @@ -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") diff --git a/systems/fx/bull_hoof_dust.tscn b/systems/fx/bull_hoof_dust.tscn new file mode 100644 index 0000000..0933c5c --- /dev/null +++ b/systems/fx/bull_hoof_dust.tscn @@ -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") diff --git a/systems/fx/bull_kick.tscn b/systems/fx/bull_kick.tscn new file mode 100644 index 0000000..e768243 --- /dev/null +++ b/systems/fx/bull_kick.tscn @@ -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") diff --git a/systems/fx/bull_roll_sparks.tscn b/systems/fx/bull_roll_sparks.tscn new file mode 100644 index 0000000..211da6a --- /dev/null +++ b/systems/fx/bull_roll_sparks.tscn @@ -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") diff --git a/systems/fx/bull_slam.tscn b/systems/fx/bull_slam.tscn new file mode 100644 index 0000000..16e7feb --- /dev/null +++ b/systems/fx/bull_slam.tscn @@ -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") diff --git a/systems/fx/effect_gallery.gd b/systems/fx/effect_gallery.gd new file mode 100644 index 0000000..bad2c97 --- /dev/null +++ b/systems/fx/effect_gallery.gd @@ -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))) diff --git a/systems/fx/effect_gallery.gd.uid b/systems/fx/effect_gallery.gd.uid new file mode 100644 index 0000000..873f498 --- /dev/null +++ b/systems/fx/effect_gallery.gd.uid @@ -0,0 +1 @@ +uid://bu2r8vtpfpwe7 diff --git a/systems/fx/effect_gallery.tscn b/systems/fx/effect_gallery.tscn new file mode 100644 index 0000000..a92bc2c --- /dev/null +++ b/systems/fx/effect_gallery.tscn @@ -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" diff --git a/systems/fx/fx.gd b/systems/fx/fx.gd new file mode 100644 index 0000000..f5e7def --- /dev/null +++ b/systems/fx/fx.gd @@ -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 diff --git a/systems/fx/fx.gd.uid b/systems/fx/fx.gd.uid new file mode 100644 index 0000000..18a9eef --- /dev/null +++ b/systems/fx/fx.gd.uid @@ -0,0 +1 @@ +uid://b223163l8yasu diff --git a/systems/fx/hit_flash.tscn b/systems/fx/hit_flash.tscn new file mode 100644 index 0000000..25453eb --- /dev/null +++ b/systems/fx/hit_flash.tscn @@ -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") diff --git a/systems/fx/one_shot_effect.gd b/systems/fx/one_shot_effect.gd new file mode 100644 index 0000000..0b1c81f --- /dev/null +++ b/systems/fx/one_shot_effect.gd @@ -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) diff --git a/systems/fx/one_shot_effect.gd.uid b/systems/fx/one_shot_effect.gd.uid new file mode 100644 index 0000000..272108e --- /dev/null +++ b/systems/fx/one_shot_effect.gd.uid @@ -0,0 +1 @@ +uid://dkoudbnrkfiaw diff --git a/systems/fx/slam_dust.tscn b/systems/fx/slam_dust.tscn new file mode 100644 index 0000000..c5e6825 --- /dev/null +++ b/systems/fx/slam_dust.tscn @@ -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") diff --git a/systems/fx/spark.tscn b/systems/fx/spark.tscn new file mode 100644 index 0000000..c513fc9 --- /dev/null +++ b/systems/fx/spark.tscn @@ -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") diff --git a/tests/fx_gallery_test.gd b/tests/fx_gallery_test.gd new file mode 100644 index 0000000..0ec4050 --- /dev/null +++ b/tests/fx_gallery_test.gd @@ -0,0 +1,51 @@ +extends SceneTree +## Smoke test for the FX library: every registered effect instantiates and attaches without error, +## and an unknown name fails soft (returns null, no crash). Catches a broken effect .tscn before it +## ships. Run: godot --headless --script tests/fx_gallery_test.gd + +var _pass := 0 +var _fail := 0 + + +func _init() -> void: + _run.call_deferred() + + +func _ok(cond: bool, msg: String) -> void: + if cond: + _pass += 1 + print(" PASS: ", msg) + else: + _fail += 1 + print(" FAIL: ", msg) + + +func _run() -> void: + var fx: Node = root.get_node("/root/Fx") + var host := Node3D.new() + root.add_child(host) + current_scene = host + + for id: StringName in [&"blood_burst", &"slam_dust", &"spark", &"hit_flash", + &"bull_slam", &"bull_kick", &"bull_dash"]: + var inst: Node = fx.call("spawn", id, Transform3D.IDENTITY, host) + _ok(inst != null and is_instance_valid(inst), "spawns %s" % id) + + # Continuous bull effects are held (attach), not fired-and-forgotten (spawn). + for id: StringName in [&"bull_hoof_dust", &"bull_charge_trail", &"bull_roll_sparks"]: + var inst: Node = fx.call("attach", id, host) + _ok(inst != null and is_instance_valid(inst), "attaches %s" % id) + + var missing: Node = fx.call("spawn", &"does_not_exist", Transform3D.IDENTITY, host) + _ok(missing == null, "unknown effect returns null instead of crashing") + + # The preview gallery scene is wired (has its controller script + label). + var gallery: Node = load("res://systems/fx/effect_gallery.tscn").instantiate() + _ok(gallery.get_script() != null, "gallery scene keeps its controller script") + host.add_child(gallery) + await process_frame + _ok(gallery.get_node_or_null("UI/Label") != null, "gallery scene has its name label") + + await process_frame + print("Results: %d passed, %d failed" % [_pass, _fail]) + quit(1 if _fail > 0 else 0) diff --git a/tests/fx_gallery_test.gd.uid b/tests/fx_gallery_test.gd.uid new file mode 100644 index 0000000..7380279 --- /dev/null +++ b/tests/fx_gallery_test.gd.uid @@ -0,0 +1 @@ +uid://c2utxg1g3cpjs diff --git a/tools/seed_fx_placeholders.gd b/tools/seed_fx_placeholders.gd new file mode 100644 index 0000000..29b9f67 --- /dev/null +++ b/tools/seed_fx_placeholders.gd @@ -0,0 +1,200 @@ +extends SceneTree +## Seeds the placeholder effect .tscn (and the gallery scene) once, headless. Those .tscn are the +## source of truth Siim edits in the inspector — this script only builds their first-draft defaults +## (ported from the old code-built particles). Re-run only to reset a placeholder from scratch: +## godot --headless --script tools/seed_fx_placeholders.gd + +const _EFFECT_SCRIPT := "res://systems/fx/one_shot_effect.gd" +const _GALLERY_SCRIPT := "res://systems/fx/effect_gallery.gd" + + +func _init() -> void: + _seed_blood_burst() + _seed_slam_dust() + _seed_spark() + _seed_hit_flash() + _seed_gallery() + print("Seeded FX placeholder scenes into res://systems/fx/") + quit() + + +# ── shared builders ───────────────────────────────────────────────────────── +func _unshaded() -> StandardMaterial3D: + var m := StandardMaterial3D.new() + m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + m.vertex_color_use_as_albedo = true + m.cull_mode = BaseMaterial3D.CULL_DISABLED + return m + + +func _sphere(radius: float) -> SphereMesh: + var s := SphereMesh.new() + s.radius = radius + s.height = radius * 2.0 + s.radial_segments = 4 + s.rings = 2 + s.material = _unshaded() + return s + + +func _ramp(c0: Color, c1: Color) -> Gradient: + var g := Gradient.new() + g.set_color(0, c0) + g.set_color(1, c1) + return g + + +func _curve(points: Array) -> Curve: + var c := Curve.new() + for pt: Vector2 in points: + c.add_point(pt) + return c + + +func _save(p: CPUParticles3D, path: String) -> void: + p.set_script(load(_EFFECT_SCRIPT)) + var packed := PackedScene.new() + packed.pack(p) + ResourceSaver.save(packed, path) + p.free() + + +# ── the placeholders (defaults ported from the old code-built particles) ───── +func _seed_blood_burst() -> void: + var p := CPUParticles3D.new() + p.name = "blood_burst" + p.mesh = _sphere(0.07) + p.color_ramp = _ramp(Color(1.0, 0.02, 0.02, 1.0), Color(0.25, 0.0, 0.0, 0.0)) + p.scale_amount_curve = _curve([Vector2(0.0, 1.0), Vector2(0.6, 0.5), Vector2(1.0, 0.0)]) + p.emitting = false + p.one_shot = true + p.explosiveness = 0.9 + p.amount = 25 + p.lifetime = 1.1 + p.randomness = 0.4 + p.local_coords = false + p.direction = Vector3(0.0, 1.0, 0.0) + p.spread = 80.0 + p.gravity = Vector3(0.0, -7.0, 0.0) + p.initial_velocity_min = 4.0 + p.initial_velocity_max = 11.0 + p.scale_amount_min = 0.5 + p.scale_amount_max = 1.6 + _save(p, "res://systems/fx/blood_burst.tscn") + + +func _seed_slam_dust() -> void: + var p := CPUParticles3D.new() + p.name = "slam_dust" + p.mesh = _sphere(0.16) + p.color_ramp = _ramp(Color(0.72, 0.60, 0.40, 0.9), Color(0.55, 0.45, 0.30, 0.0)) + p.scale_amount_curve = _curve([Vector2(0.0, 0.4), Vector2(0.35, 1.0), Vector2(1.0, 0.0)]) + p.emitting = false + p.one_shot = true + p.explosiveness = 1.0 + p.amount = 36 + p.lifetime = 0.9 + p.randomness = 0.5 + p.local_coords = false + p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE + p.emission_sphere_radius = 0.5 + p.direction = Vector3(0.0, 1.0, 0.0) + p.spread = 75.0 + p.gravity = Vector3(0.0, -6.0, 0.0) + p.initial_velocity_min = 2.5 + p.initial_velocity_max = 6.5 + p.scale_amount_min = 0.6 + p.scale_amount_max = 1.4 + _save(p, "res://systems/fx/slam_dust.tscn") + + +func _seed_spark() -> void: + var p := CPUParticles3D.new() + p.name = "spark" + p.mesh = _sphere(0.03) + p.color_ramp = _ramp(Color(1.0, 0.95, 0.6, 1.0), Color(1.0, 0.55, 0.1, 0.0)) + p.scale_amount_curve = _curve([Vector2(0.0, 1.0), Vector2(1.0, 0.0)]) + p.emitting = false + p.one_shot = true + p.explosiveness = 1.0 + p.amount = 14 + p.lifetime = 0.35 + p.randomness = 0.6 + p.local_coords = false + p.direction = Vector3(0.0, 1.0, 0.0) + p.spread = 45.0 + p.gravity = Vector3(0.0, -9.0, 0.0) + p.initial_velocity_min = 3.0 + p.initial_velocity_max = 7.0 + p.scale_amount_min = 0.4 + p.scale_amount_max = 1.0 + _save(p, "res://systems/fx/spark.tscn") + + +func _seed_hit_flash() -> void: + var p := CPUParticles3D.new() + p.name = "hit_flash" + p.mesh = _sphere(0.05) + p.color_ramp = _ramp(Color(1.0, 1.0, 1.0, 1.0), Color(1.0, 1.0, 1.0, 0.0)) + p.scale_amount_curve = _curve([Vector2(0.0, 0.2), Vector2(0.3, 1.2), Vector2(1.0, 0.0)]) + p.emitting = false + p.one_shot = true + p.explosiveness = 1.0 + p.amount = 6 + p.lifetime = 0.18 + p.randomness = 0.2 + p.local_coords = false + p.spread = 180.0 + p.gravity = Vector3.ZERO + p.initial_velocity_min = 0.5 + p.initial_velocity_max = 1.5 + p.scale_amount_min = 0.8 + p.scale_amount_max = 1.4 + _save(p, "res://systems/fx/hit_flash.tscn") + + +func _seed_gallery() -> void: + var root_node := Node3D.new() + root_node.name = "EffectGallery" + + var cam := Camera3D.new() + cam.name = "Camera3D" + cam.transform = Transform3D(Basis.from_euler(Vector3(deg_to_rad(-18.0), 0.0, 0.0)), Vector3(0.0, 2.0, 4.5)) + root_node.add_child(cam) + cam.owner = root_node + + var light := DirectionalLight3D.new() + light.name = "Sun" + light.rotation_degrees = Vector3(-50.0, -30.0, 0.0) + root_node.add_child(light) + light.owner = root_node + + var ground := MeshInstance3D.new() + ground.name = "Ground" + var plane := PlaneMesh.new() + plane.size = Vector2(12.0, 12.0) + var gmat := StandardMaterial3D.new() + gmat.albedo_color = Color(0.62, 0.53, 0.36) + plane.material = gmat + ground.mesh = plane + root_node.add_child(ground) + ground.owner = root_node + + var ui := CanvasLayer.new() + ui.name = "UI" + root_node.add_child(ui) + ui.owner = root_node + var label := Label.new() + label.name = "Label" + label.position = Vector2(24.0, 20.0) + label.text = "FX gallery" + label.add_theme_font_size_override("font_size", 28) + ui.add_child(label) + label.owner = root_node + + root_node.set_script(load(_GALLERY_SCRIPT)) + var packed := PackedScene.new() + packed.pack(root_node) + ResourceSaver.save(packed, "res://systems/fx/effect_gallery.tscn") + root_node.free() diff --git a/tools/seed_fx_placeholders.gd.uid b/tools/seed_fx_placeholders.gd.uid new file mode 100644 index 0000000..adb77e8 --- /dev/null +++ b/tools/seed_fx_placeholders.gd.uid @@ -0,0 +1 @@ +uid://csbp6mkg257uy