extends Node class_name StateAir var machine: MovementStateMachine var params: MovementParams: get: return machine.params func enter(_data: Dictionary = {}) -> void: machine.on_ground = false func exit() -> void: pass func update(delta: float) -> void: var player := machine.player var vel: Vector3 = player.velocity # ── Gravity with variable jump height ───────────────────────────────── var grav := params.gravity if vel.y < 0.0: # Falling: heavier gravity for snappy arcs grav *= params.fall_multiplier elif vel.y > 0.0 and not machine.input_jump_pressed: # Released jump early: cut the jump short grav *= params.low_jump_multiplier vel.y -= grav * delta # ── Air control ─────────────────────────────────────────────────────── var wish_dir: Vector3 = machine.wish_dir_world var hvel := Vector3(vel.x, 0.0, vel.z) if wish_dir.length_squared() > 0.01: wish_dir = wish_dir.normalized() var current_speed = hvel.length() # Add a strong force in the wish direction var air_accel = params.air_strafe_accel * delta var new_hvel = hvel + wish_dir * air_accel # Limit the speed so we don't gain speed purely from air control. # We only allow air control to change our direction (strafing) # or to accelerate us up to our walk speed if we started slow. var max_allowed_speed = maxf(current_speed, params.walk_speed) if new_hvel.length() > max_allowed_speed: new_hvel = new_hvel.normalized() * max_allowed_speed hvel = new_hvel else: # No input: slight air drag (very subtle) hvel *= (1.0 - 0.5 * delta) vel.x = hvel.x vel.z = hvel.z player.velocity = vel player.move_and_slide() # ── Landing ─────────────────────────────────────────────────────────── if player.is_on_floor(): machine.on_ground = true machine.current_jump_count = 0 # Bunny hop: if jump was buffered or pressed on landing frame if (machine.input_jump_pressed or machine.jump_buffer_time > 0.0) and machine.jump_cooldown_timer <= 0.0: var land_speed := Vector3(player.velocity.x, 0.0, player.velocity.z).length() var bhop_speed := maxf(land_speed, minf(land_speed + params.bunny_hop_speed_gain, params.bunny_hop_speed_cap)) # Maintain horizontal direction, boost speed var hdir := Vector3(player.velocity.x, 0.0, player.velocity.z) if hdir.length_squared() > 0.01: hdir = hdir.normalized() else: hdir = machine.wish_dir_world player.velocity.x = hdir.x * bhop_speed player.velocity.z = hdir.z * bhop_speed player.velocity.y = params.jump_velocity * params.bunny_hop_impulse machine.current_jump_count = 1 machine.on_ground = false machine.jump_buffer_time = 0.0 machine.register_chain_mechanic("bunny_hop") machine.jump_cooldown_timer = params.jump_cooldown if player.jump_player: player.jump_player.play() # Stay in air state return machine.switch_to("ground") return # ── Double Jump ─────────────────────────────────────────────────────── if machine.input_jump_just_pressed and machine.current_jump_count < params.double_jump_max_count and machine.jump_cooldown_timer <= 0.0: player.velocity.y = params.double_jump_velocity machine.jump_cooldown_timer = params.jump_cooldown machine.current_jump_count += 1 machine.register_chain_mechanic("double_jump") if player.double_jump_player: player.double_jump_player.play() return # ── Wall interaction (Run, Climb, Vault) ────────────────────────────── if machine.input_dir.length() > 0.1 and machine.input_dir.y <= 0.0 and machine.wall_cooldown_timer <= 0.0: var fwd_wall = machine.detect_wall_forward() if fwd_wall.hit: if fwd_wall.is_short: # Instant Vault 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() 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_cooldown_timer = 0.3 return else: # Check if moving directly into wall and looking generally towards it var look_dir := -player.global_transform.basis.z var looking_towards := look_dir.dot(-fwd_wall.normal) > 0.0 var moving_towards := wish_dir.dot(-fwd_wall.normal) > cos(deg_to_rad(params.wall_climb_max_angle)) if looking_towards and moving_towards and machine.can_wall_climb: machine.wall_normal = fwd_wall.normal machine.switch_to("wall_climb") return # Fallback to wall run var wall_n := machine.detect_wall_horizontal() if wall_n != Vector3.ZERO: var w_look_dir := -player.global_transform.basis.z var h_look := Vector3(w_look_dir.x, 0.0, w_look_dir.z).normalized() # Require looking mostly along the wall (angle > 41 degrees from normal) # and inputting mostly along the wall (angle > 31 degrees from normal, allows W+A/D diagonally into wall) if abs(h_look.dot(wall_n)) < 0.75 and abs(wish_dir.dot(wall_n)) < 0.85: machine.switch_to("wall_run") return # ── Dash ────────────────────────────────────────────────────────────── if machine.input_dash: machine.switch_to("dash") return