Move bull ability FX into the authorable systems/fx/ library
The bull's kick/dash/slam/roll and movement particles were ~112 lines of hand-built CPUParticles3D in player.gd, bypassing the Fx system entirely (Fx.spawn had zero callers) — so Siim couldn't author them in the inspector. - Add six inspector-editable effect scenes (bull_slam/kick/dash one-shots + bull_hoof_dust/charge_trail/roll_sparks continuous); wire player.gd to Fx.spawn() / new Fx.attach(); register them + the gallery + smoke test. - Extract the coded, non-particle slam rings + kick flash into a new bull_impact_fx.gd component (bull_legs.gd pattern). - Drop 12 now-obsolete DP dust look-params (the .tscn owns the look now). - Trim god-file comment noise: rewrite an AI-placeholder barrel block to typed code, drop dead commented-out bounce/topple code, fix stale post-reorg file paths (Assets/… → art/…, PascalCase scene names → snake_case). - Document the Siim workflow in systems/fx/README.md; update CLAUDE.md. player.gd 1307 → 944 lines. Full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
This commit is contained in:
@@ -43,7 +43,9 @@ art/ all raw art, grouped by role (snake_case):
|
|||||||
| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` |
|
| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` |
|
||||||
| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` |
|
| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` |
|
||||||
| `actors/player/player.tscn` | Player scene root (`CharacterBody3D`) |
|
| `actors/player/player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||||
| `actors/player/player.gd` | Movement, charge, turning logic |
|
| `actors/player/player.gd` | Movement, charge, turning, and abilities (kick/dash/slam/roll). Ability & movement particle FX are **not** built here — they're authored scenes in `systems/fx/` (`bull_*.tscn`), fired via `Fx.spawn()` (one-shots) / `Fx.attach()` (continuous) |
|
||||||
|
| `actors/player/bull_impact_fx.gd` | Bull's coded, non-particle impact FX (the slam shockwave rings + kick flash-pop — tweened meshes, not CPUParticles3D, so they can't be `systems/fx/` scenes). Created by `player.gd` |
|
||||||
|
| `actors/player/bull_legs.gd` | Bull body-wobble + tail Verlet-chain simulation. Created by `player.gd` |
|
||||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||||
| `actors/matador/matador.gd` | Matador AI (wander / ragdoll state machine) |
|
| `actors/matador/matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||||
|
|||||||
+3
-15
@@ -26,11 +26,11 @@ var _phase: Phase = Phase.WINDUP
|
|||||||
var _bull: CharacterBody3D = null
|
var _bull: CharacterBody3D = null
|
||||||
var _skeleton: Skeleton3D = null
|
var _skeleton: Skeleton3D = null
|
||||||
var _anim_player: AnimationPlayer = null
|
var _anim_player: AnimationPlayer = null
|
||||||
# Separate player baked into Assets/fx/Bear_FX.glb: each clip scales one FX mesh up from 0
|
# Separate player baked into art/fx/bear_fx.glb: each clip scales one FX mesh up from 0
|
||||||
# and back at its strike frame (self-hiding), so bear.gd just fires the matching clip.
|
# and back at its strike frame (self-hiding), so bear.gd just fires the matching clip.
|
||||||
var _fx_player: AnimationPlayer = null
|
var _fx_player: AnimationPlayer = null
|
||||||
var _fx_root: Node = null
|
var _fx_root: Node = null
|
||||||
# Shaped attack colliders live in BearAttackHitboxes.tscn (under bear_model, so they track the
|
# Shaped attack colliders live in bear_attack_hitboxes.tscn (under bear_model, so they track the
|
||||||
# bear's facing): a ground-circle cylinder for the leap slam and a forward box lane for the smash.
|
# bear's facing): a ground-circle cylinder for the leap slam and a forward box lane for the smash.
|
||||||
# Their sizes AND their travel/pop motion are authored in that scene's AnimationPlayer (clips
|
# Their sizes AND their travel/pop motion are authored in that scene's AnimationPlayer (clips
|
||||||
# "leap_slam" / "smash_travel") — scrub them there to tweak. bear.gd only fires the clip, sets the
|
# "leap_slam" / "smash_travel") — scrub them there to tweak. bear.gd only fires the clip, sets the
|
||||||
@@ -635,7 +635,7 @@ func _try_hit(reach: float) -> bool:
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
# Grab the designer-authored collider sub-scene (BearAttackHitboxes.tscn instanced under
|
# Grab the designer-authored collider sub-scene (bear_attack_hitboxes.tscn instanced under
|
||||||
# bear_model) and park both hitboxes deactivated. Shapes + travel are authored in that scene.
|
# bear_model) and park both hitboxes deactivated. Shapes + travel are authored in that scene.
|
||||||
func _setup_attack_hitboxes() -> void:
|
func _setup_attack_hitboxes() -> void:
|
||||||
var root := _mesh.get_node_or_null("AttackHitboxes") if _mesh else null
|
var root := _mesh.get_node_or_null("AttackHitboxes") if _mesh else null
|
||||||
@@ -920,18 +920,6 @@ func _die(hit_dir: Vector3) -> void:
|
|||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
|
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
|
||||||
_play(_anim_death)
|
_play(_anim_death)
|
||||||
# No death clip on the rig — topple the beast onto its side (about the hit direction,
|
|
||||||
# so it falls the way it was struck) and sink it away, then free.
|
|
||||||
#var fall_axis := Vector3(-hit_dir.z, 0.0, hit_dir.x)
|
|
||||||
#if fall_axis.length() < 0.01:
|
|
||||||
# fall_axis = Vector3.RIGHT
|
|
||||||
#var tween := create_tween()
|
|
||||||
#tween.tween_property(_mesh, "rotation",
|
|
||||||
# _mesh.rotation + fall_axis.normalized() * deg_to_rad(88.0), 0.7)\
|
|
||||||
# .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
|
||||||
#tween.tween_interval(2.5)
|
|
||||||
#tween.tween_property(_mesh, "position:y", _mesh.position.y - 3.0, 1.0)
|
|
||||||
#tween.tween_callback(queue_free)
|
|
||||||
|
|
||||||
|
|
||||||
# ── Interface shared with the matador (guarded elsewhere via has_method) ─────────────
|
# ── Interface shared with the matador (guarded elsewhere via has_method) ─────────────
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
|
|||||||
# reparents from holster to hand at this point in the animation.
|
# reparents from holster to hand at this point in the animation.
|
||||||
const _DRAW_GRAB_AT: float = 0.55
|
const _DRAW_GRAB_AT: float = 0.55
|
||||||
|
|
||||||
# Wrapper scenes (glb + an editor-tweakable hitbox / collider) — see Sword.tscn / Spear.tscn.
|
# Wrapper scenes (glb + an editor-tweakable hitbox / collider) — see sword.tscn / spear.tscn.
|
||||||
const _SWORD_SCENE: PackedScene = preload("res://actors/matador/sword.tscn")
|
const _SWORD_SCENE: PackedScene = preload("res://actors/matador/sword.tscn")
|
||||||
const _SPEAR_SCENE: PackedScene = preload("res://actors/matador/spear.tscn")
|
const _SPEAR_SCENE: PackedScene = preload("res://actors/matador/spear.tscn")
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ func _tick_attack(delta: float) -> void:
|
|||||||
if _swing_timer > 0.0:
|
if _swing_timer > 0.0:
|
||||||
_decelerate(22.0, delta)
|
_decelerate(22.0, delta)
|
||||||
_play_anim(_ANIM_ATTACK)
|
_play_anim(_ANIM_ATTACK)
|
||||||
# Damage off the sword's own BladeHitbox (Sword.tscn), riding the animated blade — gored only
|
# Damage off the sword's own BladeHitbox (sword.tscn), riding the animated blade — gored only
|
||||||
# when the thrust reaches it. Arm at the thrust (mat_stab_strike_at) and sweep for the overlap.
|
# when the thrust reaches it. Arm at the thrust (mat_stab_strike_at) and sweep for the overlap.
|
||||||
if _sword_blade_area != null and _swing_phase() >= DP.f("mat_stab_strike_at"):
|
if _sword_blade_area != null and _swing_phase() >= DP.f("mat_stab_strike_at"):
|
||||||
_sword_blade_area.activate()
|
_sword_blade_area.activate()
|
||||||
@@ -1189,7 +1189,7 @@ func _spawn_sword() -> void:
|
|||||||
_drawing = false
|
_drawing = false
|
||||||
_apply_sword_grip()
|
_apply_sword_grip()
|
||||||
|
|
||||||
# The blade hitbox (Sword.tscn, hitbox.gd) is armed only during the ATTACK thrust (see
|
# The blade hitbox (sword.tscn, hitbox.gd) is armed only during the ATTACK thrust (see
|
||||||
# _tick_attack); off while merely carried, so brushing a drawn matador never gores the bull.
|
# _tick_attack); off while merely carried, so brushing a drawn matador never gores the bull.
|
||||||
_sword_blade_area = _sword_node.get_node_or_null("BladeHitbox") as Hitbox
|
_sword_blade_area = _sword_node.get_node_or_null("BladeHitbox") as Hitbox
|
||||||
if _sword_blade_area != null:
|
if _sword_blade_area != null:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://sf5etma4kjt0
|
||||||
+61
-424
@@ -8,7 +8,7 @@ const HOOF_OFFSETS: Array = [
|
|||||||
Vector3( 0.30, -0.25, 0.60),
|
Vector3( 0.30, -0.25, 0.60),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Bull animation clips (Armature|* action names in Assets/bull.fbx). IDLE is the
|
# Bull animation clips (Armature|* action names in art/characters/bull.fbx). IDLE is the
|
||||||
# looping base pose; the others are one-shot ability clips that return to it.
|
# looping base pose; the others are one-shot ability clips that return to it.
|
||||||
const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
||||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||||
@@ -27,12 +27,11 @@ var _legs: Node
|
|||||||
var _was_on_floor: bool = false
|
var _was_on_floor: bool = false
|
||||||
var _pre_slide_vel_y: float = 0.0
|
var _pre_slide_vel_y: float = 0.0
|
||||||
|
|
||||||
var _slam_ring_mesh: ArrayMesh = null
|
# Built once and reused: a fresh StandardMaterial3D/SphereMesh per spawn makes a mobile WebGL
|
||||||
# Attack-FX resources are identical every swing, so build them once and reuse. Recreating a fresh
|
# driver recompile the shader + re-upload the mesh — a hitch "when attacking" absent on desktop.
|
||||||
# StandardMaterial3D / SphereMesh per attack makes a mobile WebGL driver recompile the shader and
|
|
||||||
# re-upload the mesh each time — a main-thread hitch "when attacking" that never shows on desktop.
|
|
||||||
var _unshaded_mat: StandardMaterial3D = null
|
var _unshaded_mat: StandardMaterial3D = null
|
||||||
var _particle_sphere_cache: Dictionary = {} # "radius:seg:ring" -> SphereMesh (shared)
|
var _particle_sphere_cache: Dictionary = {} # "radius:seg:ring" -> SphereMesh (shared)
|
||||||
|
var _impact_fx: Node = null # bull_impact_fx.gd — the coded (non-particle) slam rings + kick flash
|
||||||
|
|
||||||
# Ability system — kick=0, dash=1, slam=2, roll=3
|
# Ability system — kick=0, dash=1, slam=2, roll=3
|
||||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||||
@@ -83,8 +82,12 @@ var _huff_timer: float = 0.0
|
|||||||
|
|
||||||
enum DustState { NONE, WALK, CHARGE }
|
enum DustState { NONE, WALK, CHARGE }
|
||||||
var _dust_state: DustState = DustState.NONE
|
var _dust_state: DustState = DustState.NONE
|
||||||
var _walk_dust_ramp: Gradient = null
|
# Hoof-dust scale/velocity per state (the look — mesh, colour, curve — lives in bull_hoof_dust.tscn).
|
||||||
var _charge_dust_ramp: Gradient = null
|
# A charge sprint kicks up bigger, faster dust than a walk.
|
||||||
|
const _WALK_DUST_SCALE: Vector2 = Vector2(0.20, 0.40)
|
||||||
|
const _WALK_DUST_VEL: Vector2 = Vector2(0.8, 1.8)
|
||||||
|
const _CHARGE_DUST_SCALE: Vector2 = Vector2(0.35, 0.70)
|
||||||
|
const _CHARGE_DUST_VEL: Vector2 = Vector2(2.8, 5.5)
|
||||||
|
|
||||||
|
|
||||||
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
|
||||||
@@ -106,6 +109,7 @@ func _ready() -> void:
|
|||||||
_max_hp += _bonus_hp
|
_max_hp += _bonus_hp
|
||||||
_hp += _bonus_hp
|
_hp += _bonus_hp
|
||||||
_cube_scale = cube_guy.scale
|
_cube_scale = cube_guy.scale
|
||||||
|
_setup_impact_fx()
|
||||||
_setup_hoof_dust()
|
_setup_hoof_dust()
|
||||||
_setup_legs()
|
_setup_legs()
|
||||||
_setup_charge_trail()
|
_setup_charge_trail()
|
||||||
@@ -113,7 +117,6 @@ func _ready() -> void:
|
|||||||
_setup_audio()
|
_setup_audio()
|
||||||
_bull_anim = _find_anim_player(cube_guy)
|
_bull_anim = _find_anim_player(cube_guy)
|
||||||
_setup_bull_idle()
|
_setup_bull_idle()
|
||||||
DP.any_changed.connect(_on_dp_changed)
|
|
||||||
|
|
||||||
|
|
||||||
func _setup_legs() -> void:
|
func _setup_legs() -> void:
|
||||||
@@ -123,113 +126,35 @@ func _setup_legs() -> void:
|
|||||||
_legs.call(&"setup", self, cube_guy)
|
_legs.call(&"setup", self, cube_guy)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_impact_fx() -> void:
|
||||||
|
_impact_fx = Node.new()
|
||||||
|
_impact_fx.set_script(load("res://actors/player/bull_impact_fx.gd"))
|
||||||
|
add_child(_impact_fx)
|
||||||
|
_impact_fx.call(&"setup", self)
|
||||||
|
|
||||||
|
|
||||||
|
# The bull's ability/movement particle effects are authored as scenes in systems/fx/ (so Siim can
|
||||||
|
# tune them in the inspector). The continuous ones follow the bull — instanced once here as
|
||||||
|
# children, then toggled via `emitting`. The one-shots are fired on demand through Fx.spawn().
|
||||||
func _setup_hoof_dust() -> void:
|
func _setup_hoof_dust() -> void:
|
||||||
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
|
|
||||||
|
|
||||||
_walk_dust_ramp = Gradient.new()
|
|
||||||
_walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
|
||||||
_walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
|
||||||
|
|
||||||
# White-beige dust while running
|
|
||||||
_charge_dust_ramp = Gradient.new()
|
|
||||||
_charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
|
||||||
_charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
|
||||||
scale_curve.add_point(Vector2(0.0, 0.2))
|
|
||||||
scale_curve.add_point(Vector2(0.4, 1.0))
|
|
||||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
for offset: Vector3 in HOOF_OFFSETS:
|
for offset: Vector3 in HOOF_OFFSETS:
|
||||||
var p := CPUParticles3D.new()
|
var p := Fx.attach(&"bull_hoof_dust", cube_guy) as CPUParticles3D
|
||||||
|
if p == null:
|
||||||
|
continue
|
||||||
p.position = offset
|
p.position = offset
|
||||||
p.emitting = false
|
|
||||||
p.amount = 12
|
|
||||||
p.lifetime = DP.f("dust_lifetime")
|
|
||||||
p.explosiveness = DP.f("dust_explosiveness")
|
|
||||||
p.randomness = 0.6
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = Vector3(0.0, 1.0, 0.0)
|
|
||||||
p.spread = DP.f("dust_spread")
|
|
||||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
|
||||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
|
||||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
|
||||||
p.scale_amount_min = DP.f("walk_scale_min")
|
|
||||||
p.scale_amount_max = DP.f("walk_scale_max")
|
|
||||||
p.mesh = sphere
|
|
||||||
p.color_ramp = _walk_dust_ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
cube_guy.add_child(p)
|
|
||||||
_hoof_emitters.append(p)
|
_hoof_emitters.append(p)
|
||||||
|
|
||||||
|
|
||||||
func _setup_charge_trail() -> void:
|
func _setup_charge_trail() -> void:
|
||||||
# Blue energy trail — streams backward during a fast run / roll
|
_charge_trail_emitter = Fx.attach(&"bull_charge_trail", cube_guy) as CPUParticles3D
|
||||||
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
|
if _charge_trail_emitter != null:
|
||||||
|
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
|
||||||
var trail_ramp := Gradient.new()
|
|
||||||
trail_ramp.set_color(0, Color(0.30, 0.65, 1.0, 1.0))
|
|
||||||
trail_ramp.set_color(1, Color(0.30, 0.65, 1.0, 0.0))
|
|
||||||
|
|
||||||
var trail_scale := Curve.new()
|
|
||||||
trail_scale.add_point(Vector2(0.0, 1.0))
|
|
||||||
trail_scale.add_point(Vector2(0.6, 0.8))
|
|
||||||
trail_scale.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
_charge_trail_emitter = CPUParticles3D.new()
|
|
||||||
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
|
|
||||||
_charge_trail_emitter.emitting = false
|
|
||||||
_charge_trail_emitter.one_shot = false
|
|
||||||
_charge_trail_emitter.amount = 18
|
|
||||||
_charge_trail_emitter.lifetime = 0.30
|
|
||||||
_charge_trail_emitter.randomness = 0.4
|
|
||||||
_charge_trail_emitter.local_coords = false
|
|
||||||
_charge_trail_emitter.direction = Vector3(0.0, 0.2, 1.0).normalized()
|
|
||||||
_charge_trail_emitter.spread = 25.0
|
|
||||||
_charge_trail_emitter.gravity = Vector3(0.0, 2.0, 0.0)
|
|
||||||
_charge_trail_emitter.initial_velocity_min = 4.0
|
|
||||||
_charge_trail_emitter.initial_velocity_max = 9.0
|
|
||||||
_charge_trail_emitter.scale_amount_min = 0.4
|
|
||||||
_charge_trail_emitter.scale_amount_max = 1.2
|
|
||||||
_charge_trail_emitter.mesh = trail_sphere
|
|
||||||
_charge_trail_emitter.color_ramp = trail_ramp
|
|
||||||
_charge_trail_emitter.scale_amount_curve = trail_scale
|
|
||||||
cube_guy.add_child(_charge_trail_emitter)
|
|
||||||
|
|
||||||
|
|
||||||
func _setup_roll_sparks() -> void:
|
func _setup_roll_sparks() -> void:
|
||||||
# Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
|
_roll_spark_emitter = Fx.attach(&"bull_roll_sparks", cube_guy) as CPUParticles3D
|
||||||
# ball. World-space (local_coords off) so they peel off into a trail behind it.
|
if _roll_spark_emitter != null:
|
||||||
var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
||||||
|
|
||||||
var ramp := Gradient.new()
|
|
||||||
ramp.set_color(0, Color(1.00, 0.78, 0.32, 1.0))
|
|
||||||
ramp.set_color(1, Color(0.70, 0.42, 0.14, 0.0))
|
|
||||||
|
|
||||||
var sc := Curve.new()
|
|
||||||
sc.add_point(Vector2(0.0, 0.30))
|
|
||||||
sc.add_point(Vector2(0.3, 1.00))
|
|
||||||
sc.add_point(Vector2(1.0, 0.00))
|
|
||||||
|
|
||||||
_roll_spark_emitter = CPUParticles3D.new()
|
|
||||||
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
|
||||||
_roll_spark_emitter.emitting = false
|
|
||||||
_roll_spark_emitter.one_shot = false
|
|
||||||
_roll_spark_emitter.amount = 46
|
|
||||||
_roll_spark_emitter.lifetime = 0.45
|
|
||||||
_roll_spark_emitter.randomness = 0.7
|
|
||||||
_roll_spark_emitter.local_coords = false
|
|
||||||
_roll_spark_emitter.direction = Vector3(0.0, 1.0, 0.0)
|
|
||||||
_roll_spark_emitter.spread = 180.0
|
|
||||||
_roll_spark_emitter.gravity = Vector3(0.0, -6.0, 0.0)
|
|
||||||
_roll_spark_emitter.initial_velocity_min = 3.0
|
|
||||||
_roll_spark_emitter.initial_velocity_max = 8.0
|
|
||||||
_roll_spark_emitter.scale_amount_min = 0.5
|
|
||||||
_roll_spark_emitter.scale_amount_max = 1.3
|
|
||||||
_roll_spark_emitter.mesh = spark
|
|
||||||
_roll_spark_emitter.color_ramp = ramp
|
|
||||||
_roll_spark_emitter.scale_amount_curve = sc
|
|
||||||
cube_guy.add_child(_roll_spark_emitter)
|
|
||||||
|
|
||||||
|
|
||||||
func _setup_audio() -> void:
|
func _setup_audio() -> void:
|
||||||
@@ -251,42 +176,20 @@ func _setup_audio() -> void:
|
|||||||
add_child(_crowd_player)
|
add_child(_crowd_player)
|
||||||
|
|
||||||
|
|
||||||
|
# Toggle the hoof emitters and swap their scale/velocity for the walk vs charge feel. The blue
|
||||||
|
# charge trail is the roll/dash ability's, driven from the roll loop, not by dust state.
|
||||||
func _set_dust_state(new_state: DustState) -> void:
|
func _set_dust_state(new_state: DustState) -> void:
|
||||||
if new_state == _dust_state:
|
if new_state == _dust_state:
|
||||||
return
|
return
|
||||||
_dust_state = new_state
|
_dust_state = new_state
|
||||||
|
var scale := _CHARGE_DUST_SCALE if new_state == DustState.CHARGE else _WALK_DUST_SCALE
|
||||||
|
var vel := _CHARGE_DUST_VEL if new_state == DustState.CHARGE else _WALK_DUST_VEL
|
||||||
for p: CPUParticles3D in _hoof_emitters:
|
for p: CPUParticles3D in _hoof_emitters:
|
||||||
p.lifetime = DP.f("dust_lifetime")
|
p.scale_amount_min = scale.x
|
||||||
p.explosiveness = DP.f("dust_explosiveness")
|
p.scale_amount_max = scale.y
|
||||||
p.spread = DP.f("dust_spread")
|
p.initial_velocity_min = vel.x
|
||||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
p.initial_velocity_max = vel.y
|
||||||
match new_state:
|
p.emitting = new_state != DustState.NONE
|
||||||
DustState.NONE:
|
|
||||||
p.emitting = false
|
|
||||||
DustState.WALK:
|
|
||||||
p.color_ramp = _walk_dust_ramp
|
|
||||||
p.scale_amount_min = DP.f("walk_scale_min")
|
|
||||||
p.scale_amount_max = DP.f("walk_scale_max")
|
|
||||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
|
||||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
|
||||||
p.emitting = true
|
|
||||||
DustState.CHARGE:
|
|
||||||
p.color_ramp = _charge_dust_ramp
|
|
||||||
p.scale_amount_min = DP.f("charge_scale_min")
|
|
||||||
p.scale_amount_max = DP.f("charge_scale_max")
|
|
||||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
|
||||||
p.initial_velocity_max = DP.f("charge_vel_max")
|
|
||||||
p.emitting = true
|
|
||||||
# Blue energy trail belongs to the roll/dash ability only, not to fast WASD
|
|
||||||
# running — driven explicitly in the roll loop / _end_roll, not by dust state.
|
|
||||||
|
|
||||||
|
|
||||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
|
||||||
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
|
||||||
return
|
|
||||||
var prev := _dust_state
|
|
||||||
_dust_state = DustState.NONE
|
|
||||||
_set_dust_state(prev)
|
|
||||||
|
|
||||||
|
|
||||||
func _find_anim_player(node: Node) -> AnimationPlayer:
|
func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||||
@@ -681,13 +584,10 @@ func _hit_matadors_radius(range_m: float, strength: float, up_boost: float = 0.0
|
|||||||
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength, up_boost)
|
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength, up_boost)
|
||||||
|
|
||||||
|
|
||||||
# ── Particle helpers ──────────────────────────────────────────────────────────
|
# ── Blood-spurt particle helpers ──────────────────────────────────────────────
|
||||||
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
|
# The ability effects now live in systems/fx/; only the spear-wound blood below is still built in
|
||||||
# these three helpers remove the boilerplate each spawn function used to repeat.
|
# code. Shared material, built once so the mobile driver compiles this particle shader a single
|
||||||
|
# time (colour comes per-particle from the colour_ramp via vertex colour, not this material).
|
||||||
# One shared unshaded material for every particle FX (per-particle colour comes from the
|
|
||||||
# CPUParticles3D colour_ramp via vertex colour, not the material) — built once so the mobile
|
|
||||||
# driver compiles this shader a single time instead of on every attack.
|
|
||||||
func _unshaded_material() -> StandardMaterial3D:
|
func _unshaded_material() -> StandardMaterial3D:
|
||||||
if _unshaded_mat == null:
|
if _unshaded_mat == null:
|
||||||
_unshaded_mat = StandardMaterial3D.new()
|
_unshaded_mat = StandardMaterial3D.new()
|
||||||
@@ -698,8 +598,7 @@ func _unshaded_material() -> StandardMaterial3D:
|
|||||||
return _unshaded_mat
|
return _unshaded_mat
|
||||||
|
|
||||||
|
|
||||||
# Low-poly sphere sized for a particle mesh (height is always the diameter). Cached per size so
|
# Low-poly particle-sphere mesh (height = diameter), cached per size so repeats reuse one GPU upload.
|
||||||
# repeat attacks reuse the same mesh (one GPU upload) instead of generating + uploading a new one.
|
|
||||||
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
|
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
|
||||||
var key := "%.4f:%d:%d" % [radius, segments, rings]
|
var key := "%.4f:%d:%d" % [radius, segments, rings]
|
||||||
var cached: SphereMesh = _particle_sphere_cache.get(key)
|
var cached: SphereMesh = _particle_sphere_cache.get(key)
|
||||||
@@ -724,281 +623,31 @@ func _free_after(node: Node, delay: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _spawn_slam_fx() -> void:
|
func _spawn_slam_fx() -> void:
|
||||||
if not _slam_ring_mesh:
|
|
||||||
_slam_ring_mesh = _make_slam_ring_mesh()
|
|
||||||
var max_r := DP.f("slam_range")
|
|
||||||
var origin := global_position + Vector3(0.0, -0.5, 0.0)
|
var origin := global_position + Vector3(0.0, -0.5, 0.0)
|
||||||
_launch_slam_ring(origin, max_r, 0.00, 0.55, Color(1.00, 0.70, 0.10, 0.85))
|
_impact_fx.call(&"slam_rings", origin, DP.f("slam_range"))
|
||||||
_launch_slam_ring(origin, max_r, 0.10, 0.52, Color(1.00, 0.50, 0.10, 0.65))
|
Fx.spawn(&"bull_slam", Transform3D(Basis(), origin))
|
||||||
_launch_slam_ring(origin, max_r, 0.20, 0.48, Color(0.90, 0.30, 0.10, 0.50))
|
|
||||||
_spawn_slam_dust(origin)
|
|
||||||
camera_pivot.call(&"trigger_hit", 0.60)
|
camera_pivot.call(&"trigger_hit", 0.60)
|
||||||
|
|
||||||
|
|
||||||
func _launch_slam_ring(origin: Vector3, max_r: float,
|
|
||||||
delay: float, duration: float, col: Color) -> void:
|
|
||||||
var mat := StandardMaterial3D.new()
|
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
||||||
mat.albedo_color = Color(col.r, col.g, col.b, 0.0)
|
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
||||||
|
|
||||||
var mi := MeshInstance3D.new()
|
|
||||||
mi.mesh = _slam_ring_mesh
|
|
||||||
mi.material_override = mat
|
|
||||||
mi.scale = Vector3(0.02, 1.0, 0.02)
|
|
||||||
get_parent().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 _spawn_slam_dust(origin: Vector3) -> void:
|
|
||||||
var sphere := _particle_sphere(0.09, 4, 2, _unshaded_material())
|
|
||||||
|
|
||||||
var ramp := Gradient.new()
|
|
||||||
ramp.set_color(0, Color(0.88, 0.74, 0.46, 1.0))
|
|
||||||
ramp.set_color(1, Color(0.88, 0.74, 0.46, 0.0))
|
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
|
||||||
scale_curve.add_point(Vector2(0.0, 0.25))
|
|
||||||
scale_curve.add_point(Vector2(0.35, 1.6))
|
|
||||||
scale_curve.add_point(Vector2(1.00, 0.0))
|
|
||||||
|
|
||||||
var p := CPUParticles3D.new()
|
|
||||||
p.one_shot = true
|
|
||||||
p.explosiveness = 0.85
|
|
||||||
p.amount = 48
|
|
||||||
p.lifetime = 1.1
|
|
||||||
p.randomness = 0.5
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = Vector3.UP
|
|
||||||
p.spread = 85.0
|
|
||||||
p.gravity = Vector3(0.0, -7.0, 0.0)
|
|
||||||
p.initial_velocity_min = 3.5
|
|
||||||
p.initial_velocity_max = 10.0
|
|
||||||
p.scale_amount_min = 0.5
|
|
||||||
p.scale_amount_max = 2.0
|
|
||||||
p.mesh = sphere
|
|
||||||
p.color_ramp = ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
get_parent().add_child(p)
|
|
||||||
p.global_position = origin
|
|
||||||
p.restart()
|
|
||||||
_free_after(p, 2.5)
|
|
||||||
|
|
||||||
|
|
||||||
func _spawn_kick_fx(back_dir: Vector3) -> void:
|
func _spawn_kick_fx(back_dir: Vector3) -> void:
|
||||||
var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0)
|
var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0)
|
||||||
_spawn_kick_dust(origin, back_dir)
|
Fx.spawn(&"bull_kick", _aimed_xform(origin, back_dir))
|
||||||
_spawn_kick_stones(origin, back_dir)
|
_impact_fx.call(&"kick_flash", origin)
|
||||||
_spawn_kick_sparks(origin, back_dir)
|
|
||||||
_spawn_kick_flash(origin)
|
|
||||||
camera_pivot.call(&"trigger_hit", 0.35)
|
camera_pivot.call(&"trigger_hit", 0.35)
|
||||||
|
|
||||||
|
|
||||||
func _spawn_kick_dust(origin: Vector3, back_dir: Vector3) -> void:
|
|
||||||
var sphere := _particle_sphere(0.12, 4, 2, _unshaded_material())
|
|
||||||
|
|
||||||
var ramp := Gradient.new()
|
|
||||||
ramp.set_color(0, Color(1.0, 0.78, 0.18, 0.95))
|
|
||||||
ramp.set_color(1, Color(1.0, 0.45, 0.05, 0.0))
|
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
|
||||||
scale_curve.add_point(Vector2(0.0, 0.3))
|
|
||||||
scale_curve.add_point(Vector2(0.3, 1.5))
|
|
||||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
var p := CPUParticles3D.new()
|
|
||||||
p.one_shot = true
|
|
||||||
p.explosiveness = 0.80
|
|
||||||
p.amount = 40
|
|
||||||
p.lifetime = 1.2
|
|
||||||
p.randomness = 0.5
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = (back_dir + Vector3(0.0, 0.7, 0.0)).normalized()
|
|
||||||
p.spread = 65.0
|
|
||||||
p.gravity = Vector3(0.0, -6.0, 0.0)
|
|
||||||
p.initial_velocity_min = 3.0
|
|
||||||
p.initial_velocity_max = 9.0
|
|
||||||
p.scale_amount_min = 0.5
|
|
||||||
p.scale_amount_max = 2.2
|
|
||||||
p.mesh = sphere
|
|
||||||
p.color_ramp = ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
get_parent().add_child(p)
|
|
||||||
p.global_position = origin
|
|
||||||
p.restart()
|
|
||||||
_free_after(p, 2.5)
|
|
||||||
|
|
||||||
|
|
||||||
func _spawn_kick_stones(origin: Vector3, back_dir: Vector3) -> void:
|
|
||||||
var sphere := _particle_sphere(0.055, 3, 1, _unshaded_material())
|
|
||||||
|
|
||||||
var ramp := Gradient.new()
|
|
||||||
ramp.set_color(0, Color(0.62, 0.58, 0.52, 1.0))
|
|
||||||
ramp.set_color(1, Color(0.62, 0.58, 0.52, 0.0))
|
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
|
||||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
|
||||||
scale_curve.add_point(Vector2(0.6, 1.0))
|
|
||||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
var p := CPUParticles3D.new()
|
|
||||||
p.one_shot = true
|
|
||||||
p.explosiveness = 0.92
|
|
||||||
p.amount = 14
|
|
||||||
p.lifetime = 0.65
|
|
||||||
p.randomness = 0.6
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = (back_dir + Vector3(0.0, 0.35, 0.0)).normalized()
|
|
||||||
p.spread = 45.0
|
|
||||||
p.gravity = Vector3(0.0, -9.8, 0.0)
|
|
||||||
p.initial_velocity_min = 5.0
|
|
||||||
p.initial_velocity_max = 14.0
|
|
||||||
p.scale_amount_min = 0.5
|
|
||||||
p.scale_amount_max = 1.8
|
|
||||||
p.mesh = sphere
|
|
||||||
p.color_ramp = ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
get_parent().add_child(p)
|
|
||||||
p.global_position = origin
|
|
||||||
p.restart()
|
|
||||||
_free_after(p, 2.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _spawn_kick_flash(origin: Vector3) -> void:
|
|
||||||
var mat := _unshaded_material()
|
|
||||||
mat.albedo_color = Color(1.0, 0.97, 0.40, 0.0)
|
|
||||||
|
|
||||||
var sphere := _particle_sphere(1.0, 8, 4, mat)
|
|
||||||
|
|
||||||
var mi := MeshInstance3D.new()
|
|
||||||
mi.mesh = sphere
|
|
||||||
mi.material_override = mat
|
|
||||||
mi.scale = Vector3(0.05, 0.05, 0.05)
|
|
||||||
get_parent().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 _spawn_dash_burst() -> void:
|
func _spawn_dash_burst() -> void:
|
||||||
var box := BoxMesh.new()
|
Fx.spawn(&"bull_dash", _aimed_xform(global_position + Vector3(0.0, 0.2, 0.0), _dash_dir))
|
||||||
box.size = Vector3(0.045, 0.045, 0.30)
|
|
||||||
box.material = _unshaded_material()
|
|
||||||
|
|
||||||
var ramp := Gradient.new()
|
|
||||||
ramp.set_color(0, Color(0.50, 0.82, 1.0, 1.0))
|
|
||||||
ramp.set_color(1, Color(0.50, 0.82, 1.0, 0.0))
|
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
|
||||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
|
||||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
var p := CPUParticles3D.new()
|
|
||||||
p.one_shot = true
|
|
||||||
p.explosiveness = 0.92
|
|
||||||
p.amount = 18
|
|
||||||
p.lifetime = 0.22
|
|
||||||
p.randomness = 0.15
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = _dash_dir
|
|
||||||
p.spread = 15.0
|
|
||||||
p.gravity = Vector3.ZERO
|
|
||||||
p.initial_velocity_min = 18.0
|
|
||||||
p.initial_velocity_max = 35.0
|
|
||||||
p.scale_amount_min = 0.8
|
|
||||||
p.scale_amount_max = 2.8
|
|
||||||
p.mesh = box
|
|
||||||
p.color_ramp = ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
get_parent().add_child(p)
|
|
||||||
p.global_position = global_position + Vector3(0.0, 0.2, 0.0)
|
|
||||||
p.restart()
|
|
||||||
_free_after(p, 0.6)
|
|
||||||
|
|
||||||
|
|
||||||
func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
|
# A transform at `origin` whose local -Z faces `forward` (flattened to horizontal). The kick/dash
|
||||||
var sphere := _particle_sphere(0.04, 3, 1, _unshaded_material())
|
# effects bake their spray in local space, so aiming the spawn transform points the burst the
|
||||||
|
# right way. Falls back to an unrotated transform when `forward` has no horizontal component.
|
||||||
var ramp := Gradient.new()
|
func _aimed_xform(origin: Vector3, forward: Vector3) -> Transform3D:
|
||||||
ramp.set_color(0, Color(1.0, 0.98, 0.60, 1.0))
|
var flat := Vector3(forward.x, 0.0, forward.z)
|
||||||
ramp.set_color(1, Color(1.0, 1.0, 1.0, 0.0))
|
if flat.length_squared() < 0.0001:
|
||||||
|
return Transform3D(Basis(), origin)
|
||||||
var scale_curve := Curve.new()
|
return Transform3D(Basis(), origin).looking_at(origin + flat.normalized(), Vector3.UP)
|
||||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
|
||||||
scale_curve.add_point(Vector2(0.5, 0.6))
|
|
||||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
|
||||||
|
|
||||||
var p := CPUParticles3D.new()
|
|
||||||
p.one_shot = true
|
|
||||||
p.explosiveness = 0.95
|
|
||||||
p.amount = 22
|
|
||||||
p.lifetime = 0.28
|
|
||||||
p.randomness = 0.4
|
|
||||||
p.local_coords = false
|
|
||||||
p.direction = (back_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
|
||||||
p.spread = 30.0
|
|
||||||
p.gravity = Vector3(0.0, -14.0, 0.0)
|
|
||||||
p.initial_velocity_min = 8.0
|
|
||||||
p.initial_velocity_max = 18.0
|
|
||||||
p.scale_amount_min = 0.6
|
|
||||||
p.scale_amount_max = 2.0
|
|
||||||
p.mesh = sphere
|
|
||||||
p.color_ramp = ramp
|
|
||||||
p.scale_amount_curve = scale_curve
|
|
||||||
get_parent().add_child(p)
|
|
||||||
p.global_position = origin
|
|
||||||
p.restart()
|
|
||||||
_free_after(p, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _make_slam_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 _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
@@ -1112,23 +761,14 @@ func _physics_process(delta: float) -> void:
|
|||||||
velocity.z = flat_n.z * exit_spd
|
velocity.z = flat_n.z * exit_spd
|
||||||
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
||||||
|
|
||||||
# --- ADD THIS BREAKABLE BARREL DETECTION CODE ---
|
# Ram straight through a barrel that formed the wall we bounced off.
|
||||||
var collision = get_slide_collision(i)
|
var hit := col.get_collider()
|
||||||
var collider = collision.get_collider()
|
if hit != null and hit.has_method(&"destroy_barrel"):
|
||||||
|
hit.call(&"destroy_barrel")
|
||||||
# Check if the object we hit has the "destroy_barrel" function
|
|
||||||
if collider and collider.has_method("destroy_barrel"):
|
|
||||||
# Optional: Only break if moving fast (e.g., sprinting or dashing)
|
|
||||||
# if velocity.length() > 5.0:
|
|
||||||
collider.destroy_barrel()
|
|
||||||
|
|
||||||
# ── Landing. Don't bounce ─────────────────────────────────────────────────────
|
# ── Landing. Don't bounce ─────────────────────────────────────────────────────
|
||||||
if is_on_floor() and not _was_on_floor:
|
if is_on_floor() and not _was_on_floor:
|
||||||
velocity.y = 0.0
|
velocity.y = 0.0
|
||||||
#Bouncing landing below
|
|
||||||
#var impact := -_pre_slide_vel_y
|
|
||||||
#if impact > DP.f("bounce_min_impact") and not _slam_pending:
|
|
||||||
#velocity.y = impact * DP.f("bounce_restitution")
|
|
||||||
|
|
||||||
# ── Slam landing ──────────────────────────────────────────────────────────
|
# ── Slam landing ──────────────────────────────────────────────────────────
|
||||||
if _slam_pending and is_on_floor() and not _was_on_floor:
|
if _slam_pending and is_on_floor() and not _was_on_floor:
|
||||||
@@ -1157,8 +797,6 @@ func _physics_process(delta: float) -> void:
|
|||||||
if vel_flat.length_squared() > 0.1:
|
if vel_flat.length_squared() > 0.1:
|
||||||
_charge_trail_emitter.direction = (-vel_flat.normalized() + Vector3(0.0, 0.25, 0.0)).normalized()
|
_charge_trail_emitter.direction = (-vel_flat.normalized() + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||||
|
|
||||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
func get_hp() -> int:
|
func get_hp() -> int:
|
||||||
return _hp
|
return _hp
|
||||||
@@ -1274,7 +912,6 @@ func _spawn_blood(hit_pos: Vector3) -> void:
|
|||||||
_free_after(p, 1.5)
|
_free_after(p, 1.5)
|
||||||
|
|
||||||
|
|
||||||
#hit flashing
|
|
||||||
func _hit_flash() -> void:
|
func _hit_flash() -> void:
|
||||||
var meshes := find_children("*", "MeshInstance3D", true, false)
|
var meshes := find_children("*", "MeshInstance3D", true, false)
|
||||||
|
|
||||||
|
|||||||
@@ -46,21 +46,10 @@ func _register_all() -> void:
|
|||||||
# (The dash ability bypasses this.)
|
# (The dash ability bypasses this.)
|
||||||
_reg_f("Bull", "bull_gore_arc", 75.0, 10.0, 180.0, 5.0)
|
_reg_f("Bull", "bull_gore_arc", 75.0, 10.0, 180.0, 5.0)
|
||||||
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
||||||
|
# The dust LOOK (colour, size, velocity) lives in systems/fx/bull_hoof_dust.tscn now; these
|
||||||
|
# two only decide when walk vs charge dust kicks in by ground speed.
|
||||||
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
||||||
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
|
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
|
||||||
# ── Dust ──────────────────────────────────────────────────────────────────
|
|
||||||
_reg_f("Dust", "dust_lifetime", 0.9, 0.1, 4.0)
|
|
||||||
_reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0)
|
|
||||||
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
|
|
||||||
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
|
|
||||||
_reg_f("Dust", "walk_scale_min", 0.20, 0.05, 3.0)
|
|
||||||
_reg_f("Dust", "walk_scale_max", 0.40, 0.05, 3.0)
|
|
||||||
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
|
|
||||||
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
|
|
||||||
_reg_f("Dust", "charge_scale_min", 0.35, 0.05, 6.0)
|
|
||||||
_reg_f("Dust", "charge_scale_max", 0.70, 0.05, 6.0)
|
|
||||||
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
|
|
||||||
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
|
|
||||||
# ── Tail ──────────────────────────────────────────────────────────────────
|
# ── Tail ──────────────────────────────────────────────────────────────────
|
||||||
_reg_f("Tail", "tail_gravity", 9.8, 0.0, 30.0)
|
_reg_f("Tail", "tail_gravity", 9.8, 0.0, 30.0)
|
||||||
_reg_f("Tail", "tail_damping", 0.08, 0.0, 0.5, 0.01)
|
_reg_f("Tail", "tail_damping", 0.08, 0.0, 0.5, 0.01)
|
||||||
|
|||||||
+32
-3
@@ -6,8 +6,9 @@ change to how a hit *feels* is a `.tscn` Siim edits in the inspector — no code
|
|||||||
|
|
||||||
```gdscript
|
```gdscript
|
||||||
# anywhere in gameplay code
|
# anywhere in gameplay code
|
||||||
Fx.spawn(&"blood_burst", global_transform) # at a node
|
Fx.spawn(&"blood_burst", global_transform) # one-shot: fire and forget (self-frees)
|
||||||
Fx.spawn(&"slam_dust", Transform3D(Basis(), point)) # at a world point
|
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
|
## Siim's workflow
|
||||||
@@ -25,6 +26,33 @@ Fx.spawn(&"slam_dust", Transform3D(Basis(), point)) # at a world point
|
|||||||
New effect? Add a `fx/<name>.tscn` (root = `CPUParticles3D` with `one_shot_effect.gd` attached — copy
|
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`.
|
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)
|
## House style (keep effects reading as one game)
|
||||||
|
|
||||||
**Palette** — pull from these, don't invent per-effect colours:
|
**Palette** — pull from these, don't invent per-effect colours:
|
||||||
@@ -52,8 +80,9 @@ the inspector and safe on-device. Keep this constraint when adding effects.
|
|||||||
|
|
||||||
| File | What it is |
|
| File | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `fx.gd` | The `Fx` autoload — `spawn(name, xform, parent=null)`; name→path registry |
|
| `fx.gd` | The `Fx` autoload — `spawn()` (one-shot) / `attach()` (continuous); name→path registry |
|
||||||
| `one_shot_effect.gd` | Root helper: fires the emitter(s) once, frees the scene when done |
|
| `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** |
|
| `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) |
|
| `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 |
|
| `_seed_placeholders.gd` | One-off generator that wrote the first-draft defaults; not used at runtime |
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -4,7 +4,10 @@ extends Node3D
|
|||||||
## press Play, then edit any effect .tscn in the inspector and re-run to compare. See fx/README.md.
|
## 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 _CYCLE_SEC := 1.6
|
||||||
const _EFFECTS: Array[StringName] = [&"blood_burst", &"slam_dust", &"spark", &"hit_flash"]
|
const _EFFECTS: Array[StringName] = [
|
||||||
|
&"blood_burst", &"slam_dust", &"spark", &"hit_flash",
|
||||||
|
&"bull_slam", &"bull_kick", &"bull_dash",
|
||||||
|
]
|
||||||
|
|
||||||
@onready var _label: Label = $UI/Label
|
@onready var _label: Label = $UI/Label
|
||||||
# Reached by path rather than the Fx global so the placeholder-seeder can compile and attach this
|
# Reached by path rather than the Fx global so the placeholder-seeder can compile and attach this
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ extends Node
|
|||||||
## placeholder .tscn are seeded — see tools/seed_fx_placeholders.gd.
|
## placeholder .tscn are seeded — see tools/seed_fx_placeholders.gd.
|
||||||
|
|
||||||
const _PATHS: Dictionary = {
|
const _PATHS: Dictionary = {
|
||||||
|
# Generic hit effects.
|
||||||
&"blood_burst": "res://systems/fx/blood_burst.tscn",
|
&"blood_burst": "res://systems/fx/blood_burst.tscn",
|
||||||
&"slam_dust": "res://systems/fx/slam_dust.tscn",
|
&"slam_dust": "res://systems/fx/slam_dust.tscn",
|
||||||
&"spark": "res://systems/fx/spark.tscn",
|
&"spark": "res://systems/fx/spark.tscn",
|
||||||
&"hit_flash": "res://systems/fx/hit_flash.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 = {}
|
var _cache: Dictionary = {}
|
||||||
@@ -32,6 +40,18 @@ func spawn(effect: StringName, xform: Transform3D, parent: Node = null) -> Node:
|
|||||||
return inst
|
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:
|
func _scene(effect: StringName) -> PackedScene:
|
||||||
if _cache.has(effect):
|
if _cache.has(effect):
|
||||||
return _cache[effect]
|
return _cache[effect]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
## Bear attack-FX test. The four attack effects (crater, smash debris, left/right claw slash)
|
## Bear attack-FX test. The four attack effects (crater, smash debris, left/right claw slash)
|
||||||
## live in Assets/fx/Bear_FX.glb as meshes that rest at scale 0 at Siim's baked positions. The
|
## live in art/fx/bear_fx.glb as meshes that rest at scale 0 at Siim's baked positions. The
|
||||||
## GLB carries its own AnimationPlayer whose clips scale one mesh up at its strike frame and
|
## GLB carries its own AnimationPlayer whose clips scale one mesh up at its strike frame and
|
||||||
## back to 0 (self-hiding) while pinning the others at 0. bear.gd fires the matching clip on
|
## back to 0 (self-hiding) while pinning the others at 0. bear.gd fires the matching clip on
|
||||||
## each strike (see _play_fx). This asserts each clip pops its own mesh, self-hides by the end,
|
## each strike (see _play_fx). This asserts each clip pops its own mesh, self-hides by the end,
|
||||||
|
|||||||
@@ -26,10 +26,16 @@ func _run() -> void:
|
|||||||
root.add_child(host)
|
root.add_child(host)
|
||||||
current_scene = host
|
current_scene = host
|
||||||
|
|
||||||
for id: StringName in [&"blood_burst", &"slam_dust", &"spark", &"hit_flash"]:
|
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)
|
var inst: Node = fx.call("spawn", id, Transform3D.IDENTITY, host)
|
||||||
_ok(inst != null and is_instance_valid(inst), "spawns %s" % id)
|
_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)
|
var missing: Node = fx.call("spawn", &"does_not_exist", Transform3D.IDENTITY, host)
|
||||||
_ok(missing == null, "unknown effect returns null instead of crashing")
|
_ok(missing == null, "unknown effect returns null instead of crashing")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user