Editor
Documentation

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

ValueMeaning
0Empty
1 to 9999Built-in Hoenn tileset: frame number + 1
(k + 1) × 10000 + frame + 1Frame of the custom tileset at index k in tilesets

Tile behaviors

behaviorTile
0Normal
1 to 4Ledge jumped down, up, left, right
5Surf water (crossed on a Water type, ignores solid)
6Ice
7 to 10Push 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 town

Zones, 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.

On this page