Tiled Map Requirements
Hs2d uses Tiled maps as authoring files, but the Roku runtime expects a small canonical JSON subset. Keep the workflow explicit:
Raw Tiled exports are allowed to contain editor metadata, encoded tile data, and external JSON tilesets. Prepared runtime JSON is the normalized shape that Hs2dLevelLoader reads on device.
Prep Command
Run the prep command before adding a map to an asset bundle:
npx hst game:prep-level levels/level-1.json -o asset-bundles/my-game/levels/level-1.json
npx hst game:prep-level levels/level-1.json --pretty
The prep tool accepts JSON map files, including .tmj content when it is JSON. It does not parse XML maps or XML tilesets.
Raw Export Vs Runtime JSON
| Concern | Raw Tiled export | Prepared runtime JSON |
|---|---|---|
| Tile data | Plain numeric JSON arrays with encoding absent, or base64 strings with optional zlib/gzip compression. An explicit encoding: "csv" is not accepted by the prep tool. | Plain numeric data arrays only. No encoding or compression. |
| Tilesets | Embedded tilesets or external JSON tilesets. | One embedded active tileset, with unused tilesets removed. |
| External TSX | Tiled can reference XML .tsx. | Rejected. Save as JSON .tsj or embed the tileset. |
| Editor fields | May include version, tiledversion, compressionlevel, nextlayerid, nextobjectid, and editorsettings. | Stripped. Runtime does not need editor-only fields. |
| Infinite/chunked maps | Tiled can export chunked data for infinite maps. | Rejected. Runtime supports finite dense maps only. |
| Flipped/rotated tiles | Tiled stores flip flags in high gid bits. | Rejected. Add pre-flipped tiles to the tileset instead. |
The prep step does not replace runtime validation. It creates the canonical shape, and parseTiledJsonLevel still rejects unsupported input if a bad file reaches runtime.
Supported Runtime Subset
A runtime map must satisfy all of these rules:
| Requirement | Runtime behavior |
|---|---|
type is absent or map | Any other type is rejected. |
orientation is orthogonal | Isometric, staggered, hexagonal, and missing orientation are rejected. |
infinite is absent or false | Infinite maps are rejected. |
width, height, tilewidth, and tileheight are positive integers | Missing, zero, fractional, or negative dimensions are rejected. |
layers is an array | Missing layer arrays are rejected. |
Tile layers contain a dense data array with map width * height entries | Encoded, compressed, chunked, or wrong-length tile layers are rejected. The runtime parser validates against map dimensions, not layer-local dimensions. |
| Tile gids do not contain flip flags | Any gid >= 2^28 is rejected. |
| Zero or one tileset is present | More than one tileset is rejected by runtime. Building tile layers requires tileset metadata. |
| Layer names are stable ids | Hs2dLevel uses Tiled layer name, not numeric id, as the lookup id. |
parseTiledJsonLevel flattens group layers recursively. Native group parallax, offsets, visibility, and opacity are composed into child layers:
- parallax is multiplied down the group tree
- offsets are added
- visibility is inherited with logical AND
- opacity is multiplied and preserved on parsed layers
The world builder uses visibility to skip layers. Opacity is parsed for callers, but the current builder paths do not apply layer opacity during rendering.
Layer Types
| Tiled layer type | Runtime support |
|---|---|
imagelayer | Parsed as an image layer. Built as sky, direct parallax, or surface parallax depending on properties. |
tilelayer | Parsed as tile kinds. Built as dynamic, static, or cached tile rendering. |
objectgroup | Parsed as objects. Built as decor or entities, or left as data-only zones. |
group | Flattened by the parser; group transform state is composed into children. |
Other layer types are ignored by the parser because they do not match the supported discriminated union.
Tilesets And Tile Kinds
Runtime tile ids are rebased from Tiled gids:
Tiled gid 0 -> tile kind -1 (empty)
Tiled gid firstgid -> tile kind 0
Tiled gid firstgid+1 -> tile kind 1
Hs2dLevel exposes tile metadata through:
| API | Purpose |
|---|---|
getTileKinds(layerId) | Dense tile-kind array for a tile layer. |
getTileProperties(tileKind) | Custom tileset properties for a tile kind. |
getTileAnimation(tileKind) | Tiled animation frames as { tileKind, durationMs }. |
isSolidTile(x, y, layerId = 'terrain') | World collision helper by tile coordinate. |
isTileKindSolid(tileKind) | Solidity helper for a tile kind. |
Solidity uses a compatibility rule. If any tileset tile declares a solid property, only tiles with solid: true or solid: "true" are solid. If no tiles declare solid, any non-empty tile is treated as solid.
Object Shape
Objects are normalized into a stable Hs2dLevelObject shape:
| Field | Runtime value |
|---|---|
id | String form of Tiled object id, or the object index when no id exists. |
name | Tiled object name, default ''. |
type | Tiled object type, default ''. Entity bindings use this value as the binding key. If your Tiled UI labels this as class, confirm the exported JSON still has a type field or normalize it before runtime. |
x, y, width, height | Tiled world coordinates, defaulting missing values to 0. |
centerX, centerY | Derived from x/y/width/height. |
properties | Flattened custom properties as a plain object. |
The parser does not currently preserve Tiled object rotation, polygon geometry, text, ellipse state, or tile object gid in Hs2dLevelObject. Use rectangular point/area objects for entities and zones.
Prep Rejection Cases
hst game:prep-level rejects:
| Case | Reason |
|---|---|
| Invalid JSON | The prep tool only reads JSON/TMJ content. |
Non-map type | Hs2d levels are Tiled maps. |
| Non-orthogonal orientation | Runtime only supports orthogonal maps. |
| Infinite maps or chunked layers | Runtime requires finite dense arrays. |
| Missing layers | Runtime requires a layer array. |
| Unsupported tile encoding | Only plain arrays and base64 strings are normalized. |
| Unsupported compression | Base64 with none, zlib, or gzip is supported; zstd and others are rejected. |
| Corrupt base64 data | Tile data must decode to 32-bit little-endian gids. |
| Wrong data length | At prep time, tile count must equal layer.width * layer.height. A normally exported finite map uses map-sized layers. |
| Flip flags on tile layers or tile objects | Runtime has no flip/rotate path. |
External .tsx tilesets | Save as JSON .tsj or embed in the map. |
| More than one used tileset | Runtime supports one active tileset per map. |
Unused tilesets are dropped with warnings. External JSON tilesets are inlined relative to the map path.
Runtime Rejection Cases
parseTiledJsonLevel rejects:
| Case | Error class |
|---|---|
| Empty or invalid parsed map value | Invalid level JSON. |
Non-map type | Invalid map type. |
Orientation other than orthogonal | Unsupported map orientation. |
infinite: true | Unsupported finite-map contract. |
| Invalid map/tile dimensions | Invalid dimensions. |
Missing layers array | Missing layers. |
Tile layer chunks | Unsupported chunked layer. |
Tile layer encoding or compression | Unsupported encoded/compressed runtime data. |
Tile layer data is not an array | Unsupported runtime data. |
| Tile layer data length mismatch | Invalid data length. |
| Gid flip flags | Unsupported flip flags. |
| More than one tileset | Unsupported multiple tilesets. |
The prep and runtime checks differ slightly: prep validates against each tile layer's declared dimensions, while runtime validates every tile layer against the map dimensions. Keep finite tile layers map-sized so a file cannot pass prep and then fail at runtime.
Do And Don't
Do:
- author finite orthogonal maps
- keep layer names stable because they are runtime ids
- use one combined tileset image per map
- run
hst game:prep-levelas part of the asset pipeline - express collision through tileset
solidproperties - use object layers for entities, decor, spawn points, hazards, triggers, and other world data
Don't:
- load raw base64/compressed Tiled output directly on device
- rely on Tiled flip or rotate flags
- use multiple active tilesets in one map
- put runtime decisions in editor-only fields
- query raw JSON in frame loops
- expect object rotation, polygons, or tile object gids to be available in
Hs2dLevelObject
Source Reference
| Source | Confirms |
|---|---|
../hosanna-tools/src/support-tools/tiled-prep.ts | Prep normalization, external tileset handling, warnings, and prep-time rejection cases. |
../hosanna-tools/src/support-tools/tiled-prep.test.ts | Tested prep cases for base64, zlib, gzip, external .tsj, TSX rejection, flip flags, and multiple tilesets. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dTiledLevel.ts | Runtime parser, supported map subset, group flattening, object normalization, tileset parsing, and runtime rejection cases. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dTiledLevel.test.ts | Parser behavior for tile rebasing, layer order, image layers, group composition, custom properties, animations, and rejection cases. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dLevel.ts | Level APIs, property helpers, object lookup, parallax sprite specs, and tile solidity semantics. |