Lexicons #
ATProto schema definitions for tilemap.world records. NSID authority is the
project domain reversed: tilemap.world → world.tilemap.*.
world.tilemap.tileset #
Mirrors the editor's Tileset save format (src/store/persistence.ts). Record
key is a tid (many tilesets per repo); the editor's stable UUID lives in
stableId. Maps reference a tileset by the AT-URI of its record (see
world.tilemap.map's tilesets), so every tileset a map uses must be a
published record.
TypeScript types for this record live in src/lexicon/tileset.ts
(TilesetRecord and friends). persistence.ts converts between them and the
local save format with tilesetToRecord / recordToTileset — those take a
blob upload/resolve callback (the only field that can't match locally; see
below), so the actual PDS plumbing lives in the future sync layer.
Differences from the JSON save format #
The JSON file uses two Record<string, …> maps keyed by "col,row". Lexicon
objects can't have dynamic keys, so these become flat arrays of entries that
carry their own col/row:
Save format (Tileset) |
Lexicon record |
|---|---|
sectionAssignments: Record<key, …> |
sectionAssignments: #sectionAssignment[] |
tileProperties: Record<key, Property[]> |
tileProperties: #tileProperty[] |
version: number |
formatVersion: integer |
id: string |
stableId: string (rkey is a separate tid) |
properties, collision, frames, variations are already arrays and map 1:1.
Image: embedded vs. external (the redistribution case) #
image is a union of two variants:
#embeddedImage— the sheet stored as ablobin the repo. Use only for art that may be redistributed.#externalImage— no bytes are stored. A descriptor (name,width,height, optionalsha256,source,author,license) for a sheet the consumer must supply locally. This is the path for assets that are not licensed for redistribution: the record captures the grid geometry and how to obtain the file, the editor prompts the user for it, and — whensha256is present — verifies the supplied file is the expected one.
image is omitted entirely for a tileset that has no sheet yet.
License (license) — open enum #
A string holding an SPDX identifier. Modeled as an open enum (knownValues,
not enum): the editor suggests a set but any SPDX id — or other string — is
valid. The suggested set is GitHub's commonly-offered licenses (GET /licenses,
13 entries) plus the Creative Commons family relevant to art assets
(CC-BY-4.0, CC-BY-SA-4.0, …). Tangled has no license lexicon, so there's no
prior art to match here; the full SPDX list (~600 ids) lives at
https://spdx.org/licenses/.
There's a separate license on #externalImage — that one is about the
sheet's own licensing; this top-level license covers the tileset's contents.
world.tilemap.map #
A map is a tree of scene nodes authored in a concrete pixel coordinate
space over a single tile grid (#gridConfig: tileWidth/tileHeight in
pixels). Pixel sizes are part of the map now — they are not deferred to
export. The map references the tilesets it paints/places with in its
tilesets list (#tilesetRef[]).
Multiple tilesets (tilesets) — per-reference binding #
A map may draw from several tilesets at once. Each #tilesetRef binds a
short local ref alias (a, b, …, unique within the map) to a tileset
record's AT-URI (uri) — every tileset is a record, referenced by its
at://…/world.tilemap.tileset/… URI. Every place that names a material or
object then carries the alias, so a single layer can freely mix tiles from
different tilesets (Tiled-style):
| Reference | Tileset named by |
|---|---|
#cellPaint / #edgePaint / #cornerPaint |
tileset alias (omitted for the void sentinel — it is tileset-independent) |
#objectNode |
tileset alias (required) |
#tileBackground |
tileset alias (required) |
Material ids and object ids are unique only within their own tileset, so the
alias is what disambiguates them. The first tilesets entry is treated as
primary where a single tileset is implied. The list is empty/omitted for a map
that references no tileset yet.
Record key is a tid (many maps per repo); the editor's stable UUID lives in
stableId so a world.tilemap.world can keep referencing maps by id across
syncs. TypeScript types live in src/lexicon/map.ts (MapRecord and friends).
Nodes (a $type-discriminated union) #
nodes is an array of a union; a group may nest further nodes via its
children. Array order is draw order (first drawn first). The five kinds:
| Node | Holds |
|---|---|
#groupNode |
children[] — organising container, carries no art |
#canvasNode |
layers[] — one or more painted material layers, drawn bottom-to-top |
#objectNode |
tileset + objectId + pixel x,y — a placed tileset object |
#areaNode |
boxes[] — a region; shape is the additive union of the boxes |
Every node carries id, optional name/hidden, and a free-form properties
bag (the same escape hatch tilesets use).
Collision is an area, not its own kind. A movement-blocking region is an
#areaNode carrying the reserved property world.tilemap.collision = 1
(COLLISION_PROPERTY/isCollision in src/lexicon/map.ts). This sits in the
world.tilemap.* reserved-key namespace alongside world.tilemap.material and
world.tilemap.weight, so consumers recognise collision without app-specific
knowledge.
Canvas layers & the grid offset #
A #canvasNode holds an ordered layers[]; layer order is a strict draw
order (index 0 = bottom). The whole canvas takes one depth band via sort
(dynamic / ground / sky — see below). Each #canvasLayer paints with the
material approach in three channels and may shift the map grid by a pixel
offset (offsetX/offsetY) — e.g. with a 48px grid, 24,24 staggers a layer
half a cell. The three channels mirror the tileset's dynamic-key problem: at runtime
they are keyed by col,row (and edge orientation / corner), so on the wire each
becomes a flat array of entries carrying its own coordinates:
| Runtime (keyed map) | Lexicon (flat list) | Entry carries |
|---|---|---|
cells: Record<"c,r", mat> |
cells: #cellPaint[] |
col, row |
edges: Record<edgeKey, mat> |
edges: #edgePaint[] |
orient, col, row |
corners: Record<cornerKey,…> |
corners: #cornerPaint[] |
corner, col, row |
cells fills (material is a tileset material id — with tileset naming which
tileset by its local ref — or the void sentinel for an explicit empty cell,
in which case tileset is omitted), edges adds feature strokes over the
auto-computed Wang transitions, and corners are quarter-cell dot overrides.
Each of the three channels carries its own tileset alias, so one layer can
mix materials from different tilesets.
Depth sorting — bands & slices #
Depth runs along one map-level depthAxis (y default, or x; larger
coordinate = nearer the viewer). Objects sort by their origin coordinate on that
axis. Each canvas picks a sort band:
dynamic(default) — joins the depth pool and interleaves with objects.ground— pinned behind all dynamic content (the base ground). At most one canvas in the map holds this band.sky— pinned in front of all dynamic content (overlay). Also unique.
A dynamic canvas's layers can each be cut into bands by slices — an
ascending list of cut positions in map pixels along the axis. Each band
interleaves with objects at its own far-edge depth (the band's largest
coordinate on the axis); an empty slices means the whole layer is one band.
This is what makes a fenced-in area work without per-object data: paint the
fence on a dynamic canvas and drop a cut between its north and south runs (and
a few down the sides for per-row precision). A player inside then draws in front
of the bands above their feet and behind the bands below. Slicing is
author-controlled, so the common case costs nothing and only the fence carries a
short int array — the minimal sort dataset.
(Genuinely unit-sorted tall art should be an #objectNode, which sorts as one
unit by its feet, rather than painted cells.)
Attribution (attribution) — DID or name+url #
An array of #credit, each pairing an #agent with an open-enum role
(creator, contributor, artist, …). An #agent is identified either by
atproto identity (did, with handle as a display hint) or, for creators
not on atproto, by name (+ optional url). At least one of did / name is
expected. (Tangled credits people by DID only; we also allow name+url so
off-network artists can be credited.)