Movement And Input Examples

These scripts cover player-owned movement: air steering, jump height, wall jumps, speed tiers, rolling momentum, and charge release. They are adapted from several Test chronicle prototypes and cleaned up into reusable patterns.

Simple Air Steering

Attach this to a jump or aerial attack when you want the original controller to keep handling landing and collisions.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    -- Absolute left/right input is used for air steering because the player
    -- expects left to move left even if the character is facing right.
    local max_air_speed = 130
    local acceleration = 7

    if player.is_in_the_air then
        if player.is_holding_right and player.impulse_x < max_air_speed then
            player.impulse_x = player.impulse_x + acceleration

            -- The previous-frame flag identifies the first held frame after a tap.
            -- Snap to the starting speed once, then clear the old input state so
            -- the snap is not applied again next frame.
            if player.was_previously_holding_right then
                player.impulse_x = max_air_speed
                player.previous_held_buttons = 0
            end
        elseif player.is_holding_left and player.impulse_x > -max_air_speed then
            player.impulse_x = player.impulse_x - acceleration

            if player.was_previously_holding_left then
                player.impulse_x = -max_air_speed
                player.previous_held_buttons = 0
            end
        end
    end

    -- Only steering was changed. Let the jump controller keep running.
    return true
end

Walk And Run Air-Control Profiles

Use tiny setup scripts on different jump-start animations, then share one air-control script across all aerial states.

Walk-jump setup:

return function()
    local player = game.this_player

    if player ~= nil then
        -- Profile 1 means normal walk-jump steering.
        player.custom_value_1 = 1
    end

    return true
end

Run-jump setup:

return function()
    local player = game.this_player

    if player ~= nil then
        -- Profile 2 means faster run-jump steering.
        player.custom_value_1 = 2
    end

    return true
end

Shared aerial script:

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local initial_velocity = 50
    local max_velocity = 110
    local acceleration = 16

    if player.custom_value_1 == 2 then
        initial_velocity = 100
        max_velocity = 190
        acceleration = 20
    end

    if player.is_in_the_air then
        if player.is_holding_right and player.impulse_x < max_velocity then
            player.impulse_x = player.impulse_x + acceleration
            if player.was_previously_holding_right then
                player.impulse_x = initial_velocity
                player.previous_held_buttons = 0
            end
        elseif player.is_holding_left and player.impulse_x > -max_velocity then
            player.impulse_x = player.impulse_x - acceleration
            if player.was_previously_holding_left then
                player.impulse_x = -initial_velocity
                player.previous_held_buttons = 0
            end
        end
    else
        -- Grounded direction changes update facing and reset to normal profile.
        if player.is_holding_right and not player.is_facing_right then
            player.is_facing_right = true
            player.custom_value_1 = 1
        elseif player.is_holding_left and player.is_facing_right then
            player.is_facing_right = false
            player.custom_value_1 = 1
        end
    end

    return true
end

Variable Jump Height

This script shortens upward momentum when the player releases jump early. It can be layered into an existing jump script that still returns true.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local small_jump_force = 262

    -- Negative impulse_y means the character is rising.
    -- If the jump button is no longer held, clamp upward speed.
    if player.impulse_y < -small_jump_force and not player.is_holding_button_c then
        player.impulse_y = -small_jump_force
    end

    return true
end

Full Manual Air Physics

Use this when an aerial attack owns movement and landing. It handles hits, gravity, horizontal steering, wall stopping, and landing.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local gravity = 14
    local air_acceleration = 6
    local top_speed = 393

    -- Since this script replaces the controller, it must check interruptions.
    if player.check_being_hit() then
        return false
    end

    -- Let active hitboxes connect. If the move hits something, rebound both
    -- horizontal and vertical impulses to produce a bounce-like feel.
    if player.check_hitting_entities() then
        player.impulse_x = -player.impulse_x
        player.impulse_y = -player.impulse_y
    end

    player.impulse_y = player.impulse_y + gravity

    if player.is_holding_left then
        player.impulse_x = player.impulse_x - air_acceleration
    elseif player.is_holding_right then
        player.impulse_x = player.impulse_x + air_acceleration
    end

    if player.impulse_x > top_speed then
        player.impulse_x = top_speed
    elseif player.impulse_x < -top_speed then
        player.impulse_x = -top_speed
    end

    player.velocity_x = player.impulse_x
    player.position_offset_y = player.position_offset_y + player.impulse_y

    if player.is_pressing_against_walls(true) then
        player.impulse_x = 0
    end

    if player.impulse_y > 0 and player.position_offset_y >= 0 then
        player.impulse_y = 0
        player.position_offset_y = 0
        player.set_animation(0)
        player.object_mode = 2
    end

    return false
end

Wall Jump

This script freezes briefly on the wall, flips facing once, then lets the player launch away.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local wall_freeze_frames = 8

    if player.current_animation_frame < wall_freeze_frames then
        if player.current_animation_frame == 0 then
            -- Flip once at the start so forward now points away from the wall.
            player.is_facing_right = not player.is_facing_right
        end

        -- Stop the normal controller while the character clings to the wall.
        return false
    end

    local away_speed = 131
    if player.is_holding_forward then
        away_speed = 196
    elseif player.is_holding_backward then
        away_speed = 65
    end

    player.velocity_x = away_speed * player.direction_signal
    player.impulse_y = player.impulse_y + 13
    player.position_offset_y = player.position_offset_y + player.impulse_y

    if player.pressed_button_c and player.is_pressing_against_walls(true) then
        player.set_animation(56)
        player.impulse_y = -307
        player.play_sound("My Pack", "wall-jump", 2)
    end

    return false
end

Rolling Momentum Controller

This is a fuller player controller: acceleration, deceleration, depth movement, wall checks, jump, pickup, roll transition, and animation speed control.

