Bull roll ability, console screen, tests
This commit is contained in:
@@ -8,6 +8,14 @@ const HOOF_OFFSETS: Array = [
|
||||
Vector3( 0.30, -0.25, 0.60),
|
||||
]
|
||||
|
||||
# Bull animation clips (Armature|* action names in Assets/bull.fbx). IDLE is the
|
||||
# looping base pose; the others are one-shot ability clips that return to it.
|
||||
const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||
const _ANIM_SLAM: StringName = &"Armature|SLAM"
|
||||
const _ANIM_DASH: StringName = &"Armature|DASH"
|
||||
const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
||||
|
||||
@onready var camera_pivot: Node3D = $Camera3D
|
||||
@onready var cube_guy: Node3D = $bull
|
||||
|
||||
@@ -23,12 +31,13 @@ var _charge_was_full: bool = false
|
||||
|
||||
var _slam_ring_mesh: ArrayMesh = null
|
||||
|
||||
# Ability system — kick=0, slam=1, dash=2
|
||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0]
|
||||
# Ability system — kick=0, slam=1, dash=2, roll=3
|
||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||
var _ability_active: bool = false
|
||||
var _ability_timer: float = 0.0
|
||||
var _active_ability: int = -1
|
||||
var _dash_dir: Vector3 = Vector3.ZERO
|
||||
var _roll_dir: Vector3 = Vector3.ZERO
|
||||
var _bull_anim: AnimationPlayer = null
|
||||
|
||||
var _charge_ready_emitter: CPUParticles3D = null
|
||||
@@ -68,6 +77,7 @@ func _ready() -> void:
|
||||
_setup_charge_bar()
|
||||
_setup_audio()
|
||||
_bull_anim = _find_anim_player(cube_guy)
|
||||
_setup_bull_idle()
|
||||
DP.any_changed.connect(_on_dp_changed)
|
||||
_roll_damping = DP.f("roll_damping")
|
||||
|
||||
@@ -339,6 +349,28 @@ func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||
return null
|
||||
|
||||
|
||||
# IDLE ships one-shot; make it loop and start it so the bull breathes at rest
|
||||
# instead of freezing on a bind pose. It's the base every ability blends back to.
|
||||
func _setup_bull_idle() -> void:
|
||||
if _bull_anim == null:
|
||||
return
|
||||
# IDLE and ROLL both loop as continuous states; the ability clips are one-shot.
|
||||
for clip: StringName in [_ANIM_IDLE, _ANIM_ROLL]:
|
||||
if _bull_anim.has_animation(clip):
|
||||
_bull_anim.get_animation(clip).loop_mode = Animation.LOOP_LINEAR
|
||||
_play_idle()
|
||||
|
||||
|
||||
# Return to the looping idle (and undo any ability speed-scale). Cross-fades so a
|
||||
# finishing ability clip eases back to idle instead of snapping.
|
||||
func _play_idle() -> void:
|
||||
if _bull_anim == null:
|
||||
return
|
||||
_bull_anim.speed_scale = 1.0
|
||||
if _bull_anim.has_animation(_ANIM_IDLE):
|
||||
_bull_anim.play(_ANIM_IDLE, 0.2)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# Abilities can be chained: a new one interrupts whatever is currently
|
||||
# active, gated only by each ability's own cooldown.
|
||||
@@ -348,6 +380,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_activate_slam()
|
||||
elif event.is_action_pressed(&"ability_dash") and ability_cd[2] <= 0.0:
|
||||
_activate_dash()
|
||||
elif event.is_action_pressed(&"ability_roll") and ability_cd[3] <= 0.0:
|
||||
_activate_roll()
|
||||
|
||||
|
||||
func ability_cooldown_fraction(slot: int) -> float:
|
||||
@@ -356,6 +390,7 @@ func ability_cooldown_fraction(slot: int) -> float:
|
||||
0: maxcd = DP.f("kick_cooldown")
|
||||
1: maxcd = DP.f("slam_cooldown")
|
||||
2: maxcd = DP.f("dash_cooldown")
|
||||
3: maxcd = DP.f("roll_cooldown")
|
||||
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
|
||||
|
||||
|
||||
@@ -370,9 +405,9 @@ func _activate_kick() -> void:
|
||||
_ability_timer = 0.6
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
if _bull_anim and _bull_anim.has_animation(&"Armature|FOOTKICK"):
|
||||
_bull_anim.play(&"Armature|FOOTKICK")
|
||||
_ability_timer = _bull_anim.get_animation(&"Armature|FOOTKICK").length
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_KICK):
|
||||
_bull_anim.play(_ANIM_KICK)
|
||||
_ability_timer = _bull_anim.get_animation(_ANIM_KICK).length
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
var back := -Vector3(raw.x, 0.0, raw.z).normalized()
|
||||
_spawn_kick_fx(back)
|
||||
@@ -393,10 +428,10 @@ func _activate_slam() -> void:
|
||||
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
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_SLAM):
|
||||
var anim_len := _bull_anim.get_animation(_ANIM_SLAM).length
|
||||
_bull_anim.speed_scale = anim_len / maxf(air_time, 0.01)
|
||||
_bull_anim.play(&"Armature|SLAM")
|
||||
_bull_anim.play(_ANIM_SLAM)
|
||||
|
||||
|
||||
func _activate_dash() -> void:
|
||||
@@ -406,9 +441,9 @@ func _activate_dash() -> void:
|
||||
_ability_timer = 0.45
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
if _bull_anim and _bull_anim.has_animation(&"Armature|DASH"):
|
||||
_bull_anim.play(&"Armature|DASH")
|
||||
_ability_timer = _bull_anim.get_animation(&"Armature|DASH").length
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_DASH):
|
||||
_bull_anim.play(_ANIM_DASH)
|
||||
_ability_timer = _bull_anim.get_animation(_ANIM_DASH).length
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
|
||||
velocity.x = _dash_dir.x * DP.f("dash_speed")
|
||||
@@ -420,6 +455,98 @@ func _activate_dash() -> void:
|
||||
_huff_player.play()
|
||||
|
||||
|
||||
func _activate_roll() -> void:
|
||||
ability_cd[3] = DP.f("roll_cooldown")
|
||||
_ability_active = true
|
||||
_active_ability = 3
|
||||
_ability_timer = DP.f("roll_duration")
|
||||
# Combine with any charge already underway: keep the current heading + speed,
|
||||
# floored to a launch minimum so even a standing roll shoves off like a boulder.
|
||||
var flat := Vector3(velocity.x, 0.0, velocity.z)
|
||||
if flat.length() > 1.0:
|
||||
_roll_dir = flat.normalized()
|
||||
else:
|
||||
var f := cube_guy.global_transform.basis.z
|
||||
_roll_dir = Vector3(f.x, 0.0, f.z).normalized()
|
||||
var launch := maxf(flat.length(), DP.f("roll_min_speed"))
|
||||
velocity.x = _roll_dir.x * launch
|
||||
velocity.z = _roll_dir.z * launch
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_bull_anim.play(_ANIM_ROLL)
|
||||
_legs.set(&"tail_ragdoll", true) # tail streams out behind
|
||||
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch")) # horns down, plowing
|
||||
_spawn_dash_burst()
|
||||
camera_pivot.call(&"trigger_hit", 0.35)
|
||||
if _huff_player:
|
||||
_huff_player.pitch_scale = randf_range(0.8, 1.0)
|
||||
_huff_player.play()
|
||||
|
||||
|
||||
# One rolling frame: steer, ramp/retain speed, move, then ricochet off walls and
|
||||
# bowl through matadors. Fully replaces the normal locomotion path while active.
|
||||
func _tick_roll(delta: float, steer: Vector3) -> void:
|
||||
var steer_flat := Vector3(steer.x, 0.0, steer.z)
|
||||
if steer_flat.length() > 0.1:
|
||||
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
|
||||
_roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized()
|
||||
|
||||
# Ramp toward the cruise top speed; anything above it (from wall boosts) bleeds
|
||||
# off slowly via roll_momentum instead of snapping back — that's the fun carry.
|
||||
var speed := Vector2(velocity.x, velocity.z).length()
|
||||
var top := DP.f("roll_max_speed")
|
||||
if speed < top:
|
||||
speed = move_toward(speed, top, DP.f("roll_accel") * delta)
|
||||
else:
|
||||
speed = maxf(top, speed * pow(DP.f("roll_momentum"), delta))
|
||||
speed = minf(speed, DP.f("roll_wall_cap"))
|
||||
velocity.x = _roll_dir.x * speed
|
||||
velocity.z = _roll_dir.z * speed
|
||||
|
||||
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, atan2(_roll_dir.x, _roll_dir.z), delta * 12.0)
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
|
||||
_bull_anim.speed_scale = clampf(speed / maxf(top, 1.0), 0.6, 2.4)
|
||||
|
||||
_set_dust_state(DustState.CHARGE)
|
||||
if _charge_trail_emitter:
|
||||
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||
|
||||
var pre_wall := Vector3(velocity.x, 0.0, velocity.z)
|
||||
move_and_slide()
|
||||
_was_on_floor = is_on_floor()
|
||||
_roll_wall_ricochet(pre_wall)
|
||||
# Bowling strike: fling matadors we plow into up and outward like pins (and the
|
||||
# resulting kills feed the HUD combo meter).
|
||||
_hit_matadors_radius(DP.f("roll_hit_radius"), DP.f("roll_hit_strength"), DP.f("roll_hit_up"))
|
||||
|
||||
|
||||
# On hitting a wall mid-roll, mirror the heading and BOOST speed (capped) instead of
|
||||
# bleeding it — banking off walls is how you build ludicrous momentum.
|
||||
func _roll_wall_ricochet(pre_wall: Vector3) -> void:
|
||||
for i in get_slide_collision_count():
|
||||
var col := get_slide_collision(i)
|
||||
var normal := col.get_normal()
|
||||
if absf(normal.y) >= 0.5:
|
||||
continue
|
||||
var flat_n := Vector3(normal.x, 0.0, normal.z).normalized()
|
||||
var refl := pre_wall - 2.0 * pre_wall.dot(flat_n) * flat_n
|
||||
refl.y = 0.0
|
||||
if refl.length() < 0.01:
|
||||
return
|
||||
_roll_dir = refl.normalized()
|
||||
var boosted := minf(pre_wall.length() * DP.f("roll_wall_boost"), DP.f("roll_wall_cap"))
|
||||
velocity.x = _roll_dir.x * boosted
|
||||
velocity.z = _roll_dir.z * boosted
|
||||
var hit := col.get_position()
|
||||
_spawn_kick_sparks(hit, flat_n)
|
||||
_spawn_kick_flash(hit)
|
||||
camera_pivot.call(&"trigger_hit", clampf(boosted / 90.0, 0.25, 1.0))
|
||||
if _huff_player:
|
||||
_huff_player.pitch_scale = randf_range(1.05, 1.35)
|
||||
_huff_player.play()
|
||||
return
|
||||
|
||||
|
||||
func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float,
|
||||
dir: Vector3 = Vector3.ZERO) -> void:
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
@@ -807,7 +934,7 @@ func _physics_process(delta: float) -> void:
|
||||
var on_floor := is_on_floor()
|
||||
|
||||
# ── Ability cooldowns ────────────────────────────────────────────────────────
|
||||
for i: int in 3:
|
||||
for i: int in 4:
|
||||
ability_cd[i] = maxf(0.0, ability_cd[i] - delta)
|
||||
if _ability_active:
|
||||
_ability_timer -= delta
|
||||
@@ -817,8 +944,14 @@ func _physics_process(delta: float) -> void:
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
_slam_pending = false
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_play_idle()
|
||||
|
||||
# ── Boulder roll ──────────────────────────────────────────────────────────
|
||||
# While rolling, the ability owns movement entirely (its own accel, low friction
|
||||
# and wall ricochet) — bypass the normal input/kick/damping path below.
|
||||
if _active_ability == 3 and _ability_active:
|
||||
_tick_roll(delta, direction)
|
||||
return
|
||||
|
||||
# ── Mouse button single-thrust actions ───────────────────────────────────────
|
||||
# Left only → thrust NE (forward-right diagonal)
|
||||
@@ -911,8 +1044,7 @@ func _physics_process(delta: float) -> void:
|
||||
_ability_timer = 0.0
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_play_idle()
|
||||
_spawn_slam_fx()
|
||||
_hit_matadors_radius(DP.f("slam_range"), DP.f("slam_strength"), DP.f("slam_launch_up"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user