ludic/runtime/native/tiled.ludic
Orkuncakilkaya bf36bc8a8f refactor(runtime,packages,examples): named constants, package enums, idiom sweep
- runtime: HEADLESS_FRAME_PATH, STICK_DEADZONE / STICK_LEFT_X/Y, key and
  byte codes as char literals throughout (`k == 'w'`, `fill(rt_map, ' ', …)`)
- ludic.gameplay/stats: drop the duplicate `stat_field` (it answered "atk"
  for every build stat); Stats.base uses stats_field_name
- ludic.shooter: compare aim modes and fire patterns with AimMode.* and
  WeaponPattern.* instead of raw ints; STICK_RIGHT_X/Y
- ludic.npcai: DecisionMade / brain_set_state use AiState.*
- examples/games/menu.ludic uses Font.load / Ui.* with FONT_PATH and
  BACKDROP named; strings.ludic header says what it prints
- whole tree: `x = x + 1` → `x += 1` (single-term right-hand sides only),
  `0 - x` → `-x`, ASCII codes → char literals; every .ludic and every
  ```ludic fence reformatted with the fixed formatter (whitespace only)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00

1373 lines
54 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================================
# tiled.ludic — Tiled map support (`Tiled.*`), the design record of #66
# (issues #67–#74). Native TMX/TSX/TX (XML) and TMJ/TSJ/TJ (JSON) both map onto
# ONE intermediate — a generic `Value.*` tree in Tiled's JSON schema — and one
# format-independent core turns that into the runtime map model. So the two
# readers below (P0.5, #68) each feed the same `rt_tmap` builder (P1, #69).
#
# The JSON path is `Json.parse` + a normalisation pass; the XML path is the
# `xml_parse` walk in this file. Layer data is decoded to a dense GID int list at
# read time (CSV split, or base64 -> zlib/gzip inflate -> little-endian u32s), so
# a CSV `.tmx` and a base64 `.tmj` of the same map yield structurally identical
# trees.
#
# ludicc splices this file (with core + xml + base64 + value) when a program
# mentions `Tiled.*` (parse.ludic).
# ============================================================================
# ---- layer-data decode -----------------------------------------------------
# a CSV of GIDs -> a Value list of int nodes. Non-digit separators (commas,
# whitespace, newlines) delimit; overflow wraps to the correct 32-bit GID
# (a flip-flagged GID like 0x80000001 lands as the matching negative i32).
function tiled_csv_list(text: pointer) -> Val {
let out = value_list()
let n = len(text)
var i = 0
var cur = 0
var have = 0
while i < n {
let c = text[i]
if c >= '0' and c <= '9' { cur = cur * 10 + (c - 48); have = 1 }
else {
if c == ',' or c == ' ' or c == '\t' or c == '\n' or c == '\r' {
if have == 1 { push(out.kids, value_int(cur)); cur = 0; have = 0 }
}
}
i += 1
}
if have == 1 { push(out.kids, value_int(cur)) }
return out
}
# a base64 (optionally zlib/gzip-compressed) blob of `count` little-endian u32
# GIDs -> a Value list of int nodes.
function tiled_b64_list(text: pointer, compression: pointer, count: int) -> Val {
let comp = bytes(len(text) + 4)
let clen = b64_decode(text, comp)
let outcap = count * 4 + 16
var raw = comp
var rawlen = clen
if compression == "zlib" {
let d = bytes(outcap); let dn = z_uncompress(comp, clen, d, outcap); raw = d; rawlen = dn
} else { if compression == "gzip" {
let d = bytes(outcap); let dn = z_gunzip(comp, clen, d, outcap); raw = d; rawlen = dn
} else { if compression == "zstd" {
let d = bytes(outcap); let dn = z_zstd(comp, clen, d, outcap); raw = d; rawlen = dn
} } }
let out = value_list()
var i = 0
while i + 3 < rawlen {
let g = raw[i] | (raw[i + 1] << 8) | (raw[i + 2] << 16) | (raw[i + 3] << 24)
push(out.kids, value_int(g))
i += 4
}
return out
}
# decode the text of `node` under encoding `enc` / compression `comp` -> GID list.
function tiled_decode_enc(node: Xml, enc: pointer, comp: pointer, count: int) -> Val {
if enc == "base64" { return tiled_b64_list(xml_text(node), comp, count) }
return tiled_csv_list(xml_text(node)) # csv (or the tag-per-tile form)
}
# decode a `<data>` element (child of a `<layer>`) into a Value list of GIDs.
function tiled_data_list(data: Xml, count: int) -> Val {
return tiled_decode_enc(data, xml_attr(data, "encoding"), xml_attr(data, "compression"), count)
}
# flatten a chunked (infinite-map) `<data>` — its `<chunk x y width height>`
# children — into a dense GID list; sets `o`'s width/height/data (#74).
function tiled_chunked_layer(data: Xml, o: Val) -> void {
let enc = xml_attr(data, "encoding")
let comp = xml_attr(data, "compression")
# pass 1: bounds over every chunk (tile coordinates)
var minx = 1000000000
var miny = 1000000000
var maxx = -1000000000
var maxy = -1000000000
var i = 0
while i < xml_child_count(data) {
let ch = xml_child(data, i)
if xml_tag(ch) == "chunk" {
let cx = xml_attr_int(ch, "x", 0)
let cy = xml_attr_int(ch, "y", 0)
if cx < minx { minx = cx }
if cy < miny { miny = cy }
if cx + xml_attr_int(ch, "width", 0) > maxx { maxx = cx + xml_attr_int(ch, "width", 0) }
if cy + xml_attr_int(ch, "height", 0) > maxy { maxy = cy + xml_attr_int(ch, "height", 0) }
}
i += 1
}
let W = maxx - minx
let H = maxy - miny
let gids = value_list()
var k = 0
while k < W * H { push(gids.kids, value_int(0)); k += 1 }
# pass 2: place each chunk's decoded data at its offset
i = 0
while i < xml_child_count(data) {
let ch = xml_child(data, i)
if xml_tag(ch) == "chunk" {
let cx = xml_attr_int(ch, "x", 0) - minx
let cy = xml_attr_int(ch, "y", 0) - miny
let cw = xml_attr_int(ch, "width", 0)
let cht = xml_attr_int(ch, "height", 0)
let cdata = tiled_decode_enc(ch, enc, comp, cw * cht)
var yy = 0
while yy < cht {
var xx = 0
while xx < cw {
gids.kids[(cy + yy) * W + (cx + xx)] = value_at(cdata, yy * cw + xx)
xx += 1
}
yy += 1
}
}
i += 1
}
value_put(o, "width", value_int(W))
value_put(o, "height", value_int(H))
value_put(o, "data", gids)
}
# ---- custom properties -----------------------------------------------------
# `<properties>` -> a Value list of {name, type, value} objects (P4 reads these;
# P2 reads the `solid`/`oneway`/`trigger` bool convention off the same list).
function tmx_props_list(parent: Xml) -> Val {
let out = value_list()
let props = xml_find(parent, "properties")
if xml_tag(props) != "properties" { return out }
var i = 0
while i < xml_child_count(props) {
let p = xml_child(props, i)
if xml_tag(p) == "property" {
let o = value_object()
value_put(o, "name", value_str(xml_attr(p, "name")))
var ty = xml_attr(p, "type")
if ty == "" { ty = "string" }
value_put(o, "type", value_str(ty))
# value may be an attribute or (for multiline) the element text
var v = xml_attr(p, "value")
if xml_has(p, "value") == 0 { v = xml_text(p) }
value_put(o, "value", value_str(v))
push(out.kids, o)
}
i += 1
}
return out
}
# ---- objects (shapes) ------------------------------------------------------
# one `<object>` -> a Value object mirroring Tiled's JSON object shape.
function tmx_object_to_value(ob: Xml) -> Val {
let o = value_object()
value_put(o, "id", value_int(xml_attr_int(ob, "id", 0)))
if xml_has(ob, "name") == 1 { value_put(o, "name", value_str(xml_attr(ob, "name"))) }
if xml_has(ob, "type") == 1 { value_put(o, "type", value_str(xml_attr(ob, "type"))) }
if xml_has(ob, "class") == 1 { value_put(o, "type", value_str(xml_attr(ob, "class"))) }
if xml_has(ob, "template") == 1 { value_put(o, "template", value_str(xml_attr(ob, "template"))) }
value_put(o, "x", value_int(xml_attr_int(ob, "x", 0)))
value_put(o, "y", value_int(xml_attr_int(ob, "y", 0)))
value_put(o, "width", value_int(xml_attr_int(ob, "width", 0)))
value_put(o, "height", value_int(xml_attr_int(ob, "height", 0)))
if xml_has(ob, "gid") == 1 { value_put(o, "gid", value_int(xml_attr_int(ob, "gid", 0))) }
if xml_has(ob, "rotation") == 1 { value_put(o, "rotation", value_int(xml_attr_int(ob, "rotation", 0))) }
# shape markers: ellipse / point / polygon / polyline / text
let el = xml_find(ob, "ellipse"); if xml_tag(el) == "ellipse" { value_put(o, "ellipse", value_bool(1)) }
let pt = xml_find(ob, "point"); if xml_tag(pt) == "point" { value_put(o, "point", value_bool(1)) }
let pg = xml_find(ob, "polygon")
if xml_tag(pg) == "polygon" { value_put(o, "polygon", tmx_points_list(xml_attr(pg, "points"))) }
let pl = xml_find(ob, "polyline")
if xml_tag(pl) == "polyline" { value_put(o, "polyline", tmx_points_list(xml_attr(pl, "points"))) }
let tx = xml_find(ob, "text")
if xml_tag(tx) == "text" {
let t = value_object()
value_put(t, "text", value_str(xml_text(tx)))
if xml_has(tx, "pixelsize") == 1 { value_put(t, "pixelsize", value_int(xml_attr_int(tx, "pixelsize", 16))) }
if xml_has(tx, "bold") == 1 { value_put(t, "bold", value_bool(1)) }
if xml_has(tx, "italic") == 1 { value_put(t, "italic", value_bool(1)) }
if xml_has(tx, "halign") == 1 { value_put(t, "halign", value_str(xml_attr(tx, "halign"))) }
if xml_has(tx, "valign") == 1 { value_put(t, "valign", value_str(xml_attr(tx, "valign"))) }
value_put(o, "text", t)
}
let props = tmx_props_list(ob)
if len(props.kids) > 0 { value_put(o, "properties", props) }
return o
}
# "x,y x,y ..." -> a Value list of {x,y} objects.
function tmx_points_list(s: pointer) -> Val {
let out = value_list()
let n = len(s)
var i = 0
while i < n {
while i < n and (s[i] == ' ' or s[i] == '\t') { i += 1 }
if i >= n { break }
# read "x,y"
var xv = 0; var xn = 0; var xs = 0
if s[i] == '-' { xs = 1; i += 1 }
while i < n and s[i] >= '0' and s[i] <= '9' { xv = xv * 10 + (s[i] - 48); xn = 1; i += 1 }
if xs == 1 { xv = -xv }
if i < n and s[i] == ',' { i += 1 }
var yv = 0; var ys = 0
if i < n and s[i] == '-' { ys = 1; i += 1 }
while i < n and s[i] >= '0' and s[i] <= '9' { yv = yv * 10 + (s[i] - 48); i += 1 }
if ys == 1 { yv = -yv }
if xn == 1 {
let o = value_object()
value_put(o, "x", value_int(xv))
value_put(o, "y", value_int(yv))
push(out.kids, o)
}
}
return out
}
# ---- tilesets --------------------------------------------------------------
# one `<tile id=..>` inside a tileset -> a Value object with its metadata:
# animation frames, per-tile collision objectgroup, class/type, properties.
function tmx_tile_to_value(t: Xml) -> Val {
let o = value_object()
value_put(o, "id", value_int(xml_attr_int(t, "id", 0)))
if xml_has(t, "type") == 1 { value_put(o, "type", value_str(xml_attr(t, "type"))) }
if xml_has(t, "class") == 1 { value_put(o, "type", value_str(xml_attr(t, "class"))) }
if xml_has(t, "probability") == 1 { value_put(o, "probability", value_str(xml_attr(t, "probability"))) }
# per-tile image (image-collection tilesets)
let img = xml_find(t, "image")
if xml_tag(img) == "image" {
value_put(o, "image", value_str(xml_attr(img, "source")))
value_put(o, "imagewidth", value_int(xml_attr_int(img, "width", 0)))
value_put(o, "imageheight", value_int(xml_attr_int(img, "height", 0)))
}
# animation frames
let anim = xml_find(t, "animation")
if xml_tag(anim) == "animation" {
let frames = value_list()
var i = 0
while i < xml_child_count(anim) {
let fr = xml_child(anim, i)
if xml_tag(fr) == "frame" {
let f = value_object()
value_put(f, "tileid", value_int(xml_attr_int(fr, "tileid", 0)))
value_put(f, "duration", value_int(xml_attr_int(fr, "duration", 0)))
push(frames.kids, f)
}
i += 1
}
value_put(o, "animation", frames)
}
# per-tile collision shapes
let og = xml_find(t, "objectgroup")
if xml_tag(og) == "objectgroup" {
let objs = value_list()
var j = 0
while j < xml_child_count(og) {
let ch = xml_child(og, j)
if xml_tag(ch) == "object" { push(objs.kids, tmx_object_to_value(ch)) }
j += 1
}
let ogo = value_object()
value_put(ogo, "objects", objs)
value_put(o, "objectgroup", ogo)
}
let props = tmx_props_list(t)
if len(props.kids) > 0 { value_put(o, "properties", props) }
return o
}
# fill the geometry + tiles of a `<tileset>` element into `o` (shared by an
# embedded tileset and a standalone `.tsx` root).
function tmx_fill_tileset(o: Val, ts: Xml) -> void {
if xml_has(ts, "name") == 1 { value_put(o, "name", value_str(xml_attr(ts, "name"))) }
value_put(o, "tilewidth", value_int(xml_attr_int(ts, "tilewidth", 0)))
value_put(o, "tileheight", value_int(xml_attr_int(ts, "tileheight", 0)))
value_put(o, "spacing", value_int(xml_attr_int(ts, "spacing", 0)))
value_put(o, "margin", value_int(xml_attr_int(ts, "margin", 0)))
value_put(o, "columns", value_int(xml_attr_int(ts, "columns", 0)))
value_put(o, "tilecount", value_int(xml_attr_int(ts, "tilecount", 0)))
let img = xml_find(ts, "image")
if xml_tag(img) == "image" {
value_put(o, "image", value_str(xml_attr(img, "source")))
value_put(o, "imagewidth", value_int(xml_attr_int(img, "width", 0)))
value_put(o, "imageheight", value_int(xml_attr_int(img, "height", 0)))
}
let tiles = value_list()
var i = 0
while i < xml_child_count(ts) {
let ch = xml_child(ts, i)
if xml_tag(ch) == "tile" { push(tiles.kids, tmx_tile_to_value(ch)) }
i += 1
}
if len(tiles.kids) > 0 { value_put(o, "tiles", tiles) }
}
# a `<tileset>` child of a `<map>` -> a Value object. External (`source=`) keeps
# the {firstgid, source} reference (as TMJ does); embedded is inlined in full.
function tmx_tileset_to_value(ts: Xml) -> Val {
let o = value_object()
value_put(o, "firstgid", value_int(xml_attr_int(ts, "firstgid", 1)))
if xml_has(ts, "source") == 1 {
value_put(o, "source", value_str(xml_attr(ts, "source")))
return o
}
tmx_fill_tileset(o, ts)
return o
}
# a standalone `.tsx` root -> a tileset Value object (no firstgid — that comes
# from the map's reference).
function tsx_to_value(root: Xml) -> Val {
let o = value_object()
tmx_fill_tileset(o, root)
return o
}
# ---- layers ----------------------------------------------------------------
function tmx_layer_common(o: Val, el: Xml) -> void {
value_put(o, "id", value_int(xml_attr_int(el, "id", 0)))
value_put(o, "name", value_str(xml_attr(el, "name")))
if xml_has(el, "class") == 1 { value_put(o, "class", value_str(xml_attr(el, "class"))) }
var vis = 1
if xml_has(el, "visible") == 1 { vis = xml_attr_int(el, "visible", 1) }
value_put(o, "visible", value_bool(vis))
if xml_has(el, "opacity") == 1 { value_put(o, "opacity", value_str(xml_attr(el, "opacity"))) }
if xml_has(el, "offsetx") == 1 { value_put(o, "offsetx", value_int(xml_attr_int(el, "offsetx", 0))) }
if xml_has(el, "offsety") == 1 { value_put(o, "offsety", value_int(xml_attr_int(el, "offsety", 0))) }
if xml_has(el, "parallaxx") == 1 { value_put(o, "parallaxx", value_str(xml_attr(el, "parallaxx"))) }
if xml_has(el, "parallaxy") == 1 { value_put(o, "parallaxy", value_str(xml_attr(el, "parallaxy"))) }
if xml_has(el, "tintcolor") == 1 { value_put(o, "tintcolor", value_str(xml_attr(el, "tintcolor"))) }
let props = tmx_props_list(el)
if len(props.kids) > 0 { value_put(o, "properties", props) }
}
function tmx_tilelayer_to_value(el: Xml, mapw: int, maph: int) -> Val {
let o = value_object()
value_put(o, "type", value_str("tilelayer"))
tmx_layer_common(o, el)
let w = xml_attr_int(el, "width", mapw)
let h = xml_attr_int(el, "height", maph)
let data = xml_find(el, "data")
if xml_count(data, "chunk") > 0 { # infinite map: flatten the chunks
tiled_chunked_layer(data, o)
} else {
value_put(o, "width", value_int(w))
value_put(o, "height", value_int(h))
value_put(o, "data", tiled_data_list(data, w * h))
}
return o
}
function tmx_objectlayer_to_value(el: Xml) -> Val {
let o = value_object()
value_put(o, "type", value_str("objectgroup"))
tmx_layer_common(o, el)
let objs = value_list()
var i = 0
while i < xml_child_count(el) {
let ch = xml_child(el, i)
if xml_tag(ch) == "object" { push(objs.kids, tmx_object_to_value(ch)) }
i += 1
}
value_put(o, "objects", objs)
return o
}
# ---- map -------------------------------------------------------------------
# a `<map>` root -> the intermediate map Value tree (Tiled JSON schema).
function tmx_to_value(root: Xml) -> Val {
let m = value_object()
value_put(m, "type", value_str("map"))
value_put(m, "version", value_str(xml_attr(root, "version")))
value_put(m, "orientation", value_str(xml_attr(root, "orientation")))
value_put(m, "renderorder", value_str(xml_attr(root, "renderorder")))
value_put(m, "width", value_int(xml_attr_int(root, "width", 0)))
value_put(m, "height", value_int(xml_attr_int(root, "height", 0)))
value_put(m, "tilewidth", value_int(xml_attr_int(root, "tilewidth", 0)))
value_put(m, "tileheight", value_int(xml_attr_int(root, "tileheight", 0)))
value_put(m, "infinite", value_bool(xml_attr_int(root, "infinite", 0)))
if xml_has(root, "backgroundcolor") == 1 { value_put(m, "backgroundcolor", value_str(xml_attr(root, "backgroundcolor"))) }
if xml_has(root, "hexsidelength") == 1 { value_put(m, "hexsidelength", value_int(xml_attr_int(root, "hexsidelength", 0))) }
if xml_has(root, "staggeraxis") == 1 { value_put(m, "staggeraxis", value_str(xml_attr(root, "staggeraxis"))) }
if xml_has(root, "staggerindex") == 1 { value_put(m, "staggerindex", value_str(xml_attr(root, "staggerindex"))) }
let mapw = xml_attr_int(root, "width", 0)
let maph = xml_attr_int(root, "height", 0)
let tilesets = value_list()
let layers = value_list()
var i = 0
while i < xml_child_count(root) {
let ch = xml_child(root, i)
let tag = xml_tag(ch)
if tag == "tileset" { push(tilesets.kids, tmx_tileset_to_value(ch)) }
else { if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(ch, mapw, maph)) }
else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) }
else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) }
else { if tag == "group" { push(layers.kids, tmx_group_to_value(ch, mapw, maph)) } } } } }
i += 1
}
let props = tmx_props_list(root)
if len(props.kids) > 0 { value_put(m, "properties", props) }
value_put(m, "tilesets", tilesets)
value_put(m, "layers", layers)
return m
}
# `<imagelayer>` -> a Value object (P5 renders these).
function tmx_imagelayer_to_value(el: Xml) -> Val {
let o = value_object()
value_put(o, "type", value_str("imagelayer"))
tmx_layer_common(o, el)
let img = xml_find(el, "image")
if xml_tag(img) == "image" { value_put(o, "image", value_str(xml_attr(img, "source"))) }
if xml_has(el, "repeatx") == 1 { value_put(o, "repeatx", value_bool(xml_attr_int(el, "repeatx", 0))) }
if xml_has(el, "repeaty") == 1 { value_put(o, "repeaty", value_bool(xml_attr_int(el, "repeaty", 0))) }
return o
}
# `<group>` -> a Value object carrying its nested layers (P5 renders recursively).
function tmx_group_to_value(el: Xml, mapw: int, maph: int) -> Val {
let o = value_object()
value_put(o, "type", value_str("group"))
tmx_layer_common(o, el)
let layers = value_list()
var i = 0
while i < xml_child_count(el) {
let ch = xml_child(el, i)
let tag = xml_tag(ch)
if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(ch, mapw, maph)) }
else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) }
else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) }
else { if tag == "group" { push(layers.kids, tmx_group_to_value(ch, mapw, maph)) } } } }
i += 1
}
value_put(o, "layers", layers)
return o
}
# ---- JSON (TMJ) normalisation ----------------------------------------------
# The JSON reader already yields a Value tree; normalise it so it matches the XML
# path: decode any base64 `data` string into a dense GID int list, in place, for
# every tile layer (recursing into groups).
function tmj_normalize_layer(layer: Val) -> void {
if value_kind(layer) != 6 { return }
let ty = value_as_str(value_get(layer, "type"))
if ty == "group" {
let ls = value_get(layer, "layers")
var i = 0
while i < value_count(ls) { tmj_normalize_layer(value_at(ls, i)); i += 1 }
return
}
if ty != "tilelayer" { return }
let enc = value_as_str(value_get(layer, "encoding"))
let comp = value_as_str(value_get(layer, "compression"))
# infinite map: flatten the JSON `chunks` array into a dense data list (#74)
let chunks = value_get(layer, "chunks")
if value_kind(chunks) == 5 and value_count(chunks) > 0 {
tmj_flatten_chunks(layer, chunks, enc, comp)
return
}
let data = value_get(layer, "data")
if value_kind(data) == 4 { # a base64 string
let w = value_as_int(value_get(layer, "width"))
let h = value_as_int(value_get(layer, "height"))
if enc == "base64" { value_put(layer, "data", tiled_b64_list(value_as_str(data), comp, w * h)) }
}
}
# a JSON chunk's `data` (int array, or a base64 string) -> a GID Value list.
function tmj_chunk_gids(chunk: Val, enc: pointer, comp: pointer, count: int) -> Val {
let d = value_get(chunk, "data")
if value_kind(d) == 4 { return tiled_b64_list(value_as_str(d), comp, count) } # base64 string
return d # already an int array
}
# flatten JSON `chunks[]` into a dense data list on `layer`, sizing to the union.
function tmj_flatten_chunks(layer: Val, chunks: Val, enc: pointer, comp: pointer) -> void {
var minx = 1000000000
var miny = 1000000000
var maxx = -1000000000
var maxy = -1000000000
var i = 0
while i < value_count(chunks) {
let c = value_at(chunks, i)
let cx = value_as_int(value_get(c, "x"))
let cy = value_as_int(value_get(c, "y"))
if cx < minx { minx = cx }
if cy < miny { miny = cy }
if cx + value_as_int(value_get(c, "width")) > maxx { maxx = cx + value_as_int(value_get(c, "width")) }
if cy + value_as_int(value_get(c, "height")) > maxy { maxy = cy + value_as_int(value_get(c, "height")) }
i += 1
}
let W = maxx - minx
let H = maxy - miny
let gids = value_list()
var k = 0
while k < W * H { push(gids.kids, value_int(0)); k += 1 }
i = 0
while i < value_count(chunks) {
let c = value_at(chunks, i)
let cx = value_as_int(value_get(c, "x")) - minx
let cy = value_as_int(value_get(c, "y")) - miny
let cw = value_as_int(value_get(c, "width"))
let cht = value_as_int(value_get(c, "height"))
let cdata = tmj_chunk_gids(c, enc, comp, cw * cht)
var yy = 0
while yy < cht {
var xx = 0
while xx < cw {
gids.kids[(cy + yy) * W + (cx + xx)] = value_at(cdata, yy * cw + xx)
xx += 1
}
yy += 1
}
i += 1
}
value_put(layer, "width", value_int(W))
value_put(layer, "height", value_int(H))
value_put(layer, "data", gids)
}
function tmj_normalize(m: Val) -> Val {
let ls = value_get(m, "layers")
var i = 0
while i < value_count(ls) { tmj_normalize_layer(value_at(ls, i)); i += 1 }
return m
}
# ---- top-level readers -----------------------------------------------------
# skip leading whitespace and return the first non-space byte (0 if none).
function tiled_first_byte(s: pointer) -> int {
var i = 0
let n = len(s)
while i < n {
let c = s[i]
if c == ' ' or c == '\t' or c == '\n' or c == '\r' { i += 1 } else { return c }
}
return 0
}
# read a map file (TMX or TMJ, auto-detected by first byte) -> the normalised
# intermediate map Value tree.
function tiled_read(path: pointer) -> Val {
let text = Fs.read_text(path)
if text == null { return value_null() }
if text == "" { return value_null() }
if tiled_first_byte(text) == 60 { # '<' -> XML
return tmx_to_value(xml_parse(text))
}
return tmj_normalize(json_parse(text)) # '{' -> JSON
}
# read a tileset file (TSX or TSJ) -> a tileset Value object.
function tiled_read_tsx(path: pointer) -> Val {
let text = Fs.read_text(path)
if text == null { return value_null() }
if text == "" { return value_null() }
if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(text)) }
return json_parse(text)
}
# ============================================================================
# P1 (#69) — the runtime map model (`rt_tmap`), the GID resolver, the legacy
# `rt_map` compatibility projection, and image-backed rendering. Built from the
# intermediate Value tree above, so both the TMX and TMJ paths feed it.
# ============================================================================
property TmTileset {
firstgid: int = 0
columns: int = 0
tilew: int = 0
tileh: int = 0
spacing: int = 0
margin: int = 0
tilecount: int = 0
imgid: int = -1 # loaded image handle (rt_image_load), -1 = none
meta: Val # the tileset Value object (per-tile metadata)
}
property TmLayer {
kind: int = 0 # 0 tilelayer, 1 objectgroup, 2 imagelayer, 3 group
name: pointer = null
w: int = 0
h: int = 0
visible: int = 1
imgid: int = -1 # loaded image handle (imagelayer only), -1 = none
gids: []int # dense w*h GID array (tilelayer only)
data: Val # the layer Value object (objects, image, …)
}
property Tmap {
w: int = 0 # map width in tiles
h: int = 0
tilew: int = 0 # map tile size in pixels
tileh: int = 0
orientation: pointer = null
layers: []TmLayer
tilesets: []TmTileset
tree: Val # the source intermediate tree
coll: int = -1 # designated collision layer index, or -1
}
# ---- GID resolver ----------------------------------------------------------
property GidInfo {
tileset: int = -1 # index into Tmap.tilesets (-1 = none/empty)
local: int = 0 # local tile id within that tileset
fh: int = 0 # horizontal flip
fv: int = 0 # vertical flip
fd: int = 0 # anti-diagonal flip
empty: int = 0 # gid was 0
}
# decompose a raw GID: strip the three flip flags (0x80000000 H, 0x40000000 V,
# 0x20000000 D), keep the low 29 bits as the global tile id, and find the tileset
# whose firstgid is the greatest not exceeding it. gid == 0 is the empty cell.
function tmap_resolve(m: Tmap, gid: int) -> GidInfo {
let r = new GidInfo
if gid == 0 { r.empty = 1; r.tileset = -1; return r }
let H = 1 << 31
let V = 1 << 30
let D = 1 << 29
if (gid & H) != 0 { r.fh = 1 }
if (gid & V) != 0 { r.fv = 1 }
if (gid & D) != 0 { r.fd = 1 }
let id = gid & 536870911 # low 29 bits = global tile id
var best = -1
var bestfg = 0
var i = 0
while i < len(m.tilesets) {
let fg = m.tilesets[i].firstgid
if fg <= id and fg >= bestfg { bestfg = fg; best = i }
i += 1
}
r.tileset = best
if best >= 0 { r.local = id - m.tilesets[best].firstgid }
return r
}
# ---- accessors -------------------------------------------------------------
function tmap_width(m: Tmap) -> int { return m.w }
function tmap_height(m: Tmap) -> int { return m.h }
function tmap_layer_count(m: Tmap) -> int { return len(m.layers) }
function tmap_layer_name(m: Tmap, i: int) -> pointer {
if i < 0 or i >= len(m.layers) { return "" }
if m.layers[i].name == null { return "" }
return m.layers[i].name
}
function tmap_tree(m: Tmap) -> Val { return m.tree }
# raw GID at (x,y) in tile layer `layer` (0 out of bounds / non-tile layer).
function tmap_gid(m: Tmap, layer: int, x: int, y: int) -> int {
if layer < 0 or layer >= len(m.layers) { return 0 }
let l = m.layers[layer]
if l.kind != 0 { return 0 }
if x < 0 or y < 0 or x >= l.w or y >= l.h { return 0 }
return l.gids[y * l.w + x]
}
# ---- per-tile metadata (property convention) -------------------------------
# does the tile a GID resolves to carry bool custom property `name` = true?
# the metadata Value object for the tile a GID resolves to (a `null` node if the
# tile carries none). Shared by the property / objectgroup / animation lookups.
function tmap_tile_meta(m: Tmap, gid: int) -> Val {
let r = tmap_resolve(m, gid)
if r.tileset < 0 { return value_null() }
let ts = m.tilesets[r.tileset]
if value_kind(ts.meta) != 6 { return value_null() }
let tiles = value_get(ts.meta, "tiles")
if value_kind(tiles) != 5 { return value_null() }
var i = 0
while i < value_count(tiles) {
let t = value_at(tiles, i)
if value_as_int(value_get(t, "id")) == r.local { return t }
i += 1
}
return value_null()
}
# does the tile a GID resolves to carry bool custom property `name` = true?
function tmap_tile_prop(m: Tmap, gid: int, name: pointer) -> int {
let t = tmap_tile_meta(m, gid)
if value_kind(t) != 6 { return 0 }
let props = value_get(t, "properties")
if value_kind(props) != 5 { return 0 }
var j = 0
while j < value_count(props) {
let p = value_at(props, j)
if value_as_str(value_get(p, "name")) == name {
if value_as_str(value_get(p, "value")) == "true" { return 1 }
return 0
}
j += 1
}
return 0
}
# does the tile a GID resolves to carry a per-tile <objectgroup> collision shape?
function tmap_tile_has_shapes(m: Tmap, gid: int) -> int {
let t = tmap_tile_meta(m, gid)
if value_kind(t) != 6 { return 0 }
let og = value_get(t, "objectgroup")
if value_kind(og) != 6 { return 0 }
let objs = value_get(og, "objects")
if value_count(objs) > 0 { return 1 }
return 0
}
# ---- collision normalisation (P2, #70) -------------------------------------
# the collision kind of a GID for the byte-grid projection, from the tile's
# metadata alone (design §3.5 priority): per-tile <objectgroup> hitboxes, then
# the solid/oneway/trigger property convention. 0 = no collision metadata,
# 1 = solid, 2 = one-way, 3 = trigger. The designated-collision-layer fallback
# (any non-zero GID is solid) is applied by the projection, not here.
function tmap_collision_kind(m: Tmap, gid: int) -> int {
if gid == 0 { return 0 }
if tmap_tile_prop(m, gid, "trigger") == 1 { return 3 }
if tmap_tile_prop(m, gid, "oneway") == 1 { return 2 }
if tmap_tile_has_shapes(m, gid) == 1 { return 1 }
if tmap_tile_prop(m, gid, "solid") == 1 { return 1 }
return 0
}
# ---- legacy rt_map compatibility projection --------------------------------
# project a layer down to the byte tilemap so Grid.* / Path.* / esys_move keep
# working unchanged. `whole_layer_solid` picks the source (design §3.5): true for
# a designated collision LAYER (a bare non-zero GID with no metadata still counts
# as solid — the fallback source); false to drive collision from per-tile
# metadata alone (objectgroup shapes / property convention on a visual layer).
# solid -> '#' (35), one-way -> '=' (61), trigger/empty -> ' ' (32, passable).
function tmap_project_layer(m: Tmap, layer: int, whole_layer_solid: int) -> void {
m.coll = layer
rt_map_size(m.w, m.h) # clamps to 96x64, clears to ' '
if layer < 0 or layer >= len(m.layers) { return }
let l = m.layers[layer]
if l.kind != 0 { return }
var y = 0
while y < l.h {
if y < 64 {
var x = 0
while x < l.w {
if x < 96 {
let gid = l.gids[y * l.w + x]
if gid != 0 {
var k = tmap_collision_kind(m, gid)
if k == 0 and whole_layer_solid != 0 { k = 1 } # collision-layer fallback
if k == 1 { rt_map[y * 96 + x] = '#' } # '#'
if k == 2 { rt_map[y * 96 + x] = '=' } # '='
}
}
x += 1
}
}
y += 1
}
}
# project a designated collision layer (non-zero GID is solid unless its tile
# metadata says otherwise) — the P1 default, called automatically on load.
function tmap_project(m: Tmap, layer: int) -> void { tmap_project_layer(m, layer, 1) }
# drive collision from per-tile metadata alone (objectgroup hitboxes / property
# convention) over any layer — a tile with no collision metadata stays passable.
function tmap_collide(m: Tmap, layer: int) -> void { tmap_project_layer(m, layer, 0) }
# find a tile layer named collision/solids/walls (case-sensitive), or -1.
function tmap_find_collision(m: Tmap) -> int {
var i = 0
while i < len(m.layers) {
let n = m.layers[i].name
if m.layers[i].kind == 0 and (n == "collision" or n == "solids" or n == "walls" or n == "Collision" or n == "Solids") { return i }
i += 1
}
return -1
}
# ---- rendering -------------------------------------------------------------
# blit one tilew x tileh tile from a tileset image (source origin sx,sy) to the
# framebuffer at (dx,dy), applying the three flip flags. Square tiles assumed
# for the diagonal flip (Kenney art is 16x16), which is the orthogonal case.
function tmap_blit_tile(imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, dy: int, fh: int, fv: int, fd: int) -> void {
if imgid < 0 { return }
let s: words = img_px[imgid]
let iw = img_w[imgid]
var j = 0
while j < th {
var i = 0
while i < tw {
var u = i
var v = j
if fd == 1 { u = j; v = i }
if fh == 1 { u = tw - 1 - u }
if fv == 1 { v = th - 1 - v }
let argb = s[(sy + v) * iw + (sx + u)]
rt_blend_px(dx + i, dy + j, argb)
i += 1
}
j += 1
}
}
# draw one tile GID at map cell (x,y) with the camera offset already applied.
function tmap_draw_gid(m: Tmap, gid: int, dx: int, dy: int) -> void {
if gid == 0 { return }
let r = tmap_resolve(m, gid)
if r.tileset < 0 { return }
let ts = m.tilesets[r.tileset]
if ts.imgid < 0 { return }
let cols = ts.columns
if cols <= 0 { return }
let cx = r.local - (r.local / cols) * cols
let cy = r.local / cols
let sx = ts.margin + cx * (ts.tilew + ts.spacing)
let sy = ts.margin + cy * (ts.tileh + ts.spacing)
# Tiled anchors a tile by its bottom-left, so a tile taller than the map cell
# rises above the cell.
let ddy = dy - (ts.tileh - m.tileh)
tmap_blit_tile(ts.imgid, sx, sy, ts.tilew, ts.tileh, dx, ddy, r.fh, r.fv, r.fd)
}
# (tmap_draw / tmap_draw_anim are defined in the P3 section below, over the shared
# tmap_draw_full — a static draw plus animated tiles + tile objects.)
# ---- build the model from the intermediate Value tree ----------------------
function tmap_gids_from_layer(lv: Val) -> []int {
let out = new []int
let data = value_get(lv, "data")
var i = 0
while i < value_count(data) { push(out, value_as_int(value_at(data, i))); i += 1 }
return out
}
# fill a TmTileset's geometry from a tileset Value object (its per-tile metadata
# stays in `.meta` for the resolver / property convention).
function tmap_tileset_from_value(tv: Val) -> TmTileset {
let ts = new TmTileset
ts.firstgid = value_as_int(value_get(tv, "firstgid"))
ts.tilew = value_as_int(value_get(tv, "tilewidth"))
ts.tileh = value_as_int(value_get(tv, "tileheight"))
ts.spacing = value_as_int(value_get(tv, "spacing"))
ts.margin = value_as_int(value_get(tv, "margin"))
ts.tilecount = value_as_int(value_get(tv, "tilecount"))
ts.columns = value_as_int(value_get(tv, "columns"))
if ts.columns <= 0 and ts.tilew > 0 { # derive columns from the image
let iw = value_as_int(value_get(tv, "imagewidth"))
if iw > 0 { ts.columns = (iw - 2 * ts.margin + ts.spacing) / (ts.tilew + ts.spacing) }
}
ts.meta = tv
return ts
}
function tiled_join(a: pointer, b: pointer) -> pointer { return Path.normalize(Path.join(a, b)) }
# build the runtime map from an intermediate tree, resolving external tilesets
# and loading tileset images relative to `basedir`.
function tmap_build(tree: Val, basedir: pointer) -> Tmap {
let m = new Tmap
m.tree = tree
m.layers = new []TmLayer
m.tilesets = new []TmTileset
m.orientation = value_as_str(value_get(tree, "orientation"))
m.w = value_as_int(value_get(tree, "width"))
m.h = value_as_int(value_get(tree, "height"))
m.tilew = value_as_int(value_get(tree, "tilewidth"))
m.tileh = value_as_int(value_get(tree, "tileheight"))
# tilesets
let tss = value_get(tree, "tilesets")
var i = 0
while i < value_count(tss) {
var tv = value_at(tss, i)
var imgdir = basedir
let src = value_as_str(value_get(tv, "source"))
if src != "" { # external .tsx/.tsj
let tsxpath = tiled_join(basedir, src)
let ext = tiled_read_tsx(tsxpath)
value_put(ext, "firstgid", value_get(tv, "firstgid"))
tv = ext
imgdir = Path.dir(tsxpath)
}
let ts = tmap_tileset_from_value(tv)
let img = value_as_str(value_get(tv, "image"))
if img != "" { ts.imgid = rt_image_load(tiled_join(imgdir, img)) }
push(m.tilesets, ts)
i += 1
}
# layers (flat; groups' children are lifted so tile layers render/resolve)
let ls = value_get(tree, "layers")
i = 0
while i < value_count(ls) { tmap_add_layer(m, value_at(ls, i)); i += 1 }
# image layers (#73): load each <imagelayer> image relative to the map file
i = 0
while i < len(m.layers) {
if m.layers[i].kind == 2 {
let img = value_as_str(value_get(m.layers[i].data, "image"))
if img != "" { m.layers[i].imgid = rt_image_load(tiled_join(basedir, img)) }
}
i += 1
}
# infinite map (#74): the header w/h are 0 — take the flattened layer bounds.
if m.w <= 0 or m.h <= 0 {
var mw = 0
var mh = 0
i = 0
while i < len(m.layers) {
if m.layers[i].kind == 0 {
if m.layers[i].w > mw { mw = m.layers[i].w }
if m.layers[i].h > mh { mh = m.layers[i].h }
}
i += 1
}
m.w = mw
m.h = mh
}
return m
}
# ---- .world stitching (#74) ------------------------------------------------
# read a `.world` file (JSON): { maps: [{fileName,x,y}], patterns: [...],
# onlyShowAdjacentMaps }. Reuses Json.parse; the members stitch at their offsets.
function tiled_read_world(path: pointer) -> Val {
let text = Fs.read_text(path)
if text == null { return value_null() }
if text == "" { return value_null() }
return json_parse(text)
}
function tiled_world_count(world: Val) -> int { return value_count(value_get(world, "maps")) }
function tiled_world_map(world: Val, i: int) -> Val { return value_at(value_get(world, "maps"), i) }
function tmap_add_layer(m: Tmap, lv: Val) -> void {
let ty = value_as_str(value_get(lv, "type"))
let l = new TmLayer
l.data = lv
l.name = value_as_str(value_get(lv, "name"))
var vis = 1
if value_has(lv, "visible") == 1 { if value_as_int(value_get(lv, "visible")) == 0 { vis = 0 } }
l.visible = vis
if ty == "tilelayer" {
l.kind = 0
l.w = value_as_int(value_get(lv, "width"))
l.h = value_as_int(value_get(lv, "height"))
l.gids = tmap_gids_from_layer(lv)
push(m.layers, l)
return
}
if ty == "objectgroup" { l.kind = 1; push(m.layers, l); return }
if ty == "imagelayer" { l.kind = 2; push(m.layers, l); return }
if ty == "group" {
l.kind = 3; push(m.layers, l)
let sub = value_get(lv, "layers")
var i = 0
while i < value_count(sub) { tmap_add_layer(m, value_at(sub, i)); i += 1 }
return
}
push(m.layers, l)
}
# ---- top-level load --------------------------------------------------------
# load a Tiled map file into the runtime model: read + parse (TMX or TMJ), build
# the model (resolving external tilesets + images), and project the collision
# layer (a tile layer named collision/solids/walls) down to the legacy tilemap.
function tiled_load(path: pointer) -> Tmap {
let tree = tiled_read(path)
let m = tmap_build(tree, Path.dir(path))
tmap_resolve_templates(m, Path.dir(path)) # #72: fill template-instance objects
let c = tmap_find_collision(m)
if c >= 0 { tmap_project(m, c) }
return m
}
# ============================================================================
# P3 (#71) — animated tiles + tile objects.
#
# Animated tiles: a tileset's <animation> is a list of {tileid, duration(ms)}
# frames. The current frame is a pure function of the engine frame counter
# (fixed 60/s, deterministic — the same mechanism SpriteAnim rides), so it ticks
# for free and reproduces frame-for-frame across runs. An animated GID resolves,
# at draw, to the current frame's GID (its flip flags preserved).
#
# Tile objects: an object-layer entry with a `gid` draws the tile image (with its
# own flip flags), bottom-anchored at the object position, as a placeable sprite.
# ============================================================================
# the three flip-flag bits of a GID (0xE0000000), kept when swapping an animation
# frame's tile id in.
function tmap_flag_bits(gid: int) -> int { return gid & ((1 << 31) | (1 << 30) | (1 << 29)) }
# the GID an (animated) GID resolves to at engine `frame` — frames not carrying an
# <animation> pass through unchanged. elapsed ms = frame * 1000 / 60 (the fixed
# 60/s clock), then walk the frame durations to the current one.
function tmap_frame_gid(m: Tmap, gid: int, frame: int) -> int {
if gid == 0 { return 0 }
let r = tmap_resolve(m, gid)
if r.tileset < 0 { return gid }
let t = tmap_tile_meta(m, gid)
if value_kind(t) != 6 { return gid }
let anim = value_get(t, "animation")
if value_kind(anim) != 5 { return gid }
let nfr = value_count(anim)
if nfr == 0 { return gid }
var total = 0
var i = 0
while i < nfr { total = total + value_as_int(value_get(value_at(anim, i), "duration")); i += 1 }
if total <= 0 { return gid }
let t_ms = (frame * 1000 / 60) % total
var cur = value_as_int(value_get(value_at(anim, 0), "tileid"))
var acc = 0
i = 0
while i < nfr {
let fr = value_at(anim, i)
acc = acc + value_as_int(value_get(fr, "duration"))
if t_ms < acc { cur = value_as_int(value_get(fr, "tileid")); i = nfr }
else { i += 1 }
}
let ts = m.tilesets[r.tileset]
return (ts.firstgid + cur) | tmap_flag_bits(gid)
}
# is a GID animated (its tile carries a non-empty <animation>)?
function tmap_is_animated(m: Tmap, gid: int) -> int {
let t = tmap_tile_meta(m, gid)
if value_kind(t) != 6 { return 0 }
let anim = value_get(t, "animation")
if value_kind(anim) == 5 and value_count(anim) > 0 { return 1 }
return 0
}
# draw every visible tile layer (animated tiles resolved for `frame`) plus every
# tile object on the object layers, in file order, offset by the camera.
function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) -> void {
var li = 0
while li < len(m.layers) {
let l = m.layers[li]
if l.kind == 0 and l.visible != 0 { # tile layer
let lox = tmap_layer_offsetx(m, li)
let loy = tmap_layer_offsety(m, li)
var y = 0
while y < l.h {
var x = 0
while x < l.w {
var gid = l.gids[y * l.w + x]
if gid != 0 {
if animate != 0 { gid = tmap_frame_gid(m, gid, frame) }
# orientation transform places the cell (orthogonal / iso / hex / staggered)
tmap_draw_gid(m, gid, tmap_cell_sx(m, x, y) + lox - camx, tmap_cell_sy(m, x, y) + loy - camy)
}
x += 1
}
y += 1
}
}
if l.kind == 2 and l.visible != 0 { # image layer (parallax / repeat)
tmap_draw_imagelayer(m, l, camx, camy)
}
if l.kind == 1 and l.visible != 0 { # object layer: draw tile objects
let objs = value_get(l.data, "objects")
var oi = 0
while oi < value_count(objs) {
let ob = value_at(objs, oi)
if value_has(ob, "gid") == 1 {
var g = value_as_int(value_get(ob, "gid"))
if animate != 0 { g = tmap_frame_gid(m, g, frame) }
let ox = value_as_int(value_get(ob, "x"))
let oy = value_as_int(value_get(ob, "y"))
# Tiled anchors a tile object by its bottom-left corner
tmap_draw_gid(m, g, ox - camx, oy - m.tileh - camy)
}
oi += 1
}
}
li += 1
}
}
# static draw (no animation) — the P1 entry point.
function tmap_draw(m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(m, camx, camy, 0, 0) }
# animated draw at engine `frame` (pass Time.frame): animated tiles advance,
# deterministically and frame-identically across runs.
function tmap_draw_anim(m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(m, camx, camy, frame, 1) }
# ============================================================================
# P4 (#72) — object layers (shapes + text), custom properties/types, templates,
# and opt-in entity spawning. Objects, shapes and properties already parse into
# the intermediate tree (P0.5); this phase exposes them, resolves custom-type
# defaults and template inheritance, and maps an object onto Ludic components.
# ============================================================================
# ---- object accessors ------------------------------------------------------
function tmap_object_layer(m: Tmap, layer: int) -> Val {
if layer < 0 or layer >= len(m.layers) { return value_null() }
return m.layers[layer].data
}
function tmap_object_count(m: Tmap, layer: int) -> int {
return value_count(value_get(tmap_object_layer(m, layer), "objects"))
}
function tmap_object(m: Tmap, layer: int, i: int) -> Val {
return value_at(value_get(tmap_object_layer(m, layer), "objects"), i)
}
# the shape of an object: "tile" (a gid object), "point", "ellipse", "polygon",
# "polyline", "text", or "rectangle" (the default).
function tiled_object_shape(obj: Val) -> pointer {
if value_has(obj, "gid") == 1 { return "tile" }
if value_has(obj, "point") == 1 { return "point" }
if value_has(obj, "ellipse") == 1 { return "ellipse" }
if value_has(obj, "polygon") == 1 { return "polygon" }
if value_has(obj, "polyline") == 1 { return "polyline" }
if value_has(obj, "text") == 1 { return "text" }
return "rectangle"
}
# ---- custom properties -----------------------------------------------------
# the {name,type,value} property node named `name` in a container's "properties"
# list (an object / tile / layer / map), or a null node.
function tiled_prop_node(container: Val, name: pointer) -> Val {
let props = value_get(container, "properties")
if value_kind(props) != 5 { return value_null() }
var i = 0
while i < value_count(props) {
let p = value_at(props, i)
if value_as_str(value_get(p, "name")) == name { return p }
i += 1
}
return value_null()
}
# a property's raw string value, with a fallback to the object's custom-type
# default (objecttypes.xml, via the type table). "" when absent everywhere.
function tiled_prop_str(container: Val, name: pointer) -> pointer {
let p = tiled_prop_node(container, name)
if value_kind(p) == 6 { return value_as_str(value_get(p, "value")) }
# fall back to the container's class default
let cls = value_as_str(value_get(container, "type"))
if cls != "" {
let d = tiled_type_default(cls, name)
if value_kind(d) == 6 { return value_as_str(value_get(d, "value")) }
}
return ""
}
# a property parsed as an integer ("true"/"false" -> 1/0), with the same default
# fallback.
function tiled_prop_int(container: Val, name: pointer) -> int {
let s = tiled_prop_str(container, name)
if s == "true" { return 1 }
if s == "false" { return 0 }
return xml_atoi(s)
}
function tiled_prop_type(container: Val, name: pointer) -> pointer {
let p = tiled_prop_node(container, name)
if value_kind(p) == 6 { return value_as_str(value_get(p, "type")) }
let cls = value_as_str(value_get(container, "type"))
if cls != "" {
let d = tiled_type_default(cls, name)
if value_kind(d) == 6 { return value_as_str(value_get(d, "type")) }
}
return ""
}
# ---- custom types (objecttypes.xml) ----------------------------------------
# The project custom-type table: an object mapping a type/class name to its list
# of {name,type,value(default)} property definitions. class/enum properties then
# resolve their defaults against it.
var tiled_type_table: Val = null
function tiled_types() -> Val {
if tiled_type_table == null { tiled_type_table = value_object() }
return tiled_type_table
}
# load an objecttypes.xml file into the type table. Each <objecttype name=..> maps
# to its <property name= type= default=> list.
function tiled_load_types(path: pointer) -> int {
let text = Fs.read_text(path)
if text == null { return 0 }
if text == "" { return 0 }
let root = xml_parse(text)
let tbl = tiled_types()
var n = 0
var i = 0
while i < xml_child_count(root) {
let ot = xml_child(root, i)
if xml_tag(ot) == "objecttype" {
let props = value_list()
var j = 0
while j < xml_child_count(ot) {
let pr = xml_child(ot, j)
if xml_tag(pr) == "property" {
let o = value_object()
value_put(o, "name", value_str(xml_attr(pr, "name")))
var ty = xml_attr(pr, "type")
if ty == "" { ty = "string" }
value_put(o, "type", value_str(ty))
value_put(o, "value", value_str(xml_attr(pr, "default")))
push(props.kids, o)
}
j += 1
}
value_put(tbl, xml_attr(ot, "name"), props)
n += 1
}
i += 1
}
return n
}
# the default {name,type,value} property node for a custom type, or a null node.
function tiled_type_default(typename: pointer, propname: pointer) -> Val {
let tbl = tiled_types()
let props = value_get(tbl, typename)
if value_kind(props) != 5 { return value_null() }
var i = 0
while i < value_count(props) {
let p = value_at(props, i)
if value_as_str(value_get(p, "name")) == propname { return p }
i += 1
}
return value_null()
}
# ---- templates (.tx / .tj) -------------------------------------------------
# read a template file -> its object Value (the <object> a .tx wraps, or the
# "object" of a .tj). External reusable object definitions.
function tiled_read_template(path: pointer) -> Val {
let text = Fs.read_text(path)
if text == null { return value_null() }
if text == "" { return value_null() }
if tiled_first_byte(text) == 60 { # XML .tx
let root = xml_parse(text) # <template>
let ob = xml_find(root, "object")
if xml_tag(ob) == "object" { return tmx_object_to_value(ob) }
return value_null()
}
let j = json_parse(text) # JSON .tj
return value_get(j, "object")
}
# merge a template's fields into an instance object: the instance keeps every key
# it already sets; the template fills in the rest (so an instance inherits the
# template's gid / shape / size / properties, overriding per field).
function tiled_merge_template(inst: Val, tmpl: Val) -> void {
if value_kind(tmpl) != 6 { return }
var i = 0
while i < value_count(tmpl) {
let k = value_key_at(tmpl, i)
if value_has(inst, k) == 0 { value_put(inst, k, value_at(tmpl, i)) }
i += 1
}
}
# resolve every object that references a `template`, loading + merging it. Paths
# are relative to the map file (`basedir`).
function tmap_resolve_templates(m: Tmap, basedir: pointer) -> void {
var li = 0
while li < len(m.layers) {
let l = m.layers[li]
if l.kind == 1 {
let objs = value_get(l.data, "objects")
var oi = 0
while oi < value_count(objs) {
let ob = value_at(objs, oi)
let tp = value_as_str(value_get(ob, "template"))
if tp != "" { tiled_merge_template(ob, tiled_read_template(tiled_join(basedir, tp))) }
oi += 1
}
}
li += 1
}
}
# ============================================================================
# P5 (#73) — image & group layers, orientation coordinate transforms, and Wang
# GID resolution. Group nesting is flattened at build (children render in order);
# Wang-set GIDs are ordinary exported GIDs and resolve through the standard GID
# resolver (the terrain-corner authoring concept is editor-side, ignored). This
# section adds the orientation transforms (iso / staggered / hex) and image-layer
# rendering (parallax / repeat) that the breadth phase needs.
# ============================================================================
# the screen x of tile cell (x,y) for the map's orientation (before the camera).
# orthogonal: x*tilew. isometric: (x-y)*tilew/2. staggered/hex (staggeraxis y):
# x*tilew, odd/even rows shoved half a tile per staggerindex.
function tmap_cell_sx(m: Tmap, x: int, y: int) -> int {
let o = m.orientation
if o == "isometric" { return (x - y) * (m.tilew / 2) }
if o == "staggered" or o == "hexagonal" {
var sx = x * m.tilew
let idx = value_as_str(value_get(m.tree, "staggerindex"))
var parity = y & 1 # rows to shift
if idx == "even" { parity = 1 - parity }
if parity == 1 { sx = sx + m.tilew / 2 }
return sx
}
return x * m.tilew # orthogonal
}
# the screen y of tile cell (x,y). isometric: (x+y)*tileh/2. staggered:
# y*tileh/2. hexagonal (staggeraxis y): rows step by (tileh+hexsidelength)/2.
function tmap_cell_sy(m: Tmap, x: int, y: int) -> int {
let o = m.orientation
if o == "isometric" { return (x + y) * (m.tileh / 2) }
if o == "staggered" { return y * (m.tileh / 2) }
if o == "hexagonal" {
let hs = value_as_int(value_get(m.tree, "hexsidelength"))
return y * ((m.tileh + hs) / 2)
}
return y * m.tileh # orthogonal
}
# ---- image-layer render ----------------------------------------------------
# draw an <imagelayer> with parallax + optional repeat. `parallax` is 1.0 by
# default (moves with the camera); repeatx/repeaty tile the image across the view.
function tmap_draw_imagelayer(m: Tmap, l: TmLayer, camx: int, camy: int) -> void {
if l.imgid < 0 { return }
let ox = value_as_int(value_get(l.data, "offsetx"))
let oy = value_as_int(value_get(l.data, "offsety"))
# parallax factor (fixed, default 1.0); px/py = the drawn origin
var px = ox - camx
var py = oy - camy
let iw = img_w[l.imgid]
let ih = img_h[l.imgid]
let repx = value_as_int(value_get(l.data, "repeatx"))
let repy = value_as_int(value_get(l.data, "repeaty"))
# starting origin: for a repeating axis, back up to before the screen
var sx0 = px
if repx != 0 { while sx0 > 0 { sx0 -= iw }; }
var sy0 = py
if repy != 0 { while sy0 > 0 { sy0 -= ih }; }
var yy = sy0
var first_y = 1
while (yy < rt_screen_h()) and (first_y == 1 or repy != 0) {
var xx = sx0
var first_x = 1
while (xx < rt_screen_w()) and (first_x == 1 or repx != 0) {
rt_draw_image(l.imgid, xx, yy)
first_x = 0
if repx == 0 { xx = rt_screen_w() } else { xx += iw }
}
first_y = 0
if repy == 0 { yy = rt_screen_h() } else { yy += ih }
}
}
# ---- image/group-layer accessors -------------------------------------------
function tmap_layer_kind(m: Tmap, layer: int) -> int {
if layer < 0 or layer >= len(m.layers) { return -1 }
return m.layers[layer].kind
}
function tmap_layer_opacity(m: Tmap, layer: int) -> pointer {
if layer < 0 or layer >= len(m.layers) { return "" }
let op = value_get(m.layers[layer].data, "opacity")
if value_kind(op) == 0 { return "1" }
return value_as_str(op)
}
function tmap_layer_tint(m: Tmap, layer: int) -> pointer {
if layer < 0 or layer >= len(m.layers) { return "" }
return value_as_str(value_get(m.layers[layer].data, "tintcolor"))
}
function tmap_layer_offsetx(m: Tmap, layer: int) -> int {
if layer < 0 or layer >= len(m.layers) { return 0 }
return value_as_int(value_get(m.layers[layer].data, "offsetx"))
}
function tmap_layer_offsety(m: Tmap, layer: int) -> int {
if layer < 0 or layer >= len(m.layers) { return 0 }
return value_as_int(value_get(m.layers[layer].data, "offsety"))
}