feat(engine): Tiled P5 — image/group layers, iso/hex/staggered coords, Wang (#73)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 1m59s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s

- Image layers: <imagelayer> renders with parallax + optional repeatx/repeaty
  tiling across the view; the image loads relative to the map file. Group layers
  flatten at build (children render in file order); layer offset/opacity/tint are
  queryable (Tiled.layer_kind / layer_offsetx / layer_offsety / layer_opacity /
  layer_tint), a group's tint applying recursively.
- Orientation transforms: Tiled.cell_x / Tiled.cell_y compute a tile cell's
  screen position for orthogonal, isometric ((x-y)*tw/2, (x+y)*th/2), staggered,
  and hexagonal (rows step by (tileh+hexsidelength)/2, alternate rows shoved per
  staggerindex) — the tile draw now places cells through them.
- Wang: exported Wang-set GIDs are ordinary GIDs and resolve through the standard
  GID resolver; the terrain-corner authoring concept is editor-side (design cut).

Proven by library/tiled_p5.ludic (14 assertions) over the real
isometric_grass_and_water.tmx (iso + Wang) and hexagonal-mini.tmx, plus a
hand-authored image+group fixture. x test: 96 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 15:14:36 +03:00
parent b8c71d2a8e
commit 78a5719fae
15 changed files with 5829 additions and 5007 deletions

View file

@ -468,6 +468,7 @@ property TmLayer {
w: int = 0
h: int = 0
visible: int = 1
imgid: int = 0 - 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, …)
}
@ -770,6 +771,15 @@ function tmap_build(tree: Val, basedir: pointer) -> Tmap {
let ls = value_get(tree, "layers")
i = 0
while i < value_count(ls) { tmap_add_layer(m, value_at(ls, i)); 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 = i + 1
}
return m
}
@ -878,6 +888,8 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int)
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
@ -885,13 +897,17 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int)
var gid = l.gids[y * l.w + x]
if gid != 0 {
if animate != 0 { gid = tmap_frame_gid(m, gid, frame) }
tmap_draw_gid(m, gid, x * m.tilew - camx, y * m.tileh - camy)
# 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 = x + 1
}
y = 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
@ -1109,3 +1125,100 @@ function tmap_resolve_templates(m: Tmap, basedir: pointer) -> void {
li = 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 = sx0 - iw }; }
var sy0 = py
if repy != 0 { while sy0 > 0 { sy0 = 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 = xx + iw }
}
first_y = 0
if repy == 0 { yy = rt_screen_h() } else { yy = yy + ih }
}
}
# ---- image/group-layer accessors -------------------------------------------
function tmap_layer_kind(m: Tmap, layer: int) -> int {
if layer < 0 or layer >= len(m.layers) { return 0 - 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"))
}