local acceleration_x = 3
local deceleration_x = 32
local friction_x = 3
local top_speed_x = 393

local acceleration_z = 1
local deceleration_z = 17
local friction_z = 1
local top_speed_z = 196

local breaking_speed = 262
local rolling_minimum_speed = 32
local jump_force = 426

local function abs(value)
    if value < 0 then
        return -value
    end
    return value
end

local function stop_all_motion(player)
    player.velocity_x = 0
    player.velocity_y = 0
    player.impulse_x = 0
    player.impulse_z = 0
end

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    if player.check_being_hit_or_grabbed() then
        return false
    end

    local is_breaking = false

    if player.is_holding_left then
        if player.impulse_x > 0 then
            is_breaking = true
            player.impulse_x = player.impulse_x - deceleration_x
            if player.impulse_x < 0 then
                player.impulse_x = 0
            end
        elseif player.impulse_x > -top_speed_x then
            player.impulse_x = player.impulse_x - acceleration_x
        end
    elseif player.is_holding_right then
        if player.impulse_x < 0 then
            is_breaking = true
            player.impulse_x = player.impulse_x + deceleration_x
            if player.impulse_x > 0 then
                player.impulse_x = 0
            end
        elseif player.impulse_x < top_speed_x then
            player.impulse_x = player.impulse_x + acceleration_x
        end
    else
        if player.impulse_x > 0 then
            player.impulse_x = player.impulse_x - friction_x
            if player.impulse_x < 0 then
                player.impulse_x = 0
            end
        elseif player.impulse_x < 0 then
            player.impulse_x = player.impulse_x + friction_x
            if player.impulse_x > 0 then
                player.impulse_x = 0
            end
        end
    end

    if player.impulse_x > top_speed_x then
        player.impulse_x = top_speed_x
    elseif player.impulse_x < -top_speed_x then
        player.impulse_x = -top_speed_x
    end

    -- Depth movement uses impulse_z and velocity_y.
    if player.is_holding_up then
        player.impulse_z = player.impulse_z - acceleration_z
    elseif player.is_holding_down then
        player.impulse_z = player.impulse_z + acceleration_z
    else
        if player.impulse_z > 0 then
            player.impulse_z = player.impulse_z - friction_z
            if player.impulse_z < 0 then
                player.impulse_z = 0
            end
        elseif player.impulse_z < 0 then
            player.impulse_z = player.impulse_z + friction_z
            if player.impulse_z > 0 then
                player.impulse_z = 0
            end
        end
    end

    if player.impulse_z > top_speed_z then
        player.impulse_z = top_speed_z
    elseif player.impulse_z < -top_speed_z then
        player.impulse_z = -top_speed_z
    end

    player.velocity_x = player.impulse_x
    player.velocity_y = player.impulse_z

    local total_speed = abs(player.impulse_x) + abs(player.impulse_z)

    if player.is_pressing_against_walls(true) then
        if player.is_holding_left or player.is_holding_right then
            player.set_animation(44)
        else
            player.impulse_x = 0
        end
        total_speed = 0
    end

    if player.pressed_button_b and total_speed < rolling_minimum_speed and player.check_pick_up_item() then
        player.set_animation(14)
        player.object_mode = 44
        stop_all_motion(player)
        return false
    end

    if player.pressed_button_c or player.pressed_button_a then
        player.set_animation(2)
        player.impulse_y = -jump_force
        player.play_sound("My Pack", "jump", 2)
        return false
    end

    if player.is_holding_button_b then
        if total_speed < rolling_minimum_speed then
            player.set_animation(46)
            stop_all_motion(player)
        else
            player.set_animation(48)
            player.play_sound("My Pack", "roll", 2)
        end
        return false
    end

    if player.impulse_x ~= 0 then
        player.is_facing_right = player.impulse_x > 0
    end

    if is_breaking and total_speed > breaking_speed then
        player.set_animation(49)
    elseif total_speed >= top_speed_x then
        player.set_animation(40)
    elseif total_speed > 0 then
        player.set_animation(1)

        -- Slower frame advance at low speed. Animation frames normally advance
        -- after scripts, so rewinding by one frame holds the frame longer.
        local frame_duration = 9 - (total_speed / 65)
        player.custom_value_1 = player.custom_value_1 + 1
        if player.custom_value_1 >= frame_duration then
            player.custom_value_1 = 0
        else
            player.current_animation_frame = player.current_animation_frame - 1
        end
    else
        player.set_animation(0)
    end

    return false
end

Charge And Release Dash

Hold one button to charge, tap another button to increase charge, release to dash.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local max_dash_speed = 786

    if player.check_being_hit() then
        return false
    end

    -- Gradually bleed charge so repeated taps matter.
    player.custom_value_2 = player.custom_value_2 - player.custom_value_2 / 32

    if player.pressed_button_c then
        player.custom_value_2 = player.custom_value_2 + 2

        local pitch_percentage = 100 + player.custom_value_2 * 2
        if pitch_percentage > 170 then
            pitch_percentage = 170
        end

        player.play_sound("My Pack", "charge-tick", 2, pitch_percentage)
        player.jump_spam_timer = 0
        player.special_spam_timer = 0
    end

    if not player.is_holding_button_b then
        player.set_animation(48)

        local dash_speed = 8 + player.custom_value_2 / 2
        dash_speed = (dash_speed * 255 * 255) / 1000
        if dash_speed > max_dash_speed then
            dash_speed = max_dash_speed
        end

        player.impulse_x = dash_speed * player.direction_signal
        player.custom_value_2 = 0
        player.jump_spam_timer = 0
        player.special_spam_timer = 0
        player.play_sound("My Pack", "dash-release", 2)
    end

    return false
end