Entity Files

An entity is a playable character, an enemy, an item, a container, or a piece of scenery. Everything about it lives in one folder:

Entities/<Entity Name>/          own content
Entity Replacements/Classic/<Entity Name>/   replacement for a Classic entity

Both use exactly the same file set. The folder name is the entity name used by every entityName reference.

File Required Purpose
spriteSettings.json yes How many sprite frames the entity has, and where each one's pivot sits.
animations.json yes Which frame is shown for how long, per animation.
logic.json yes Gameplay behaviour: stats, boxes, events, scripts.
dependencies.json no Declared references to other packs' assets.
<i>.png as needed Own sprite frames, zero-based.
portrait.png, preview.png no Character select art.
Palettes/palette <n>.png no Palette guides for this entity. See Palette Files.

Three ideas govern all of it: the sprite index model, the reference fallback model, and the parallel animation arrays. Get these right and the rest is data entry.

The sprite index model

Everything that names a sprite uses an effective index, and effective indices are not PNG numbers.

An entity may borrow another entity's sprite frames by setting referenceEntityPackName and referenceEntityName in spriteSettings.json. When it does, the combined frame list is:

index 0 .. R-1      the reference entity's frames, in its own order
index R .. R+N-1    this entity's own frames, where own frame k is the file k.png

R is the reference entity's total frame count. N is the length of this entity's pivots array.

Without a reference, R is 0 and effective index k is simply k.png.

This matters in three places:

  • animations.json frames[].imageId is an effective index.
  • spriteSettings.json iconIndex is an effective index.
  • Changing the sprite reference changes R, which shifts every own frame's effective index. The Character Editor remaps imageId values when you change the reference; a hand edit must do the same.

pivots defines the frame count, not the files. If pivots has 12 entries, the entity has own frames 0.png through 11.png. A 12.png sitting in the folder is invisible. A missing 7.png with a pivot at index 7 is a frame that draws nothing.

The reference fallback model

logic.json can set referencePackName and referenceEntityName to inherit behaviour from another entity, usually a Classic one. This is how "an Axel that plays like Axel but hits harder" is authored.

The fallback rule is blunt and worth memorising:

On an entity with a logic reference, every numeric field left at 0, and every array left null, falls through to the referenced entity.

So on a referencing entity you cannot say "this character's walk speed is zero". Writing "walkSpeed": 0 and omitting walkSpeed mean the same thing: use the reference's value. Fields that behave this way are marked falls through in the table below.

sprite references and logic references are independent. An entity can borrow Classic Axel's art while inheriting Classic Blaze's logic, or borrow art with no logic reference at all.

There is a second, per-animation reference: animationsLogic[i].referenceBehaviour, which borrows one Classic move controller for one animation. It is subject to an override rule described under Animation logic.

spriteSettings.json

Root object: SpriteSettingsData.

Field Type Meaning
scale float Uniform draw scale for own frames. 0 is treated as 1.
shadowAlwaysOn bool Draw the ground shadow even in states that normally hide it.
useSourceAnimations bool Visual skins only: draw the original gameplay entity's animation sequence using this skin's frame providers. All source frame indices, including playable variants' appended frames, must exist in the skin. Gameplay logic stays unchanged. Defaults to false.
pivots array of {x, y} floats One pivot per own frame, in pixels from the image's bottom-left corner: x from the left edge, y up from the bottom edge. Array length is the own frame count.
iconIndex int Effective sprite index used as the HUD icon. -1 for none.
referenceEntityPackName string Pack owning the entity whose frames are borrowed.
referenceEntityName string Entity whose frames are borrowed.
{
  "scale": 1.0,
  "shadowAlwaysOn": false,
  "iconIndex": 4,
  "pivots": [
    { "x": 22.0, "y": 1.0 },
    { "x": 22.0, "y": 1.0 }
  ]
}

