Files
Papay-Shooter/movement/states/state_grapple.gd
T
Nicholas ButzkeandClaude Fable 5 0ba14a9469 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]>
2026-07-17 11:58:14 -04:00

111 lines
4.0 KiB
GDScript

extends Node
class_name StateGrapple
## Swing grapple with active rope control:
## - the rope acts as a hard pendulum constraint at its current length
## - holding forward reels in (rope shortens, converts to speed)
## - holding back pays rope out (up to the original latch length)
## - strafe input steers tangentially around the swing sphere
## - jump detaches with a boost along your current motion
var machine: MovementStateMachine
var params: MovementParams:
get: return machine.params
var _rope_length: float = 0.0
func enter(_data: Dictionary = {}) -> void:
machine.on_ground = false
machine.wall_normal = Vector3.ZERO
machine.wall_side = 0.0
machine.register_chain_mechanic("grapple")
_rope_length = machine.grapple_length
func exit() -> void:
pass
func update(delta: float) -> void:
var player := machine.player
var vel: Vector3 = player.velocity
# If player releases the button, disconnect
if not machine.input_grapple:
machine.switch_to("air")
return
var grapple_pos: Vector3 = machine.grapple_point
var to_point: Vector3 = grapple_pos - player.global_position
var current_dist: float = to_point.length()
if current_dist < 0.01:
machine.switch_to("air")
return
var dir_to_point: Vector3 = to_point / current_dist
# Jump to detach and get a boost
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
vel.y = maxf(vel.y, params.jump_velocity)
# Boost along current motion so the release keeps the swing's energy
var h_vel := Vector3(vel.x, 0.0, vel.z)
if h_vel.length_squared() > 0.1:
var boost := h_vel.normalized() * params.grapple_jump_boost * 0.5
vel.x += boost.x
vel.z += boost.z
# Plus a steer boost toward held direction
var h_look = machine.wish_dir_world
if h_look.length_squared() > 0.1:
var h_boost = h_look.normalized() * params.grapple_jump_boost * 0.5
vel.x += h_boost.x
vel.z += h_boost.z
player.velocity = vel
machine.jump_cooldown_timer = params.jump_cooldown
machine.register_chain_mechanic("grapple_jump")
machine.switch_to("air")
if player.jump_player:
player.jump_player.play()
return
# Apply gravity
vel.y -= params.gravity * delta
# Base reel: constant gentle pull toward the hook
vel += dir_to_point * params.grapple_pull_force * delta
# ── Active rope control ───────────────────────────────────────────────
var fwd_input := -machine.input_dir.y # +1 holding forward, -1 back
if fwd_input > 0.1:
# Reel in: shorten rope and pull hard — converts to swing speed
vel += dir_to_point * params.grapple_reel_force * fwd_input * delta
_rope_length = maxf(_rope_length - 8.0 * fwd_input * delta, 2.0)
elif fwd_input < -0.1:
# Pay out rope back toward the original latch length
_rope_length = minf(_rope_length + 8.0 * -fwd_input * delta, machine.grapple_length)
# Keep the working length taut to where we actually are (swinging inside
# the sphere shortens the constraint, giving crisp Tarzan arcs)
_rope_length = minf(_rope_length, maxf(current_dist, 2.0))
# ── Pendulum constraint at the current rope length ────────────────────
if current_dist > _rope_length:
# Kill outward radial velocity (the rope is taut and inextensible)
var radial_vel = vel.project(dir_to_point)
if radial_vel.dot(dir_to_point) < 0:
vel -= radial_vel
# Spring correction toward the sphere surface
var diff = current_dist - _rope_length
vel += dir_to_point * diff * params.grapple_spring_strength * delta
# ── Tangential steering (strafe around the swing sphere) ──────────────
var strafe_input := machine.input_dir.x
if absf(strafe_input) > 0.1:
var right := player.global_transform.basis.x
var tangent_steer := right - right.project(dir_to_point)
if tangent_steer.length_squared() > 0.01:
vel += tangent_steer.normalized() * params.grapple_air_control * strafe_input * delta
player.velocity = vel
player.move_and_slide()