extends Node class_name StateWallRun ## Wall run that preserves momentum: on attach we keep speed along the wall ## (plus some upward carry) and accelerate toward wall_run_speed instead of ## hard-setting velocity, so entering fast stays fast and entering slow ramps ## up smoothly. Gravity fades in over the run so the start feels planted. var machine: MovementStateMachine var params: MovementParams: get: return machine.params var elapsed: float = 0.0 var _current_tangent: Vector3 = Vector3.ZERO func enter(_data: Dictionary = {}) -> void: elapsed = 0.0 machine.register_chain_mechanic("wall_run") machine.on_ground = false # Keep some upward carry so jumping into a wall run flows; the wall # still "catches" downward momentum. machine.player.velocity.y = clampf(machine.player.velocity.y, 0.0, params.wall_run_entry_max_up) if machine.player.wallrun_player and not machine.player.wallrun_player.playing: machine.player.wallrun_player.play() var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z) if machine.player.has_method("update_wallrun_audio"): machine.player.update_wallrun_audio(hvel.length(), machine.wall_normal) _current_tangent = machine.wall_normal.cross(Vector3.UP).normalized() if hvel.dot(_current_tangent) < 0.0: _current_tangent = -_current_tangent # Camera tilt var rig = _get_camera_rig() if rig: rig.set_wall_tilt(machine.wall_side) func exit() -> void: # Clear camera tilt var rig = _get_camera_rig() if rig: rig.clear_wall_tilt() if machine.player.wallrun_player: machine.player.wallrun_player.stop() func update(delta: float) -> void: elapsed += delta if elapsed > params.wall_run_duration: _detach(0.4) return var player := machine.player var vel: Vector3 = player.velocity if player.wallrun_player and not player.wallrun_player.playing: player.wallrun_player.play() # ── Gradual gravity pull (starts weightless, ramps up) ──────────────── var gravity_factor := 0.05 + 0.95 * pow(elapsed / params.wall_run_duration, 1.5) vel.y -= params.wall_run_gravity * gravity_factor * delta # ── Accelerate along wall tangent (keeps momentum, no hard set) ─────── var wall_tangent := machine.wall_normal.cross(Vector3.UP).normalized() if _current_tangent.dot(wall_tangent) < 0.0: wall_tangent = -wall_tangent _current_tangent = wall_tangent var hvel := Vector3(vel.x, 0.0, vel.z) var along := hvel.dot(wall_tangent) if player.has_method("update_wallrun_audio"): player.update_wallrun_audio(hvel.length(), machine.wall_normal) var target_speed: float = maxf(machine.get_effective_speed(params.wall_run_speed), along) along = move_toward(along, target_speed, params.wall_run_accel * delta) # Redirect all horizontal velocity along the wall (kills the into-wall part) hvel = wall_tangent * along vel.x = hvel.x vel.z = hvel.z # ── Look away to break wall run ─────────────────────────────────────── var look_dir := -player.global_transform.basis.z if look_dir.dot(machine.wall_normal) > 0.4: _detach(0.3) return # ── Wall jump ───────────────────────────────────────────────────────── if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0: # Base jump off velocity on current preserved momentum var jump_vel: Vector3 = _current_tangent * along # Spring off the wall jump_vel += machine.wall_normal * params.wall_run_jump_off_normal var h_look := Vector3(look_dir.x, 0.0, look_dir.z) if h_look.length_squared() > 0.01: jump_vel += h_look.normalized() * params.wall_run_jump_horizontal jump_vel.y = params.wall_run_auto_jump_speed player.velocity = jump_vel machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 machine.wall_cooldown_timer = 0.3 machine.register_chain_mechanic("wall_jump") machine.jump_cooldown_timer = params.jump_cooldown machine.movement_event.emit("jump", {}) if player.jump_player: player.jump_player.play() machine.switch_to("air") return # ── Wall cling (no movement input) ──────────────────────────────────── if machine.input_dir.length() < 0.1: machine.switch_to("wall_cling") return # ── Push slightly toward wall to maintain contact ───────────────────── vel -= machine.wall_normal * 2.0 player.velocity = vel player.move_and_slide() # ── Check still on wall ─────────────────────────────────────────────── var still_on_wall := machine.detect_wall_horizontal() if still_on_wall == Vector3.ZERO: _detach(0.0) return # ── Hit floor during wall run ───────────────────────────────────────── if player.is_on_floor(): machine.on_ground = true machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 machine.current_jump_count = 0 machine.switch_to("ground") func _detach(cooldown: float) -> void: machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 if cooldown > 0.0: machine.wall_cooldown_timer = cooldown machine.switch_to("air") func _get_camera_rig(): if machine.player and machine.player.has_node("HeadPivot"): return machine.player.get_node("HeadPivot") return null