Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt
Use this file to discover all available pages before exploring further.
AOT source files are split into two zones. The static declaration zone runs during the Bun build: defineTileset, defineSprite, defineMap, defineGame, and JSX scene prefabs are all executed at build time, filling the compiler’s registry. The residual script zone is never executed — script(function* () { ... }) bodies are read from the TypeScript AST and lowered by the compiler to stack VM bytecode. Unsupported constructs in script bodies are compile-time errors, not silent runtime failures.
The two zones
The DSL is deliberately hybrid:
- Tile layers stay on the typed builder path so TypeScript can enforce tileset membership at compile time.
- Scene entities (NPCs, signs, warps, spawn points) are grouped using build-time JSX components.
- Dialogue and interaction logic lives in residual script generators that the compiler lowers to bytecode.
/** @jsxImportSource @pocketjs/aot */
import {
ascii,
defineMap,
Npc,
PlayerSpawn,
script,
say,
tile,
} from "@pocketjs/aot";
import { hero, town } from "./assets";
const ElderTalk = script(function* () {
yield say("Welcome to the route.");
});
function TownEntities() {
return (
<>
<PlayerSpawn id="spawn" at={[6, 8]} facing="down" />
<Npc id="elder" sprite={hero} at={[6, 6]} facing="down" onTalk={ElderTalk} />
</>
);
}
export const Town = defineMap("town")
.tileset(town)
.layer(
ascii`
####
#..#
####
`.legend({
"#": tile("wall"),
".": tile("grass"),
}),
)
.entities(<TownEntities />)
.done();
Tilesets
defineTileset declares the graphics palette and per-tile pixel data. The compiler bakes each tile’s px rows (8 rows of 8 hex-nibble palette indices) into a GBA-native 4bpp 8×8 tile. The palette array provides up to 16 RGB triples for BG palette bank 0.
import { defineTileset } from "@pocketjs/aot";
export const town = defineTileset("town", {
palette: [
[0, 0, 0], // index 0: transparent / backdrop
[80, 120, 48], // index 1: grass green
[64, 64, 64], // index 2: wall grey
],
tiles: {
grass: { px: Array.from({ length: 8 }, () => "11111111") },
wall: { solid: true, px: Array.from({ length: 8 }, () => "22222222") },
path: { px: Array.from({ length: 8 }, () => "33333333") },
water: { solid: true, px: Array.from({ length: 8 }, () => "44444444") },
},
});
Each tile entry can carry:
| Field | Type | Description |
|---|
px | string[] | 8 rows of 8 hex-nibble characters (palette indices 0–f). |
solid | boolean? | true → the tile is impassable; populates the collision table. |
The v1 compiler enforces one tileset per game. All maps must share the same named tileset.
Sprites
defineSprite declares a character sprite: its pixel dimensions (v1: 16×16), OBJ palette, and per-direction walk frames. Each facing is an array of frames, where each frame is an array of 16 rows of 16 hex-nibble palette indices.
import { defineSprite } from "@pocketjs/aot";
export const hero = defineSprite("hero", {
size: [16, 16],
palette: [[0, 0, 0], [200, 160, 100], /* ... */],
facings: {
down: [/* frame 0 rows */, /* frame 1 rows */],
up: [/* frame 0 rows */, /* frame 1 rows */],
left: [/* frame 0 rows */, /* frame 1 rows */],
right: [/* frame 0 rows */, /* frame 1 rows */],
},
});
The compiler splits each 16×16 frame into four 8×8 OBJ tiles (top-left, top-right, bottom-left, bottom-right) in GBA 1D OAM mapping order. v1 supports at most 16 sprites, one OBJ palette bank each.
Tile map builder
Maps are built with the fluent defineMap builder. Calling .tileset(town) carries the concrete tileset type into the chain so that .layer(...) can verify legend tile names against town.tiles at the TypeScript level — before the compiler runs.
const MyMap = defineMap("mymap")
.tileset(town)
.layer(
ascii`
#########
#.......#
#..HHH..#
#.......#
#########
`.legend({
"#": tile("tree"),
".": tile("grass"),
H: tile("wall"),
}),
)
.entities(<MyMapEntities />)
.done();
The ascii template tag
ascii is a tagged template literal that normalizes leading/trailing blank lines and common indentation, then returns an AsciiLayer object with a .legend(...) method. The legend maps each character in the ASCII grid to a tile name. TypeScript infers the set of characters present in the template literal and requires a legend entry for each one.
If the legend references a tile name not declared in the tileset (tile("water") when the tileset has no water tile), tsc emits an error before any ROM is built.
Builder methods
| Method | Description |
|---|
.tileset(ts) | Set the tileset; propagates the tile-name type into .layer(...). |
.layer(spec) | Add a tile layer from an ascii template tag spec with a .legend({...}) call. |
.entities(...children) | Add JSX entity children: <Npc>, <Sign>, <Warp>, <PlayerSpawn>, <Entrance>. |
.spawn(id) | Fluent PlayerSpawn placement — .spawn("spawn").at(x, y).facing("down"). |
.entrance(id) | Fluent Entrance placement. |
.npc(id) | Fluent Npc builder — .npc("elder").sprite(hero).at(6,6).facing("down").talk(script). |
.sign(text) | Fluent Sign — .sign("POCKET TOWN").at(1, 0). |
.warp(to) | Fluent Warp — .warp("route101:north").at(9, 17). |
.done() | Finalize and register the MapDecl; returns it for use in defineGame. |
Entity prefabs (JSX)
JSX is used one layer above tile layers. Pure components run during the build, expand into static scene node trees, and never reach the GBA. The supported host element types are:
<PlayerSpawn>
Marks where the player appears when entering a map via the start field in defineGame, or via a warp that resolves to this entrance id.
<PlayerSpawn id="spawn" at={[9, 14]} facing="up" />
<Entrance>
A named warp destination — other maps’ <Warp to="this-map:entrance-id"> resolve to this position.
<Entrance id="south" at={[9, 15]} facing="up" />
<Npc>
An NPC actor. The sprite prop takes a SpriteId returned by defineSprite. onTalk takes a ScriptRef from script(function* () { ... }).
<Npc id="rival" sprite={hero} at={[12, 9]} facing="down" onTalk={RivalTalk} />
<Npc id="mom" sprite={hero} at={[5, 8]} facing="right" movement="static" onTalk={MomTalk} />
Supported movement values: "static" (default), "wander", "patrolH", "patrolV".
<Sign>
A sign post. The compiler generates a synthetic one-line text script and places a solid actor at the tile position. If the tileset contains a tile named "sign", that tile is substituted at the sign’s position in the tile grid.
<Sign text="LITTLEROOT TOWN. Home of new trainers." at={[8, 4]} />
<Warp>
A tile that teleports the player to another map entrance. The compiler resolves "destMap:entranceId" to concrete tile coordinates and a facing direction at build time.
<Warp to="route101:north" at={[9, 17]} />
Warp resolution is validated at build time. If destMap does not exist or entranceId is not declared on that map, the compiler emits an error. There are no unresolved warp references at runtime.
Scripts (residual zone)
Dialogue and interaction scripts are declared with script(function* () { ... }). The generator body is never executed — the compiler reads it as an AST and lowers supported statements to stack VM bytecode. Unsupported JavaScript constructs (loops, try/catch, arbitrary expressions, dynamic values) are hard compile errors.
const RivalTalk = script(function* () {
yield lockPlayer();
yield facePlayer("rival");
if (yield hasFlag("beat_rival_1")) {
yield say("The road ahead is tougher than it looks.");
} else {
yield say("You made it! Want to test your first build?");
const answer = yield choose(["Battle", "Maybe later"] as const);
switch (answer) {
case "Battle":
yield battle("rival_1");
yield setFlag("beat_rival_1");
yield giveItem("potion", 1);
yield say("Take this Potion. You will need it.");
break;
case "Maybe later":
yield say("No problem. I will be right here.");
break;
}
}
yield releasePlayer();
});
Supported ops
| Op | Lowers to | Description |
|---|
say(text) | TEXT <id> | Show a text box; suspend until the player presses A. |
choose(options) | CHOICE n, t0..t(n-1) | Show a choice menu (1–7 options); suspend, push chosen index. |
hasFlag(id) | PUSH_FLAG <id> | Push the flag value (0 or 1) onto the stack. |
setFlag(id) | SET_FLAG <id> | Set a flag to 1. |
clearFlag(id) | CLEAR_FLAG <id> | Set a flag to 0. |
lockPlayer() | LOCK_PLAYER | Prevent the player from moving. |
releasePlayer() | RELEASE_PLAYER | Allow the player to move again. |
facePlayer(actor) | FACE_PLAYER <slot> | Turn an actor to face the player. |
battle(id) | BATTLE <id> | Trigger a battle (v1 stub: pushes 1=win). |
giveItem(id, n) | GIVE_ITEM <id> <n> | Give the player an item (v1 stub). |
wait(frames) | WAIT <n> | Suspend for N frames. |
setVar(id, val) | SET_VAR <id> <val> | Set an integer variable. |
addVar(id, delta) | ADD_VAR <id> <delta> | Add a delta to an integer variable. |
getVar(id) | PUSH_VAR <id> | Push a variable’s value onto the stack. |
playSfx(id) | PLAY_SFX <id> | Play a sound effect (v1 stub, no-op). |
Supported control flow
| Pattern | Description |
|---|
if (yield hasFlag(...)) { ... } else { ... } | Conditional branch on a flag or other value-returning op. |
const x = yield choose([...]); switch (x) { ... } | Choice menu dispatch. The switch must immediately follow the const declaration and its cases must match the choose options exactly. |
Sequence of yield op(...) statements | Linear script execution. |
Unsupported DSL functions (compile errors in script bodies)
The following DSL functions exist in @pocketjs/aot for typing purposes but are not yet supported by the residualizer — using them inside a script() body is a hard compile error (unsupported script op):
warpTo(dest) — the WARP opcode exists in the VM ISA but the residualizer does not yet lower warpTo calls. Use <Warp> elements in the static declaration zone to connect maps.
takeItem(id, n) — not lowered; planned for a future release.
What the compiler rejects
Any JavaScript not in the supported set above is a hard compile error. This includes:
for, while, or do...while loops
try/catch/finally
- Arbitrary expressions or computed values in op arguments (op arguments must be compile-time constants)
- Calling functions other than the supported DSL ops inside
yield
- Nested
script() calls
- Any
if condition that is not yield <value-op>(...)
defineGame
defineGame registers the top-level game declaration. The start field is a "mapName:entranceId" string resolved at build time.
export default defineGame({
title: "POCKET TOWN",
start: "littleroot:spawn",
maps: [Littleroot, Route101],
sprites: ["hero"],
items: ["potion"],
battles: ["rival_1"],
flags: ["beat_rival_1", "intro_done"],
});
Pre-declaring flags, items, battles, and vars in defineGame ensures stable numeric IDs regardless of script order. The compiler validates that all referenced ids exist.
The full demo
The aot/demo/game.tsx file is the canonical example. It declares two maps (Littleroot Town and Route 101), three NPC scripts with branching dialogue and a choice menu, and a warp between them. The aot/demo/assets.ts file declares the town tileset and hero sprite using deterministically generated pixel data from aot/demo/imagegen/.
Tile pixel data in real projects is generated from source art (Aseprite slices, PNG sheets) by a custom build step. The demo’s imagegen/build-assets.ts extracts 4bpp palette-indexed tiles from a retro RPG sprite sheet and writes them as TypeScript constants. TMX/Aseprite import are documented follow-ups for v2.