feat(stdlib): Tiled P2 — collision normalisation into the Solids feed (#70)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m51s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 28s

Normalise three collision sources into the byte tilemap esys_move / Grid.* /
Path.* read, in the design's priority order (§3.5):

- per-tile <objectgroup> hitboxes (Tiled.tile_shapes) — the precise source;
- the solid/oneway/trigger bool property convention (Tiled.collision_kind);
- the designated collision layer — any non-zero GID solid (Tiled.project),
  the fallback source, applied automatically on load.

Tiled.collide drives collision from a visual layer's per-tile metadata alone
(a tile with no collision metadata stays passable). tmap_collision_kind
classifies a GID (0 none / 1 solid / 2 one-way / 3 trigger) from its metadata;
the projection adds the collision-layer fallback.

Proven by library/tiled_p2.ludic (14 assertions): kind classification for each
source, the property convention and collision-layer fallback producing an
identical Solids feed, a per-tile-<objectgroup> floor blocking an esys_move
mover, and A* over a loaded map matching the hand-authored baseline. x test:
93 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 14:50:27 +03:00
parent bc301c8d17
commit 0cb57774d7
12 changed files with 5425 additions and 4991 deletions

View file

@ -541,42 +541,76 @@ function tmap_gid(m: Tmap, layer: int, x: int, y: int) -> int {
# ---- per-tile metadata (property convention) -------------------------------
# does the tile a GID resolves to carry bool custom property `name` = true?
function tmap_tile_prop(m: Tmap, gid: int, name: pointer) -> int {
# 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 0 }
if r.tileset < 0 { return value_null() }
let ts = m.tilesets[r.tileset]
if value_kind(ts.meta) != 6 { return 0 }
if value_kind(ts.meta) != 6 { return value_null() }
let tiles = value_get(ts.meta, "tiles")
if value_kind(tiles) != 5 { return 0 }
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 {
let props = value_get(t, "properties")
if value_kind(props) == 5 {
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 = j + 1
}
}
if value_as_int(value_get(t, "id")) == r.local { return t }
i = 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
}
i = i + 1
j = 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 the designated collision layer down to the byte tilemap so Grid.* /
# Path.* / esys_move keep working unchanged: an empty cell is ' ' (passable), a
# tile whose GID carries the `oneway` property is '=' (61), any other non-zero
# GID is '#' (35, solid). The legacy 96x64 cap clips, so old code is byte-exact.
function tmap_project(m: Tmap, layer: int) -> void {
# 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 }
@ -590,9 +624,10 @@ function tmap_project(m: Tmap, layer: int) -> void {
if x < 96 {
let gid = l.gids[y * l.w + x]
if gid != 0 {
var ch = 35 # '#'
if tmap_tile_prop(m, gid, "oneway") == 1 { ch = 61 } # '='
rt_map[y * 96 + x] = ch
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] = 35 } # '#'
if k == 2 { rt_map[y * 96 + x] = 61 } # '='
}
}
x = x + 1
@ -602,6 +637,14 @@ function tmap_project(m: Tmap, layer: int) -> void {
}
}
# 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