Bull roll ability, console screen, tests

This commit is contained in:
2026-07-28 23:10:38 +03:00
parent a0b248748c
commit ebb6ec6335
16 changed files with 1494 additions and 283 deletions
+60 -11
View File
@@ -18,6 +18,7 @@ var _sim: PhysicalBoneSimulator3D = null
var _anim_player: AnimationPlayer = null
var _wander_target: Vector3 = Vector3.ZERO
var _idle_timer: float = 0.0
var _idle_anim: StringName = _ANIM_IDLE
var _bull: CharacterBody3D = null
var _step_dir: Vector3 = Vector3.ZERO
var _brace_timer: float = 0.0
@@ -26,6 +27,7 @@ var _dodge_cd: float = 0.0
var _will_dodge: bool = true
var _attack_timer: float = 0.0
var _swing_timer: float = 0.0
var _lunge_timer: float = 0.0
var _roll_timer: float = 0.0
var _roll_dir: Vector3 = Vector3.ZERO
var _throw_timer: float = 0.0
@@ -57,6 +59,10 @@ const _ANIM_ATTACK: StringName = &"Attack"
const _ANIM_ROLL: StringName = &"Roll"
const _ANIM_DRAW: StringName = &"Draw_weapon"
# Idle taunt clips — one is picked at random each time the matador pauses to
# taunt while wandering, so the crowd doesn't see the same gesture every time.
const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
# 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
@@ -82,7 +88,7 @@ func _ready() -> void:
if _skeleton:
_sim = MatadorRagdoll.build(_skeleton)
if _anim_player:
for anim in [_ANIM_RUN, _ANIM_IDLE, _ANIM_ATTACK, _ANIM_ROLL]:
for anim in [_ANIM_RUN, _ANIM_ATTACK, _ANIM_ROLL] + _ANIM_TAUNTS:
_ensure_loop(anim)
if not _anim_player.has_animation(_ANIM_RUN):
push_warning("Matador: expected animations not found. Available: %s" %
@@ -125,6 +131,10 @@ func _acquire_bull() -> void:
func _physics_process(delta: float) -> void:
_acquire_bull()
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
var o := global_position + Vector3.UP * 0.6
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
if not is_on_floor():
velocity += get_gravity() * delta
@@ -266,7 +276,7 @@ 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)
_play_anim(_ANIM_IDLE)
_play_anim(_idle_anim)
move_and_slide()
return
@@ -274,6 +284,7 @@ func _tick_wander(delta: float) -> void:
_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"))
_idle_anim = _pick_taunt()
_pick_wander_target()
else:
var dir := _steer_clear(to_target.normalized(), delta)
@@ -340,17 +351,23 @@ func _tick_brace(delta: float) -> void:
_dodge_cd = DP.f("mat_dodge_cooldown")
# Pase phase: sharp lateral step, then immediately exploit with an attack
# Pase phase: sharp lateral step biased toward the bull so the drawn blade sweeps
# across its path as it charges by; face the bull so the sword points at it.
func _tick_sidestep(delta: float) -> void:
_step_timer -= delta
if _step_timer <= 0.0:
_start_attack()
return
var dir := _step_dir
var to_bull := _bull.global_position - global_position
to_bull.y = 0.0
if to_bull.length() > 0.1:
dir = (_step_dir + to_bull.normalized() * DP.f("mat_pass_lunge")).normalized()
var spd := DP.f("mat_step_speed")
velocity.x = _step_dir.x * spd
velocity.z = _step_dir.z * spd
_face_velocity(delta, _TURN_SHARP)
_play_anim(_ANIM_ROLL)
velocity.x = dir.x * spd
velocity.z = dir.z * spd
_face_point(_bull.global_position, delta, _TURN_SHARP)
_play_anim(_ANIM_ATTACK)
move_and_slide()
@@ -380,9 +397,14 @@ func _tick_attack(delta: float) -> void:
# 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") and _sword_in_hand:
_swing_timer = _anim_length(_ANIM_ATTACK, 0.9)
_lunge_timer = DP.f("mat_lunge_time")
if _swing_timer > 0.0:
_decelerate(22.0, delta)
_lunge_timer = maxf(0.0, _lunge_timer - delta)
if _lunge_timer > 0.0 and dist > 0.1:
_accelerate(to_bull.normalized(), DP.f("mat_lunge_speed"), delta)
else:
_decelerate(22.0, delta)
_play_anim(_ANIM_ATTACK)
else:
var dir := _steer_clear(to_bull.normalized(), delta)
@@ -533,7 +555,18 @@ func _release_sword() -> void:
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
rb.add_child(cs)
var dir := (_throw_dir + Vector3.UP * 0.12).normalized()
# Lead the target: aim where the bull will be when the sword arrives, so a
# moving bull isn't simply behind the throw by the time it lands.
var aim := _throw_dir
if is_instance_valid(_bull):
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
var flat := _bull.global_position - gx.origin
flat.y = 0.0
var lead := (_bull.global_position + _bull.velocity * (flat.length() / speed)) - gx.origin
lead.y = 0.0
if lead.length() > 0.1:
aim = lead.normalized()
var dir := (aim + 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")
@@ -559,6 +592,11 @@ func _tick_ragdoll(_delta: float) -> void:
move_and_slide()
# Current state as its enum name (WANDER / FLEE / …), for the debug state overlay.
func ai_state_name() -> String:
return State.keys()[_state]
func apply_ability_hit(hit_dir: Vector3, strength: float, up_boost: float = 0.0) -> void:
if _state == State.RAGDOLL or _state == State.ROLL:
return
@@ -585,7 +623,7 @@ func _on_body_entered(body: Node3D) -> void:
func _on_blade_hit(body: Node3D) -> void:
if _state == State.RAGDOLL or _state != State.ATTACK:
if _state != State.ATTACK and _state != State.SIDESTEP:
return
if not body.is_in_group(&"player"):
return
@@ -709,6 +747,16 @@ func _ensure_loop(anim_name: StringName) -> void:
anim.loop_mode = Animation.LOOP_LINEAR
func _pick_taunt() -> StringName:
var choices: Array[StringName] = []
for anim in _ANIM_TAUNTS:
if _anim_player != null and _anim_player.has_animation(anim):
choices.append(anim)
if choices.is_empty():
return _ANIM_IDLE
return choices[randi() % choices.size()]
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
@@ -807,7 +855,8 @@ func _spawn_sword() -> void:
# 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.
func _wants_sword_drawn() -> bool:
return _state == State.ATTACK or _state == State.THROW
return _state == State.ATTACK or _state == State.THROW \
or _state == State.BRACE or _state == State.SIDESTEP
func _update_sword_carry() -> void: