Level Files

A level is stored as a pair of files that must stay aligned:

Levels/level 1.json           gameplay: scenes, walls, scroll, spawns
Levels/level 1 - art.json     visuals: layers, images, animations, music
Levels/level 1 - cutscenes.json   optional cutscenes for that level

Both files contain a scenes array, and scene i in one is the same scene as scene i in the other. The gameplay file decides where you can walk and what spawns; the art file decides what you see and hear.

Three more files use the art format on their own, with no gameplay counterpart:

Levels/title.json           title screen
Levels/presentation.json    opening presentation
Levels/ending.json          ending sequence

And the same five art-format file names appear under Level Replacements/Classic/ when you are reskinning the Classic game rather than building your own levels.

Every image named by any of these files is a file in Levels/Art/.

level <n>.json

Root object: LevelData.

Field Type Meaning
entitiesScale float Uniform scale applied to characters and items in this level. 0 is treated as 1. The view draws the world at this scale about world Y 220, which stays on the screen bottom: below 1 the playable width is 400 / entitiesScale world pixels (screen_width, half extra width 40 / entitiesScale), the view shows 224 / entitiesScale pixels above Y 220, and scroll limits and camera positions stay in world pixels. Background art is not scaled: draw it at entitiesScale times the world's size. Jump-in entrances and respawns start that much higher, so they still fall in from above the view.
Scenes array of SceneData Ordered scenes. Written with a capital S by the Level Editor; scenes also loads, because Newtonsoft matches case-insensitively.

SceneData

Transitions

Field Type Meaning
sceneEnterCodeId int How players enter: 0 JumpInFromTop, 1 Standing, 2 WalkFromLeft, 3 StartAhead.
sceneExitCodeId int How the scene ends. See the table below.
startOffsetType int Camera start offset preset: 0 Default, 1 ClosedRoom, 2 Stage1Poster, 3 Stage3SwingingBoat, 4 Stage5TopBoat, 5 Stage7Elevator, 6 Stage8Elevator.
screenShakeType int 0 Default, 1 Truck, 2 Boat.
centerScreenForWidescreen bool Centre the scene horizontally on widescreen instead of anchoring left.

sceneExitCodeId values: 0 None, 1 SimpleFadeOut, 2 LeaveToTheRight, 3 BreakTruckDoor, 4 EnterBaseballElevator, 5 LeaveBaseballElevator, 6 Level8ElevatorHealth, 7 LevelEnding, 8 EndGame.

6 is the odd one out: it is a health refill, not an exit, and the scene does not end on it.

Palette

Field Type Meaning
alternativePalette string Name of an alternative palette applied to enemies in this scene. Empty means default.
copyAltPalette bool Reuse the previous scene's alternative palette instead of loading this scene's own.

Walls and walkable area

Field Type Meaning
wallLimitsData array of exactly 6 ints The walkable area, in pixels. See below.
wallFlags int Bit flags over wallLimitsData.
bottomWallLimit int Absolute world Y of the lowest walkable depth for players and enemies. 0 keeps the native limit, which follows the camera (camera Y + 216): set it when a scripted camera Y (a bobbing ship, a shaking elevator) must not push characters up.
respawnPosition {x, y} int Where a respawning (or continuing) player drops in, in world pixels: x from the playable view's left edge (negative: from its right edge, and the player faces left), y the absolute world Y, like bottomWallLimit, so a scripted camera Y (a bobbing ship, a shaking elevator) does not move it. Leave it unset in a scene whose camera scrolls vertically. Each further player drops 16 pixels higher. The spot is kept above bottomWallLimit and, with customWallShapes, moved to the nearest point clear of walls and pits. Zero on both axes keeps the native spot, at the bottom left of the screen.
customWallShapes array Extra collision shapes beyond the rectangular limits.

wallLimitsData positions, in order:

Index Level Editor label Meaning
0 Horizon Y (Base) The base depth line: the nearest edge of the walkable floor.
1 Max Depth Y The far edge of the walkable floor.
2 Left Gate X Where the left wall blocks movement.
3 L. Diag Anchor Anchor for a slanted left wall.
4 R. Diag Anchor Anchor for a slanted right wall.
5 Right Gate X Where the right wall blocks movement.

wallFlags bits: 1 left wall enabled, 2 right wall enabled, 4 left wall is vertical rather than slanted. Remaining bits are unused.

When the left wall flag is set, the engine requires index 1 to equal index 0, and the Level Editor enforces that. A deep floor with a far wall is only possible with the left wall off.

customWallShapes entries (WallCollisionShapeData):

