Events And Script Timing

Scripts run as part of entity movement logic. They are most commonly attached to an animation in logic.json, next to duration, hitboxes, body boxes, throw logic, logic events, and animation behaviour.

Animation Update Order

For local player, enemy, and item controllers, the important order is:

  1. Pack-level pre-entity scripts run.
  2. Logic events on the current animation run.
  3. If the animation has events, default physics may run before Lua.
  4. For a local custom enemy, its entity-level aiScript runs.
  5. If the AI script returns false, the entity update stops for this frame.
  6. The current animation's script runs.
  7. If the animation script returns false, the entity update stops for this frame.
  8. If it returns true, animation behaviour can run.
  9. If no animation behaviour handles the move, fallback/original behaviour can run.
  10. Pack-level post-entity scripts run.
  11. Later in the frame, animation frame counters advance.

This order is why frame checks work: current_animation_frame == 0 is visible to the script before the animation system increments it.

Events Vs Scripts Vs Animation Behaviour

Use logic events for deterministic frame-bound actions:

  • play a sound at frame 4
  • set velocity at frame 7
  • spawn an item at frame 12
  • jump to another animation on landing
  • enable or disable invincibility

Use Lua when the action needs state:

  • choose an animation based on distance or input
  • detect a parry window by predicting incoming hits
  • alter damage only while blocking
  • run custom projectile physics
  • use custom values as per-entity timers
  • stop the reference controller for only some frames

Use animation behaviour when you want the original Classic controller to keep driving the move after your custom events or script. If a script needs to inspect or assign object_mode, look up the technical values in Entity Types and Behaviour Modes.

Use an entity-level enemy aiScript when a decision must apply across several animations or states—for example choosing targets while standing, continuing a retreat while walking, and selecting an attack after lining up. This avoids copying the same decision script into every animation. Keep move-specific effects in animation scripts.

Entity-Level Enemy AI

Add aiScript beside animationsLogic in the enemy's logic.json:

"aiScript": {
  "packName": "My Pack",
  "scriptName": "Enemy AI"
}

It receives the normal game bridge, and game.this_enemy identifies the enemy whose update is running. The hook belongs to the entity, not to a level: every instance spawned from that custom entity uses the script automatically.

return true augments the existing AI by allowing the native controller to continue. return false replaces it for the current frame. A replacement script must perform any required hit, movement, collision, landing, and state-transition work that the skipped native controller would otherwise have done.

Return Values In Animation Scripts

return true from the update function says "I changed some state, now continue the normal update path."

Typical uses, inside the update function:

-- Set a parameter used by a shared air-control script,
-- then let the original jump controller continue.
game.this_character.custom_value_1 = 2
return true

return false says "Lua owns this frame."

Typical uses, inside the update function:

-- Run custom projectile movement and stop the fallback item controller.
item.check_projectile_hits()
item.velocity_x = item.velocity_x + item.impulse_x
item.update_position_and_check_world_collisions()
return false

Returning false is powerful, but it also means you are responsible for whatever the skipped controller would have done: hit checks, hit pause, movement, acceleration, landing, world collision, animation changes, and cleanup.

Script Frequency

An animation script's update function is called on every update while the animation is active. Write it so it is safe to run more than once.

Use frame checks for one-time actions:

return function()
    local character = game.this_character

    if character ~= nil and character.current_animation_frame == 12 then
        -- Offsets and velocity are relative to facing direction inside TrySpawnItem.
        character.try_spawn_item("My Pack", "Fireball", 0, -1, 12, 0, 520, 0)
    end

    return true
end

Use custom values for persistent per-object state:

return function()
    local item = game.this_item

    if item == nil then
        return true
    end

    if item.custom_value_1 == 0 then
        item.custom_value_1 = 1
        item.play_sound("My Pack", "spawn", 0)
    end

    return true
end

Pack-Level Scripts

config.json can define preEntityUpdateScripts and postEntityUpdateScripts. These run for each active player, enemy, and item around the entity's normal update.

Pre-entity scripts can cancel the entity update by returning false. In that case the movement controller and the post-entity scripts for that entity are skipped. Post-entity scripts run after the entity update, but their return value is accumulated only for the script array and does not re-run the skipped controller.

Pack-level hooks are useful for broad mechanics such as global input rules, challenge mutators, or passive status effects. Keep them cheap: they can execute once per active entity per frame (see Performance).

Animation Advancement

Animation frames are advanced by AllEntitiesAnimationsController after movement logic. When an animation changes, set_animation(id, start_frame) sets next_animation_id and next_animation_frame; the animation controller later copies those into current_animation_id and current_animation_frame.

This means a script can call set_animation(10, 4) and still see the old current_animation_id until the animation step applies the queued change. Use return false after a decisive state change when you need to prevent the old controller from acting on the old animation for the rest of the frame.

Hit Pause

Characters and items have hit_pause_timer and custom_hit_pause_delay_frames. Default physics decrements hit pause and returns early. If Lua replaces physics with return false, you must decrement or honor hit pause yourself, early in the update function:

if character.hit_pause_timer > 0 then
    character.hit_pause_timer = character.hit_pause_timer - 1
    return false
end

Event Override Detail

If an animation defines logic events, those events can change how much of the original behaviour runs. The local controllers treat event-driven moves as custom logic and may run default physics before Lua. This prevents original behaviour-mode side effects from blindly running after custom events.

Scripts can still run on animations that also have events. Be deliberate:

  • events set fixed state at exact frames
  • Lua branches on live state
  • animation behaviour provides inherited controller logic