Grabs And Throws Examples

Grab scripts are state machines shared by two characters: the attacker and the victim. The attacker usually owns the script, but the script must keep the victim aligned, vulnerable or invulnerable, animated, and released at the correct time.

The examples below use neutral animation IDs and behaviour modes. Replace them with the IDs used by your character logic.

Important Grab Rules

  • character.victim resolves attacked_character_id. It becomes nil if no valid victim is connected.
  • front_grab_offset is read from the attacker's logic data. It is the stable anchor for holding the victim in front of the attacker.
  • position_offset_y controls height. A grabbed airborne victim usually has a negative offset.
  • mode_specific_timer is used by several original grab behaviours as a release timer. Holding it high prevents unwanted automatic release.
  • try_grab(target, check_vicinity, enforcement, snap_to_victim) is useful when a script needs to convert a hit or branch back into a grab.
  • cancel_grab() clears the grab relationship before a throw release or recovery.

Force A Back Grab Into A Front Grab

Attach this to a back-grab hold or back-grab transition. When the animation ends, it turns the victim around and enters the normal front-grab hold.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    player.is_vulnerable = true

    if player.is_animation_over and player.victim ~= nil then
        -- Face the victim toward the attacker. Front-grab logic expects this.
        player.victim.is_facing_right = not player.is_facing_right

        -- Animation 4 is the example front-grab hold.
        -- Object mode 26 is the example front-grab behaviour mode.
        player.set_animation(4)
        player.object_mode = 26

        -- We made a decisive state change. Stop the previous grab controller.
        return false
    end

    return true
end

Grab Hit Repositioning

Use this when a grab attack has an impact section and then returns to a hold. The victim offset changes during the hit frames, then snaps back to the carry offset.

local grabbed_offset_x = 1040
local grabbed_offset_y = -1800

local hit_offset_x = 100
local hit_offset_y = -1500

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    player.is_vulnerable = true
    player.object_mode = 22

    local victim = player.victim

    if victim == nil then
        return true
    end

    victim.is_vulnerable = true

    if player.is_animation_over then
        -- Return to the hold animation at the frame where the victim is already
        -- aligned. This avoids replaying the initial snap frames.
        player.set_animation(4, 9)
    end

    if player.current_animation_frame < 6 then
        victim.position_offset_y = hit_offset_y
        victim.position_offset_x = hit_offset_x
    else
        victim.position_offset_y = grabbed_offset_y
        victim.position_offset_x = grabbed_offset_x
    end

    if not player.is_facing_right then
        victim.position_offset_x = -victim.position_offset_x
    end

    return true
end

Walk While Holding A Victim

This is the core grab-hold controller: keep the victim attached, let the attacker walk, and branch into throws or jumps.

local grabbed_offset_x = 1040
local grabbed_offset_y = -1800

local grab_walk_speed_x = 80
local grab_walk_speed_y = 50

local prepare_jump_offset_x = 800
local prepare_jump_offset_y = -1000

local function get_extra_bob_offset(player)
    local frame = player.current_animation_frame % 35

    -- Small vertical offsets make the victim follow the walk cycle.
    -- The values are in script units, not sprite pixels.
    if frame < 3 then
        return 4
    elseif (frame >= 3 and frame < 6) or frame >= 33 then
        return 3
    elseif (frame >= 6 and frame < 9) or
           (frame >= 18 and frame < 27) or
           frame >= 30 then
        return 2
    end

    return 0
