sword holster, health
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://matador.gd" id="1_matgd"]
|
[ext_resource type="Script" path="res://matador.gd" id="1_matgd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://ch5fiscgxfmpc" path="res://Blender/matador.glb" id="2_matfbx"]
|
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||||
radius = 0.25
|
radius = 0.25
|
||||||
|
|||||||
@@ -115,6 +115,36 @@ func _register_all() -> void:
|
|||||||
_reg_f("Matador", "mat_attack_min_dist", 2.5, 0.5, 6.0, 0.1)
|
_reg_f("Matador", "mat_attack_min_dist", 2.5, 0.5, 6.0, 0.1)
|
||||||
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05)
|
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05)
|
||||||
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
|
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
|
||||||
|
# Draw the sword from the holster once the bull closes to within this range;
|
||||||
|
# re-sheathe when it retreats well beyond it (hysteresis in _update_sword_carry).
|
||||||
|
_reg_f("Matador", "mat_draw_range", 12.0, 2.0, 40.0)
|
||||||
|
# Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts.
|
||||||
|
_reg_f("Matador", "mat_anim_blend", 0.12, 0.0, 0.5, 0.01)
|
||||||
|
# ── Sword ─────────────────────────────────────────────────────────────────
|
||||||
|
# Local seating of the sword on its weapon bones (hand grip + hip holster).
|
||||||
|
# Defaults are identity — the bones are oriented in Blender to seat the sword
|
||||||
|
# directly; these exist to fine-tune in-game without re-exporting.
|
||||||
|
_reg_f("Sword", "sword_pos_x", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "sword_pos_y", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "sword_pos_z", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "sword_rot_x", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "sword_rot_y", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "sword_rot_z", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
# ── Sword throw (matador ranged attack) ──────────────────────────────────
|
||||||
|
# Probability of committing to a throw each time the matador becomes eligible;
|
||||||
|
# kept low so they favour closing in for a melee Attack over throwing.
|
||||||
|
_reg_f("Sword", "mat_throw_chance", 0.15, 0.0, 1.0, 0.05)
|
||||||
|
# Delay before a fresh sword appears in the holster after one is thrown.
|
||||||
|
_reg_f("Sword", "mat_resword_delay", 2.0, 0.0, 10.0, 0.5)
|
||||||
|
_reg_f("Sword", "mat_throw_range", 18.0, 5.0, 40.0)
|
||||||
|
_reg_f("Sword", "mat_throw_min_dist", 5.0, 2.0, 15.0, 0.1)
|
||||||
|
_reg_f("Sword", "mat_throw_windup", 0.7, 0.2, 2.0, 0.05)
|
||||||
|
_reg_f("Sword", "mat_throw_release", 0.55, 0.1, 0.95, 0.01)
|
||||||
|
_reg_f("Sword", "mat_throw_speed", 18.0, 5.0, 60.0)
|
||||||
|
_reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0)
|
||||||
|
_reg_f("Sword", "mat_throw_arm_cock", 70.0,-180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
|
||||||
# ── Abilities ─────────────────────────────────────────────────────────────
|
# ── Abilities ─────────────────────────────────────────────────────────────
|
||||||
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
|
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "kick_range", 3.5, 0.5, 8.0, 0.1)
|
_reg_f("Abilities", "kick_range", 3.5, 0.5, 8.0, 0.1)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var _toggle_btn: Button
|
|||||||
var _spawner: Node
|
var _spawner: Node
|
||||||
var _player: Node = null
|
var _player: Node = null
|
||||||
var _cd_overlays: Array[ColorRect] = []
|
var _cd_overlays: Array[ColorRect] = []
|
||||||
|
var _health_segments: Array[ColorRect] = []
|
||||||
|
var _health_connected: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -27,6 +29,7 @@ func _ready() -> void:
|
|||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
_build_controls_panel()
|
_build_controls_panel()
|
||||||
_build_ability_bar()
|
_build_ability_bar()
|
||||||
|
_build_health_bar()
|
||||||
_find_spawner.call_deferred()
|
_find_spawner.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +39,10 @@ func _process(_delta: float) -> void:
|
|||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
_player = players[0]
|
_player = players[0]
|
||||||
return
|
return
|
||||||
|
if not _health_connected:
|
||||||
|
_health_connected = true
|
||||||
|
_player.health_changed.connect(_on_health_changed)
|
||||||
|
_on_health_changed(_player.health)
|
||||||
for i: int in _cd_overlays.size():
|
for i: int in _cd_overlays.size():
|
||||||
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
||||||
var ov := _cd_overlays[i]
|
var ov := _cd_overlays[i]
|
||||||
@@ -68,6 +75,57 @@ func _on_reset_pressed() -> void:
|
|||||||
_spawner.reset_spawn()
|
_spawner.reset_spawn()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_health_bar() -> void:
|
||||||
|
var bar := HBoxContainer.new()
|
||||||
|
bar.add_theme_constant_override("separation", 4)
|
||||||
|
bar.anchor_left = 0.5
|
||||||
|
bar.anchor_right = 0.5
|
||||||
|
bar.anchor_top = 0.0
|
||||||
|
bar.anchor_bottom = 0.0
|
||||||
|
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
bar.offset_left = -90.0
|
||||||
|
bar.offset_top = 16.0
|
||||||
|
bar.offset_bottom = 16.0
|
||||||
|
add_child(bar)
|
||||||
|
|
||||||
|
var bg_style := StyleBoxFlat.new()
|
||||||
|
bg_style.bg_color = _BG
|
||||||
|
bg_style.corner_radius_top_left = 4
|
||||||
|
bg_style.corner_radius_top_right = 4
|
||||||
|
bg_style.corner_radius_bottom_left = 4
|
||||||
|
bg_style.corner_radius_bottom_right = 4
|
||||||
|
bg_style.border_width_left = 1
|
||||||
|
bg_style.border_width_right = 1
|
||||||
|
bg_style.border_width_top = 1
|
||||||
|
bg_style.border_width_bottom = 1
|
||||||
|
bg_style.border_color = _BORDER
|
||||||
|
bg_style.content_margin_left = 6.0
|
||||||
|
bg_style.content_margin_right = 6.0
|
||||||
|
bg_style.content_margin_top = 6.0
|
||||||
|
bg_style.content_margin_bottom = 6.0
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.add_theme_stylebox_override("panel", bg_style)
|
||||||
|
bar.add_child(panel)
|
||||||
|
|
||||||
|
var inner := HBoxContainer.new()
|
||||||
|
inner.add_theme_constant_override("separation", 3)
|
||||||
|
panel.add_child(inner)
|
||||||
|
|
||||||
|
for i: int in 10:
|
||||||
|
var seg := ColorRect.new()
|
||||||
|
seg.custom_minimum_size = Vector2(14.0, 14.0)
|
||||||
|
seg.color = Color(0.72, 0.08, 0.08)
|
||||||
|
inner.add_child(seg)
|
||||||
|
_health_segments.append(seg)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_health_changed(new_health: int) -> void:
|
||||||
|
for i: int in _health_segments.size():
|
||||||
|
_health_segments[i].color = Color(0.72, 0.08, 0.08) if i < new_health \
|
||||||
|
else Color(0.20, 0.10, 0.10)
|
||||||
|
|
||||||
|
|
||||||
func _build_ability_bar() -> void:
|
func _build_ability_bar() -> void:
|
||||||
var bar := HBoxContainer.new()
|
var bar := HBoxContainer.new()
|
||||||
bar.add_theme_constant_override("separation", 10)
|
bar.add_theme_constant_override("separation", 10)
|
||||||
|
|||||||
+439
-53
@@ -6,8 +6,9 @@ extends CharacterBody3D
|
|||||||
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
||||||
# ATTACK → post-dodge or mid-range: chase and strike
|
# ATTACK → post-dodge or mid-range: chase and strike
|
||||||
# ROLL → hit by bull but roll chance fires: survive and recover
|
# 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
|
# 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
|
signal killed
|
||||||
|
|
||||||
@@ -24,27 +25,53 @@ var _step_timer: float = 0.0
|
|||||||
var _dodge_cd: float = 0.0
|
var _dodge_cd: float = 0.0
|
||||||
var _will_dodge: bool = true
|
var _will_dodge: bool = true
|
||||||
var _attack_timer: float = 0.0
|
var _attack_timer: float = 0.0
|
||||||
|
var _swing_timer: float = 0.0
|
||||||
var _roll_timer: float = 0.0
|
var _roll_timer: float = 0.0
|
||||||
var _roll_dir: Vector3 = Vector3.ZERO
|
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_dir: Vector3 = Vector3.ZERO
|
||||||
var _steer_cd: float = 0.0
|
var _steer_cd: float = 0.0
|
||||||
var _ole_player: AudioStreamPlayer = null
|
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 _death_player: AudioStreamPlayer = null
|
||||||
var _blood_burst: CPUParticles3D = null
|
var _blood_burst: CPUParticles3D = null
|
||||||
|
|
||||||
@onready var _mesh: Node3D = $matador2
|
@onready var _mesh: Node3D = $matador2
|
||||||
@onready var _hit_area: Area3D = $HitArea
|
@onready var _hit_area: Area3D = $HitArea
|
||||||
@onready var _body_col: CollisionShape3D = $CollisionShape3D
|
|
||||||
|
|
||||||
const _ANIM_RUN: StringName = &"Run"
|
const _ANIM_RUN: StringName = &"Run"
|
||||||
const _ANIM_IDLE: StringName = &"Taunt"
|
const _ANIM_IDLE: StringName = &"Taunt"
|
||||||
const _ANIM_ATTACK: StringName = &"Attack"
|
const _ANIM_ATTACK: StringName = &"Attack"
|
||||||
const _ANIM_ROLL: StringName = &"Roll"
|
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_LOOKAHEAD: float = 3.5 # longer = turns before hitting wall
|
||||||
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
|
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:
|
func _ready() -> void:
|
||||||
@@ -59,7 +86,11 @@ func _ready() -> void:
|
|||||||
if not _anim_player.has_animation(_ANIM_RUN):
|
if not _anim_player.has_animation(_ANIM_RUN):
|
||||||
push_warning("Matador: expected animations not found. Available: %s" %
|
push_warning("Matador: expected animations not found. Available: %s" %
|
||||||
str(_anim_player.get_animation_list()))
|
str(_anim_player.get_animation_list()))
|
||||||
|
_anim_player.playback_default_blend_time = DP.f("mat_anim_blend")
|
||||||
_anim_player.play(_ANIM_IDLE)
|
_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)
|
_hit_area.body_entered.connect(_on_body_entered)
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
_setup_blood_burst()
|
_setup_blood_burst()
|
||||||
@@ -75,17 +106,35 @@ func _ready() -> void:
|
|||||||
add_child(_death_player)
|
add_child(_death_player)
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _on_dp_changed(_key: String, _value: Variant) -> void:
|
||||||
if _bull == null:
|
_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")
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
_bull = players[0] as CharacterBody3D
|
_bull = players[0] as CharacterBody3D
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
_acquire_bull()
|
||||||
|
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
velocity += get_gravity() * delta
|
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:
|
if _state != State.RAGDOLL and _bull != null:
|
||||||
_update_ai_state()
|
_update_ai_state()
|
||||||
|
|
||||||
@@ -96,11 +145,26 @@ func _physics_process(delta: float) -> void:
|
|||||||
State.SIDESTEP: _tick_sidestep(delta)
|
State.SIDESTEP: _tick_sidestep(delta)
|
||||||
State.ATTACK: _tick_attack(delta)
|
State.ATTACK: _tick_attack(delta)
|
||||||
State.ROLL: _tick_roll(delta)
|
State.ROLL: _tick_roll(delta)
|
||||||
|
State.THROW: _tick_throw(delta)
|
||||||
State.RAGDOLL: _tick_ragdoll(delta)
|
State.RAGDOLL: _tick_ragdoll(delta)
|
||||||
|
|
||||||
|
_update_sword_carry()
|
||||||
|
_advance_draw(delta)
|
||||||
|
|
||||||
|
|
||||||
func _update_ai_state() -> void:
|
func _update_ai_state() -> void:
|
||||||
|
if _state == State.THROW:
|
||||||
|
return
|
||||||
var dist := global_position.distance_to(_bull.global_position)
|
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:
|
match _state:
|
||||||
State.WANDER:
|
State.WANDER:
|
||||||
if dist < DP.f("mat_flee_range"):
|
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():
|
if space.intersect_ray(params).is_empty():
|
||||||
_steer_dir = desired_dir
|
_steer_dir = desired_dir
|
||||||
return 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()
|
candidates.shuffle()
|
||||||
for angle: float in candidates:
|
for angle: float in candidates:
|
||||||
var dir := desired_dir.rotated(Vector3.UP, angle)
|
var dir := desired_dir.rotated(Vector3.UP, angle)
|
||||||
@@ -169,41 +233,62 @@ func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
|
|||||||
return desired_dir
|
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:
|
func _tick_wander(delta: float) -> void:
|
||||||
if _idle_timer > 0.0:
|
# Plant the feet while unsheathing instead of sliding through the draw clip.
|
||||||
_idle_timer -= delta
|
if _drawing or _idle_timer > 0.0:
|
||||||
velocity.x = move_toward(velocity.x, 0.0, 10.0 * delta)
|
_idle_timer = maxf(0.0, _idle_timer - delta)
|
||||||
velocity.z = move_toward(velocity.z, 0.0, 10.0 * delta)
|
_decelerate(10.0, delta)
|
||||||
_play_anim(_ANIM_IDLE)
|
_play_anim(_ANIM_IDLE)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
return
|
return
|
||||||
|
|
||||||
var dx: float = _wander_target.x - global_position.x
|
var to_target := Vector3(
|
||||||
var dz: float = _wander_target.z - global_position.z
|
_wander_target.x - global_position.x, 0.0, _wander_target.z - global_position.z)
|
||||||
if Vector2(dx, dz).length() < 0.8:
|
if to_target.length() < 0.8:
|
||||||
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
else:
|
else:
|
||||||
var dir := _steer_clear(Vector3(dx, 0.0, dz).normalized(), delta)
|
var dir := _steer_clear(to_target.normalized(), delta)
|
||||||
var spd := DP.f("mat_walk_speed")
|
_accelerate(dir, DP.f("mat_walk_speed"), delta)
|
||||||
velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
|
_face_velocity(delta, _TURN_MOVE)
|
||||||
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)
|
|
||||||
_play_anim(_ANIM_RUN)
|
_play_anim(_ANIM_RUN)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
func _tick_flee(delta: float) -> void:
|
func _tick_flee(delta: float) -> void:
|
||||||
var away := (global_position - _bull.global_position)
|
var away := global_position - _bull.global_position
|
||||||
away.y = 0.0
|
away.y = 0.0
|
||||||
if away.length() < 0.01:
|
if away.length() < 0.01:
|
||||||
away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
||||||
var dir := _steer_clear(away.normalized(), delta)
|
var dir := _steer_clear(away.normalized(), delta)
|
||||||
var spd := DP.f("mat_flee_speed")
|
_accelerate(dir, DP.f("mat_flee_speed"), delta)
|
||||||
velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
|
_face_velocity(delta, _TURN_MOVE)
|
||||||
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)
|
|
||||||
_play_anim(_ANIM_RUN)
|
_play_anim(_ANIM_RUN)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
@@ -227,13 +312,8 @@ func _start_brace() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _tick_brace(delta: float) -> void:
|
func _tick_brace(delta: float) -> void:
|
||||||
velocity.x = move_toward(velocity.x, 0.0, 18.0 * delta)
|
_decelerate(18.0, delta)
|
||||||
velocity.z = move_toward(velocity.z, 0.0, 18.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:
|
|
||||||
_mesh.rotation.y = lerp_angle(
|
|
||||||
_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 14.0)
|
|
||||||
_play_anim(_ANIM_ATTACK)
|
_play_anim(_ANIM_ATTACK)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
@@ -260,7 +340,7 @@ func _tick_sidestep(delta: float) -> void:
|
|||||||
var spd := DP.f("mat_step_speed")
|
var spd := DP.f("mat_step_speed")
|
||||||
velocity.x = _step_dir.x * spd
|
velocity.x = _step_dir.x * spd
|
||||||
velocity.z = _step_dir.z * 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)
|
_play_anim(_ANIM_ROLL)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
@@ -268,38 +348,46 @@ func _tick_sidestep(delta: float) -> void:
|
|||||||
func _start_attack() -> void:
|
func _start_attack() -> void:
|
||||||
_state = State.ATTACK
|
_state = State.ATTACK
|
||||||
_attack_timer = DP.f("mat_attack_duration")
|
_attack_timer = DP.f("mat_attack_duration")
|
||||||
|
_swing_timer = 0.0
|
||||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
|
||||||
|
|
||||||
func _tick_attack(delta: float) -> void:
|
func _tick_attack(delta: float) -> void:
|
||||||
_attack_timer -= delta
|
_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
|
to_bull.y = 0.0
|
||||||
var dist := to_bull.length()
|
var dist := to_bull.length()
|
||||||
|
|
||||||
if dist > 0.1:
|
# Commit to a full swing once in range instead of flipping Run/Attack at the
|
||||||
_mesh.rotation.y = lerp_angle(
|
# distance boundary every frame (the old foot-slide jitter). When the swing
|
||||||
_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 10.0)
|
# 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"):
|
||||||
if dist > DP.f("mat_attack_min_dist"):
|
_swing_timer = _anim_length(_ANIM_ATTACK, 0.9)
|
||||||
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)
|
|
||||||
else:
|
|
||||||
velocity.x = move_toward(velocity.x, 0.0, 14.0 * delta)
|
|
||||||
velocity.z = move_toward(velocity.z, 0.0, 14.0 * delta)
|
|
||||||
|
|
||||||
|
if _swing_timer > 0.0:
|
||||||
|
_decelerate(22.0, delta)
|
||||||
_play_anim(_ANIM_ATTACK)
|
_play_anim(_ANIM_ATTACK)
|
||||||
|
else:
|
||||||
|
var dir := _steer_clear(to_bull.normalized(), delta)
|
||||||
|
_accelerate(dir, DP.f("mat_attack_speed"), delta)
|
||||||
|
_play_anim(_ANIM_RUN)
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
func _enter_roll(hit_dir: Vector3) -> void:
|
func _enter_roll(hit_dir: Vector3) -> void:
|
||||||
_state = State.ROLL
|
_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
|
hit_dir.y = 0.0
|
||||||
var side := 1.0 if randf() > 0.5 else -1.0
|
var side := 1.0 if randf() > 0.5 else -1.0
|
||||||
_roll_dir = hit_dir.normalized().rotated(Vector3.UP, PI * 0.5 * side)
|
_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:
|
func _tick_roll(delta: float) -> void:
|
||||||
@@ -311,11 +399,143 @@ func _tick_roll(delta: float) -> void:
|
|||||||
var spd := DP.f("mat_step_speed")
|
var spd := DP.f("mat_step_speed")
|
||||||
velocity.x = _roll_dir.x * spd
|
velocity.x = _roll_dir.x * spd
|
||||||
velocity.z = _roll_dir.z * 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)
|
_play_anim(_ANIM_ROLL)
|
||||||
move_and_slide()
|
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:
|
func _tick_ragdoll(_delta: float) -> void:
|
||||||
velocity.x = 0.0
|
velocity.x = 0.0
|
||||||
velocity.z = 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:
|
func apply_ability_hit(hit_dir: Vector3, strength: float) -> void:
|
||||||
if _state == State.RAGDOLL or _state == State.ROLL:
|
if _state == State.RAGDOLL or _state == State.ROLL:
|
||||||
return
|
return
|
||||||
if _bull == null:
|
_acquire_bull()
|
||||||
var players := get_tree().get_nodes_in_group(&"player")
|
|
||||||
if not players.is_empty():
|
|
||||||
_bull = players[0] as CharacterBody3D
|
|
||||||
_enter_ragdoll(hit_dir, strength)
|
_enter_ragdoll(hit_dir, strength)
|
||||||
|
|
||||||
|
|
||||||
@@ -350,8 +567,21 @@ func _on_body_entered(body: Node3D) -> void:
|
|||||||
_enter_ragdoll(hit_dir, player.velocity.length())
|
_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:
|
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||||
_state = State.RAGDOLL
|
_state = State.RAGDOLL
|
||||||
|
if _sword_blade_area:
|
||||||
|
_sword_blade_area.monitoring = false
|
||||||
killed.emit()
|
killed.emit()
|
||||||
var cleanup_timer := get_tree().create_timer(4.0)
|
var cleanup_timer := get_tree().create_timer(4.0)
|
||||||
cleanup_timer.timeout.connect(func() -> void:
|
cleanup_timer.timeout.connect(func() -> void:
|
||||||
@@ -476,9 +706,17 @@ func _ensure_loop(anim_name: StringName) -> void:
|
|||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
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:
|
func _play_anim(anim_name: StringName) -> void:
|
||||||
if _anim_player == null:
|
if _anim_player == null:
|
||||||
return
|
return
|
||||||
|
if _drawing:
|
||||||
|
return # let the unholster clip play through uninterrupted
|
||||||
if _anim_player.current_animation == anim_name:
|
if _anim_player.current_animation == anim_name:
|
||||||
return
|
return
|
||||||
_anim_player.play(anim_name)
|
_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)
|
_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 ─────────────────────────────────────────────────────────────
|
# ── Ragdoll setup ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ var _bull_anim: AnimationPlayer = null
|
|||||||
var _charge_ready_emitter: CPUParticles3D = null
|
var _charge_ready_emitter: CPUParticles3D = null
|
||||||
var _slam_pending: bool = false
|
var _slam_pending: bool = false
|
||||||
|
|
||||||
|
const MAX_HEALTH: int = 10
|
||||||
|
var health: int = MAX_HEALTH
|
||||||
|
signal health_changed(new_health: int)
|
||||||
|
|
||||||
var _huff_player: AudioStreamPlayer = null
|
var _huff_player: AudioStreamPlayer = null
|
||||||
var _crowd_player: AudioStreamPlayer = null
|
var _crowd_player: AudioStreamPlayer = null
|
||||||
var _huff_timer: float = 0.0
|
var _huff_timer: float = 0.0
|
||||||
@@ -693,3 +697,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
_set_dust_state(DustState.NONE)
|
_set_dust_state(DustState.NONE)
|
||||||
|
|
||||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
func take_sword_hit() -> void:
|
||||||
|
health = maxi(0, health - 1)
|
||||||
|
health_changed.emit(health)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://1y07gn65ktsi" path="res://Animations/ANIM_publik_mees.glb" id="7_xlvrw"]
|
[ext_resource type="PackedScene" uid="uid://1y07gn65ktsi" path="res://Animations/ANIM_publik_mees.glb" id="7_xlvrw"]
|
||||||
[ext_resource type="Script" uid="uid://dr6phggarm73e" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"]
|
[ext_resource type="Script" uid="uid://dr6phggarm73e" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"]
|
||||||
[ext_resource type="Script" uid="uid://di0jafakftp3n" path="res://publikum_controller.gd" id="9_pubctrl"]
|
[ext_resource type="Script" uid="uid://di0jafakftp3n" path="res://publikum_controller.gd" id="9_pubctrl"]
|
||||||
[ext_resource type="PackedScene" uid="uid://xyvank512l5n" path="res://Assets/sword.glb" id="10_5juve"]
|
|
||||||
[ext_resource type="Script" uid="uid://xu6osmd62rj2" path="res://hud.gd" id="10_hud"]
|
[ext_resource type="Script" uid="uid://xu6osmd62rj2" path="res://hud.gd" id="10_hud"]
|
||||||
|
|
||||||
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"]
|
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"]
|
||||||
@@ -360,9 +359,6 @@ transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97
|
|||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=755853425]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=755853425]
|
||||||
environment = SubResource("Environment_73fnb")
|
environment = SubResource("Environment_73fnb")
|
||||||
|
|
||||||
[node name="sword2" parent="." unique_id=862505781 instance=ExtResource("10_5juve")]
|
|
||||||
transform = Transform3D(-0.88628644, 0, 0.46313745, 0, 1, 0, -0.46313745, 0, -0.88628644, 0.2825427, 2.2783415, 1.9611025)
|
|
||||||
|
|
||||||
[editable path="Player"]
|
[editable path="Player"]
|
||||||
[editable path="Player/bull"]
|
[editable path="Player/bull"]
|
||||||
[editable path="Node3D/arena_placeholder_v01"]
|
[editable path="Node3D/arena_placeholder_v01"]
|
||||||
|
|||||||
Reference in New Issue
Block a user