feat: overhaul movement for flow — momentum-preserving states, smooth crouch, landing feel
- Quake-style air-strafe accelerate (real speed gain from strafing; external speed from dash/rockets never clamped) - Wall run: keeps entry momentum (incl. upward carry), accelerates along the wall instead of hard-setting velocity, gravity fades in over the run - Slide: slope physics (gravity projected along floor accelerates downhill), flat-ground decel instead of multiplicative friction, entry boost, direct slide->wallrun and slide->dash transitions - Dash: cooldown 10s -> 2s, per-player (was a static shared across instances), momentum fully kept on exit, FOV punch event - Grapple: taut pendulum with active rope control (W reels in, S pays out), release keeps swing energy - Wall climb: carries upward momentum in, jump-off kick, shared vault helper - Crouch capsule: single owner in the machine, smoothly lerped, ceiling check; camera eye height follows via crouch factor (no more head snapping) - Landing: machine emits 'land' events scaled by impact; camera dip + pitched down thud; footsteps get pitch variation - Camera rig: event-driven (land dip, dash FOV kick, vault pitch impulse), slide roll tilt - Model: Jump vs Fall split by vertical velocity, Land one-shot on heavy landings, wall-run body lean (synced to remotes via synced_wall_side) Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c4a538a469
commit
0ba14a9469
@@ -1,12 +1,16 @@
|
||||
extends Node
|
||||
class_name StateSlide
|
||||
|
||||
## Momentum slide. Friction is a flat deceleration on level ground but gravity
|
||||
## projected along the floor accelerates you downhill, so slopes are the fast
|
||||
## route. The capsule stays low via the machine's crouch handling (slide counts
|
||||
## as crouched); no capsule fiddling here.
|
||||
|
||||
var machine: MovementStateMachine
|
||||
var params: MovementParams:
|
||||
get: return machine.params
|
||||
|
||||
var elapsed: float = 0.0
|
||||
var _saved_capsule_height: float = 0.0
|
||||
var _slide_direction: Vector3 = Vector3.ZERO
|
||||
|
||||
|
||||
@@ -17,12 +21,6 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
if machine.player.slide_player and not machine.player.slide_player.playing:
|
||||
machine.player.slide_player.play()
|
||||
|
||||
# Save original capsule height and halve it
|
||||
var shape: CapsuleShape3D = _get_capsule()
|
||||
if shape:
|
||||
_saved_capsule_height = shape.height
|
||||
shape.height = _saved_capsule_height * 0.5
|
||||
|
||||
# Slide in the direction of current velocity (momentum-based)
|
||||
var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
||||
if hvel.length_squared() > 0.01:
|
||||
@@ -33,54 +31,70 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
_slide_direction.y = 0.0
|
||||
_slide_direction = _slide_direction.normalized()
|
||||
|
||||
# Set initial slide velocity
|
||||
var slide_speed := maxf(hvel.length(), params.slide_speed)
|
||||
machine.player.velocity.x = _slide_direction.x * machine.get_effective_speed(slide_speed)
|
||||
machine.player.velocity.z = _slide_direction.z * machine.get_effective_speed(slide_speed)
|
||||
# Entry speed: keep momentum, add a flat boost when actually moving fast on
|
||||
# the ground (rewards slide-cancelling sprints without making crouch a brake)
|
||||
var entry_speed := hvel.length()
|
||||
if machine.player.is_on_floor() and entry_speed > params.walk_speed * 0.8:
|
||||
entry_speed += params.slide_boost
|
||||
entry_speed = maxf(entry_speed, params.slide_speed * 0.75)
|
||||
machine.player.velocity.x = _slide_direction.x * machine.get_effective_speed(entry_speed)
|
||||
machine.player.velocity.z = _slide_direction.z * machine.get_effective_speed(entry_speed)
|
||||
|
||||
|
||||
func exit() -> void:
|
||||
machine.slide_cooldown_timer = params.slide_cooldown
|
||||
|
||||
if machine.player.slide_player:
|
||||
machine.player.slide_player.stop()
|
||||
|
||||
# Restore original capsule height
|
||||
var shape: CapsuleShape3D = _get_capsule()
|
||||
if shape and _saved_capsule_height > 0.0:
|
||||
shape.height = _saved_capsule_height
|
||||
|
||||
|
||||
func update(delta: float) -> void:
|
||||
elapsed += delta
|
||||
var player := machine.player
|
||||
|
||||
|
||||
if player.slide_player and not player.slide_player.playing:
|
||||
player.slide_player.play()
|
||||
|
||||
|
||||
var vel: Vector3 = player.velocity
|
||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||
var hspeed := hvel.length()
|
||||
|
||||
# ── Apply friction to horizontal velocity ─────────────────────────────
|
||||
vel.x *= pow(params.slide_friction, delta * 10.0)
|
||||
vel.z *= pow(params.slide_friction, delta * 10.0)
|
||||
# ── Slope physics: gravity projected along the floor plane ────────────
|
||||
var on_slope := false
|
||||
if player.is_on_floor():
|
||||
var floor_n := player.get_floor_normal()
|
||||
if floor_n.y < 0.999:
|
||||
# Downhill direction on this slope
|
||||
var downhill := (Vector3.DOWN - floor_n * Vector3.DOWN.dot(floor_n))
|
||||
if downhill.length_squared() > 0.0001:
|
||||
downhill = downhill.normalized()
|
||||
var steepness := 1.0 - floor_n.y # 0 flat .. ~0.3 steep
|
||||
var slope_pull := downhill * params.slide_slope_accel * steepness * 8.0
|
||||
hvel += Vector3(slope_pull.x, 0.0, slope_pull.z) * delta
|
||||
on_slope = downhill.dot(_slide_direction) > 0.1
|
||||
|
||||
# ── Allow slight steering ─────────────────────────────────────────────
|
||||
# ── Friction: flat decel on level ground, nearly free downhill ────────
|
||||
if hspeed > 0.01:
|
||||
var friction := params.slide_friction_flat
|
||||
if on_slope:
|
||||
friction *= 0.15
|
||||
var new_speed := maxf(hvel.length() - friction * delta, 0.0)
|
||||
if hvel.length() > 0.001:
|
||||
hvel = hvel.normalized() * new_speed
|
||||
|
||||
# ── Steering: bend velocity toward input without changing speed ───────
|
||||
var wish_dir := machine.wish_dir_world
|
||||
if wish_dir.length_squared() > 0.01:
|
||||
if wish_dir.length_squared() > 0.01 and hvel.length_squared() > 0.01:
|
||||
wish_dir = wish_dir.normalized()
|
||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||
var current_speed := hvel.length()
|
||||
|
||||
# Slowly bend velocity towards wish_dir
|
||||
var steer_speed = 3.0 * delta
|
||||
var new_hvel = hvel.lerp(wish_dir * current_speed, steer_speed)
|
||||
vel.x = new_hvel.x
|
||||
vel.z = new_hvel.z
|
||||
|
||||
if new_hvel.length_squared() > 0.01:
|
||||
_slide_direction = new_hvel.normalized()
|
||||
var speed_now := hvel.length()
|
||||
var steered := hvel.lerp(wish_dir * speed_now, params.slide_steer_rate * delta)
|
||||
if steered.length_squared() > 0.01:
|
||||
hvel = steered.normalized() * speed_now
|
||||
_slide_direction = hvel.normalized()
|
||||
|
||||
# ── Gravity (for slopes) ──────────────────────────────────────────────
|
||||
vel.x = hvel.x
|
||||
vel.z = hvel.z
|
||||
|
||||
# ── Gravity (for slopes / edges) ──────────────────────────────────────
|
||||
if not player.is_on_floor():
|
||||
vel.y -= params.gravity * delta
|
||||
else:
|
||||
@@ -97,10 +111,41 @@ func update(delta: float) -> void:
|
||||
machine.on_ground = false
|
||||
|
||||
# ── End conditions ────────────────────────────────────────────────────
|
||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
var out_speed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
|
||||
# Jump out of slide: momentum-preserving hop
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
var hv := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hv.length_squared() > 0.01:
|
||||
var dir := hv.normalized()
|
||||
var jump_speed := maxf(out_speed, minf(out_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
||||
player.velocity.x = dir.x * jump_speed
|
||||
player.velocity.z = dir.z * jump_speed
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.on_ground = false
|
||||
machine.register_chain_mechanic("slide_jump")
|
||||
machine.movement_event.emit("jump", {})
|
||||
if player.jump_player:
|
||||
player.jump_player.play()
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Slide straight onto a wall → wall run (keeps the flow going)
|
||||
if not player.is_on_floor() and machine.wall_cooldown_timer <= 0.0 and out_speed > params.slide_min_speed:
|
||||
var wall_n := machine.detect_wall_horizontal()
|
||||
if wall_n != Vector3.ZERO and abs(machine.wish_dir_world.dot(wall_n)) < 0.85:
|
||||
machine.switch_to("wall_run")
|
||||
return
|
||||
|
||||
# Dash out of slide
|
||||
if machine.input_dash and machine.dash_cooldown_timer <= 0.0:
|
||||
machine.switch_to("dash")
|
||||
return
|
||||
|
||||
# Slide ended: too slow
|
||||
if hspeed < params.slide_min_speed:
|
||||
if out_speed < params.slide_min_speed:
|
||||
machine.switch_to("ground")
|
||||
return
|
||||
|
||||
@@ -113,27 +158,3 @@ func update(delta: float) -> void:
|
||||
if not machine.on_ground and machine.coyote_timer <= 0.0:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Jump out of slide
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hvel.length_squared() > 0.01:
|
||||
var dir := hvel.normalized()
|
||||
var current_speed := hvel.length()
|
||||
var jump_speed := maxf(current_speed, minf(current_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
||||
player.velocity.x = dir.x * jump_speed
|
||||
player.velocity.z = dir.z * jump_speed
|
||||
machine.current_jump_count = 1
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.on_ground = false
|
||||
machine.register_chain_mechanic("slide_jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
|
||||
func _get_capsule() -> CapsuleShape3D:
|
||||
for child in machine.player.get_children():
|
||||
if child is CollisionShape3D and child.shape is CapsuleShape3D:
|
||||
return child.shape as CapsuleShape3D
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user