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 } }]
}
]
}
]
}
Related pages¶
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.