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:
- players
- enemies
- items
- 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:
1when facing right-1when 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.