City file format
What's inside an exported .json city, for backups, scripts and tools.
Export .json saves the whole city as one JSON file. You can edit it with a script and bring it back with Import file in Your cities. Missing or broken fields are filled in with defaults on import.
To have an AI agent write a whole game file, see Make a game with an AI agent: its guide, /agents.md, covers this format in depth with a tile catalog and a builder script.
City
type City = {
v: 1;
id: string;
name: string; // up to 40 characters
author: string;
width: number; // tiles
height: number;
layers: {
ground: number[]; // width × height tile values, row by row
decor: number[]; // the Objects layer
above: number[];
};
solid: number[]; // 1 = blocked
behavior: number[]; // tile behaviors, see below (0 = normal)
zone: number[]; // 0 = none, n = zones[n - 1]
gate: number[]; // 0 = open, n = the story step whose `n` is n
zones: Zone[];
npcs: Npc[];
spawn: { x: number; y: number };
doors: Door[];
triggers: Trigger[]; // event areas that play a script when stepped in
story: Step[]; // main story and side quest steps, in order within each
quests: Quest[]; // side quests
interiors: City[]; // interiors and outdoor maps, same shape, without story, quests, zones, tilesets, music
outdoor?: boolean; // on an outdoor map (route, town): joined to the region at its edges
region?: { x: number; y: number }; // its position on the region grid, in tiles (the outside map is at 0, 0)
tilesets?: Tileset[];
sprites?: Sprite[]; // custom looks for people and Pokémon
music?: { town?: string; battle?: string };
weather?: "rain" | "snow" | "fog" | "sandstorm"; // per map, clear when not set
time?: "morning" | "evening" | "night" | "clock"; // per map, day when not set; "clock" follows the player's clock
type?: "outdoor" | "indoor" | "cave"; // per map, picks the automatic battle scene (outdoor for the outside and outdoor maps, indoor for interiors)
dark?: boolean; // per map: dark until the player has HM05 Flash
song?: string; // per map: its own music (an uploaded track url), the town music when not set
rules?: { difficulty?: "easy" | "hard"; caps?: number[]; followers?: false }; // outside map only: trainer difficulty (normal when not set), level caps by badges, no Pokémon following the player
start?: string; // outside map only: id of the map new players start in, on its spawn (the outside map when not set)
intro?: { speech: string }; // outside map only: what the Professor says before a new game starts (blank lines split pages)
battle?: { image: string; platforms: boolean }; // per map battle background: a picture url, or an Emerald scene "/game/battle/<scene>.png" (tall_grass, long_grass, sand, rock, cave, water, pond_water, underwater, sky, building, stadium); maps without one use the outside map's, then the automatic scene
updated: number; // timestamp in milliseconds
};Every grid is a flat array of width × height numbers. The tile at column x and row y is at index y * width + x.
Tile values
| Value | Meaning |
|---|---|
0 | Empty |
1 to 9999 | Built-in Hoenn tileset: frame number + 1 |
(k + 1) × 10000 + frame + 1 | Frame of the custom tileset at index k in tilesets |
Tile behaviors
behavior | Tile |
|---|---|
0 | Normal |
1 to 4 | Ledge jumped down, up, left, right |
5 | Surf water (crossed on a Water type, ignores solid) |
6 | Ice |
7 to 10 | Push tile moving the player up, down, left, right |
Story steps
type Step = {
id: string; // used by people's story settings
quest?: string; // side quest id, main story when not set
goal?: string; // what the Journal says while it's the next step
n: number; // used by gate tiles, stays the same when steps are reordered
name: string;
blocked: string; // message at the gates
blocker?: "wall" | "object" | "person";
tile?: number; // tile drawn on the gates when blocker is "object"
};
type Quest = {
id: string;
name: string;
after?: string; // step id: the quest starts once it's done
};People
type Npc = {
x: number;
y: number;
sprite: string; // "" for signs and Pokémon, "custom:<id>" for a custom sprite
creature?: { species: number; level: number; look?: string }; // a Pokémon on the map, look = custom sprite id
dir: "up" | "down" | "left" | "right";
name: string;
dialogue: string; // an empty line starts a new page
role: "talk" | "trainer" | "healer" | "wild" | "raid" | "shop" | "item" | "obstacle" | "daycare"; // "wild": a Pokémon to battle and catch, "raid": a raid den, "shop": a Poké Mart clerk, "item": an item ball holding `gift`, "obstacle": cleared with a field move, "daycare": raises a Pokémon
shop?: ItemId[]; // what a clerk sells, an early-game stock when not set
gift?: { item: ItemId; count: number }; // given the first time the player talks to them (or found in an item)
hidden?: boolean; // an item hidden on the ground instead of a ball
grow?: number; // a berry tree instead: hours until it can be picked again
field?: "cut" | "smash" | "strength"; // an obstacle: small tree, cracked rock or boulder
script?: { actions: Action[]; once: boolean }; // plays instead of their usual part when talked to
approach?: boolean; // walks up to the player and talks the first time they see them
wander: boolean;
path?: {
points: { x: number; y: number }[]; // stops after where they stand
loop: boolean; // true: back to the start, false: walk back the same way
run: boolean;
pause: number; // seconds at each stop
};
trainer?: {
team: {
species: number; // national Pokédex numbers, 1 to 1025
level: number;
held?: ItemId;
moves?: string[]; // up to 4 move keys from its level-up list ("rock-throw"), automatic when not set
ability?: string; // one of its species' abilities ("sturdy"), random when not set
nature?: string; // "adamant"…, random when not set
nickname?: string;
shiny?: boolean;
}[];
gym: boolean;
badge: string;
defeat: string;
lines?: { last?: string; low?: string }; // said mid-battle: sending out their last Pokémon, and when it's low on HP
rules?: { difficulty?: "easy" | "normal" | "hard"; weather?: "clear" | "rain" | "sandstorm" | "snow"; scene?: string; noItems?: boolean; double?: boolean }; // over the town's and map's
};
story?: {
after?: string; // Shows up (step id)
until?: string; // Leaves
requires?: string; // Needs first
locked?: string; // Says until then
completes?: string; // Talking (or beating them) completes
done?: string; // Says afterwards
reward?: { species?: number; level?: number; balls?: number; potions?: number; money?: number };
};
};
// Sold by default: "poke-ball" | "great-ball" | "potion" | "super-potion" | "antidote" | "paralyze-heal" | "awakening" | "burn-heal" | "full-heal" | "repel" | "escape-rope"
// Every item id is listed in the AI guide (/agents.md, section 5.3): stronger potions, revives, Rare Candy, evolution stones, the Linking Cord, held evolution items, the Exp. Share
// Key items (given, not sold): "old-rod" | "good-rod" | "super-rod" | "hm-flash"
// Held items: "oran-berry" | "sitrus-berry" | "lum-berry" | "leftovers" | "quick-claw" | "charcoal" | "mystic-water" | "miracle-seed" | "magnet" | "silk-scarf"
type ItemId = string;Events and scripts
type Trigger = {
id: string;
x: number; // top-left tile of the area
y: number;
w: number;
h: number;
name: string;
once: boolean; // plays only the first time, per player
after?: string; // only once this step is done
until?: string; // no more once this step is done
actions: Action[];
};
// `who` is someone's index in the map's `npcs`, or "player"
type Action =
| { do: "say"; text: string; who?: number } // no `who`: narration
| { do: "ask"; text: string; who?: number; yes: Action[]; no: Action[] } // a YES/NO question, then one branch
| { do: "if"; check: Check; then: Action[]; else: Action[] } // a check, then one branch
| { do: "come"; who: number } // notice the player and walk up to them
| { do: "move"; who: number | "player"; steps: ("up" | "down" | "left" | "right")[] }
| { do: "face"; who: number | "player"; dir: "up" | "down" | "left" | "right" }
| { do: "back"; who: number } // walk back to where they stand
| { do: "wait"; seconds: number } // 0 to 10
| { do: "battle"; who: number } // a trainer or wild Pokémon of the map, stops the script if the player loses
| { do: "give"; item: ItemId; count: number }
| { do: "take"; item: ItemId; count: number } // take items from the bag
| { do: "pokemon"; species: number; level: number } // the player may nickname it
| { do: "starter"; species: number[]; level: number } // the player picks one of them (up to 6) from the Professor's bag
| { do: "money"; amount: number } // below 0 takes money
| { do: "heal" }
| { do: "step"; step: string } // complete a story step
| { do: "warp"; to: string; x: number; y: number }; // map id ("" = outside) and tile, ends the script
type Check =
| { has: "item"; item: ItemId; count: number }
| { has: "step"; step: string }
| { has: "money"; amount: number }
| { has: "pokemon"; species: number } // in the party
| { has: "badges"; count: number }; // badges of this townZones, doors and tilesets
type Zone = {
name: string;
rate: number; // 0.01 to 0.4, chance per step (walking and surfing)
color: number; // editor overlay color, like 0x2ecc40
creatures: {
species: number;
min: number;
max: number;
method?: "surf" | "fish"; // walking when not set
time?: "morning" | "day" | "night"; // any time when not set
rod?: "good" | "super"; // fishing: the weakest rod that hooks it (any rod when not set)
}[];
};
type Door = {
x: number;
y: number;
to: string; // interior id, "" for outside, "?" for nowhere yet
tx: number; // arrival tile, -1 to arrive next to the matching door
ty: number;
};
type Tileset = {
id: string;
name: string;
url: string; // uploaded file or data: URL
tile: number; // tile size in pixels
columns: number;
rows: number;
};type Sprite = {
id: string;
name: string;
url: string; // uploaded file or data: URL
kind: "person" | "pokemon";
layout?: "dreammist" | "rpgvx" | "rpgxp"; // person sheets
w: number; // frame size in pixels
h: number;
};Keep tileset order
Custom tile values depend on the tileset's position in tilesets. Don't reorder or remove entries, or painted tiles will point to the wrong image.