Object Model

The Lua API mirrors the runtime model used by Streets of Rage 2 New Era. Understanding the layers is the fastest way to write scripts that work with the original engine instead of fighting it.

Containment

Player
  contains Character
    contains Item
      contains SubItem

Enemy
  contains Character
    contains Item
      contains SubItem

Standalone Item
  contains SubItem

Standalone SubItem

What Each Layer Owns

Layer Owns Examples
SubItem Visual identity, animation, palette, facing, object mode, script custom values current_animation_id, current_animation_frame, object_mode, is_facing_right, custom_value_1
Item Physical state, world position, velocity, impulses, collision, active flags, projectile fields position_x, position_offset_y, impulse_x, is_active, is_vulnerable, hit_pause_timer
Character Combat state, health, score, victims, attackers, grabs, combo state, sounds health, damage_taken, victim, attacker, check_being_hit()
Player Human input, pressed/held buttons, combo checks, player timers pressed_forward, is_holding_button_b, has_input_combo()
Enemy AI mode, target selection fields, aggressiveness, enemy-specific counters mode, mode_timer, homing_target_id, enemy_agressiveness

For Classic object_mode values, see Entity Types and Behaviour Modes. Those tables list the descriptive behaviour names together with their hex and decimal values.

Flattening

The bridge exposes lower-layer properties directly on higher-layer objects. Inside an update function:

local player = game.this_player

player.position_x = 5000
player.health = player.health - 10
player.current_animation_frame = 0

That is equivalent to walking down the internal chain from player to character to item to sub-item, but it is shorter and less error-prone. The same is true for enemies. Standalone items expose item and sub-item fields, but not character or player fields.

IDs And Objects

The engine tracks active entities by global object IDs. Some fields expose IDs; other properties resolve the ID into an object for you.

local character = game.this_character
local attacker = character.attacker

if attacker ~= nil then
    attacker.health = attacker.health - 5
end

Manual resolution is also available:

local attacker_id = character.attacker_character_id

if attacker_id >= 0 then
    local attacker = game.get_character(attacker_id)
    if attacker ~= nil then
        attacker.health = attacker.health - 5
    end
end

Use object shortcuts for readability. Store IDs in custom_value_* fields when you need a reference to survive across frames, then resolve the ID again when you need the object.

Object ID Ranges

The global order is:

  1. players
  2. enemies
  3. items
  4. sub-items

Do not hard-code exact ranges. The game has 4 player slots, 20 enemy slots, 20 item slots, and 40 sub-item slots in the current bridge, but scripts should use game.get_*, game.get_free_*, and object properties instead of numeric range checks.

Facing Direction

is_facing_right is the visual and gameplay facing flag. direction_signal is derived from it:

  • 1 when facing right
  • -1 when facing left

Most custom horizontal movement should multiply by direction_signal when the move is meant to go forward:

character.velocity_x = character.velocity_x + character.impulse_x * character.direction_signal

For absolute left/right air control, write impulse_x directly as positive or negative and let facing change separately.

Ground And Air State

is_in_the_air is derived from position_offset_y < 0. The engine uses negative vertical offsets for airborne objects and 0 for grounded objects.

Typical manual air movement:

if character.position_offset_y < 0 or character.impulse_y < 0 then
    character.position_offset_y = character.position_offset_y + character.impulse_y
    character.impulse_y = character.impulse_y + 32

    if character.position_offset_y >= 0 then
        character.position_offset_y = 0
    end
end

impulse_y controls vertical height. impulse_z contributes to velocity_y, which is depth movement on the belt-scroller plane.

Animation IDs And Frames

Lua uses editor-facing animation IDs. The engine internally stores currentAnimationId and nextAnimationId doubled. The bridge converts automatically:

character.set_animation(4)   -- editor animation 4

Setting current_animation_id directly changes the current internal animation ID while preserving the low internal bit. Prefer set_animation(id, start_frame) for normal state transitions because it also queues the starting frame.

current_animation_frame is the current animation logic frame. It is not the sprite frame index. The animation controller increments it after movement scripts run.

Custom Values

custom_value_1 through custom_value_10 live on the sub-item layer, so they are available on players, enemies, characters, items, and sub-items. They are ideal for per-entity script state:

  • one-shot flags
  • cooldowns
  • mode selectors
  • stored object IDs
  • temporary speed tiers

They persist while the object slot is alive. Clear them when reusing an object slot or when changing to a state that should not inherit old script state.

Nil Safety

Always check objects that might not exist, starting with the context object at the top of the update function:

local enemy = game.this_enemy

if enemy == nil then
    return true
end

local target = enemy.get_nearest_opponent(true, true, true)
if target == nil then
    return true
end

game.get_player(-1), game.get_enemy(-1), game.get_item(-1), and game.get_sub_item(-1) return nil. Passing a positive ID to the wrong lookup can throw an error, so use game.get_character(id) for attacker/victim IDs unless you know the target type.