cooldowns, sword holstering, ability colors

This commit is contained in:
2026-06-25 00:10:40 +03:00
parent 708a35d51f
commit b929ed2a7a
6 changed files with 268 additions and 124 deletions
+1
View File
@@ -2,3 +2,4 @@ disable:
- class-definitions-order
- no-else-return
max-line-length: 120
max-file-lines: 1200
+49
View File
@@ -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
View File
@@ -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)
+19
View File
@@ -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
+142 -101
View File
@@ -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()
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:
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 _draw_timer >= _draw_len:
_finalize_draw()
func _finalize_draw() -> void:
if not _drawing:
return
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)
# 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:
func _cancel_transitions() -> void:
_drawing = false
_sheathing = 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
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:
+32 -12
View File
@@ -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: