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:
+3
-15
@@ -26,11 +26,11 @@ var _phase: Phase = Phase.WINDUP
|
||||
var _bull: CharacterBody3D = null
|
||||
var _skeleton: Skeleton3D = 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.
|
||||
var _fx_player: AnimationPlayer = 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.
|
||||
# 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
|
||||
@@ -635,7 +635,7 @@ func _try_hit(reach: float) -> bool:
|
||||
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.
|
||||
func _setup_attack_hitboxes() -> void:
|
||||
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
|
||||
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
|
||||
_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) ─────────────
|
||||
|
||||
@@ -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.
|
||||
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 _SPEAR_SCENE: PackedScene = preload("res://actors/matador/spear.tscn")
|
||||
|
||||
@@ -603,7 +603,7 @@ func _tick_attack(delta: float) -> void:
|
||||
if _swing_timer > 0.0:
|
||||
_decelerate(22.0, delta)
|
||||
_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.
|
||||
if _sword_blade_area != null and _swing_phase() >= DP.f("mat_stab_strike_at"):
|
||||
_sword_blade_area.activate()
|
||||
@@ -1189,7 +1189,7 @@ func _spawn_sword() -> void:
|
||||
_drawing = false
|
||||
_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.
|
||||
_sword_blade_area = _sword_node.get_node_or_null("BladeHitbox") as Hitbox
|
||||
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),
|
||||
]
|
||||
|
||||
# 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.
|
||||
const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||
@@ -27,12 +27,11 @@ var _legs: Node
|
||||
var _was_on_floor: bool = false
|
||||
var _pre_slide_vel_y: float = 0.0
|
||||
|
||||
var _slam_ring_mesh: ArrayMesh = null
|
||||
# Attack-FX resources are identical every swing, so build them once and reuse. Recreating a fresh
|
||||
# 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.
|
||||
# Built once and reused: a fresh StandardMaterial3D/SphereMesh per spawn makes a mobile WebGL
|
||||
# driver recompile the shader + re-upload the mesh — a hitch "when attacking" absent on desktop.
|
||||
var _unshaded_mat: StandardMaterial3D = null
|
||||
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
|
||||
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 }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
var _walk_dust_ramp: Gradient = null
|
||||
var _charge_dust_ramp: Gradient = null
|
||||
# Hoof-dust scale/velocity per state (the look — mesh, colour, curve — lives in bull_hoof_dust.tscn).
|
||||
# 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")
|
||||
@@ -106,6 +109,7 @@ func _ready() -> void:
|
||||
_max_hp += _bonus_hp
|
||||
_hp += _bonus_hp
|
||||
_cube_scale = cube_guy.scale
|
||||
_setup_impact_fx()
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
_setup_charge_trail()
|
||||
@@ -113,7 +117,6 @@ func _ready() -> void:
|
||||
_setup_audio()
|
||||
_bull_anim = _find_anim_player(cube_guy)
|
||||
_setup_bull_idle()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
|
||||
|
||||
func _setup_legs() -> void:
|
||||
@@ -123,113 +126,35 @@ func _setup_legs() -> void:
|
||||
_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:
|
||||
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:
|
||||
var p := CPUParticles3D.new()
|
||||
var p := Fx.attach(&"bull_hoof_dust", cube_guy) as CPUParticles3D
|
||||
if p == null:
|
||||
continue
|
||||
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)
|
||||
|
||||
|
||||
func _setup_charge_trail() -> void:
|
||||
# Blue energy trail — streams backward during a fast run / roll
|
||||
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
|
||||
|
||||
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)
|
||||
_charge_trail_emitter = Fx.attach(&"bull_charge_trail", cube_guy) as CPUParticles3D
|
||||
if _charge_trail_emitter != null:
|
||||
_charge_trail_emitter.position = Vector3(0.0, 0.25, 0.3)
|
||||
|
||||
|
||||
func _setup_roll_sparks() -> void:
|
||||
# Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
|
||||
# ball. World-space (local_coords off) so they peel off into a trail behind it.
|
||||
var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
||||
|
||||
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)
|
||||
_roll_spark_emitter = Fx.attach(&"bull_roll_sparks", cube_guy) as CPUParticles3D
|
||||
if _roll_spark_emitter != null:
|
||||
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
|
||||
|
||||
|
||||
func _setup_audio() -> void:
|
||||
@@ -251,42 +176,20 @@ func _setup_audio() -> void:
|
||||
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:
|
||||
if new_state == _dust_state:
|
||||
return
|
||||
_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:
|
||||
p.lifetime = DP.f("dust_lifetime")
|
||||
p.explosiveness = DP.f("dust_explosiveness")
|
||||
p.spread = DP.f("dust_spread")
|
||||
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||
match new_state:
|
||||
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)
|
||||
p.scale_amount_min = scale.x
|
||||
p.scale_amount_max = scale.y
|
||||
p.initial_velocity_min = vel.x
|
||||
p.initial_velocity_max = vel.y
|
||||
p.emitting = new_state != DustState.NONE
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ── Particle helpers ──────────────────────────────────────────────────────────
|
||||
# All FX in this script use unshaded, vertex-coloured, alpha-blended particles;
|
||||
# these three helpers remove the boilerplate each spawn function used to repeat.
|
||||
|
||||
# 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.
|
||||
# ── Blood-spurt particle helpers ──────────────────────────────────────────────
|
||||
# The ability effects now live in systems/fx/; only the spear-wound blood below is still built in
|
||||
# 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).
|
||||
func _unshaded_material() -> StandardMaterial3D:
|
||||
if _unshaded_mat == null:
|
||||
_unshaded_mat = StandardMaterial3D.new()
|
||||
@@ -698,8 +598,7 @@ func _unshaded_material() -> StandardMaterial3D:
|
||||
return _unshaded_mat
|
||||
|
||||
|
||||
# Low-poly sphere sized for a particle mesh (height is always the diameter). Cached per size so
|
||||
# repeat attacks reuse the same mesh (one GPU upload) instead of generating + uploading a new one.
|
||||
# Low-poly particle-sphere mesh (height = diameter), cached per size so repeats reuse one GPU upload.
|
||||
func _particle_sphere(radius: float, segments: int, rings: int, mat: Material) -> SphereMesh:
|
||||
var key := "%.4f:%d:%d" % [radius, segments, rings]
|
||||
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:
|
||||
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)
|
||||
_launch_slam_ring(origin, max_r, 0.00, 0.55, Color(1.00, 0.70, 0.10, 0.85))
|
||||
_launch_slam_ring(origin, max_r, 0.10, 0.52, Color(1.00, 0.50, 0.10, 0.65))
|
||||
_launch_slam_ring(origin, max_r, 0.20, 0.48, Color(0.90, 0.30, 0.10, 0.50))
|
||||
_spawn_slam_dust(origin)
|
||||
_impact_fx.call(&"slam_rings", origin, DP.f("slam_range"))
|
||||
Fx.spawn(&"bull_slam", Transform3D(Basis(), origin))
|
||||
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:
|
||||
var origin := global_position + back_dir * 0.55 + Vector3(0.0, -0.25, 0.0)
|
||||
_spawn_kick_dust(origin, back_dir)
|
||||
_spawn_kick_stones(origin, back_dir)
|
||||
_spawn_kick_sparks(origin, back_dir)
|
||||
_spawn_kick_flash(origin)
|
||||
Fx.spawn(&"bull_kick", _aimed_xform(origin, back_dir))
|
||||
_impact_fx.call(&"kick_flash", origin)
|
||||
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:
|
||||
var box := BoxMesh.new()
|
||||
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)
|
||||
Fx.spawn(&"bull_dash", _aimed_xform(global_position + Vector3(0.0, 0.2, 0.0), _dash_dir))
|
||||
|
||||
|
||||
func _spawn_kick_sparks(origin: Vector3, back_dir: Vector3) -> void:
|
||||
var sphere := _particle_sphere(0.04, 3, 1, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.98, 0.60, 1.0))
|
||||
ramp.set_color(1, Color(1.0, 1.0, 1.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
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
|
||||
# A transform at `origin` whose local -Z faces `forward` (flattened to horizontal). The kick/dash
|
||||
# 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.
|
||||
func _aimed_xform(origin: Vector3, forward: Vector3) -> Transform3D:
|
||||
var flat := Vector3(forward.x, 0.0, forward.z)
|
||||
if flat.length_squared() < 0.0001:
|
||||
return Transform3D(Basis(), origin)
|
||||
return Transform3D(Basis(), origin).looking_at(origin + flat.normalized(), Vector3.UP)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
@@ -1112,23 +761,14 @@ func _physics_process(delta: float) -> void:
|
||||
velocity.z = flat_n.z * exit_spd
|
||||
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
||||
|
||||
# --- ADD THIS BREAKABLE BARREL DETECTION CODE ---
|
||||
var collision = get_slide_collision(i)
|
||||
var collider = collision.get_collider()
|
||||
|
||||
# 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()
|
||||
# Ram straight through a barrel that formed the wall we bounced off.
|
||||
var hit := col.get_collider()
|
||||
if hit != null and hit.has_method(&"destroy_barrel"):
|
||||
hit.call(&"destroy_barrel")
|
||||
|
||||
# ── Landing. Don't bounce ─────────────────────────────────────────────────────
|
||||
if is_on_floor() and not _was_on_floor:
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
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:
|
||||
_charge_trail_emitter.direction = (-vel_flat.normalized() + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||
|
||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
func get_hp() -> int:
|
||||
return _hp
|
||||
@@ -1274,7 +912,6 @@ func _spawn_blood(hit_pos: Vector3) -> void:
|
||||
_free_after(p, 1.5)
|
||||
|
||||
|
||||
#hit flashing
|
||||
func _hit_flash() -> void:
|
||||
var meshes := find_children("*", "MeshInstance3D", true, false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user