end

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    player.object_mode = 26
    player.is_vulnerable = true

    local victim = player.victim

    if victim == nil then
        return true
    end

    victim.is_vulnerable = true

    -- Hold the original release timer high so this script decides when the grab
    -- ends. Without this, inherited grab logic can release the victim early.
    player.mode_specific_timer = 99

    if player.pressed_button_b then
        if player.is_holding_backward or player.is_holding_forward then
            -- Direction plus attack starts the example slam.
            victim.is_facing_right = player.is_facing_right
            player.set_animation(5)
            player.object_mode = 28
            return false
        elseif victim.is_in_the_air then
            -- While the victim is airborne, choose one of two follow-up strikes.
            if player.grab_combo_count < 4 then
                player.set_animation(33)
            else
                player.set_animation(34)
            end
            return false
        end
    elseif victim.is_in_the_air then
        if player.pressed_button_c then
            -- Jump while carrying the victim. The victim is pre-positioned before
            -- the jump animation starts so the first airborne frame is stable.
            player.set_animation(3)
            player.impulse_x = 0

            victim.position_offset_y = prepare_jump_offset_y
            victim.position_offset_x = prepare_jump_offset_x
            if not player.is_facing_right then
                victim.position_offset_x = -victim.position_offset_x
            end

            return false
        elseif player.is_holding_backward then
            -- Rewind the hold cycle for a backward tug.
            player.set_animation(44)
            player.current_animation_frame = player.current_animation_frame - 2

            if player.current_animation_frame < 0 then
                player.current_animation_frame = 34
                player.has_just_teleported = true
            end
        elseif player.is_holding_forward or player.is_holding_up or player.is_holding_down then
            player.set_animation(44)
        else
            player.set_animation(4, 9)
        end
    end

    if player.current_animation_id == 4 then
        if player.current_animation_frame < 9 and victim.is_in_the_air then
            -- Skip startup frames when re-entering the hold from an airborne branch.
            player.current_animation_frame = 9
            player.has_just_teleported = true
        elseif player.current_animation_frame >= 9 then
            victim.position_offset_y = grabbed_offset_y
            victim.position_offset_x = grabbed_offset_x
            if not player.is_facing_right then
                victim.position_offset_x = -victim.position_offset_x
            end
        end
    elseif player.current_animation_id == 44 then
        if player.is_holding_right then
            player.velocity_x = grab_walk_speed_x
        elseif player.is_holding_left then
            player.velocity_x = -grab_walk_speed_x
        end

        if player.is_holding_up then
            player.velocity_y = -grab_walk_speed_y
        elseif player.is_holding_down then
            player.velocity_y = grab_walk_speed_y
        end

        local grab_offset = player.front_grab_offset
        if not player.is_facing_right then
            grab_offset = -grab_offset
        end

        victim.position_x = player.position_x + grab_offset
        victim.position_y = player.position_y + 1 + player.velocity_y
        victim.position_offset_y =
            player.position_offset_y + grabbed_offset_y +
            get_extra_bob_offset(player) * -50
    end

    -- Returning whether the victim is airborne lets the reference path continue
    -- only in the case this grab state was designed around.
    return victim.is_in_the_air
end

Air Grab Lift

This carries a victim upward, lets the player steer before the lift begins, and keeps the victim pinned to the attacker until landing.

local grabbed_offset_x = 1040
local grabbed_offset_y = -1800

local vertical_impulse = -340
local horizontal_impulse = 131

local prepare_jump_offset_x = 800
local prepare_jump_offset_y = -1000

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    player.is_vulnerable = true

    if player.check_being_hit() then
        return false
    end

    if player.current_animation_frame < 4 then
        -- Allow tiny horizontal aiming before the lift begins.
        if player.is_holding_left then
            player.impulse_x = -horizontal_impulse
        elseif player.is_holding_right then
            player.impulse_x = horizontal_impulse
        end

        return false
    elseif player.current_animation_frame == 4 then
        player.impulse_y = vertical_impulse
    end

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

    local victim = player.victim

    if player.impulse_y > 0 and player.position_offset_y >= 0 then
        -- Land back into the hold and prepare the victim for the next branch.
        player.set_animation(47)
        player.impulse_y = 0
        player.position_offset_y = 0

        if victim ~= nil then
            victim.position_offset_y = prepare_jump_offset_y
            victim.position_offset_x = prepare_jump_offset_x
            if not player.is_facing_right then
                victim.position_offset_x = -victim.position_offset_x
            end
        end

        return false
    end

    if victim == nil then
        return false
    end

    victim.is_vulnerable = true
    victim.position_offset_x = grabbed_offset_x

    local grab_offset = player.front_grab_offset
    if not player.is_facing_right then
        victim.position_offset_x = -victim.position_offset_x
        grab_offset = -grab_offset
    end

    victim.position_x = player.position_x + grab_offset
    victim.position_y = player.position_y + 1 + player.velocity_y
    victim.position_offset_y = player.position_offset_y + grabbed_offset_y

    return false