Field Type Meaning
shapeType int 0 Wall, 1 Treadmill, 2 Pit, 3 Custom1, 4 Custom2.
points array of {x, y} int Corner points in pixels, joined in order.
isClosed bool Join the last point back to the first.
collideInsideOrAbove bool Which side of the shape blocks movement.
treadmillVelocity {x, y} int Push applied to anyone standing on a Treadmill shape.
pitDamage int Damage from a Pit shape. 0 makes it harmless.
maxHeight int Wall shapes only. Above 0, the wall only stops entities lower than this many pixels above the ground (a rail or low fence): higher jumps, throws and knockdowns pass over it. A thrown character (object modes 0x10/0x12) is measured at its real height like anything else: it clears the wall during the high part of its arc and hits it when low, at the start or the end of the flight. A character left inside such a wall (it came down on it from above) is pushed out on the floor side, or, when knocked down or in a throw's airborne flight, onto a pit beyond it, through which it falls. A character whose move animation ends while it is still airborne above a pit keeps falling into it.
thrownMaxHeight int Wall shapes with a maxHeight only. Above 0, a thrown character in the airborne part of its throw passes over the wall from this height instead of maxHeight (throws arc lower than jumps). 0 uses maxHeight. Scripts mark a scripted thrown body with the item's wallThrownFlight (3 while airborne).

A player walking horizontally into a Wall edge that is close to horizontal (a shallow diagonal) stops at it; it slides along the edge in depth (up to 8 pixels) only while it is moving in depth itself. Pushing against a wall never moves a character in depth on its own.

Scrolling

Field Type Meaning
scrollXLimit int Horizontal scroll extent, in pixels. Editor-authored scenes use a negative value for a scene that scrolls to the right.
diagonalScrollLimit {x, y} int Diagonal scroll bound. Zero on both axes means horizontal scrolling only.
cinematicCameraRoute object or null Optional known directional route for cinematic presentation: {start: {x, y}, end: {x, y}}, in absolute Lua camera viewport pixels, plus optional actorY: a fixed depth, in pixels below the route's camera Y, for cinematic actors that cross the view on one line (0 or omitted: the script chooses). This declares geometry; it does not drive gameplay scrolling.

Native forward scroll geometry can be queried without a declaration. A cinematic cannot infer the route owned by an arbitrary camera script. For a scripted stage with a known straight or diagonal route, declare its endpoints for each wave; decreasing X identifies right-to-left travel. X endpoints must differ. Y is sampled linearly between them. Coordinates must match the stage script's actual bounds at the configured viewport width; no widescreen correction is inferred. Free-scrolling or changing custom routes should leave this field null, so events can fall back to an earlier scene with known directional geometry.

"cinematicCameraRoute": {
  "start": {"x": 1800, "y": 0},
  "end": {"x": 448, "y": 256}
}

Spawns

loadcues is a SceneLoadcuesData:

Field Contains
enemies1, enemies2 The two enemy wave tracks, which run in parallel.
items Items, weapons, food, and containers.
extraEnemies1, extraEnemies2, extraItems Additional content used by the higher Classic difficulties.

Enemy loadcues

CharacterLoadcueData. Every enemy in a level is one of these.

Field Type Meaning
packName string Pack owning the entity. Empty means Classic.
entityName string Entity folder name within that pack.
position_A_C {x, y} int Spawn position in pixels. x is along the scene, y is depth.
collisionBox_3_4_5 BoundingBox Collision override. See the warning below.
triggerType int What makes this wave spawn. See below.
triggerArgument int Distance, enemy count, or delay, depending on triggerType.
minimumDifficulty int Lowest difficulty this enemy appears on. Below it, it is left out entirely.
useBossSlot bool Treat as the level boss: reserves the boss slot and shows the boss health bar.
health int Starting health, nibble-encoded: (health & 0xF) + (health >> 4) * 12 hit points before the difficulty multiplier, every 12 of which is an extra life. 0 gives 8 hit points; there is no entity default. See Classic Game Mechanics.
deathScore int Points for defeating it.
enemyAgressiveness int AI aggression, cautious at low values to relentless at high ones.
introductionType int Which introduction sequence plays on appearance, such as dropping in or climbing out of a sewer.
palettePackName string Pack owning the palette. Empty means Classic.
paletteIndex int Palette index for this entity within that pack.
bikerWeaponFlag bool Give a spawned biker their pipe.
bikerGrenadeFlag bool Spawn a foot biker holding a grenade instead of a pipe. Written by the ROM importer; the Level Editor shows it as Biker Grenade.
ninjaHookHeight int Height a hooking ninja descends from, in pixels.
displayName string Editor-only label. Does not affect the game.

Trigger types, as the Level Editor presents them:

triggerType triggerArgument Spawns when
0 unused Immediately, once the previous wave is cleared.
1 distance in pixels The player has travelled that far.
2 enemy count, below 7 Fewer than that many enemies remain on screen.
2 7 or more That enemy count is reached, or a timer expires.
3 delay After that delay.

