cooldowns, sword holstering, ability colors
This commit is contained in:
@@ -2,3 +2,4 @@ disable:
|
||||
- class-definitions-order
|
||||
- no-else-return
|
||||
max-line-length: 120
|
||||
max-file-lines: 1200
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
extends CPUParticles3D
|
||||
## One-shot blood spray. Add as a child of whatever bleeds, then call burst().
|
||||
|
||||
func _ready() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 0.07
|
||||
sphere.height = 0.14
|
||||
sphere.radial_segments = 4
|
||||
sphere.rings = 2
|
||||
sphere.material = mat
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
|
||||
ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(0.6, 0.5))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
mesh = sphere
|
||||
color_ramp = ramp
|
||||
scale_amount_curve = scale_curve
|
||||
one_shot = true
|
||||
explosiveness = 0.9
|
||||
amount = 25
|
||||
lifetime = 1.1
|
||||
randomness = 0.4
|
||||
local_coords = false
|
||||
spread = 80.0
|
||||
gravity = Vector3(0.0, -7.0, 0.0)
|
||||
initial_velocity_min = 4.0
|
||||
initial_velocity_max = 11.0
|
||||
scale_amount_min = 0.5
|
||||
scale_amount_max = 1.6
|
||||
emitting = false
|
||||
|
||||
|
||||
# Fire a burst at a world position, sprayed along dir (biased upward).
|
||||
func burst(at: Vector3, dir: Vector3) -> void:
|
||||
global_position = at
|
||||
direction = (dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||
restart()
|
||||
+19
-5
@@ -115,14 +115,14 @@ func _register_all() -> void:
|
||||
_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_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)
|
||||
# Playback rate of the Draw_weapon clip used for both drawing and sheathing —
|
||||
# higher = snappier unholster/holster.
|
||||
_reg_f("Matador", "mat_draw_speed", 1.25, 0.25, 3.0, 0.05)
|
||||
# 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
|
||||
# Local seating of the sword in the right hand (drawn / fighting).
|
||||
# Defaults are identity — the bone is 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)
|
||||
@@ -130,6 +130,15 @@ func _register_all() -> void:
|
||||
_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)
|
||||
# Local seating of the sword in the hip holster (sheathed / wandering).
|
||||
# Independent of the hand grip so each pose can be tuned without affecting the
|
||||
# other. Defaults angle the blade down along the matador's side.
|
||||
_reg_f("Sword", "sword_rest_pos_x", 0.0, -1.0, 1.0, 0.01)
|
||||
_reg_f("Sword", "sword_rest_pos_y", 0.0, -1.0, 1.0, 0.01)
|
||||
_reg_f("Sword", "sword_rest_pos_z", 0.0, -1.0, 1.0, 0.01)
|
||||
_reg_f("Sword", "sword_rest_rot_x", -120.0, -180.0, 180.0, 1.0)
|
||||
_reg_f("Sword", "sword_rest_rot_y", 0.0, -180.0, 180.0, 1.0)
|
||||
_reg_f("Sword", "sword_rest_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.
|
||||
@@ -151,6 +160,11 @@ func _register_all() -> void:
|
||||
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
|
||||
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
|
||||
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
|
||||
# Descent gravity multiplier — higher = faster crash, less airtime.
|
||||
_reg_f("Abilities", "slam_fall_mult", 3.0, 1.0, 8.0, 0.5)
|
||||
# Upward launch velocity (m/s) given to a slammed matador's ragdoll — pops them
|
||||
# skyward. ~7 ≈ a 2.5 m hop; 0 disables the launch.
|
||||
_reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5)
|
||||
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
|
||||
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
|
||||
_reg_f("Abilities", "dash_cooldown", 2.0, 0.5, 10.0, 0.1)
|
||||
|
||||
@@ -20,6 +20,7 @@ var _toggle_btn: Button
|
||||
var _spawner: Node
|
||||
var _player: Node = null
|
||||
var _cd_overlays: Array[ColorRect] = []
|
||||
var _cd_labels: Array[Label] = []
|
||||
var _health_segments: Array[ColorRect] = []
|
||||
var _health_connected: bool = false
|
||||
|
||||
@@ -48,6 +49,13 @@ func _process(_delta: float) -> void:
|
||||
var ov := _cd_overlays[i]
|
||||
ov.anchor_top = 1.0 - fraction
|
||||
ov.anchor_bottom = 1.0
|
||||
var remaining: float = _player.call(&"ability_cooldown_remaining", i)
|
||||
var lbl := _cd_labels[i]
|
||||
if remaining > 0.0:
|
||||
lbl.text = "%.1fs" % remaining
|
||||
lbl.visible = true
|
||||
else:
|
||||
lbl.visible = false
|
||||
|
||||
|
||||
func _find_spawner() -> void:
|
||||
@@ -188,6 +196,17 @@ func _build_ability_slot(idx: int) -> Control:
|
||||
stack.add_child(cd_overlay)
|
||||
_cd_overlays.append(cd_overlay)
|
||||
|
||||
var cd_lbl := Label.new()
|
||||
cd_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
cd_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
cd_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
cd_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
cd_lbl.visible = false
|
||||
cd_lbl.add_theme_color_override("font_color", Color.WHITE)
|
||||
cd_lbl.add_theme_font_size_override("font_size", 18)
|
||||
stack.add_child(cd_lbl)
|
||||
_cd_labels.append(cd_lbl)
|
||||
|
||||
var key_lbl := Label.new()
|
||||
key_lbl.text = _ABILITY_KEYS[idx]
|
||||
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
|
||||
+148
-107
@@ -39,6 +39,7 @@ var _sword_rest_attach: BoneAttachment3D = null
|
||||
var _sword_node: Node3D = null
|
||||
var _sword_in_hand: bool = false
|
||||
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
|
||||
@@ -93,7 +94,8 @@ func _ready() -> void:
|
||||
_throw_aim_cd = randf_range(0.8, 2.5)
|
||||
_hit_area.body_entered.connect(_on_body_entered)
|
||||
_pick_wander_target()
|
||||
_setup_blood_burst()
|
||||
_blood_burst = preload("res://blood_burst.gd").new()
|
||||
add_child(_blood_burst)
|
||||
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
||||
_ole_player = AudioStreamPlayer.new()
|
||||
_ole_player.stream = load("res://sounds/ole.ogg")
|
||||
@@ -157,7 +159,7 @@ func _update_ai_state() -> void:
|
||||
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 _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"):
|
||||
# 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.
|
||||
@@ -260,8 +262,8 @@ func _face_point(point: Vector3, delta: float, rate: float) -> void:
|
||||
|
||||
|
||||
func _tick_wander(delta: float) -> void:
|
||||
# Plant the feet while unsheathing instead of sliding through the draw clip.
|
||||
if _drawing or _idle_timer > 0.0:
|
||||
# Plant the feet while drawing / sheathing instead of sliding through the clip.
|
||||
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)
|
||||
@@ -282,6 +284,13 @@ func _tick_wander(delta: float) -> void:
|
||||
|
||||
|
||||
func _tick_flee(delta: float) -> void:
|
||||
# Plant the feet while sheathing instead of sliding away through the clip.
|
||||
if _drawing or _sheathing:
|
||||
_decelerate(18.0, delta)
|
||||
_face_point(_bull.global_position, delta, _TURN_FACE)
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
var away := global_position - _bull.global_position
|
||||
away.y = 0.0
|
||||
if away.length() < 0.01:
|
||||
@@ -355,6 +364,12 @@ func _start_attack() -> void:
|
||||
func _tick_attack(delta: float) -> void:
|
||||
_attack_timer -= delta
|
||||
_swing_timer = maxf(0.0, _swing_timer - delta)
|
||||
# Plant the feet while drawing instead of sliding toward the bull mid-clip.
|
||||
if _drawing or _sheathing:
|
||||
_decelerate(22.0, delta)
|
||||
_face_point(_bull.global_position, delta, _TURN_FACE)
|
||||
move_and_slide()
|
||||
return
|
||||
_face_point(_bull.global_position, delta, _TURN_FACE)
|
||||
var to_bull := _bull.global_position - global_position
|
||||
to_bull.y = 0.0
|
||||
@@ -363,7 +378,7 @@ func _tick_attack(delta: float) -> void:
|
||||
# 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"):
|
||||
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)
|
||||
|
||||
if _swing_timer > 0.0:
|
||||
@@ -408,6 +423,8 @@ func _start_throw() -> void:
|
||||
_state = State.THROW
|
||||
_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
|
||||
if _anim_player:
|
||||
_anim_player.pause()
|
||||
var to_bull := _bull.global_position - global_position
|
||||
@@ -542,11 +559,11 @@ func _tick_ragdoll(_delta: float) -> void:
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func apply_ability_hit(hit_dir: Vector3, strength: float) -> void:
|
||||
func apply_ability_hit(hit_dir: Vector3, strength: float, up_boost: float = 0.0) -> void:
|
||||
if _state == State.RAGDOLL or _state == State.ROLL:
|
||||
return
|
||||
_acquire_bull()
|
||||
_enter_ragdoll(hit_dir, strength)
|
||||
_enter_ragdoll(hit_dir, strength, up_boost)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
@@ -578,7 +595,7 @@ func _on_blade_hit(body: Node3D) -> void:
|
||||
body.call(&"take_sword_hit")
|
||||
|
||||
|
||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0) -> void:
|
||||
_state = State.RAGDOLL
|
||||
if _sword_blade_area:
|
||||
_sword_blade_area.monitoring = false
|
||||
@@ -592,7 +609,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
hit_dir = hit_dir.normalized()
|
||||
var throw_dir := (hit_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
||||
|
||||
_spawn_blood_burst(hit_dir)
|
||||
_blood_burst.burst(global_position + Vector3(0.0, 0.9, 0.0), hit_dir)
|
||||
if _death_player:
|
||||
_death_player.pitch_scale = randf_range(0.9, 1.1)
|
||||
_death_player.play()
|
||||
@@ -619,6 +636,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
pb.global_transform = _skeleton.global_transform * _skeleton.get_bone_global_pose(bone_idx)
|
||||
_sim.active = true
|
||||
_sim.physical_bones_start_simulation()
|
||||
_pin_cape_to_body()
|
||||
var strength := DP.f("mat_ragdoll_impulse") * clampf(bull_speed / 15.0, 0.4, 1.3)
|
||||
var right := throw_dir.cross(Vector3.UP).normalized()
|
||||
for child: Node in _sim.get_children():
|
||||
@@ -641,61 +659,46 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
impulse = (throw_dir * 0.3 + right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||
"arm_R":
|
||||
impulse = (throw_dir * 0.3 - right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||
"Bone", "Bone.001", "Bone.002":
|
||||
# Scale by mass so the light cape gets the same velocity as the body
|
||||
# (Δv = impulse/mass) and flies along with it instead of rocketing off.
|
||||
impulse = (throw_dir + Vector3.UP * 0.35).normalized() * strength * 2.0 * pb.mass
|
||||
_:
|
||||
continue
|
||||
var noise := Vector3(randf_range(-0.15, 0.15), randf_range(-0.08, 0.15), randf_range(-0.15, 0.15))
|
||||
pb.apply_central_impulse(impulse + noise * strength * 0.2)
|
||||
|
||||
|
||||
# ── Blood burst ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _setup_blood_burst() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
var sphere := SphereMesh.new()
|
||||
sphere.radius = 0.07
|
||||
sphere.height = 0.14
|
||||
sphere.radial_segments = 4
|
||||
sphere.rings = 2
|
||||
sphere.material = mat
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
|
||||
ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||
scale_curve.add_point(Vector2(0.6, 0.5))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
_blood_burst = CPUParticles3D.new()
|
||||
_blood_burst.mesh = sphere
|
||||
_blood_burst.color_ramp = ramp
|
||||
_blood_burst.scale_amount_curve = scale_curve
|
||||
_blood_burst.one_shot = true
|
||||
_blood_burst.explosiveness = 0.9
|
||||
_blood_burst.amount = 25
|
||||
_blood_burst.lifetime = 1.1
|
||||
_blood_burst.randomness = 0.4
|
||||
_blood_burst.local_coords = false
|
||||
_blood_burst.spread = 80.0
|
||||
_blood_burst.gravity = Vector3(0.0, -7.0, 0.0)
|
||||
_blood_burst.initial_velocity_min = 4.0
|
||||
_blood_burst.initial_velocity_max = 11.0
|
||||
_blood_burst.scale_amount_min = 0.5
|
||||
_blood_burst.scale_amount_max = 1.6
|
||||
_blood_burst.emitting = false
|
||||
add_child(_blood_burst)
|
||||
# A floor slam launches the whole ragdoll straight up: a uniform per-bone Δv
|
||||
# (impulse = mass · speed) so the joints carry every bone up together.
|
||||
if up_boost > 0.0:
|
||||
for child: Node in _sim.get_children():
|
||||
if child is PhysicalBone3D:
|
||||
var pbb := child as PhysicalBone3D
|
||||
pbb.apply_central_impulse(Vector3.UP * up_boost * pbb.mass)
|
||||
|
||||
|
||||
func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||
_blood_burst.global_position = global_position + Vector3(0.0, 0.9, 0.0)
|
||||
_blood_burst.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||
_blood_burst.restart()
|
||||
# Pin the cape's root link to the chest so it stays attached at the shoulders
|
||||
# while ragdolling (its bone chain is a skeleton root, so it has no joint to the
|
||||
# body otherwise). The chain hangs and swings from this point.
|
||||
func _pin_cape_to_body() -> void:
|
||||
var cape := _physical_bone(&"Bone")
|
||||
var chest := _physical_bone(&"chest")
|
||||
if cape == null or chest == null:
|
||||
return
|
||||
var joint := PinJoint3D.new()
|
||||
_sim.add_child(joint)
|
||||
joint.global_position = cape.global_position
|
||||
joint.node_a = chest.get_path()
|
||||
joint.node_b = cape.get_path()
|
||||
|
||||
|
||||
func _physical_bone(bone: StringName) -> PhysicalBone3D:
|
||||
if not _sim:
|
||||
return null
|
||||
for child: Node in _sim.get_children():
|
||||
if child is PhysicalBone3D and (child as PhysicalBone3D).bone_name == bone:
|
||||
return child as PhysicalBone3D
|
||||
return null
|
||||
|
||||
|
||||
# ── Animation helpers ─────────────────────────────────────────────────────────
|
||||
@@ -715,8 +718,8 @@ func _anim_length(anim_name: StringName, fallback: float) -> float:
|
||||
func _play_anim(anim_name: StringName) -> void:
|
||||
if _anim_player == null:
|
||||
return
|
||||
if _drawing:
|
||||
return # let the unholster clip play through uninterrupted
|
||||
if _drawing or _sheathing:
|
||||
return # let the draw / sheathe clip play through uninterrupted
|
||||
if _anim_player.current_animation == anim_name:
|
||||
return
|
||||
_anim_player.play(anim_name)
|
||||
@@ -743,9 +746,9 @@ func _pick_wander_target() -> void:
|
||||
# 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.
|
||||
# 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.
|
||||
func _setup_sword() -> void:
|
||||
if not _skeleton:
|
||||
return
|
||||
@@ -799,63 +802,83 @@ func _spawn_sword() -> void:
|
||||
_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.
|
||||
# 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.
|
||||
func _wants_sword_drawn() -> bool:
|
||||
return _state == State.ATTACK or _state == State.THROW
|
||||
|
||||
|
||||
func _update_sword_carry() -> void:
|
||||
if not is_instance_valid(_sword_node):
|
||||
if not is_instance_valid(_sword_node) or _state == State.RAGDOLL:
|
||||
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 _wants_sword_drawn():
|
||||
if not _sword_in_hand and not _drawing:
|
||||
_begin_draw()
|
||||
elif dist > draw_r * 1.25:
|
||||
_cancel_draw()
|
||||
_carry_sword(false)
|
||||
elif _sword_in_hand or _drawing:
|
||||
# Animate the sheathe only into the unhurried locomotion states; snap for
|
||||
# a dodge / roll / brace where the body clip can't spare the time.
|
||||
if _state == State.WANDER or _state == State.FLEE:
|
||||
if not _sheathing:
|
||||
_begin_sheathe()
|
||||
else:
|
||||
_cancel_transitions()
|
||||
_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:
|
||||
if not (_anim_player and _anim_player.has_animation(_ANIM_DRAW)):
|
||||
_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()
|
||||
_sheathing = false
|
||||
_drawing = true
|
||||
_draw_timer = 0.0
|
||||
_draw_len = _anim_length(_ANIM_DRAW, 1.0) / _draw_speed()
|
||||
_anim_player.play(_ANIM_DRAW, -1.0, _draw_speed())
|
||||
_anim_player.seek(0.0, true)
|
||||
|
||||
|
||||
func _begin_sheathe() -> void:
|
||||
if not (_anim_player and _anim_player.has_animation(_ANIM_DRAW)):
|
||||
_carry_sword(false) # no clip — snap to holster
|
||||
return
|
||||
_drawing = false
|
||||
_sheathing = true
|
||||
_draw_timer = 0.0
|
||||
_draw_len = _anim_length(_ANIM_DRAW, 1.0) / _draw_speed()
|
||||
# Draw_weapon reversed from its end frame = a put-the-sword-away motion.
|
||||
_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.
|
||||
func _advance_draw(delta: float) -> void:
|
||||
if not _drawing and not _sheathing:
|
||||
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()
|
||||
var t := _draw_timer / maxf(_draw_len, 0.001)
|
||||
if _drawing:
|
||||
if not _sword_in_hand and t >= _DRAW_GRAB_AT:
|
||||
_carry_sword(true)
|
||||
if t >= 1.0:
|
||||
_drawing = false
|
||||
_carry_sword(true)
|
||||
else:
|
||||
if _sword_in_hand and t >= 1.0 - _DRAW_GRAB_AT:
|
||||
_carry_sword(false)
|
||||
if t >= 1.0:
|
||||
_sheathing = false
|
||||
_carry_sword(false)
|
||||
|
||||
|
||||
func _finalize_draw() -> void:
|
||||
if not _drawing:
|
||||
return
|
||||
func _cancel_transitions() -> void:
|
||||
_drawing = false
|
||||
_carry_sword(true)
|
||||
_sheathing = false
|
||||
|
||||
|
||||
# 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
|
||||
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
|
||||
@@ -879,10 +902,16 @@ func _carry_sword(in_hand: bool) -> void:
|
||||
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"))
|
||||
if _sword_in_hand:
|
||||
_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"))
|
||||
else:
|
||||
_sword_node.position = Vector3(
|
||||
DP.f("sword_rest_pos_x"), DP.f("sword_rest_pos_y"), DP.f("sword_rest_pos_z"))
|
||||
_sword_node.rotation_degrees = Vector3(
|
||||
DP.f("sword_rest_rot_x"), DP.f("sword_rest_rot_y"), DP.f("sword_rest_rot_z"))
|
||||
|
||||
|
||||
# ── Ragdoll setup ─────────────────────────────────────────────────────────────
|
||||
@@ -916,6 +945,11 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
["shin_R", 0.08, 0.28],
|
||||
["foot_L", 0.07, 0.18],
|
||||
["foot_R", 0.07, 0.18],
|
||||
# Muleta (cape) spine — a free 3-link chain so the cape ragdolls with the
|
||||
# body instead of hanging frozen in the air. See the cape tweak below.
|
||||
["Bone", 0.04, 0.30],
|
||||
["Bone.001", 0.04, 0.34],
|
||||
["Bone.002", 0.04, 0.30],
|
||||
]
|
||||
|
||||
for entry: Array in BONES:
|
||||
@@ -930,6 +964,13 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
pb.angular_damp = 0.2
|
||||
pb.collision_layer = 1
|
||||
pb.collision_mask = 1
|
||||
if bname.begins_with("Bone"):
|
||||
# Cape: light and draggy so it flutters like cloth; layer 0 makes its
|
||||
# collision one-way (drapes onto body/floor without shoving the corpse).
|
||||
pb.mass = 0.15
|
||||
pb.linear_damp = 0.5
|
||||
pb.angular_damp = 0.7
|
||||
pb.collision_layer = 0
|
||||
var cs := CollisionShape3D.new()
|
||||
var use_capsule: bool = entry.size() >= 3
|
||||
if use_capsule:
|
||||
|
||||
@@ -45,6 +45,8 @@ var _roll_damping: float = 0.15
|
||||
|
||||
enum DustState { NONE, WALK, CHARGE }
|
||||
var _dust_state: DustState = DustState.NONE
|
||||
var _walk_dust_ramp: Gradient = null
|
||||
var _charge_dust_ramp: Gradient = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -79,9 +81,14 @@ func _setup_hoof_dust() -> void:
|
||||
sphere.rings = 2
|
||||
sphere.material = mat
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
||||
ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
||||
_walk_dust_ramp = Gradient.new()
|
||||
_walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
||||
_walk_dust_ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
||||
|
||||
# Light blue dust while charging
|
||||
_charge_dust_ramp = Gradient.new()
|
||||
_charge_dust_ramp.set_color(0, Color(0.20, 0.40, 0.85, 1.0))
|
||||
_charge_dust_ramp.set_color(1, Color(0.20, 0.40, 0.85, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 0.2))
|
||||
@@ -105,7 +112,7 @@ func _setup_hoof_dust() -> void:
|
||||
p.scale_amount_min = DP.f("walk_scale_min")
|
||||
p.scale_amount_max = DP.f("walk_scale_max")
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.color_ramp = _walk_dust_ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
cube_guy.add_child(p)
|
||||
_hoof_emitters.append(p)
|
||||
@@ -185,12 +192,14 @@ func _set_dust_state(new_state: DustState) -> void:
|
||||
DustState.NONE:
|
||||
p.emitting = false
|
||||
DustState.WALK:
|
||||
p.color_ramp = _walk_dust_ramp
|
||||
p.scale_amount_min = DP.f("walk_scale_min")
|
||||
p.scale_amount_max = DP.f("walk_scale_max")
|
||||
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||
p.emitting = true
|
||||
DustState.CHARGE:
|
||||
p.color_ramp = _charge_dust_ramp
|
||||
p.scale_amount_min = DP.f("charge_scale_min")
|
||||
p.scale_amount_max = DP.f("charge_scale_max")
|
||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||
@@ -237,6 +246,10 @@ func ability_cooldown_fraction(slot: int) -> float:
|
||||
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
|
||||
|
||||
|
||||
func ability_cooldown_remaining(slot: int) -> float:
|
||||
return ability_cd[slot]
|
||||
|
||||
|
||||
func _activate_kick() -> void:
|
||||
ability_cd[0] = DP.f("kick_cooldown")
|
||||
_ability_active = true
|
||||
@@ -261,8 +274,10 @@ func _activate_slam() -> void:
|
||||
_slam_pending = true
|
||||
var jump_vel := DP.f("slam_jump_velocity")
|
||||
velocity.y = jump_vel
|
||||
var air_time := jump_vel / 9.8 * 2.0
|
||||
_ability_timer = air_time + 0.5
|
||||
# Airtime with an accelerated descent: rise (v/g) + fall (v / (g·√mult)).
|
||||
var fall_mult := maxf(DP.f("slam_fall_mult"), 0.01)
|
||||
var air_time := (jump_vel / 9.8) * (1.0 + 1.0 / sqrt(fall_mult))
|
||||
_ability_timer = air_time + 0.3
|
||||
if _bull_anim and _bull_anim.has_animation(&"Armature|SLAM"):
|
||||
var anim_len := _bull_anim.get_animation(&"Armature|SLAM").length
|
||||
_bull_anim.speed_scale = anim_len / maxf(air_time, 0.01)
|
||||
@@ -301,7 +316,7 @@ func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float,
|
||||
mat.call(&"apply_ability_hit", (forward + Vector3.UP * 0.4).normalized(), strength)
|
||||
|
||||
|
||||
func _hit_matadors_radius(range_m: float, strength: float) -> void:
|
||||
func _hit_matadors_radius(range_m: float, strength: float, up_boost: float = 0.0) -> void:
|
||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
|
||||
to_mat.y = 0.0
|
||||
@@ -309,7 +324,7 @@ func _hit_matadors_radius(range_m: float, strength: float) -> void:
|
||||
continue
|
||||
var away := to_mat.normalized() if to_mat.length() > 0.01 \
|
||||
else Vector3(randf() - 0.5, 0.0, randf() - 0.5).normalized()
|
||||
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength)
|
||||
mat.call(&"apply_ability_hit", (away + Vector3.UP * 0.6).normalized(), strength, up_boost)
|
||||
|
||||
|
||||
func _spawn_slam_fx() -> void:
|
||||
@@ -424,8 +439,8 @@ func _spawn_kick_dust(origin: Vector3, back_dir: Vector3) -> void:
|
||||
sphere.material = mat
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.80, 0.68, 0.42, 0.9))
|
||||
ramp.set_color(1, Color(0.80, 0.68, 0.42, 0.0))
|
||||
ramp.set_color(0, Color(1.0, 0.78, 0.18, 0.95))
|
||||
ramp.set_color(1, Color(1.0, 0.45, 0.05, 0.0))
|
||||
|
||||
var scale_curve := Curve.new()
|
||||
scale_curve.add_point(Vector2(0.0, 0.3))
|
||||
@@ -566,7 +581,12 @@ func _apply_kick(direction: Vector3) -> void:
|
||||
func _physics_process(delta: float) -> void:
|
||||
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
var gravity := get_gravity()
|
||||
# Crash a slam down hard: full leap on the way up, accelerated descent so
|
||||
# the bull spends less time floating before the impact.
|
||||
if _slam_pending and velocity.y < 0.0:
|
||||
gravity *= DP.f("slam_fall_mult")
|
||||
velocity += gravity * delta
|
||||
|
||||
# ── Camera-relative input direction ───────────────────────────────────────
|
||||
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
@@ -686,7 +706,7 @@ func _physics_process(delta: float) -> void:
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_spawn_slam_fx()
|
||||
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"))
|
||||
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"), DP.f("slam_launch_up"))
|
||||
|
||||
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||
|
||||
Reference in New Issue
Block a user