A pivot is the point of the image that sits on the character's position — for a standing character, the point between the feet. Because y counts up from the bottom edge, a standing frame whose feet touch the bottom row has a pivot y of about 1.

animations.json

Root object: AnimationsSetData, a single animations array.

The array index is the animation id. For entities based on Classic behaviour, index i corresponds to the Classic AnimationId value i * 2; the full slot table is in Enums and Identifiers. Entries may be null to leave a slot empty. Never reorder the array.

Per animation (AnimationData):

Field Type Meaning
name string Editor label. Cosmetic; the runtime addresses animations by index.
frames array Ordered frames.
referenceTotalFrames int Total frame count of the same animation on the reference entity, used by the editor to keep a reskin aligned with original timing. 0 when there is no reference.

Per frame (AnimationFrameData):

Field Type Meaning
duration int Game frames this image is held. The game runs at 60 fps.
imageId int Effective sprite index. Negative draws nothing.
weapon object or null Optional weapon overlay.

Weapon overlay (AnimationFrameWeaponData):

Field Type Meaning
direction int WeaponAngle: 0 Up, 1 UpLeft, 2 Left, 3 DownLeft, 4 Down, 5 DownRight, 6 Right, 7 UpRight.
showBehind bool Draw behind the character rather than in front.
position {x, y} int Offset from the character pivot, in pixels.
{
  "animations": [
    {
      "name": "Standing",
      "frames": [
        { "duration": 8, "imageId": 0 },
        { "duration": 8, "imageId": 1 },
        { "duration": 10, "imageId": 2 }
      ]
    },
    null,
    { "name": "Jump", "frames": [{ "duration": 4, "imageId": 12 }] }
  ]
}

The animation's length is the sum of its frame durations. That total is the timeline every frame number in logic.json is measured against. An event at "frame": 26 fires 26 game frames into the animation, which may be partway through the fourth image, not on the 26th image.

logic.json

Root object: LogicData.

Identity and references

Field Type Meaning
entityType int 0 Player, 1 BasicEnemy, 2 Enemy, 3 PickUpItem, 4 Container, 5 Scenery, 6 Other. The Character Editor only offers 0-3. BasicEnemy marks a common street enemy the difficulty system may multiply; Enemy marks a boss-class entity.
referencePackName string Pack to inherit logic from.
referenceEntityName string Entity to inherit logic from.

Movement

Field Type Falls through Meaning
walkSpeed int yes Walking speed.
runSpeed int yes Running speed.
jumpForce int yes Upward jump impulse. Falls through to the reference, then to the Classic per-character table.
jumpDelayFrames int yes Crouch frames before leaving the ground.

Combat