The split on triggerArgument 7 within type 2 is why the editor shows "Enemy Count" and "Time OR Enemy Count" as separate choices for the same underlying type.

Item loadcues

ItemLoadcueData. Weapons, food, score pickups, and breakable containers.

Field Type Meaning
packName, entityName string The object to place.
position_A_C {x, y} int Position in pixels.
collisionBox_3_4_5 BoundingBox Collision override. See the warning below.
status_2 int Spawn flags: bit 0 vulnerable, bit 1 obstacle, bit 2 active; bit 7 spawns a decorative sub-item instead of an interactive item. Use 7 for a hittable scripted fixture.
animation_6 int Animation the object starts in.
mode_7 int Object mode. For items this is (controller index + 1) * 2, so 2 is the first item controller.
height_8 int Height above the ground, in pixels.
containedItemPackName string Pack owning the item hidden inside.
containedItemEntityName string Item revealed when this container is broken open.
insideForTwoPlayersOnly_10_b7 bool Only place the contained item in a two player game.
palettePackName, paletteIndex string, int Palette for the object.

The _2, _6, _7, _8 suffixes are byte offsets in the original ROM structure. They are part of the field names; do not drop them.

collisionBox_3_4_5

The runtime reads this field as a BoundingBox. That is the canonical shape — the only one the game deserializes, and what both the ROM importer and the Level Editor write:

"collisionBox_3_4_5": {
  "halfWidth": 14,
  "height": 72,
  "depth": 6
}

All three values are raw ROM bytes, from offsets 0xD, 0xE, and 0xF. halfWidth is half the box width, so the box spans -halfWidth to +halfWidth around the spawn position. A height of 0 means "no override": the entity falls back to its animation body box, and then to its global body box.

Two superseded shapes still exist in files written by older builds. The Level Editor reads all three and always writes the canonical one, so opening a level and saving it migrates that level. tools/migrate_collision_boxes.py converts files in bulk without opening them.

Legacy shape Written by Converts to
{depth, rect: {x, y, width, height}} The runtime, before mid-2024. halfWidth = rect.width, height = rect.height, depth = depth.
{x, y, width, height} The Level Editor, before it was fixed. Nothing recoverable — an all-zero box.

rect.width is a half width, not a full width

The build that wrote the nested shape stored the raw ROM byte in rect.width, then derived rect.x = -rect.width / 2. So rect.width already holds halfWidth. Halving it when converting shrinks every box to half its intended size.

The flat shape carries no information at all: the editor parsed the field as a plain rectangle, which dropped halfWidth and depth on load and wrote them back as zeros. Levels saved in that state lost their per-spawn overrides for good, and need them re-authored.

Until a file is migrated, the runtime reads any non-canonical shape as all zeros and ignores the override entirely.

Art files

Root object: LocalLevelBackgroundData. Used by - art.json, - cutscenes.json, title.json, presentation.json, and ending.json, in Levels/ and in Level Replacements/<Target Pack>/ alike.

Field Type Meaning
scenes array of LocalSceneBackgroundData One per scene. A null entry reuses the previous scene's background.
animationsDefinition array of LocalLayerAnimation The file's animation library. Layers reference entries by array index, so reordering silently repoints every reference.

Scene background

Field Type Meaning
layers array Visual layers, drawn by zOrder.
musicPackName string Pack the music is loaded from. Empty means this pack.
musicName string Music track, base file name without extension.
screenShakeType int 0 Default, 1 Truck, 2 Boat.
cutsceneEndTimeInSeconds float How long a cutscene scene lasts before advancing. -1, the default, means it does not advance on its own — which is what gameplay scenes use.

Scene music lives here, in the art file, not in the gameplay file. Some older files carry musicName and musicPackName inside level <n>.json scenes; the runtime ignores them there.

Layers

Field Type Meaning
layerName string Editor label.
zOrder float Draw order. Lower values draw in front. Built-in backgrounds use about 10, and foregrounds use about -6.
parallax {x, y} float Scroll speed relative to the camera. 1 moves with the camera, below 1 is a slower background, above 1 a faster foreground. Defaults to {1, 1}.
autoScroll {x, y} float Constant self-scrolling in pixels per second, independent of the camera.
scrollLoopLimits {x, y} float Wrap distance per axis. A positive value tiles the layer endlessly over that span; 0 disables wrapping on that axis.
positionOffset {x, y} float Shift applied to everything on the layer, in pixels.
reactToQuakeEffect bool Shake with the screen during a quake. The shake is an offset on top of the layer's own position, so a layer that scrolls vertically or is moved by its visualScript keeps moving while it shakes.
updateType int Special per-frame behaviour. See below.
staticImages array Still images placed on the layer.
titleScreenImages array Animated entrance images, for title and character select screens.
animations array Placed instances of animationsDefinition entries.