end

Piledriver-Style Throw

This is a full manual throw controller. It disables victim vulnerability while carried, applies throw damage on impact, cancels the grab, and puts the attacker into a rebound animation.

local ground_level = 1000

local function get_victim_offset(player)
    local frame = player.current_animation_frame

    if frame < 4 then
        return -150, -1800
    elseif frame < 10 then
        return -500, -1300
    end

    return -1100, -1000
end

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

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

    if player.current_animation_id == 50 then
        -- Rebound/recovery animation after impact.
        if player.impulse_y > 0 and player.position_offset_y >= 0 then
            player.impulse_x = 0
            player.impulse_y = 0
            player.position_offset_y = 0
            player.object_mode = 2
            player.is_vulnerable = true
            player.set_animation(15)
        end

        return false
    end

    player.is_vulnerable = false

    if player.current_animation_frame == 0 then
        player.play_sound("My Pack", "throw-start", 2)
    end

    local victim = player.victim

    if player.impulse_y > 0 and player.position_offset_y >= ground_level then
        player.cancel_grab()

        -- Bounce the attacker away from the impact.
        player.impulse_x = 100
        if player.is_facing_right then
            player.impulse_x = -player.impulse_x
        end
        player.impulse_y = -200
        player.position_offset_y = -10
        player.set_animation(50)
        player.object_mode = -1

        if victim ~= nil then
            victim.position_offset_y = -10

            -- Preserve dying mode if the victim was already in it.
            local victim_was_dying = victim.object_mode == 6

            -- apply_throw releases the victim with throw damage and vertical force.
            victim.apply_throw(-2, -220, 0)
            victim.is_being_thrown = false
            victim.set_animation(20, 20)

            if victim_was_dying then
                victim.object_mode = 6
            end
        end

        return false
    end

    if victim == nil then
        return false
    end

    victim.is_vulnerable = false
    victim.is_being_thrown = true
    victim.is_facing_right = player.is_facing_right
    victim.set_animation(18)
    victim.damage_taken = 56

    if victim.current_animation_frame < 16 then
        victim.current_animation_frame = 16
    end

    local offset_x, offset_y = get_victim_offset(player)
    victim.position_offset_x = offset_x

    local grab_offset = player.front_grab_offset
    if not player.is_facing_right then
        victim.position_offset_x = -victim.position_offset_x
        grab_offset = -grab_offset
    end

    victim.position_x = player.position_x + grab_offset
    victim.position_y = player.position_y + 1 + player.velocity_y
    victim.position_offset_y = player.position_offset_y + offset_y

    return false
end

Keep A Victim Airborne At Combo End

Use this at the end of a grab combo when you want a launcher route. The script keeps the victim above the ground and lets their default physics apply the new impulse.

return function()
    local player = game.this_player

    if player == nil then
        return true
    end

    local victim = player.victim

    if victim ~= nil and victim.impulse_y > -300 then
        victim.impulse_y = -300

        -- Apply the victim's normal physics immediately so the new impulse takes
        -- effect during this frame instead of waiting for their next update.
        victim.apply_default_physics()

        return false
    end

    if player.current_animation_frame == 0 and
       player.is_holding_down and
       victim ~= nil and
       player.try_grab(victim) then
        -- Convert the combo end back into a throw if down is held.
        victim.is_facing_right = player.is_facing_right
        player.set_animation(5)
        player.object_mode = 28
        return false
    end

    player.combo_count = 0
    player.extra_delay_after_move = 8

    return true
end