Field Type Falls through Meaning
globalBodyBox BoundingBox no Default hurt volume, used on every animation that does not define its own bodyBoxes.
canUseWeapons bool no Whether the entity can pick up and use weapons.
comboRepetitions int yes Times the standing attack chain repeats before the finisher.
grabComboRepetitions int yes Times the grab attack repeats before the grab finisher.
chargeDelay int no Frames a button must be held before a charged move is available.
noChargeFlash bool no Players only: true turns off the white flash shown when holding attack completes the charge. The charge and the charged attack still work. Default false keeps the flash.
grabRange int yes Horizontal reach of a grab attempt, in pixels.
frontGrabOffset int yes Distance the victim snaps to when grabbed from the front.
backGrabOffset int yes Distance the victim snaps to when grabbed from behind.
heldFrontGrabOffset int no Distance this entity is held at when grabbed from the front, whoever grabs it (pixels; 0 = the grabber's frontGrabOffset).
heldBackGrabOffset int no Same, when grabbed from behind (0 = the grabber's backGrabOffset).
knockedDownQuakeEffect float yes Screen shake when this entity hits the ground knocked down.
thrownQuakeEffect float yes Screen shake when this entity lands from a throw.

BoundingBox is { "halfWidth": int, "height": int, "depth": int }. The volume spans -halfWidth..+halfWidth horizontally, 0..height upwards from the feet, and ±depth on the depth axis.

Canonical globalCombatBounds and animation bodyBoxes[].combatBounds use enabled, xMin, xMax, heightMin, heightMax, depthMin, and depthMax. An optional regions array supplies disjoint hurt rectangles in the same entity-local coordinates. The outer rectangle still provides world and grab dimensions; attack contact must intersect one enabled region. Omit regions for one solid rectangle, or use an empty array for no hurt contact. Regions mirror with facing and scale independently of sprite rendering. The Character Editor preserves these regions when saving the outer rectangle. On an item that hits as a projectile, its body regions are also its striking rectangles: a hit needs one of them to meet the target (thrown bodies keep the outer rectangle).

Projectile behaviour

Applies when this entity is thrown or spawned as a projectile.

Field Type Falls through Meaning
projectileDamage int yes Damage on hit.
projectileHitPause int yes Hit-stop frames on hit.
projectileHitSoundName string yes Sound on hit, base file name without extension.
projectileHitSoundPack string yes Pack owning that sound.
projectileHitSoundChannel int yes Audio channel slot.

Sounds

Playable entities may declare playerUpdateScript (runs before the controller in every animation), specialMoveScript (special eligibility and entry cost), and invincibleOffensiveSpecial (invulnerability on offensive-special entry). These fields apply to the entity itself, without reference fallback. The special hook must explicitly return true; it replaces the native health gate and later health deduction. See Lua scripting for the hook contract and scripted cancel entry.

sounds is a DefaultSoundsData: a packName plus sixteen named groups. Each group is an array of base sound file names without extension; the runtime picks one at random each time. A null group falls through to the reference entity.

Groups: jump, landing, quake, attack, weaponThrow, jumpAttack, blitz, defspecial, offspecial, grabMove1, grabMove2, grabMove3, death, run, lowHealth, lastLifeDeath.

"sounds": {
  "packName": "My Pack",
  "attack": ["punch-a", "punch-b"],
  "death": ["scream"]
}

aiData

A raw two-dimensional enemy AI table imported from the ROM. Hand-authoring is not supported. Copy it from a reference entity, or leave it out and let the logic reference supply it.

Animation logic

animationsLogic is an array parallel to animations.json animations. Index i describes animation i. Entries may be null, and the array may be shorter than the animation list.

Field Type Meaning
duration int Logic length of the animation in game frames.
hitboxes array Attack boxes active during frame windows.
bodyBoxes array Per-frame hurt volumes overriding globalBodyBox.
throwLogic object or null Grab and throw choreography.

For moving or aerial throws, throwLogic.relativeToAttacker: true anchors victim offsets to the attacker's current position and height for both enemy and player victims. It omits the legacy front/back grab-distance addition. Author the offsets with the usual victim-facing X sign and set useLegacyHardcodedMechanics: false. The default remains false, preserving existing pack choreography. | events | array | Frame-bound logic events. | | script | object or null | Lua script run every frame of this animation. | | referenceBehaviour | object or null | Classic move controller to borrow. |

Events override borrowed behaviour

If events is non-empty, the animation runs its events plus the default physics step and skips the referenced Classic move controller entirely. This is deliberate: it stops original mode side effects — forced mode changes, timers, hard-coded spawns, damage transitions — from leaking into a custom event-driven animation. A script still runs either way; referenceBehaviour and Classic fallback only apply to animations without events.

animationsLogic[i].name also appears in files the Character Editor writes. It is a mirror of the matching animations.json name, kept for readability, and the runtime ignores it.

Hitboxes

Field Type Meaning
frame int First animation frame the box is active on.
duration int Frames it stays active.
hitBox {x, y, width, height} int Geometry in pixels relative to the entity pivot. Mirrored automatically when facing left.
damage int Damage on hit.
knockDown bool Send the victim into the knocked-down state.
hitBackwards bool Push the victim away from the attacker's back rather than its front.
hitPause int Hit-stop frames applied to both parties.
hitEachTargetOnce bool While the hitbox stays active, strike each target once, on any frame (up to four). Without it, once the hitbox connects it skips anyone this character hit last, until the hitbox changes.
soundName string Sound on connect, base name without extension.
packName string Pack owning that sound.
channel int Audio channel slot.
soundId int Classic sound slot used when soundName is empty.

At most one hitbox is active at a time. They are scanned in array order, so keep them ordered by frame and non-overlapping.

Body boxes

Field Type Meaning
frame int First animation frame.
duration int Frames active.
boundingBox BoundingBox Hurt volume for that window.

Outside every body box window, globalBodyBox applies.

Throw logic

Field Type Meaning
offsets array Keyframes placing the held opponent.
releaseVelocity {x, y} int Velocity given to the opponent on release. x is whole pixels per frame, pushed towards the victim's own back. y is raw 16.16 fixed point (65536 per pixel), negative is up: -180224 is 2.75 px per frame upward. Not the 1/1000 unit used by VelocityEvent.
releaseDamage int Damage applied when the thrown opponent lands.
useLegacyHardcodedMechanics bool Index the offsets by the victim's animation tick and ignore opponentFrame, as every ROM throw does. When false the thrower's tick indexes the offsets and opponentFrame freezes the victim on that tick. It does not skip the offsets.

Each offset (OffsetData):

Field Type Meaning
frame int Animation tick (game frames elapsed, not image index) the placement applies from.
offset {x, y} int Opponent placement in pixels. Negative y is up. x is mirrored so that negative is in front of the opponent.
opponentAnimation int Animation id forced on the opponent every frame. Leaving it 0 forces Standing; use the victim slots 18-23 described in Classic Game Mechanics.
opponentFrame int Tick the opponent's animation is held at while this keyframe is active. Only used when useLegacyHardcodedMechanics is false.
opponentFacesSameDirection bool Face the opponent the same way as the holder rather than towards it.

Negative frame values appear in some existing files, left behind by editor frame-shift operations. They never match a real frame and are dead data.

Scripts

"script": { "packName": "My Pack", "scriptName": "Block" }

scriptName resolves to Scripts/<scriptName>.lua inside packName. The .lua extension is optional and appended when missing. See Lua Scripting.

At the root of logic.json, playable entities may set hudScript to draw pack-owned sprites beside their own health bar. It runs in a separate, view-only Lua VM and receives copied player state. Items may set pickupScript; it runs when a player attempts to collect the item. Returning true consumes the item; returning false or failing leaves it on the ground. Unknown custom items with no pickupScript are ignored. Neither field is inherited from a logic reference. See Custom HUD and pickups.

A weapon item may set "throwOnlyWeapon": true at the root of its logic.json. It is then only thrown, like the Classic grenade: a player's attack throws it instead of swinging or stabbing with it, whatever weapon it references (a pepper bottle that references the Classic Knife, for example). The flag is read from the item's own logic, not inherited from a logic reference. Scripts read it as is_throw_only_weapon.

Two more weapon options, also read from the item's own logic only:

  • "meleeOnlyWeapon": true: the weapon is never thrown. A player's throw inputs act as if it could not be thrown: the back attack inputs make a back attack, the pick up button drops it. Scripts read it as is_melee_only_weapon.
  • "brokenWeapon": { "packName": ..., "entityName": ... }: on its first hit in a weapon attack the held item becomes that entity, still in the same hand, and the weapon's breakSound plays (a bottle that cracks into a broken bottle). Native characters break it from their weapon attack; scripted ones call break_held_weapon() after a hit. Scripts read it as is_breakable_weapon.

"breakSound": { "packName": ..., "soundName": ... } also names the sound a container item plays when it breaks open, in place of the referenced Classic container's.

What kind of weapon an item is comes from the Classic weapon it references: Knife, Kunai, Pipe, Sword or Grenade. The native weapon attack and throw use it, and scripts read it as classic_name. Scripts that give weapons their own moves should test classic_name (with is_throw_only_weapon and is_breakable_weapon) rather than the item's name, so that every pack's knife counts as a knife: the Streets of Rage 1 knife is SOR1 item 08, a baseball bat or a plank references the Classic Pipe.

On a character, the weapon throw animation (slot 44) runs the native throw, which releases the weapon at the tick the referenced Classic character releases it: Axel 7, Blaze 12, Skate 5, others 9. A character whose throw has other art or timing sets "weaponThrowFrame" on that animationsLogic entry to the animation tick of its release. 0 keeps the Classic tick.

Reference behaviour

"referenceBehaviour": { "packName": "Classic", "type": 0, "animationId": 0, "objectMode": 2 }

objectMode is a move controller mode value: for players and enemies it is controller index * 2; for items it is (controller index + 1) * 2. type is the reference entity's type hash within the pack. The full tables are in Behaviour Modes.

Logic events

Each entry in events is an object with a $type and that type's own fields. The editor display names are mapped to these class names in Logic Events.

$type must read exactly Sor2NewEra.Gameplay.<ClassName>, Sor2NewEra.

Most events fire on a single frame. Some instead watch a window with afterFrame and beforeFrame, where beforeFrame of 0 means no upper bound. A few watch continuously from an initialFrame.

Movement

$type class Fields Effect
VelocityEvent frame, velocity {x,y,z}, useAxisFlags, affectX, affectY, affectZ Set velocity. Without useAxisFlags, a zero component still overwrites that axis; with it, only the flagged axes are touched.
AccelerationEvent frame, acceleration {x,y,z}, useAxisFlags, affectX, affectY, affectZ Set acceleration, same axis rules.
InputVelocityEvent strength {x,y}, afterFrame, beforeFrame Apply stick-driven velocity during a window.
InputAccelerationEvent strength {x,y} Apply stick-driven acceleration on every frame of the animation.
PositionOffsetEvent frame, offset {x,y,z} Teleport, in pixels. x mirrors with facing, z moves on the depth axis. y is added to the depth velocity, not to height; use VelocityEvent for vertical motion.
StopEvent frame Stop movement.
StopOnHorizontalStop none Stop once horizontal motion ends.

On the axis vectors, x is horizontal, y is vertical (the jump axis), and z is depth.

Animation flow

$type class Fields Effect
JumpToAnimationEvent frame, animationId, animationFrame Switch to another animation.
JumpToAnimationOnInputButton input, animationId, afterFrame, beforeFrame Switch when a button is pressed during the window. input is an InputButtons flag value.
JumpToAnimationOnInputCombo input, animationId, afterFrame, beforeFrame Switch when a button chain is entered. input is a string, one character per step.
JumpToAnimationOnHit animationId, afterFrame, beforeFrame Switch when this entity's attack connects.
JumpToAnimationOnLanding initialFrame, animationId, animationFrame Switch on landing.
JumpToFrameOnLanding initialFrame, jumpFrame Jump within the same animation on landing.
JumpToFrameOnHorizontalStop initialFrame, jumpFrame Jump within the same animation once horizontal motion ends.

animationId is the array index in animations.json, the same index used everywhere else.

Combo notation for JumpToAnimationOnInputCombo: one character per step, matched in order. ^ up, v down, < left, > right, and a b c x y z for the face buttons. Parsed case-insensitively. Left and right mirror with facing, so a combo written facing right works facing left. "v>a" is down, forward, A.

Spawning and interaction

$type class Fields Effect
SpawnEnemyEvent frame, packName, entityName, animationId, mode Spawn an enemy.
SpawnItemEvent frame, packName, entityName, animationId, mode, lifetime, positionOffset, velocity, isProjectile, damage, hitPause, hitSoundName, hitSoundPack, hitSoundChannel Spawn an item, optionally as a damaging projectile. This is how fireballs are built.
GrabEvent frame Grab the character this entity is currently attacking.
FlipEvent frame Reverse facing.
DestroyEvent frame Destroy this entity.

SpawnItemEvent positionOffset and velocity are in internal position units, not pixels: about 65.5 units per pixel, +x forward, +y up, so editor-written values are typically in the thousands. lifetime of 0 means 60 frames, not "never expire". See Classic Game Mechanics.

State

$type class Fields Effect
InvincibilityEvent frame, invincible Turn invincibility on or off. Always pair the two.
HealthEvent frame, health Add health. Negative subtracts, which is how special-move self-damage is authored.
SetCustomValueEvent frame, valueId, value Write a custom value slot, readable and writable from Lua.

Audio

$type class Fields Effect
SoundEvent frame, packName, soundName, soundNames, channel Play a sound. soundNames is a random pool and takes precedence over soundName when non-empty.
MusicEvent frame, packName, musicName Change the music track.

channel decides which audio slot the sound occupies. Sounds on the same slot cut each other off, so use different slots for sounds that should overlap. 0 sfx, 1 sfxBgm, 2 voice and 3 prioritary are the named slots, but the runtime treats the value as a plain index and allocates as many slots as it sees, so any non-negative number is a valid private channel.

Example

A fireball spawned on frame 40 of animation 47, with a voice line on frame 36:

{
  "duration": 60,
  "hitboxes": [],
  "bodyBoxes": [],
  "events": [
    {
      "$type": "Sor2NewEra.Gameplay.SoundEvent, Sor2NewEra",
      "frame": 36,
      "packName": "My Pack",
      "soundName": "hadoken",
      "channel": 2
    },
    {
      "$type": "Sor2NewEra.Gameplay.SpawnItemEvent, Sor2NewEra",
      "frame": 40,
      "packName": "My Pack",
      "entityName": "Fireball",
      "animationId": 0,
      "mode": -1,
      "lifetime": 0,
      "positionOffset": { "x": 2750, "y": 2750 },
      "velocity": { "x": 250, "y": 0 },
      "isProjectile": true,
      "damage": 30,
      "hitPause": 6
    }
  ]
}

dependencies.json

Optional. Declares assets from other packs that this entity needs, so they are loaded and shipped.

{
  "entities": [{ "packName": "Classic", "entityName": "Shiva" }],
  "sounds": [{ "packName": "Classic", "soundName": "29" }]
}

The file may also contain a literal null, which the editor writes for an entity with no declared dependencies. That is the same as the file being absent.

Visual afterimages

Set afterimages: true on an animationsLogic entry to show recent player poses through the existing Ninpo shadow renderer. This is visual only: it does not enable clone attacks or add collision bodies. Defaults to false, is suppressed during cinematics, and rejects history from another entity or animation. Requires the generic authored-afterimage engine support.

Weapon health budgets

logic.weaponHealth > 0 opts a weapon into a health budget instead of Classic's drop counter. weaponDropHealthCost is consumed on landing or swap. animationsLogic[].weaponHealthCost charges on completion if at least one hit connected (misses and interrupted moves do not pay), while hitboxes[].weaponHealthCost charges per connected hitbox; both are independent of damage multipliers. Exhaustion drops the held weapon and its next landing expires it. All counters are in ItemModel, not Lua upvalues, and are restored by rollback. Zero-valued configuration preserves Classic behaviour. Normal attacks without authored wear do not consume health.

Lua item.weapon_health reads/writes the rollback health counter, allowing original spawn or container tables to override the default budget. This does not enable the feature on entities whose weaponHealth configuration is zero.

weaponContactHealthCost optionally overrides a positive hitbox wear cost using the held weapon's own value. Zero preserves attack-authored costs. Zero-cost hitboxes remain free, and animation-completion fees are unaffected. Use this for weapons whose wear differs even when they share a character move.