updateType values: 0 Normal, 1 BackyardRain, 2 TruckBrokenDoor, 3 Stage4Elevator, 4 ArenaSpectators, 5 Stage7Elevator, 6 Stage7ElevatorExtra, 7 Stage8Elevator, 8 AntiCameraY, 9 WaveEffect, 10 SmallWaveEffect, 11 SwingingBoat.

These reproduce specific original-game effects and mostly assume the scene is laid out like the stage they came from. 0 is what a new layer wants.

Local WaveEffect and SmallWaveEffect layers use the Classic wave shader, wave tables and effect-camera ordering. Static images and animated children on all matching layers receive that effect. Dry scenes reset their render layers and disable the effect camera. useClassicLayerMotion: true separately delegates the layer transform to the original controller, preserving elevator and other stage-specific movement. For stationary Classic layers, staticParallaxType also preserves widescreen registration: 0 Normal, 1 Skyline, 2 Stage7Elevator. It requires useClassicLayerMotion: true; the controller then supplies the native -34px vertical offset, so do not add that offset a second time in positionOffset.

Images and animations

staticImages entries:

Field Type Meaning
imageName string File name inside Levels/Art/, including its extension.
position {x, y} float Position within the layer, in pixels.
scale float Size multiplier. 0 is treated as 1, and the absolute value is used.

titleScreenImages entries add an entrance animation on top of those three fields:

Field Type Meaning
initialXPosition float Horizontal offset the image starts from.
initialDelay float Seconds to wait before it starts moving.
lerpSpeed float How quickly it eases towards position. Higher catches up faster.
wiggleAmplitude float How far it wiggles off its path, in pixels. 0 disables.
wiggleFrequency float How fast it wiggles.
requireCharacterName bool Only use this entry when the character's name matches, rather than for everyone.
isAICharacter bool Present this portrait as computer-controlled rather than as a player.

animations entries are just a placement:

Field Type Meaning
animationId int Index into this file's animationsDefinition.
position {x, y} float Placement within the layer, in pixels.

animationsDefinition entries hold frames:

Field Type Meaning
imageName string File name inside Levels/Art/, including extension.
delayFrames int Game frames this image is held.
pivotInPixels {x, y} float The point of the image that sits at the placement position, in pixels from its bottom-left corner.
scale float Size multiplier. 0 is treated as 1.

The same animation can be placed many times on many layers; that is the point of the shared library.

Example

{
  "animationsDefinition": [
    {
      "frames": [
        { "imageName": "water-1.png", "delayFrames": 8, "pivotInPixels": { "x": 0, "y": 0 }, "scale": 1 },
        { "imageName": "water-2.png", "delayFrames": 8, "pivotInPixels": { "x": 0, "y": 0 }, "scale": 1 }
      ]
    }
  ],
  "scenes": [
    {
      "musicPackName": "My Pack",
      "musicName": "stage-1",
      "cutsceneEndTimeInSeconds": -1,
      "layers": [
        {
          "layerName": "Sky",
          "zOrder": -30,
          "parallax": { "x": 0.25, "y": 0.25 },
          "scrollLoopLimits": { "x": 512, "y": 0 },
          "staticImages": [
            { "imageName": "clouds.png", "position": { "x": 0, "y": 0 }, "scale": 1 }
          ]
        },
        {
          "layerName": "Water",
          "zOrder": -10,
          "parallax": { "x": 1, "y": 1 },
          "animations": [{ "animationId": 0, "position": { "x": 120, "y": 40 } }]
        }
      ]
    }
  ]
}

Scripted stage extensions

Scenes support scriptedItems for explicit item spawning through game.spawn_scene_item(index). Scenes support updateScript, and updateScriptDuringCinematics: true to keep running it, for presentation only, behind a gameplay cinematic. Art layers support tilemap and visualScript. See the scripted-stage Lua documentation for coordinates, data formats and rollback rules.

Scene enterScript uses the same reference as updateScript. scriptedEntrance skips automatic entrance walking when the script owns placement. scriptedClock leaves the HUD timer under Lua control. scriptedEnemies disables automatic enemy cues so the stage script can spawn individual enemies1 cues with game.spawn_scene_enemy(index) and maintain persistent area encounters. See Scripted stages for lifecycle, units and rollback rules.

Scripted waves inside a scene

scriptedWaveCount (default 0, equivalent to one) exposes multiple authored waves in the custom-game wave selector. scriptedWaveCustomValue (0–16; zero disables seeding) selects the game custom variable receiving the one-based starting wave. These are camera-lock encounter groups within one background scene; scene fades still occur only at physical scene boundaries. Use game.complete_scene() from the gameplay script to execute the configured walk/fade or stage-clear exit before advancing to the next physical scene.