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:
2026-09-08 14:56:01 +03:00
parent 9458085abb
commit c0a022fed4
26 changed files with 1174 additions and 0 deletions
+200
View File
@@ -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()