Release 0.2.0: device-aware controls, spear, settings, shaders
Show touch controls only on touch platforms and keyboard hints only on desktop, via a shared Controls.use_touch_ui() gate (is_touchscreen_available is unreliable with emulate_touch_from_mouse). Bumps version to 0.2.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+185
-124
@@ -22,6 +22,7 @@ var _sim: PhysicalBoneSimulator3D = null
|
||||
var _anim_player: AnimationPlayer = null
|
||||
var _wander_target: Vector3 = Vector3.ZERO
|
||||
var _idle_timer: float = 0.0
|
||||
var _taunt_swap: float = 0.0
|
||||
var _idle_anim: StringName = _ANIM_IDLE
|
||||
var _bull: CharacterBody3D = null
|
||||
var _step_dir: Vector3 = Vector3.ZERO
|
||||
@@ -50,8 +51,8 @@ var _drawing: bool = false
|
||||
var _sheathing: 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 _spear_node: Node3D = null
|
||||
var _blade_hit_cd: float = 0.0
|
||||
var _death_player: AudioStreamPlayer = null
|
||||
var _blood_burst: CPUParticles3D = null
|
||||
@@ -74,6 +75,9 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
|
||||
const _DRAW_GRAB_AT: float = 0.55
|
||||
|
||||
const _SWORD_SCENE: PackedScene = preload("res://Assets/sword.glb")
|
||||
const _SPEAR_SCENE: PackedScene = preload("res://Assets/spear.glb")
|
||||
|
||||
const _GRAVITY: float = 9.8 # arena gravity, for the spear's ballistic arc
|
||||
|
||||
const _STEER_LOOKAHEAD: float = 3.5 # longer = turns before hitting wall
|
||||
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
|
||||
@@ -95,8 +99,9 @@ func _ready() -> void:
|
||||
if _skeleton:
|
||||
_sim = MatadorRagdoll.build(_skeleton)
|
||||
if _anim_player:
|
||||
for anim in [_ANIM_RUN, _ANIM_ATTACK, _ANIM_ROLL] + _ANIM_TAUNTS:
|
||||
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
|
||||
_ensure_loop(anim)
|
||||
_flatten_roll_travel()
|
||||
if not _anim_player.has_animation(_ANIM_RUN):
|
||||
push_warning("Matador: expected animations not found. Available: %s" %
|
||||
str(_anim_player.get_animation_list()))
|
||||
@@ -104,7 +109,7 @@ func _ready() -> void:
|
||||
_anim_player.play(_ANIM_IDLE)
|
||||
_setup_sword()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
_throw_aim_cd = randf_range(0.3, 1.0)
|
||||
_throw_aim_cd = randf_range(2.0, 5.0)
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_pick_wander_target()
|
||||
_blood_burst = preload("res://blood_burst.gd").new()
|
||||
@@ -148,12 +153,6 @@ func _physics_process(delta: float) -> void:
|
||||
_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(delta)
|
||||
|
||||
@@ -216,20 +215,30 @@ func _choose_intent() -> Intent:
|
||||
var throw_ok := _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \
|
||||
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range")
|
||||
|
||||
# Only the nearest few matadors work the bull at once; the rest hang back and
|
||||
# taunt (see _engagement_rank). This turns a crowd into a bullring — matadors
|
||||
# spreading out and taking turns — instead of a swarm all piling on together.
|
||||
var is_engager := _engagement_rank() < maxi(1, int(DP.f("mat_engage_slots")))
|
||||
# Supporting matadors strut and circle constantly; the lead only struts between
|
||||
# passes, once the bull is back out past horn reach.
|
||||
var strut := 0.65 if not is_engager else 0.3 * beyond
|
||||
var engage_mult := 1.0 if is_engager else 0.15
|
||||
|
||||
var score := {
|
||||
# Only drift when the bull is out past throw range; up close this is ~0.
|
||||
Intent.WANDER: DP.f("mat_w_showmanship") * 0.3 * beyond,
|
||||
# Showmanship: circle the bull at a taunting distance and pose for the crowd.
|
||||
Intent.WANDER: DP.f("mat_w_showmanship") * strut,
|
||||
# Retreat only when the bull is close AND actually dangerous.
|
||||
Intent.FLEE: DP.f("mat_w_flee") * close * (0.25 + 0.75 * danger),
|
||||
# Press the attack; ease off a touch when a charge is barrelling in.
|
||||
# Press the attack; ease off a touch when a charge is barrelling in. Supporting
|
||||
# matadors barely press — they yield the pass to whoever's engaging.
|
||||
Intent.ATTACK: DP.f("mat_w_aggression") * (0.45 + 0.55 * close) \
|
||||
* (1.0 - 0.55 * danger) * dodge_ready,
|
||||
* (1.0 - 0.55 * danger) * dodge_ready * engage_mult,
|
||||
# Sidestep / plant against an incoming charge.
|
||||
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
|
||||
# Hurl the blade whenever there's a clean lane at range — the ranged threat
|
||||
# that punishes a bull for hanging back out of horn reach.
|
||||
Intent.THROW: ((DP.f("mat_w_showmanship") * 0.55 + DP.f("mat_w_aggression") * 0.6) \
|
||||
* (0.35 + 0.65 * (1.0 - close))) if throw_ok else -1.0,
|
||||
# Hurl the blade — an occasional ranged flourish from an engaging matador when
|
||||
# there's a clean lane, not a barrage (see mat_w_throw). Supporters never pelt.
|
||||
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
|
||||
if (throw_ok and is_engager) else -1.0,
|
||||
}
|
||||
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
|
||||
|
||||
@@ -277,6 +286,40 @@ func _enter_intent(intent: Intent) -> void:
|
||||
_start_throw()
|
||||
|
||||
|
||||
# How many other live matadors are closer to the bull than this one. 0 = the
|
||||
# closest (the natural lead engager); the utility brain gives the nearest
|
||||
# mat_engage_slots the go-ahead to press while the rest spread out and taunt.
|
||||
func _engagement_rank() -> int:
|
||||
if _bull == null:
|
||||
return 0
|
||||
var my_d := global_position.distance_squared_to(_bull.global_position)
|
||||
var rank := 0
|
||||
for node: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
if node == self:
|
||||
continue
|
||||
var other := node as Node3D
|
||||
if other == null or not is_instance_valid(other) or not other.call(&"is_active"):
|
||||
continue
|
||||
if other.global_position.distance_squared_to(_bull.global_position) < my_d:
|
||||
rank += 1
|
||||
return rank
|
||||
|
||||
|
||||
# Live and vying for the bull's attention — a ragdolled corpse doesn't hold a slot.
|
||||
func is_active() -> bool:
|
||||
return _state != State.RAGDOLL
|
||||
|
||||
|
||||
# True when this matador is safely out of the action — not one of the engaging few,
|
||||
# and with the bull far enough off to strike a long, unhurried taunt for the crowd.
|
||||
func _out_of_harm() -> bool:
|
||||
if _bull == null:
|
||||
return true
|
||||
if _engagement_rank() < maxi(1, int(DP.f("mat_engage_slots"))):
|
||||
return false
|
||||
return global_position.distance_to(_bull.global_position) >= DP.f("mat_taunt_ring") * 0.6
|
||||
|
||||
|
||||
# Bull is moving fast and aimed within ~30° of the matador
|
||||
func _is_charge_incoming() -> bool:
|
||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||
@@ -351,6 +394,14 @@ func _tick_wander(delta: float) -> void:
|
||||
if _drawing or _sheathing or _idle_timer > 0.0:
|
||||
_idle_timer = maxf(0.0, _idle_timer - delta)
|
||||
_decelerate(10.0, delta)
|
||||
# Taunt toward the bull so the posturing plays to it, not off into space.
|
||||
if _bull != null and _idle_timer > 0.0:
|
||||
_face_point(_bull.global_position, delta, _TURN_MOVE)
|
||||
# Cycle gestures through a long hold so a posing matador stays lively.
|
||||
_taunt_swap -= delta
|
||||
if _taunt_swap <= 0.0 and not _drawing and not _sheathing:
|
||||
_idle_anim = _pick_taunt()
|
||||
_taunt_swap = randf_range(1.5, 3.0)
|
||||
_play_anim(_idle_anim)
|
||||
move_and_slide()
|
||||
return
|
||||
@@ -358,8 +409,12 @@ func _tick_wander(delta: float) -> void:
|
||||
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"))
|
||||
# A matador out of harm's way settles into a long taunt for the crowd; one still
|
||||
# in the mix only pauses briefly between passes.
|
||||
_idle_timer = randf_range(DP.f("mat_taunt_hold") * 0.7, DP.f("mat_taunt_hold") * 1.3) \
|
||||
if _out_of_harm() else randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
||||
_idle_anim = _pick_taunt()
|
||||
_taunt_swap = randf_range(1.5, 3.0)
|
||||
_pick_wander_target()
|
||||
else:
|
||||
var dir := _steer_clear(to_target.normalized(), delta)
|
||||
@@ -528,7 +583,8 @@ func _start_throw() -> void:
|
||||
_throw_timer = DP.f("mat_throw_windup")
|
||||
velocity = Vector3.ZERO
|
||||
_cancel_transitions()
|
||||
_carry_sword(true) # blade must be in hand for the wind-up and release
|
||||
_carry_sword(false) # the sword is melee-only — holster it for the throw
|
||||
_spawn_spear_in_hand() # a fresh spear is drawn for the overhand throw
|
||||
if _anim_player:
|
||||
_anim_player.pause()
|
||||
var to_bull := _bull.global_position - global_position
|
||||
@@ -552,8 +608,8 @@ func _tick_throw(delta: float) -> void:
|
||||
_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 prev < t_rel and phase >= t_rel and is_instance_valid(_spear_node):
|
||||
_release_spear()
|
||||
|
||||
if _throw_timer <= 0.0:
|
||||
_end_throw()
|
||||
@@ -562,20 +618,18 @@ func _tick_throw(delta: float) -> void:
|
||||
|
||||
func _end_throw() -> void:
|
||||
_state = State.FLEE
|
||||
_clear_spear() # drop any spear still in hand if the wind-up was cut short
|
||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||
# Space throws out so the utility brain doesn't immediately vote another one,
|
||||
# but keep them frequent enough to pressure a bull that hangs back at range.
|
||||
_throw_aim_cd = randf_range(1.6, 3.2)
|
||||
# Space throws well apart so a hurl stays a flourish, not a barrage.
|
||||
_throw_aim_cd = randf_range(4.0, 7.0)
|
||||
_intent = Intent.FLEE
|
||||
# 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).
|
||||
# Blend to locomotion so the manually-posed throw arm eases out (resume() snaps).
|
||||
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.
|
||||
# AnimationPlayer is paused. Swing is on bone-local X (see CLAUDE.md), DP-tunable.
|
||||
func _pose_throw_arm(phase: float) -> void:
|
||||
if not _skeleton:
|
||||
return
|
||||
@@ -605,82 +659,59 @@ func _set_bone_swing(bone: String, deg: float) -> void:
|
||||
_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):
|
||||
# Draw a fresh spear into the throwing hand for the wind-up (no melee collider).
|
||||
func _spawn_spear_in_hand() -> void:
|
||||
if is_instance_valid(_spear_node) or _sword_hand_attach == null:
|
||||
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
|
||||
_spear_node = _SPEAR_SCENE.instantiate() as Node3D
|
||||
_sword_hand_attach.add_child(_spear_node)
|
||||
_spear_node.position = Vector3(
|
||||
DP.f("spear_pos_x"), DP.f("spear_pos_y"), DP.f("spear_pos_z"))
|
||||
_spear_node.rotation_degrees = Vector3(
|
||||
DP.f("spear_rot_x"), DP.f("spear_rot_y"), DP.f("spear_rot_z"))
|
||||
|
||||
var rb := RigidBody3D.new()
|
||||
rb.collision_layer = 0
|
||||
rb.collision_mask = 1
|
||||
rb.contact_monitor = true
|
||||
rb.max_contacts_reported = 6
|
||||
# Fly nearly straight to the aim point instead of lobbing — the old horizontal
|
||||
# throw sailed clean over the bull's low body. A small gravity_scale keeps a
|
||||
# touch of drop for feel without dropping short.
|
||||
rb.gravity_scale = DP.f("mat_throw_gravity")
|
||||
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. Fattened so a fast throw
|
||||
# reliably overlaps the bull's collision spheres instead of tunnelling past.
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = 0.14
|
||||
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)
|
||||
func _clear_spear() -> void:
|
||||
if is_instance_valid(_spear_node):
|
||||
_spear_node.queue_free()
|
||||
_spear_node = null
|
||||
|
||||
# Aim at the bull's BODY (its collision spheres ride low, ~0.5 m below the
|
||||
# origin), leading a moving target so it arrives where the bull will be. The aim
|
||||
# keeps its vertical component so the blade drives into the body, not over it.
|
||||
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
|
||||
var dir := (_throw_dir + Vector3.UP * 0.0)
|
||||
|
||||
# Hurl the held spear on a ballistic arc that lands on the bull's low body. The
|
||||
# launch velocity is solved so gravity (mat_throw_gravity) carries it down onto the
|
||||
# lead point; SpearProjectile aims the shaft along the arc and embeds it on a hit.
|
||||
func _release_spear() -> void:
|
||||
if not is_instance_valid(_spear_node):
|
||||
return
|
||||
var spear := _spear_node
|
||||
var origin := spear.global_position
|
||||
_spear_node = null
|
||||
|
||||
var hs := maxf(DP.f("mat_throw_speed"), 0.1) # horizontal speed
|
||||
var g := _GRAVITY * maxf(DP.f("mat_throw_gravity"), 0.05)
|
||||
# Aim low (the bull's collision spheres ride ~0.5 m below its origin) and lead it
|
||||
# across the arc's flight time.
|
||||
var target := origin + _throw_dir * 5.0
|
||||
if is_instance_valid(_bull):
|
||||
var target := _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
|
||||
var flat := target - gx.origin
|
||||
flat.y = 0.0
|
||||
var flight := flat.length() / speed
|
||||
target += _bull.velocity * flight
|
||||
var to_target := target - gx.origin
|
||||
if to_target.length() > 0.1:
|
||||
dir = to_target.normalized()
|
||||
# A little aim scatter so throws are a threat to READ and dodge, not a hitscan —
|
||||
# a bull that keeps moving can slip them, a stationary one gets pinned.
|
||||
target = _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
|
||||
var lead := target - origin
|
||||
lead.y = 0.0
|
||||
target += _bull.velocity * (lead.length() / hs)
|
||||
var flat := target - origin
|
||||
var rise := flat.y
|
||||
flat.y = 0.0
|
||||
var dist := flat.length()
|
||||
var hdir := flat.normalized() if dist > 0.01 else _throw_dir
|
||||
# Aim scatter so the arc can be read and sidestepped, not a hitscan.
|
||||
var spread := deg_to_rad(DP.f("mat_throw_spread"))
|
||||
dir = dir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
|
||||
rb.linear_velocity = dir * 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", "thrown")
|
||||
)
|
||||
|
||||
var cleanup := get_tree().create_timer(6.0)
|
||||
cleanup.timeout.connect(func() -> void:
|
||||
if is_instance_valid(rb):
|
||||
rb.queue_free()
|
||||
)
|
||||
hdir = hdir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
|
||||
# Ballistic solve: with horizontal speed hs over distance dist, the flight lasts t;
|
||||
# pick the vertical launch speed that lands the arc on the aim point's height.
|
||||
var t := dist / hs
|
||||
var vy := (rise + 0.5 * g * t * t) / t
|
||||
var vel := hdir * hs + Vector3.UP * vy
|
||||
SpearProjectile.launch(get_tree().current_scene, spear, origin, vel,
|
||||
maxf(DP.f("mat_throw_gravity"), 0.05))
|
||||
|
||||
|
||||
func _tick_ragdoll(_delta: float) -> void:
|
||||
@@ -756,6 +787,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0)
|
||||
_state = State.RAGDOLL
|
||||
if _sword_blade_area:
|
||||
_sword_blade_area.monitoring = false
|
||||
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
|
||||
killed.emit()
|
||||
var cleanup_timer := get_tree().create_timer(4.0)
|
||||
cleanup_timer.timeout.connect(func() -> void:
|
||||
@@ -866,6 +898,32 @@ func _ensure_loop(anim_name: StringName) -> void:
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
|
||||
# Roll was authored *travelling* (COG drives ~4.5 m forward on Z), which snapped the
|
||||
# mesh back when the clip ended. _tick_roll moves the dodge, so strip that COG travel.
|
||||
func _flatten_roll_travel() -> void:
|
||||
if _anim_player == null or not _anim_player.has_animation(_ANIM_ROLL):
|
||||
return
|
||||
var anim := _anim_player.get_animation(_ANIM_ROLL)
|
||||
if anim.has_meta(&"_travel_flattened"):
|
||||
return
|
||||
anim.set_meta(&"_travel_flattened", true)
|
||||
var cog := anim.find_track(NodePath("Armature/Skeleton3D:COG"), Animation.TYPE_POSITION_3D)
|
||||
if cog < 0:
|
||||
return
|
||||
# Snapshot the COG travel before mutating (flattening COG first would zero it).
|
||||
var travel: Dictionary = {}
|
||||
for i in anim.get_track_count():
|
||||
if anim.track_get_type(i) == Animation.TYPE_POSITION_3D:
|
||||
for k in anim.track_get_key_count(i):
|
||||
var t := anim.track_get_key_time(i, k)
|
||||
travel[t] = anim.position_track_interpolate(cog, t).z
|
||||
for i in anim.get_track_count():
|
||||
if anim.track_get_type(i) == Animation.TYPE_POSITION_3D:
|
||||
for k in anim.track_get_key_count(i):
|
||||
var v: Vector3 = anim.track_get_key_value(i, k)
|
||||
anim.track_set_key_value(i, k, v - Vector3(0.0, 0.0, travel[anim.track_get_key_time(i, k)]))
|
||||
|
||||
|
||||
func _pick_taunt() -> StringName:
|
||||
var choices: Array[StringName] = []
|
||||
for anim in _ANIM_TAUNTS:
|
||||
@@ -893,13 +951,25 @@ func _play_anim(anim_name: StringName) -> void:
|
||||
|
||||
|
||||
func _pick_wander_target() -> void:
|
||||
# 35% chance: pick a point orbiting close to the bull for a daring pass
|
||||
if _bull != null and randf() < 0.35:
|
||||
var angle := randf() * TAU
|
||||
var r := randf_range(1.5, 4.0)
|
||||
# Circle the bull for showmanship: mostly settle on the taunt ring (spreading the
|
||||
# crowd out around it), with an occasional close pass for daring flair.
|
||||
if _bull != null:
|
||||
var bull_flat := _bull.global_position
|
||||
bull_flat.y = 0.0
|
||||
var r: float
|
||||
if randf() < 0.2:
|
||||
r = randf_range(1.5, 4.0) # close daring pass
|
||||
else:
|
||||
var ring := DP.f("mat_taunt_ring")
|
||||
r = randf_range(ring * 0.7, ring * 1.3) # the showmanship ring
|
||||
var angle := randf() * TAU
|
||||
_wander_target = bull_flat + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
|
||||
# Keep the pose inside the fighting floor rather than backing onto the benches.
|
||||
var arena_r := DP.f("arena_spawn_radius")
|
||||
var flat := Vector2(_wander_target.x, _wander_target.z)
|
||||
if flat.length() > arena_r:
|
||||
flat = flat.normalized() * arena_r
|
||||
_wander_target = Vector3(flat.x, 0.0, flat.y)
|
||||
return
|
||||
var radius := DP.f("mat_wander_radius")
|
||||
var angle := randf() * TAU
|
||||
@@ -909,13 +979,10 @@ func _pick_wander_target() -> void:
|
||||
|
||||
# ── 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). Each bone has its own
|
||||
# DP-tunable local offset/rotation (sword_* for the hand, sword_rest_* for the
|
||||
# holster) so the two poses can be seated independently without re-exporting.
|
||||
# The rig (matador_v03) carries two purpose-built bones: weapon_bone (right-hand
|
||||
# grip) and weapon_rest_bone (hip holster). The sword rides in the holster and is
|
||||
# carried to the hand in combat (see _carry_sword); each bone has its own DP-tunable
|
||||
# local offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
||||
func _setup_sword() -> void:
|
||||
if not _skeleton:
|
||||
return
|
||||
@@ -934,8 +1001,7 @@ func _setup_sword() -> void:
|
||||
_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.
|
||||
# Instantiate the matador's sword into the holster, with its own blade collider.
|
||||
func _spawn_sword() -> void:
|
||||
if is_instance_valid(_sword_node):
|
||||
return
|
||||
@@ -949,15 +1015,14 @@ func _spawn_sword() -> void:
|
||||
_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).
|
||||
# Blade collider along +Z (hilt at origin), tipped from the capsule's default Y.
|
||||
# Only monitors while the sword is in hand (see _carry_sword).
|
||||
var blade_cap := CapsuleShape3D.new()
|
||||
blade_cap.radius = 0.18
|
||||
blade_cap.height = 1.30
|
||||
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()
|
||||
@@ -969,12 +1034,11 @@ func _spawn_sword() -> void:
|
||||
_sword_blade_area.body_entered.connect(_on_blade_hit)
|
||||
|
||||
|
||||
# The sword is carried by ACTION, not distance: drawn only while striking
|
||||
# (ATTACK / THROW) and holstered while running, dodging, bracing or taunting.
|
||||
# THROW snaps the blade out (its arm cock-back covers the motion); everything
|
||||
# else animates with the Draw_weapon clip — forward to draw, reversed to sheathe.
|
||||
# The sword is MELEE-only: drawn while fighting up close (ATTACK) or guarding
|
||||
# (BRACE / SIDESTEP), holstered otherwise. Ranged throws use a spear instead
|
||||
# (see _spawn_spear_in_hand), so THROW keeps the sword holstered.
|
||||
func _wants_sword_drawn() -> bool:
|
||||
return _state == State.ATTACK or _state == State.THROW \
|
||||
return _state == State.ATTACK \
|
||||
or _state == State.BRACE or _state == State.SIDESTEP
|
||||
|
||||
|
||||
@@ -1019,8 +1083,7 @@ func _begin_sheathe() -> void:
|
||||
_anim_player.play(_ANIM_DRAW, -1.0, -_draw_speed(), true)
|
||||
|
||||
|
||||
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored for
|
||||
# the reverse sheathe), and the transition ends when the clip's runtime elapses.
|
||||
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored on the reverse).
|
||||
func _advance_draw(delta: float) -> void:
|
||||
if not _drawing and not _sheathing:
|
||||
return
|
||||
@@ -1049,9 +1112,7 @@ func _draw_speed() -> float:
|
||||
return maxf(DP.f("mat_draw_speed"), 0.1)
|
||||
|
||||
|
||||
# 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.
|
||||
# Move the sword between hand grip and hip holster, reapplying the local seating.
|
||||
func _carry_sword(in_hand: bool) -> void:
|
||||
if not is_instance_valid(_sword_node):
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user