317 lines
9.7 KiB
GDScript
317 lines
9.7 KiB
GDScript
extends CharacterBody3D
|
|
|
|
enum State { WANDER, FLEE, DODGE, RAGDOLL }
|
|
|
|
var _state: State = State.WANDER
|
|
var _skeleton: Skeleton3D = null
|
|
var _sim: PhysicalBoneSimulator3D = null
|
|
var _anim_player: AnimationPlayer = null
|
|
var _wander_target: Vector3 = Vector3.ZERO
|
|
var _idle_timer: float = 0.0
|
|
var _bull: CharacterBody3D = null
|
|
var _dodge_dir: Vector3 = Vector3.ZERO
|
|
var _dodge_timer: float = 0.0
|
|
var _dodge_cd: float = 0.0
|
|
|
|
@onready var _mesh: Node3D = $matador_v02
|
|
@onready var _hit_area: Area3D = $HitArea
|
|
@onready var _body_col: CollisionShape3D = $CollisionShape3D
|
|
|
|
const _WALK_ANIM: StringName = &"Armature|walk"
|
|
const _IDLE_ANIM: StringName = &"Armature|idle"
|
|
|
|
|
|
func _ready() -> void:
|
|
add_to_group(&"matador")
|
|
_skeleton = _find_skeleton(_mesh)
|
|
_anim_player = _find_anim_player(_mesh)
|
|
if _skeleton:
|
|
_sim = _setup_physical_bones()
|
|
if _anim_player:
|
|
_ensure_loop(_WALK_ANIM)
|
|
if _anim_player.has_animation(_IDLE_ANIM):
|
|
_anim_player.play(_IDLE_ANIM)
|
|
else:
|
|
push_warning("Matador: idle animation not found. Available: %s" %
|
|
str(_anim_player.get_animation_list()))
|
|
_hit_area.body_entered.connect(_on_body_entered)
|
|
_pick_wander_target()
|
|
|
|
|
|
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
|
|
|
|
if not is_on_floor():
|
|
velocity += get_gravity() * delta
|
|
|
|
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
|
|
|
if _state != State.RAGDOLL and _bull != null:
|
|
_update_ai_state()
|
|
|
|
match _state:
|
|
State.WANDER: _tick_wander(delta)
|
|
State.FLEE: _tick_flee(delta)
|
|
State.DODGE: _tick_dodge(delta)
|
|
State.RAGDOLL: _tick_ragdoll(delta)
|
|
|
|
|
|
func _update_ai_state() -> void:
|
|
var dist := global_position.distance_to(_bull.global_position)
|
|
match _state:
|
|
State.WANDER:
|
|
if dist < DP.f("mat_flee_range"):
|
|
_state = State.FLEE
|
|
State.FLEE:
|
|
if dist < DP.f("mat_dodge_range") and _dodge_cd <= 0.0:
|
|
_start_dodge()
|
|
elif dist > DP.f("mat_flee_range") * 1.3:
|
|
_state = State.WANDER
|
|
_pick_wander_target()
|
|
State.DODGE:
|
|
pass
|
|
|
|
|
|
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)
|
|
_play_anim(_IDLE_ANIM)
|
|
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:
|
|
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
|
_pick_wander_target()
|
|
else:
|
|
var dir := Vector3(dx, 0.0, dz).normalized()
|
|
var spd := DP.f("mat_walk_speed")
|
|
velocity.x = dir.x * spd
|
|
velocity.z = dir.z * spd
|
|
_mesh.rotation.y = lerp_angle(
|
|
_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
|
_play_anim(_WALK_ANIM)
|
|
move_and_slide()
|
|
|
|
|
|
func _tick_flee(delta: float) -> void:
|
|
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)
|
|
away = away.normalized()
|
|
var spd := DP.f("mat_flee_speed")
|
|
velocity.x = away.x * spd
|
|
velocity.z = away.z * spd
|
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
|
_play_anim(_WALK_ANIM)
|
|
move_and_slide()
|
|
|
|
|
|
func _start_dodge() -> void:
|
|
_state = State.DODGE
|
|
var bull_vel_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
|
var bull_fwd: Vector3
|
|
if bull_vel_flat.length() > 1.0:
|
|
bull_fwd = bull_vel_flat.normalized()
|
|
else:
|
|
bull_fwd = (_bull.global_position - global_position)
|
|
bull_fwd.y = 0.0
|
|
bull_fwd = bull_fwd.normalized()
|
|
# Step sideways relative to the bull's travel direction, biased slightly backward
|
|
var side := 1.0 if randf() > 0.5 else -1.0
|
|
var perp := bull_fwd.rotated(Vector3.UP, PI * 0.5 * side)
|
|
_dodge_dir = (perp * 0.7 + (-bull_fwd) * 0.3).normalized()
|
|
_dodge_timer = DP.f("mat_dodge_duration")
|
|
|
|
|
|
func _tick_dodge(delta: float) -> void:
|
|
_dodge_timer -= delta
|
|
if _dodge_timer <= 0.0:
|
|
_state = State.FLEE
|
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
|
return
|
|
var spd := DP.f("mat_dodge_speed")
|
|
velocity.x = _dodge_dir.x * spd
|
|
velocity.z = _dodge_dir.z * spd
|
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 12.0)
|
|
_play_anim(_WALK_ANIM)
|
|
move_and_slide()
|
|
|
|
|
|
func _tick_ragdoll(_delta: float) -> void:
|
|
velocity.x = 0.0
|
|
velocity.z = 0.0
|
|
move_and_slide()
|
|
|
|
|
|
func _on_body_entered(body: Node3D) -> void:
|
|
if _state == State.RAGDOLL:
|
|
return
|
|
if not body.is_in_group(&"player"):
|
|
return
|
|
var player := body as CharacterBody3D
|
|
if player.velocity.length() < DP.f("mat_hit_threshold"):
|
|
return
|
|
var hit_dir := (global_position - player.global_position).normalized()
|
|
_enter_ragdoll(hit_dir, player.velocity.length())
|
|
|
|
|
|
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
|
_state = State.RAGDOLL
|
|
hit_dir.y = 0.0
|
|
hit_dir = hit_dir.normalized()
|
|
|
|
# Keep the CharacterBody3D stationary: physics bones simulate relative to the
|
|
# skeleton root, so sliding the root makes every bone pose diverge.
|
|
velocity = Vector3.ZERO
|
|
|
|
# Remove from collision layer so the bull passes through, but keep the
|
|
# shape enabled — move_and_slide() still needs it to detect the floor.
|
|
collision_layer = 0
|
|
|
|
if _anim_player:
|
|
_anim_player.pause() # pause keeps current frame; stop() resets to T-pose
|
|
|
|
if not _sim:
|
|
return
|
|
# Teleport each physics body to its current bone world transform, then
|
|
# enable per-bone simulation. _sim.active alone does NOT set simulate_physics;
|
|
# without it _process_modification() skips every bone and writes nothing.
|
|
for child: Node in _sim.get_children():
|
|
if not (child is PhysicalBone3D):
|
|
continue
|
|
var pb := child as PhysicalBone3D
|
|
var bone_idx := _skeleton.find_bone(pb.bone_name)
|
|
if bone_idx >= 0:
|
|
pb.global_transform = _skeleton.global_transform * _skeleton.get_bone_global_pose(bone_idx)
|
|
pb.simulate_physics = true
|
|
_sim.active = true
|
|
var strength := DP.f("mat_ragdoll_impulse")
|
|
for child: Node in _sim.get_children():
|
|
if child is PhysicalBone3D:
|
|
var scatter := Vector3(
|
|
randf_range(-0.4, 0.4),
|
|
randf_range(0.1, 0.5),
|
|
randf_range(-0.4, 0.4))
|
|
(child as PhysicalBone3D).apply_central_impulse(
|
|
(hit_dir + scatter).normalized() * strength)
|
|
|
|
|
|
# ── Animation helpers ─────────────────────────────────────────────────────────
|
|
|
|
func _ensure_loop(anim_name: StringName) -> void:
|
|
if _anim_player.has_animation(anim_name):
|
|
var anim := _anim_player.get_animation(anim_name)
|
|
anim.loop_mode = Animation.LOOP_LINEAR
|
|
|
|
|
|
func _play_anim(anim_name: StringName) -> void:
|
|
if not _anim_player:
|
|
return
|
|
if not _anim_player.has_animation(anim_name):
|
|
return
|
|
if _anim_player.current_animation != anim_name:
|
|
_anim_player.play(anim_name)
|
|
|
|
|
|
func _reset_bone_poses() -> void:
|
|
for i: int in _skeleton.get_bone_count():
|
|
_skeleton.set_bone_pose_rotation(i, Quaternion.IDENTITY)
|
|
|
|
|
|
func _pick_wander_target() -> void:
|
|
var radius := DP.f("mat_wander_radius")
|
|
var angle := randf() * TAU
|
|
var dist := randf_range(2.0, radius)
|
|
_wander_target = Vector3(cos(angle) * dist, 0.0, sin(angle) * dist)
|
|
|
|
|
|
# ── Ragdoll setup ─────────────────────────────────────────────────────────────
|
|
|
|
func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
|
for child: Node in _skeleton.get_children():
|
|
if child is PhysicalBoneSimulator3D:
|
|
(child as PhysicalBoneSimulator3D).active = false
|
|
return child as PhysicalBoneSimulator3D
|
|
|
|
var sim := PhysicalBoneSimulator3D.new()
|
|
sim.active = false
|
|
_skeleton.add_child(sim)
|
|
|
|
const BONES: Array = [
|
|
["matador", 0.18],
|
|
["COG", 0.12],
|
|
["chest", 0.13, 0.30],
|
|
["head", 0.14],
|
|
["collarbone_L", 0.06, 0.18],
|
|
["collarbone_R", 0.06, 0.18],
|
|
["arm_L", 0.07, 0.22],
|
|
["arm_R", 0.07, 0.22],
|
|
["forearm_L", 0.06, 0.22],
|
|
["forearm_R", 0.06, 0.22],
|
|
["hand_L", 0.05, 0.12],
|
|
["hand_R", 0.05, 0.12],
|
|
["leg_L", 0.10, 0.32],
|
|
["leg_R", 0.10, 0.32],
|
|
["shin_L", 0.08, 0.28],
|
|
["shin_R", 0.08, 0.28],
|
|
["foot_L", 0.07, 0.18],
|
|
["foot_R", 0.07, 0.18],
|
|
]
|
|
|
|
for entry: Array in BONES:
|
|
var bname: String = entry[0]
|
|
if _skeleton.find_bone(bname) == -1:
|
|
continue
|
|
var pb := PhysicalBone3D.new()
|
|
pb.bone_name = bname
|
|
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
|
pb.linear_damp = 3.0
|
|
pb.angular_damp = 4.0
|
|
pb.collision_layer = 1
|
|
pb.collision_mask = 1
|
|
var cs := CollisionShape3D.new()
|
|
var use_capsule: bool = entry.size() >= 3
|
|
if use_capsule:
|
|
var cap := CapsuleShape3D.new()
|
|
cap.radius = entry[1]
|
|
cap.height = entry[2]
|
|
cs.shape = cap
|
|
else:
|
|
var sph := SphereShape3D.new()
|
|
sph.radius = entry[1]
|
|
cs.shape = sph
|
|
pb.add_child(cs)
|
|
sim.add_child(pb)
|
|
|
|
return sim
|
|
|
|
|
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
|
|
|
func _find_skeleton(node: Node) -> Skeleton3D:
|
|
if node is Skeleton3D:
|
|
return node as Skeleton3D
|
|
for child: Node in node.get_children():
|
|
var r := _find_skeleton(child)
|
|
if r:
|
|
return r
|
|
return null
|
|
|
|
|
|
func _find_anim_player(node: Node) -> AnimationPlayer:
|
|
if node is AnimationPlayer:
|
|
return node as AnimationPlayer
|
|
for child: Node in node.get_children():
|
|
var r := _find_anim_player(child)
|
|
if r:
|
|
return r
|
|
return null
|