@@ -6,8 +6,9 @@ extends CharacterBody3D
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
# ATTACK → post-dodge or mid-range: chase and strike
# ROLL → hit by bull but roll chance fires: survive and recover
# THROW → planted, winding up an overhand throw, then releasing the sword
# RAGDOLL → hit by bull, no roll chance
enum State { WANDER , FLEE , BRACE , SIDESTEP , ATTACK , ROLL , RAGDOLL }
enum State { WANDER , FLEE , BRACE , SIDESTEP , ATTACK , ROLL , THROW , RAGDOLL }
signal killed
@@ -24,27 +25,53 @@ var _step_timer: float = 0.0
var _dodge_cd : float = 0.0
var _will_dodge : bool = true
var _attack_timer : float = 0.0
var _swing_timer : float = 0.0
var _roll_timer : float = 0.0
var _roll_dir : Vector3 = Vector3 . ZERO
var _throw_timer : float = 0.0
var _throw_aim_cd : float = 0.0
var _throw_dir : Vector3 = Vector3 . ZERO
var _steer_dir : Vector3 = Vector3 . ZERO
var _steer_cd : float = 0.0
var _ole_player : AudioStreamPlayer = null
var _sword_hand_attach : BoneAttachment3D = null
var _sword_rest_attach : BoneAttachment3D = null
var _sword_node : Node3D = null
var _sword_in_hand : bool = false
var _drawing : bool = false
var _draw_timer : float = 0.0
var _draw_len : float = 0.0
var _resword_timer : float = 0.0
var _sword_blade_area : Area3D = null
var _blade_hit_cd : float = 0.0
var _death_player : AudioStreamPlayer = null
var _blood_burst : CPUParticles3D = null
@onready var _mesh : Node3D = $matador2
@onready var _hit_area : Area3D = $HitArea
@onready var _body_col : CollisionShape3D = $CollisionShape3D
const _ANIM_RUN : StringName = & " Run "
const _ANIM_IDLE : StringName = & " Taunt "
const _ANIM_ATTACK : StringName = & " Attack "
const _ANIM_ROLL : StringName = & " Roll "
const _ANIM_DRAW : StringName = & " Draw_weapon "
# Fraction of the draw clip at which the hand grabs the sword — the sword
# reparents from holster to hand at this point in the animation.
const _DRAW_GRAB_AT : float = 0.55
const _SWORD_SCENE : PackedScene = preload ( " res://Assets/sword.glb " )
# Candidate steering offsets (radians) tried in order when a wall is ahead
const _STEER_ANGLES : Array = [ 0.0 , - 0.35 , 0.35 , - 0.7 , 0.7 , - 1.1 , 1.1 , PI ]
const _STEER_LOOKAHEAD : float = 3.5 # longer = turns before hitting wall
const _STEER_INTERVAL : float = 0.12 # recompute direction at most ~8× /sec
# Side angles (radians) tried when the straight-ahead path is blocked.
const _STEER_CANDIDATES : Array = [ - 0.35 , 0.35 , - 0.7 , 0.7 , - 1.1 , 1.1 , PI ]
# Movement tuning
const _ACCEL_FACTOR : float = 8.0 # velocity ramp = speed * factor * delta
const _TURN_MOVE : float = 8.0 # face the travel direction while roaming
const _TURN_FACE : float = 12.0 # face the bull while engaging
const _TURN_SHARP : float = 20.0 # snap onto a sidestep / roll direction
func _ready ( ) - > void :
@@ -59,7 +86,11 @@ func _ready() -> void:
if not _anim_player . has_animation ( _ANIM_RUN ) :
push_warning ( " Matador: expected animations not found. Available: %s " %
str ( _anim_player . get_animation_list ( ) ) )
_anim_player . playback_default_blend_time = DP . f ( " mat_anim_blend " )
_anim_player . play ( _ANIM_IDLE )
_setup_sword ( )
DP . any_changed . connect ( _on_dp_changed )
_throw_aim_cd = randf_range ( 0.8 , 2.5 )
_hit_area . body_entered . connect ( _on_body_entered )
_pick_wander_target ( )
_setup_blood_burst ( )
@@ -75,17 +106,35 @@ func _ready() -> void:
add_child ( _death_player )
func _on_dp_changed ( _key : String , _value : Variant ) - > void :
_apply_sword_grip ( )
if _anim_player :
_anim_player . playback_default_blend_time = DP . f ( " mat_anim_blend " )
func _acquire_bull ( ) - > void :
if _bull != null :
return
var players := get_tree ( ) . get_nodes_in_group ( & " player " )
if not players . is_empty ( ) :
_bull = players [ 0 ] as CharacterBody3D
func _physics_process ( delta : float ) - > void :
if _bull == null :
var players := get_tree ( ) . get_nodes_in_group ( & " player " )
if not players . is_empty ( ) :
_bull = players [ 0 ] as CharacterBody3D
_acquire_bull ( )
if not is_on_floor ( ) :
velocity + = get_gravity ( ) * delta
_dodge_cd = maxf ( 0.0 , _dodge_cd - delta )
_dodge_cd = maxf ( 0.0 , _dodge_cd - delta )
_blade_hit_cd = maxf ( 0.0 , _blade_hit_cd - delta )
_throw_aim_cd = maxf ( 0.0 , _throw_aim_cd - delta )
# Pull a replacement sword from the holster a beat after throwing the last one.
if _state != State . RAGDOLL and not is_instance_valid ( _sword_node ) \
and ( _sword_rest_attach != null or _sword_hand_attach != null ) :
_resword_timer - = delta
if _resword_timer < = 0.0 :
_spawn_sword ( )
if _state != State . RAGDOLL and _bull != null :
_update_ai_state ( )
@@ -96,11 +145,26 @@ func _physics_process(delta: float) -> void:
State . SIDESTEP : _tick_sidestep ( delta )
State . ATTACK : _tick_attack ( delta )
State . ROLL : _tick_roll ( delta )
State . THROW : _tick_throw ( delta )
State . RAGDOLL : _tick_ragdoll ( delta )
_update_sword_carry ( )
_advance_draw ( delta )
func _update_ai_state ( ) - > void :
if _state == State . THROW :
return
var dist := global_position . distance_to ( _bull . global_position )
if ( _state == State . WANDER or _state == State . FLEE or _state == State . ATTACK ) \
and _throw_aim_cd < = 0.0 and _sword_in_hand and is_instance_valid ( _sword_node ) \
and dist > = DP . f ( " mat_throw_min_dist " ) and dist < = DP . f ( " mat_throw_range " ) :
# Roll for a throw; on a miss, wait before becoming eligible again so the
# matador commits to chasing for a melee Attack instead of throwing early.
if randf ( ) < DP . f ( " mat_throw_chance " ) :
_start_throw ( )
return
_throw_aim_cd = randf_range ( 2.5 , 5.0 )
match _state :
State . WANDER :
if dist < DP . f ( " mat_flee_range " ) :
@@ -156,7 +220,7 @@ func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
if space . intersect_ray ( params ) . is_empty ( ) :
_steer_dir = desired_dir
return desired_dir
var candidates : Array = [ - 0.35 , 0.35 , - 0.7 , 0.7 , - 1.1 , 1.1 , PI ]
var candidates : = _STEER_CANDIDATES . duplicate ( )
candidates . shuffle ( )
for angle : float in candidates :
var dir := desired_dir . rotated ( Vector3 . UP , angle )
@@ -169,41 +233,62 @@ func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
return desired_dir
# Ramp horizontal velocity toward dir*spd; vertical (gravity) is left untouched.
func _accelerate ( dir : Vector3 , spd : float , delta : float ) - > void :
velocity . x = move_toward ( velocity . x , dir . x * spd , spd * _ACCEL_FACTOR * delta )
velocity . z = move_toward ( velocity . z , dir . z * spd , spd * _ACCEL_FACTOR * delta )
func _decelerate ( rate : float , delta : float ) - > void :
velocity . x = move_toward ( velocity . x , 0.0 , rate * delta )
velocity . z = move_toward ( velocity . z , 0.0 , rate * delta )
# Turn the mesh to face travel direction — skipped at near-zero speed so a
# stopping matador doesn't snap to atan2(0, 0) == facing +Z (a jitter source).
func _face_velocity ( delta : float , rate : float ) - > void :
if Vector2 ( velocity . x , velocity . z ) . length_squared ( ) > 0.04 :
_mesh . rotation . y = lerp_angle (
_mesh . rotation . y , atan2 ( velocity . x , velocity . z ) , delta * rate )
func _face_point ( point : Vector3 , delta : float , rate : float ) - > void :
var to := point - global_position
to . y = 0.0
if to . length_squared ( ) > 0.01 :
_mesh . rotation . y = lerp_angle ( _mesh . rotation . y , atan2 ( to . x , to . z ) , delta * rate )
func _tick_wander ( delta : float ) - > void :
if _idle_timer > 0.0 :
_idle_timer - = delta
velocity . x = move_toward ( velocity . x , 0.0 , 10.0 * delta )
velocity . z = move_toward ( velocity . z , 0.0 , 10.0 * delta )
# Plant the feet while unsheathing instead of sliding through the draw clip.
if _drawing or _idle_timer > 0.0 :
_idle_timer = maxf ( 0.0 , _idle_timer - delta )
_decelerate ( 10.0 , delta )
_play_anim ( _ANIM_IDLE )
move_and_slide ( )
return
var dx : float = _wander_target . x - global_position . x
var dz : float = _wander_target . z - global_position . z
if Vector2 ( dx , dz ) . length ( ) < 0.8 :
var to_target : = Vector3 (
_wander_target . x - global_position . x , 0.0 , _wander_target . z - global_position . z )
if to_target . length ( ) < 0.8 :
_idle_timer = randf_range ( DP . f ( " mat_idle_min " ) , DP . f ( " mat_idle_max " ) )
_pick_wander_target ( )
else :
var dir := _steer_clear ( Vector3 ( dx , 0.0 , dz ) . normalized ( ) , delta )
var spd := DP . f ( " mat_walk_speed " )
velocity . x = move_toward ( velocity . x , dir . x * spd , spd * 8.0 * delta )
velocity . z = move_toward ( velocity . z , dir . z * spd , spd * 8.0 * delta )
_mesh . rotation . y = lerp_angle (
_mesh . rotation . y , atan2 ( velocity . x , velocity . z ) , delta * 8.0 )
var dir := _steer_clear ( to_target . normalized ( ) , delta )
_accelerate ( dir , DP . f ( " mat_walk_speed " ) , delta )
_face_velocity ( delta , _TURN_MOVE )
_play_anim ( _ANIM_RUN )
move_and_slide ( )
func _tick_flee ( delta : float ) - > void :
var away := ( global_position - _bull . global_position )
var away := global_position - _bull . global_position
away . y = 0.0
if away . length ( ) < 0.01 :
away = Vector3 ( randf ( ) - 0.5 , 0.0 , randf ( ) - 0.5 )
var dir := _steer_clear ( away . normalized ( ) , delta )
var spd := DP . f ( " mat_flee_speed " )
velocity . x = move_toward ( velocity . x , dir . x * spd , spd * 8.0 * delta )
velocity . z = move_toward ( velocity . z , dir . z * spd , spd * 8.0 * delta )
_mesh . rotation . y = lerp_angle ( _mesh . rotation . y , atan2 ( velocity . x , velocity . z ) , delta * 8.0 )
_accelerate ( dir , DP . f ( " mat_flee_speed " ) , delta )
_face_velocity ( delta , _TURN_MOVE )
_play_anim ( _ANIM_RUN )
move_and_slide ( )
@@ -227,13 +312,8 @@ func _start_brace() -> void:
func _tick_brace ( delta : float ) - > void :
velocity . x = move_toward ( velocity . x , 0.0 , 18.0 * delta )
velocity . z = move_toward ( velocity . z , 0.0 , 18.0 * delta )
var to_bull := ( _bull . global_position - global_position )
to_bull . y = 0.0
if to_bull . length ( ) > 0.1 :
_mesh . rotation . y = lerp_angle (
_mesh . rotation . y , atan2 ( to_bull . x , to_bull . z ) , delta * 14.0 )
_decelerate ( 18.0 , delta )
_face_point ( _bull . global_position , delta , _TURN_FACE )
_play_anim ( _ANIM_ATTACK )
move_and_slide ( )
@@ -260,7 +340,7 @@ func _tick_sidestep(delta: float) -> void:
var spd := DP . f ( " mat_step_speed " )
velocity . x = _step_dir . x * spd
velocity . z = _step_dir . z * spd
_mesh . rotation . y = lerp_angle ( _mesh . rotation . y , atan2 ( velocity . x , velocity . z ) , delta * 20.0 )
_face_velocity ( delta , _TURN_SHARP )
_play_anim ( _ANIM_ROLL )
move_and_slide ( )
@@ -268,38 +348,46 @@ func _tick_sidestep(delta: float) -> void:
func _start_attack ( ) - > void :
_state = State . ATTACK
_attack_timer = DP . f ( " mat_attack_duration " )
_swing_timer = 0.0
_dodge_cd = DP . f ( " mat_dodge_cooldown " )
func _tick_attack ( delta : float ) - > void :
_attack_timer - = delta
var to_bull : = ( _bull . global_position - global_position )
_swing_timer = maxf ( 0.0 , _swing_timer - delta )
_face_point ( _bull . global_position , delta , _TURN_FACE )
var to_bull := _bull . global_position - global_position
to_bull . y = 0.0
var dist := to_bull . length ( )
if dist > 0.1 :
_mesh . rotation . y = lerp_angle (
_mesh . rotation . y , atan2 ( to_bull . x , to_bull . z ) , delta * 10.0 )
# Commit to a full swing once in range instead of flipping Run/Attack at the
# distance boundary every frame (the old foot-slide jitter). When the swing
# ends the matador re-closes if the bull slipped away, or stabs again if not.
if _swing_timer < = 0.0 and dist < = DP . f ( " mat_attack_min_dist " ) :
_swing_timer = _anim_length ( _ANIM_ATTACK , 0.9 )
if dist > DP . f ( " mat_attack_min_dist " ) :
var dir := _steer_clear ( to_bull . normalized ( ) , delta )
var spd := DP . f ( " mat_attack_speed " )
velocity . x = move_toward ( velocity . x , dir . x * spd , spd * 8.0 * delta )
velocity . z = move_toward ( velocity . z , dir . z * spd , spd * 8.0 * delta )
if _swing_timer > 0.0 :
_decelerate ( 22.0 , delta )
_play_anim ( _ANIM_ATTACK )
else :
velocity . x = move_toward ( velocity . x , 0.0 , 14.0 * delta )
velocity . z = move_toward ( velocity . z , 0.0 , 14.0 * delta )
var dir : = _steer_clear ( to_bull . normalized ( ) , delta )
_accelerate ( dir , DP . f ( " mat_attack_speed " ) , delta )
_play_anim ( _ANIM_RUN )
_play_anim ( _ANIM_ATTACK )
move_and_slide ( )
func _enter_roll ( hit_dir : Vector3 ) - > void :
_state = State . ROLL
_roll_timer = DP . f ( " mat_roll_duration " )
# Match the state to the actual clip so the roll plays through once instead of
# cutting to Run partway. mat_roll_duration is only a fallback.
_roll_timer = _anim_length ( _ANIM_ROLL , DP . f ( " mat_roll_duration " ) )
hit_dir . y = 0.0
var side := 1.0 if randf ( ) > 0.5 else - 1.0
_roll_dir = hit_dir . normalized ( ) . rotated ( Vector3 . UP , PI * 0.5 * side )
if _anim_player and _anim_player . has_animation ( _ANIM_ROLL ) :
_anim_player . play ( _ANIM_ROLL )
_anim_player . seek ( 0.0 , true )
func _tick_roll ( delta : float ) - > void :
@@ -311,11 +399,143 @@ func _tick_roll(delta: float) -> void:
var spd := DP . f ( " mat_step_speed " )
velocity . x = _roll_dir . x * spd
velocity . z = _roll_dir . z * spd
_mesh . rotation . y = lerp_angle ( _mesh . rotation . y , atan2 ( velocity . x , velocity . z ) , delta * 20.0 )
_face_velocity ( delta , _TURN_SHARP )
_play_anim ( _ANIM_ROLL )
move_and_slide ( )
func _start_throw ( ) - > void :
_state = State . THROW
_throw_timer = DP . f ( " mat_throw_windup " )
velocity = Vector3 . ZERO
if _anim_player :
_anim_player . pause ( )
var to_bull := _bull . global_position - global_position
to_bull . y = 0.0
_throw_dir = to_bull . normalized ( ) if to_bull . length ( ) > 0.1 else _mesh . global_transform . basis . z
func _tick_throw ( delta : float ) - > void :
_decelerate ( 20.0 , delta )
_face_point ( _bull . global_position , delta , _TURN_FACE )
var to_bull := _bull . global_position - global_position
to_bull . y = 0.0
if to_bull . length ( ) > 0.1 :
_throw_dir = to_bull . normalized ( )
var dur := maxf ( DP . f ( " mat_throw_windup " ) , 0.001 )
var prev := clampf ( 1.0 - _throw_timer / dur , 0.0 , 1.0 )
_throw_timer - = delta
var phase := clampf ( 1.0 - _throw_timer / dur , 0.0 , 1.0 )
_pose_throw_arm ( phase )
var t_rel := DP . f ( " mat_throw_release " )
if prev < t_rel and phase > = t_rel and is_instance_valid ( _sword_node ) :
_release_sword ( )
if _throw_timer < = 0.0 :
_end_throw ( )
move_and_slide ( )
func _end_throw ( ) - > void :
_state = State . FLEE
_dodge_cd = DP . f ( " mat_dodge_cooldown " )
# Blend straight into locomotion so the manually-posed throw arm eases out
# (a bare resume() would snap and leave the player on whatever it paused on).
if _anim_player :
_anim_player . play ( _ANIM_RUN )
# Drive arm_R + forearm_R through a cock-back → release arc while the
# AnimationPlayer is paused. Swing is on bone-local X (see CLAUDE.md rig notes);
# angles are DP-tunable so the arc can be dialled in per rig.
func _pose_throw_arm ( phase : float ) - > void :
if not _skeleton :
return
var t_rel := DP . f ( " mat_throw_release " )
var cock := DP . f ( " mat_throw_arm_cock " )
var release := DP . f ( " mat_throw_arm_release " )
var elbow := DP . f ( " mat_throw_elbow " )
var arm_ang : float
var elbow_ang : float
if phase < t_rel :
var u := phase / maxf ( t_rel , 0.001 )
arm_ang = lerpf ( 0.0 , cock , u )
elbow_ang = lerpf ( 0.0 , elbow , u )
else :
var u := ( phase - t_rel ) / maxf ( 1.0 - t_rel , 0.001 )
arm_ang = lerpf ( cock , release , u )
elbow_ang = lerpf ( elbow , 0.0 , u )
_set_bone_swing ( " arm_R " , arm_ang )
_set_bone_swing ( " forearm_R " , elbow_ang )
func _set_bone_swing ( bone : String , deg : float ) - > void :
var idx := _skeleton . find_bone ( bone )
if idx < 0 :
return
var rest := _skeleton . get_bone_rest ( idx ) . basis . get_rotation_quaternion ( )
_skeleton . set_bone_pose_rotation ( idx , rest * Quaternion ( Vector3 . RIGHT , deg_to_rad ( deg ) ) )
# Detach the sword from the hand and launch it as a spinning physics projectile
# that damages the bull on contact, then settles and despawns.
func _release_sword ( ) - > void :
if not is_instance_valid ( _sword_node ) :
return
var sword := _sword_node
var blade := _sword_blade_area
var gx := sword . global_transform
_sword_node = null
_sword_blade_area = null
_sword_in_hand = false
_resword_timer = DP . f ( " mat_resword_delay " )
# The RigidBody owns collisions now — silence the melee blade Area it carries.
if is_instance_valid ( blade ) :
blade . monitoring = false
var rb := RigidBody3D . new ( )
rb . collision_layer = 0
rb . collision_mask = 1
rb . contact_monitor = true
rb . max_contacts_reported = 4
get_tree ( ) . current_scene . add_child ( rb )
rb . global_transform = gx
sword . reparent ( rb , true )
# Physics collider along the blade (GLB +Z), which equals rb-local +Z because
# the mesh kept its global transform and rb adopted it.
var cap := CapsuleShape3D . new ( )
cap . radius = 0.06
cap . height = 1.4
var cs := CollisionShape3D . new ( )
cs . shape = cap
cs . position = Vector3 ( 0.0 , 0.0 , 0.7 )
cs . rotation_degrees = Vector3 ( 90.0 , 0.0 , 0.0 )
rb . add_child ( cs )
var dir := ( _throw_dir + Vector3 . UP * 0.12 ) . normalized ( )
rb . linear_velocity = dir * DP . f ( " mat_throw_speed " )
rb . angular_velocity = dir . cross ( Vector3 . UP ) . normalized ( ) * - DP . f ( " mat_throw_spin " )
var hit_done := [ false ]
rb . body_entered . connect ( func ( body : Node ) - > void :
if hit_done [ 0 ] :
return
if body . is_in_group ( & " player " ) :
hit_done [ 0 ] = true
body . call ( & " take_sword_hit " )
)
var cleanup := get_tree ( ) . create_timer ( 6.0 )
cleanup . timeout . connect ( func ( ) - > void :
if is_instance_valid ( rb ) :
rb . queue_free ( )
)
func _tick_ragdoll ( _delta : float ) - > void :
velocity . x = 0.0
velocity . z = 0.0
@@ -325,10 +545,7 @@ func _tick_ragdoll(_delta: float) -> void:
func apply_ability_hit ( hit_dir : Vector3 , strength : float ) - > void :
if _state == State . RAGDOLL or _state == State . ROLL :
return
if _bull == null :
var players := get_tree ( ) . get_nodes_in_group ( & " player " )
if not players . is_empty ( ) :
_bull = players [ 0 ] as CharacterBody3D
_acquire_bull ( )
_enter_ragdoll ( hit_dir , strength )
@@ -350,8 +567,21 @@ func _on_body_entered(body: Node3D) -> void:
_enter_ragdoll ( hit_dir , player . velocity . length ( ) )
func _on_blade_hit ( body : Node3D ) - > void :
if _state == State . RAGDOLL or _state != State . ATTACK :
return
if not body . is_in_group ( & " player " ) :
return
if _blade_hit_cd > 0.0 :
return
_blade_hit_cd = 0.5
body . call ( & " take_sword_hit " )
func _enter_ragdoll ( hit_dir : Vector3 , bull_speed : float ) - > void :
_state = State . RAGDOLL
if _sword_blade_area :
_sword_blade_area . monitoring = false
killed . emit ( )
var cleanup_timer := get_tree ( ) . create_timer ( 4.0 )
cleanup_timer . timeout . connect ( func ( ) - > void :
@@ -476,9 +706,17 @@ func _ensure_loop(anim_name: StringName) -> void:
anim . loop_mode = Animation . LOOP_LINEAR
func _anim_length ( anim_name : StringName , fallback : float ) - > float :
if _anim_player and _anim_player . has_animation ( anim_name ) :
return _anim_player . get_animation ( anim_name ) . length
return fallback
func _play_anim ( anim_name : StringName ) - > void :
if _anim_player == null :
return
if _drawing :
return # let the unholster clip play through uninterrupted
if _anim_player . current_animation == anim_name :
return
_anim_player . play ( anim_name )
@@ -499,6 +737,154 @@ func _pick_wander_target() -> void:
_wander_target = Vector3 ( cos ( angle ) * dist , 0.0 , sin ( angle ) * dist )
# ── Sword attachment ──────────────────────────────────────────────────────────
# The rig (matador_v03) carries two purpose-built bones for the sword:
# weapon_bone — the grip pose in the right hand (drawn / fighting)
# weapon_rest_bone — the holstered pose at the hip (sheathed / wandering)
# The sword lives in the holster by default and is carried to the hand while the
# matador is in a combat state (see _carry_sword). Both bones are oriented in
# Blender to seat the sword directly, so the local offset/rotation default to
# identity and stay DP-tunable for fine-tuning without re-exporting.
func _setup_sword ( ) - > void :
if not _skeleton :
return
var has_hand := _skeleton . find_bone ( " weapon_bone " ) != - 1
var has_rest := _skeleton . find_bone ( " weapon_rest_bone " ) != - 1
if not has_hand and not has_rest :
return
if has_hand :
_sword_hand_attach = BoneAttachment3D . new ( )
_sword_hand_attach . bone_name = " weapon_bone "
_skeleton . add_child ( _sword_hand_attach )
if has_rest :
_sword_rest_attach = BoneAttachment3D . new ( )
_sword_rest_attach . bone_name = " weapon_rest_bone "
_skeleton . add_child ( _sword_rest_attach )
_spawn_sword ( )
# Instantiate a fresh sword into the holster, with its own blade collider. Called
# at spawn and again after a throw, so a matador can keep drawing replacements.
func _spawn_sword ( ) - > void :
if is_instance_valid ( _sword_node ) :
return
if _sword_rest_attach == null and _sword_hand_attach == null :
return
_sword_node = _SWORD_SCENE . instantiate ( ) as Node3D
# Start holstered (fall back to the hand if the rig only has one of the bones).
var start_attach := _sword_rest_attach if _sword_rest_attach else _sword_hand_attach
start_attach . add_child ( _sword_node )
_sword_in_hand = start_attach == _sword_hand_attach
_drawing = false
_apply_sword_grip ( )
# Blade collider spans the steel (hilt at local origin, blade running +Z out
# to ~1.4 m). Only monitors while the sword is in hand (see _carry_sword).
var blade_cap := CapsuleShape3D . new ( )
blade_cap . radius = 0.10
blade_cap . height = 1.10
var blade_cs := CollisionShape3D . new ( )
blade_cs . shape = blade_cap
blade_cs . position = Vector3 ( 0.0 , 0.0 , 0.8 )
# CapsuleShape3D runs along Y by default; tip it onto +Z to follow the blade.
blade_cs . rotation_degrees = Vector3 ( 90.0 , 0.0 , 0.0 )
_sword_blade_area = Area3D . new ( )
_sword_blade_area . collision_layer = 0
_sword_blade_area . collision_mask = 1
_sword_blade_area . monitoring = _sword_in_hand
_sword_blade_area . add_child ( blade_cs )
_sword_node . add_child ( _sword_blade_area )
_sword_blade_area . body_entered . connect ( _on_blade_hit )
# Draw the sword as the bull approaches and re-sheathe it once the bull is far
# again — so a matador wanders with the sword holstered and pulls it out to
# engage. Drawing plays the Draw_weapon clip; re-sheathing snaps back. Hysteresis
# (1.25× on the re-sheathe) avoids flicker at the threshold.
func _update_sword_carry ( ) - > void :
if not is_instance_valid ( _sword_node ) :
return
if _state == State . RAGDOLL or _bull == null :
_cancel_draw ( )
_carry_sword ( false )
return
var dist := global_position . distance_to ( _bull . global_position )
var draw_r := DP . f ( " mat_draw_range " )
if dist < = draw_r :
if not _sword_in_hand and not _drawing :
_begin_draw ( )
elif dist > draw_r * 1.25 :
_cancel_draw ( )
_carry_sword ( false )
func _begin_draw ( ) - > void :
if _anim_player and _anim_player . has_animation ( _ANIM_DRAW ) :
_drawing = true
_draw_len = _anim_length ( _ANIM_DRAW , 1.0 )
_draw_timer = 0.0
_anim_player . play ( _ANIM_DRAW )
_anim_player . seek ( 0.0 , true )
else :
_carry_sword ( true ) # no draw clip on this rig — snap to hand
func _advance_draw ( delta : float ) - > void :
if not _drawing :
return
# A combat state needs the body animations back at once — finish instantly.
if _state != State . WANDER and _state != State . FLEE :
_finalize_draw ( )
return
_draw_timer + = delta
if not _sword_in_hand and _draw_timer > = _draw_len * _DRAW_GRAB_AT :
_carry_sword ( true )
if _draw_timer > = _draw_len :
_finalize_draw ( )
func _finalize_draw ( ) - > void :
if not _drawing :
return
_drawing = false
_carry_sword ( true )
# Abort an in-progress draw without forcing the sword into the hand, so the
# caller can re-holster (the suppressed locomotion anim resumes next frame).
func _cancel_draw ( ) - > void :
_drawing = false
# Move the sword between the hand grip and the hip holster. Reparenting follows
# whichever bone the BoneAttachment tracks; the local grip transform is reapplied
# so the seating is identical in both sockets.
func _carry_sword ( in_hand : bool ) - > void :
if not is_instance_valid ( _sword_node ) :
return
if in_hand == _sword_in_hand :
return
var target := _sword_hand_attach if in_hand else _sword_rest_attach
if target == null :
return
_sword_in_hand = in_hand
_sword_node . reparent ( target , false )
_apply_sword_grip ( )
if _sword_blade_area :
_sword_blade_area . monitoring = in_hand
func _apply_sword_grip ( ) - > void :
if not is_instance_valid ( _sword_node ) :
return
_sword_node . position = Vector3 (
DP . f ( " sword_pos_x " ) , DP . f ( " sword_pos_y " ) , DP . f ( " sword_pos_z " ) )
_sword_node . rotation_degrees = Vector3 (
DP . f ( " sword_rot_x " ) , DP . f ( " sword_rot_y " ) , DP . f ( " sword_rot_z " ) )
# ── Ragdoll setup ─────────────────────────────────────────────────────────────
func _setup_physical_bones ( ) - > PhysicalBoneSimulator3D :