diff --git a/README.md b/README.md index 9ca9e59..d6f15af 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Multi-layer tile-grid map implementation with vertex-painted autotile loading + UV resolution, walkability + sight-blocking queries, and a v3 multi-layer render pipeline with opaque-ceiling cache. -**Version:** 0.5.4 +**Version:** 0.5.6 **Lib-ID:** lib-core.maps **Requires:** (none) **Tags:** maps, tile-grid, multi-layer, vertex-painting, autotile, blob-14, override, walkability, tilemap @@ -18,19 +18,35 @@ v3 multi-layer render pipeline with opaque-ceiling cache. | v2 | Multi-atlas + multi-layer with packed-u32 GIDs (atlas_idx + tile_id + rotation). `set_cell_gid`, `save_to_disk`. | | v3 | Per-layer `material` (atlas-alias), optional `vertices` ((W+1)·(H+1) grid), optional `overrides` ({"x:y": int OR {slot, rot, flip}}). Renderer derives slot/rot/flip from neighbour-bitmask blob-gating; overrides force-place specific orientations. | -## Painting model (v3 autotile) +## Painting model (v3 autotile, dual-grid) -- **Vertex grid**: (W+1)·(H+1) cells. Any-corner rule: cell (x,y) is - material iff any of its 4 corner vertices is painted. -- **Blob-gating + SLOT_LOOKUP**: each material cell's 8-bit - neighbour-bitmask (clockwise from N) is blob-gated (diagonals zeroed - unless both adjacent cardinals are set), then mapped to one of 14 - canonical slots × {0,1,2,3} rotation × {0,1} flip via a D4-orbit - table. 47 distinct gated bitmasks total. +Two grids offset by half a tile. The PAINT grid (called `vertices` in +storage, "map-tiles" in design docs) is what users paint. The RENDER +grid (called `cells`) is offset by (+0.5, +0.5) tile and is where +sprites sit. Each render-cell spans 4 surrounding paint-tiles which +act as its 4 corners (TL, TR, BL, BR). + +- **Paint grid**: (W+1)·(H+1) cells. Any-corner rule: render-cell + (x,y) is material iff any of its 4 corner paint-tiles is painted. +- **Bitmask + SLOT_LOOKUP** (0.5.6 dual-grid native): each material + render-cell's 8-bit neighbour-bitmask (clockwise from N) is derived + from its own 4 corner paint-tiles — cardinal bit set iff ≥1 of the + edge's 2 paint-tiles painted, diagonal bit set iff the corner + paint-tile is painted. Blob-gating then zeroes diagonals whose 2 + adjacent cardinals are not both set. The gated bitmask maps to one + of 14 canonical slots × {0,1,2,3} rotation × {0,1} flip via a + D4-orbit table. - **Override sublayer**: sparse `{"x:y": slot}` or `{"x:y": {slot, rot, flip}}`. Takes precedence over the bitmask-derived slot AND forces material-presence on the cell. +**0.5.6 fix:** pre-0.5.6 derived the bitmask from the material status +of the 8 neighbour render-cells, which violated dual-grid semantics — +two cells whose shared edge had no painted paint-tiles still saw each +other as material whenever any unrelated corner of either was painted, +producing connected blobs across visually empty paint-tile gaps. See +plan `2026-05-29-painting-model-rethink`. + ## Topology diff --git a/init.lua b/init.lua index 1d06b53..1b5b30a 100644 --- a/init.lua +++ b/init.lua @@ -140,21 +140,36 @@ local LAYER_ORDER_TOP_DOWN = { "topsurface", "surface", "subsurface", "foundation", } --- 47-Blob Autotile (Schema-v3 vertex-painted layers, 0.5.0c) +-- Dual-Grid Autotile (Schema-v3 vertex-painted layers, 0.5.0c) -- --- The cell-state for an autotile cell is derived from the layer's --- vertex grid using the "any-corner" rule: cell (x, y) is material --- iff any of its 4 corner vertices (x, y), (x+1, y), (x, y+1), or --- (x+1, y+1) is painted. A single painted vertex thus produces a --- 2x2 mini-blob of material cells centred on the vertex. +-- Two grids, offset by half a tile. The PAINT grid (called `vertices` +-- in code, "map-tiles" in design docs) stores material flags at +-- integer positions. The RENDER grid (called `cells`) is offset by +-- (+0.5, +0.5) tile from the paint grid and is where sprites sit. +-- Each render-cell (x, y) spans 4 surrounding paint-tiles, which act +-- as its 4 corner vertices: TL=(x,y), TR=(x+1,y), BL=(x,y+1), +-- BR=(x+1,y+1). -- --- Given the derived per-cell material-state, each cell computes its --- 8-bit neighbour bitmask (clockwise from north, LSB-first): N, NE, --- E, SE, S, SW, W, NW. The blob-gating rule then zeroes a diagonal --- bit unless both adjacent cardinal bits are set. The resulting --- gated bitmask resolves through SLOT_LOOKUP into one of the 14 --- canonical S-V2E2-RM-Blob slots plus the rotation + flip that --- transforms the canonical sprite into the rendered one. +-- Material rule ("any-corner"): render-cell (x, y) is material iff +-- any of its 4 corner paint-tiles is painted. A single painted tile +-- thus produces a 2x2 of material render-cells centred on it. +-- +-- Bitmask rule (0.5.6, dual-grid native): each render-cell's 8-bit +-- neighbour bitmask (clockwise from N: N, NE, E, SE, S, SW, W, NW) +-- is derived from its own 4 corner paint-tiles. A cardinal bit is +-- set iff at least one of the 2 paint-tiles on that edge is painted; +-- a diagonal bit is set iff the corner paint-tile in that direction +-- is painted. Then blob-gating zeroes any diagonal bit whose 2 +-- adjacent cardinal bits are not both set. The gated bitmask +-- resolves through SLOT_LOOKUP into one of the 14 canonical +-- S-V2E2-RM-Blob slots plus the rotation + flip that transforms the +-- canonical sprite into the rendered one. +-- +-- Pre-0.5.6 derived the bitmask from the material status of the 8 +-- neighbour render-cells, which violated dual-grid semantics: two +-- render-cells with an empty shared edge could see each other as +-- material whenever any unrelated corner of either was painted, +-- producing connected blobs across visually empty paint-tile gaps. -- ===================================================================== -- Canonical 14 slots, keyed by their lowest-numbered representative @@ -259,16 +274,33 @@ end -- Returns the blob-gated 8-bit neighbour bitmask for cell (x, y) given -- the layer's vertex grid + map size. Bit layout (clockwise from N): -- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW. +-- +-- 0.5.6: bits derive from the render-cell's own 4 corner map-tiles +-- (= the 4 surrounding vertices in dual-grid terms), not from the +-- material status of the 8 neighbour cells. A cardinal bit is set iff +-- at least one of the 2 vertices on that edge is painted; a diagonal +-- bit is set iff the corner vertex in that direction is painted. +-- Pre-0.5.6 used cell-neighbour-material, which made two cells with a +-- shared unpainted edge see each other as material whenever any other +-- corner of either was painted — producing connected blobs across +-- visually-empty map-tile gaps. See plan +-- 2026-05-29-painting-model-rethink for the discovery. local function compute_cell_bitmask_v3(vertices, w, h, x, y) + local vw = w + 1 + local vh = h + 1 + local TL = vertex_at(vertices, vw, vh, x, y ) ~= 0 + local TR = vertex_at(vertices, vw, vh, x + 1, y ) ~= 0 + local BL = vertex_at(vertices, vw, vh, x, y + 1) ~= 0 + local BR = vertex_at(vertices, vw, vh, x + 1, y + 1) ~= 0 local b = 0 - if cell_has_material(vertices, w, h, x, y - 1) then b = b | 0x01 end -- N - if cell_has_material(vertices, w, h, x + 1, y - 1) then b = b | 0x02 end -- NE - if cell_has_material(vertices, w, h, x + 1, y ) then b = b | 0x04 end -- E - if cell_has_material(vertices, w, h, x + 1, y + 1) then b = b | 0x08 end -- SE - if cell_has_material(vertices, w, h, x, y + 1) then b = b | 0x10 end -- S - if cell_has_material(vertices, w, h, x - 1, y + 1) then b = b | 0x20 end -- SW - if cell_has_material(vertices, w, h, x - 1, y ) then b = b | 0x40 end -- W - if cell_has_material(vertices, w, h, x - 1, y - 1) then b = b | 0x80 end -- NW + if TL or TR then b = b | 0x01 end -- N edge: TL or TR painted + if TR then b = b | 0x02 end -- NE corner: TR painted + if TR or BR then b = b | 0x04 end -- E edge: TR or BR painted + if BR then b = b | 0x08 end -- SE corner: BR painted + if BL or BR then b = b | 0x10 end -- S edge: BL or BR painted + if BL then b = b | 0x20 end -- SW corner: BL painted + if TL or BL then b = b | 0x40 end -- W edge: TL or BL painted + if TL then b = b | 0x80 end -- NW corner: TL painted return apply_blob_gating(b) end diff --git a/manifest.lib b/manifest.lib index b5f2cfd..d77b291 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1 +1 @@ -{"id":"lib-core.maps","version":"0.5.5","api_min":"0.1"} +{"id":"lib-core.maps","version":"0.5.6","api_min":"0.1"} \ No newline at end of file