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() # Give an initial upward boost var player := machine.player 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 # ── 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! _vault() return # ── Movement ────────────────────────────────────────────────────────── # Slow down over time var speed_factor = maxf(0.0, 1.0 - (elapsed / params.wall_climb_duration)) 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") func _vault() -> void: var player := machine.player var look_dir := -player.global_transform.basis.z var h_look := Vector3(look_dir.x, 0.0, look_dir.z) if h_look.length_squared() > 0.01: h_look = h_look.normalized() # Impulse player.velocity = h_look * params.wall_climb_vault_forward player.velocity.y = params.wall_climb_vault_up if player.vault_player: player.vault_player.play() # Quick camera animation (tilt up and forward bob) var camera = player.camera if camera: var tween = player.create_tween() tween.tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x + 10.0, 0.15).set_trans(Tween.TRANS_SINE) tween.parallel().tween_property(camera, "v_offset", -0.2, 0.15).set_trans(Tween.TRANS_SINE) tween.chain().tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x, 0.2).set_trans(Tween.TRANS_SINE) tween.parallel().tween_property(camera, "v_offset", 0.0, 0.2).set_trans(Tween.TRANS_SINE) machine.wall_normal = Vector3.ZERO machine.wall_side = 0.0 machine.wall_cooldown_timer = 0.3 machine.switch_to("air")