extends Node class_name StateWallClimb var machine: MovementStateMachine var params: MovementParams: get: return machine.params var elapsed: float = 0.0 func enter(_data: Dictionary = {}) -> void: elapsed = 0.0 machine.register_chain_mechanic("wall_climb") machine.on_ground = false if machine.player.wallrun_player and not machine.player.wallrun_player.playing: machine.player.wallrun_player.play() # Carry existing upward momentum into the climb so jump→climb chains flow var player := machine.player player.velocity.y = maxf(player.velocity.y, params.wall_climb_speed) var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z) player.velocity.x = hvel.x * 0.5 player.velocity.z = hvel.z * 0.5 func exit() -> void: if machine.player.wallrun_player: machine.player.wallrun_player.stop() func update(delta: float) -> void: elapsed += delta var player := machine.player if player.wallrun_player and not player.wallrun_player.playing: player.wallrun_player.play() # ── Time limit ──────────────────────────────────────────────────────── if elapsed > params.wall_climb_duration: _detach() return # ── Look/Move away to transition to wall run ────────────────────────── var look_dir := -player.global_transform.basis.z var wish_dir := machine.wish_dir_world var looking_towards := look_dir.dot(-machine.wall_normal) > 0.0 var moving_sideways := false if wish_dir.length_squared() > 0.01: moving_sideways = wish_dir.dot(-machine.wall_normal) <= cos(deg_to_rad(params.wall_climb_max_angle)) if not looking_towards or moving_sideways: # We are looking away or moving sideways, transition to wall run # The wall run state will figure out tangent and side var wall_n = machine.detect_wall_horizontal() if wall_n != Vector3.ZERO: machine.switch_to("wall_run") return else: _detach() return # ── Jump off the wall (backward kick) ───────────────────────────────── if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0: player.velocity = machine.wall_normal * params.wall_run_jump_off_normal player.velocity.y = params.jump_velocity 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() _detach() return # ── Ledge Vault Check ───────────────────────────────────────────────── var fwd_wall = machine.detect_wall_forward() if not fwd_wall.hit: # The forward lower ray missed, we fell off the wall completely _detach() return if fwd_wall.is_short: # The upper ray missed, meaning we reached the top of the wall! machine.do_vault() machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 machine.switch_to("air") return # ── Movement: ease out over the climb ───────────────────────────────── var speed_factor: float = pow(maxf(0.0, 1.0 - (elapsed / params.wall_climb_duration)), 0.7) player.velocity.y = params.wall_climb_speed * speed_factor # Push slightly into the wall to maintain contact player.velocity -= machine.wall_normal * 2.0 player.move_and_slide() # ── Hit floor during wall climb (should be rare) ────────────────────── 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() -> void: machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 machine.wall_cooldown_timer = 0.3 machine.can_wall_climb = false machine.switch_to("air")