Mirrors the cache-invalidation that set_vertex and set_cell_gid
do. Without this, a cell-tile material write would update the
storage but the opaque-ceiling cache could continue to report a
stale state until something else invalidated it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing set_vertex / get_vertex pattern. Bounds and
unknown-layer guards raise the same shape of error as the v3
write-API family. Lazy-allocates layer.cells_material on first
write so maps that never use cell-tile painting stay byte-clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the optional layer.cells_material array to the v3 schema
validator, the per-cell read helper cells_material_at, the
lazy-allocator ensure_cells_material, and extends
cell_has_material plus compute_cell_bitmask_v3 to consult it.
No public-API change yet; cell_has_material's signature gains a
cells_material parameter that all internal callers thread through.
A layer with no cells_material entry remains behaviourally
identical to v0.5.6. Public APIs and the bitmask branch land in
later commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
compute_cell_bitmask_v3 now reads the 8-bit neighbour bitmask from
the render-cell's own 4 corner paint-tiles (cardinal bit set iff
at least one of the 2 paint-tiles on that edge is painted; diagonal
bit set iff the corner paint-tile is painted), not from the
any-corner material status of the 8 neighbour render-cells.
Pre-0.5.6 used cell-neighbour-material, which let two cells share
connectivity across an empty paint-tile gap whenever any unrelated
corner of either was painted — producing connected blob shapes
where two visually separated 2x2 islands were expected.
Atlas, paint storage, override sublayer, public API and the slot
lookup table all unchanged. Doc comment and README painting-model
section rewritten to describe the dual-grid offset explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes what the v3 vertex/material render path computes internally:
per (layer, x, y), returns {slot, rot, flip} for material cells,
nil for non-material. Override entry wins (normalised via existing
normalize_override_entry); otherwise resolves through
compute_cell_bitmask_v3 + SLOT_LOOKUP.
Primary consumer: debug-overlay tooling (map-editor 0.2.0c.3) that
labels each rendered cell with its blob-14 slot identity for
authoring verification.
Public accessors for palette-like consumers (map-editor 0.2.0c.2)
that need to render real tile thumbnails. All three return nil when
the atlas-idx is out of range, the slot has no tile in the atlas,
or the texture failed to load (headless test env).
atlas_tile_uv resolves the slot via the same `slot_NN_` regex used
internally by the v3 vertex/material render path, so callers see
the same slot -> tile_id mapping the renderer uses.
build_map_v3 caches an alias->atlas dict (atlas_by_alias) at map-load
time, pointing at the resolved stubs from load_tilemap. load_textures
later REPLACES m.atlases[i] in-place with the populated record
(diffuse_texture_handle + per-tile UVs from the lib-asset's
tiles.atlas.json) but never updated atlas_by_alias.
Effect: the v3 vertex/material render path resolves
m.atlas_by_alias[layer.material] to the PRE-load stub, finds no
diffuse handle, and short-circuits to the MISSING_ASSET_COLOR
fallback — every material-bound cell renders solid yellow regardless
of the bitmask/slot computation.
Fix: after each m.atlases[atlas_idx] = ... write in load_textures,
also overwrite m.atlas_by_alias[atlas_alias] with the new record.
Guarded on `if m.atlas_by_alias` so v2 maps (no autotile path) are
unaffected.
Found by the editor's freshly-bound work map after 0.2.0b.1 — vagrant
was unaffected by the bug because its hand-authored map was loaded
fine on the very first frame, masking the issue.
Vertex-grid write/read for the autotile painting path. The map-editor
0.2.0a Auto-Tile mode is the primary consumer; painting a single
vertex causes up to 4 surrounding cells to flip to material via the
any-corner rule. Coordinate-checks reject OOB writes; grid is lazily
allocated on first paint.
Three small follow-ups bundled into a single version bump:
1. Real tile.opaque consumption (cell_is_opaque_on_layer)
v0.5.0e shipped a slot-13 heuristic for vertex-painted layers
because the atlas-baker wasn't computing real opacity. Atlas-baker
v0.2.0 now sets tile.opaque via alpha-analysis (all-alpha-255
detection). Both vertex-painted and tiles[] paths now resolve the
actual slot and read its tile.opaque from atlas metadata. Pre-
v0.2.0 atlases without the flag report not-opaque (safe-
conservative: extra draws, no missed cells).
2. Manifest-schema patch (reserved fields, accept-but-ignore)
Type-checked acceptance of forthcoming manifest fields the
validator will need before consumers can ship them in real
manifests without breaking changes:
map-level: z_level (int), z_below (string)
per-layer: base_color (string #RRGGBB), tint_override (table),
collision_policy (table), traversal_modes (table),
foundation_mode (array)
Future slices (lib-core.maps v0.6 multi-z-level, lib-core.actor
movement-modes, etc.) wire the fields into actual behaviour.
3. Override format extension {slot, rot, flip} (design paper §15.1)
Override entries now accept EITHER a bare integer slot_id (compact
canonical-orientation form, backwards-compat with 0.5.0d) OR an
object `{slot, rot?, flip?}` for explicit-transform placement
(unblocks map-editor v0.2's Direct-mode Transform controls).
New normalize_override_entry helper converts disk-form to runtime
{slot, rot, flip}. Validator (validate_map_table_v3) type-checks
per-entry. set_override accepts either form and auto-stores as
compact (bare-int) when rot+flip both 0, object form otherwise —
minimises disk diff for the common canonical case. get_override
returns normalized form regardless of how the entry is stored.
draw_layer + cell_is_opaque_on_layer updated to read the
normalized form.
Closure-gate: milestone-check.sh ctest + test-all-modules GREEN.
SPOREL_CI=1 vagrant-skeleton rc=0 with 60 render_frame_ok; no
magenta-placeholders, no render-hook errors. Existing override-cells
(vagrant's 3 wall pillars) still render correctly through the new
normalize path; opaque-ceiling cache now reads real atlas data.
Consumer dep-bumps for v0.5.1 land in their own commits per repo:
sporel-lib-core.render, sporel-module-vagrant-skeleton, sporel-
module-map-editor[-test].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverses the internal resolved-atlas representation back to the
v2 on-disk shape (atlas_aliases as strings, only whitelisted layers
emitted, roof key omitted when nil). Hand-rolled JSON stringifier
because the engine exposes no cjson Lua binding and load_json has
no symmetric save_json counterpart. Output is byte-deterministic
(sorted object keys) and pretty-printed with 2-space indent.
Validates layer name against VALID_LAYER_NAMES, bounds-checks the
target cell, auto-allocates the layer table if the caller writes
to a previously-empty layer. Marks the map as dirty for callers
that track in-memory mutations.
When a consumer renders via maps.draw_map_pre/post_entities without
having called maps.load_textures (or when asset-alias resolution
failed), the per-atlas diffuse_texture_handle is nil. The previous
draw_layer passed nil to draw_sprite_transform and crashed. Now it
falls back to engine.render.draw_rect with the classic magenta
missing-asset color so the consumer sees the gap visually instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an atlas declares tile_size_px = null (variable-size tiles), the
load_textures call was overwriting the map's tile_size with nil, causing
an arithmetic error in draw_layer on the first render frame.
Guard the tile_size update so it only applies when tile_size_px is
non-nil; maps using variable-size atlases keep their tile_size from
the stub default (32) set at load time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
draw_layer now samples tile sub-rectangles from the atlas diffuse
texture via the ADR-0044 source-rect render primitive (13-arg
draw_sprite_transform). load_textures gains a walkable field in
tile-records (from atlas JSON) so gameplay queries work after the
tiles-dir is removed. load_tilemap falls back to an atlas-bootstrap
stub when the legacy tilemap JSON is absent, allowing maps.load to
succeed before load_textures is called. Texture loading uses pcall
to degrade gracefully in headless environments. Bumps lib version to
0.3.0 to flag the breaking change in tile-record shape (id is now
integer, name is the stable string identifier).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the per-tile texture-handle pattern with per-atlas handles
plus per-tile UV-rects. Each map-lib atlas record now holds a tiles-by-
id dictionary loaded from tiles.atlas.json and two texture handles
(diffuse + height) loaded from the paired PNGs. Tile-records expose
{id, name, uv, blocks_sight} fields; the breaking-change to numeric id
+ named name flows to all consumers in this slice.
Adds get_layer_diffuse_texture and get_layer_height_texture that
aggregate to a single handle when every cell in a layer references the
same atlas, returning nil for multi-atlas layers so lighting can fall
back to per-cell sampling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update README.md to version 0.2.0: add API entries for the 16 new
public functions (encode_gid, decode_gid, upgrade_v1_to_v2,
validate_map_table_v2, atlas_count, atlas_id_at, has_layer, is_indoor,
cell_gid, tile_at_layer, blocks_walk, blocks_sight,
iterate_layers_pre_entities, iterate_layers_post_entities,
draw_map_pre_entities, draw_map_post_entities), add the schema-v2
reference section documenting layer-stack, packed-u32 GID bit layout,
auto-migration, and a v0.2.0 changelog entry.
Fix tile-id OOB validation regression in build_map_v2: after v1->v2
upgrade, packed GIDs were not checked against atlas palette sizes.
Restore the check in build_map_v2 so loading a map with an out-of-range
tile_id still produces a fatal error with the same message pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M.load now auto-upgrades v1 maps to v2 shape (no m.tilemap field), but
load_textures still dereferenced m.tilemap unconditionally and crashed on
every v2 consumer. Extract per-tilemap loop into a helper and dispatch by
schema_version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds LAYER_ORDER_PRE/POST_ENTITIES constants, iterate_layers_pre/post_entities
helpers, draw_layer and draw_v1_legacy locals, and draw_map_pre/post_entities
public functions. draw_map becomes a backward-compat wrapper over both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the v1 is_walkable stub with a v2-aware version that requires
surface tile present and no blocking wall layer cell. Add blocks_walk
(wall-layer OOB-safe check) and blocks_sight (wall + upper_wall with
per-tile blocks_sight override). v1 path unchanged via schema_version
branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces M.tile_at_layer(layer_name, x, y, map_id) for direct per-layer
tile lookup with proper atlas resolution. Refactors the v2 branch of M.tile_at
to delegate to tile_at_layer("surface", ...) instead of duplicating the GID
decode logic inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds M.cell_gid(layer_name, x, y, map_id) to read raw GID values from
named layers, enabling callers to decode atlas/tile/rotation from any
layer without going through tile_at().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the v1-only M.load path with a unified v2 load path that
auto-upgrades v1 JSON on read. Add build_map_v2 to produce map records
with atlases[], layers{}, and roof arrays. Add atlas_count, atlas_id_at,
has_layer, and is_indoor to the public API. Update tile_at to decode GIDs
from the surface layer for v2 maps, keeping M.create/build_map for
programmatic callers that supply a v1-style tilemap_table directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds validate_map_table_v2 — enforces schema_version=2, required
fields (id, size, atlases), non-empty atlases[], per-layer tile-count
check against w*h, and a VALID_LAYER_NAMES whitelist that logs a
warning for unknown layers instead of rejecting them.
Moves require_field before the v2 validator so it is in scope at
call time. engine.warn replaced with engine.print (no warn binding).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Converts legacy single-tilemap maps (tiles[], tile_rotations[]) into the
schema-v2 format (atlases[], layers.surface with packed GID cells).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
encode_gid / decode_gid pack atlas_index (8 bits), tile_id (20 bits), and
rotation (2 bits) into a single u32; gid == 0 is reserved as the empty-cell
sentinel. Lua 5.4 native bitwise operators used throughout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprite-mode in draw_map:
- tilemap-tiles with texture atlas-id render via
engine.render.draw_sprite_transform with rotation pivot at tile center
- tiles without texture continue to render via draw_rect with color
(Phase 1 mode preserved)
Tile rotation:
- maps support an optional tile_rotations parallel array in the map
JSON with per-cell 90-degree rotation values (0/90/180/270)
- absent or nil entries default to 0 (no rotation)
- enables 9-segment tile reuse via rotation rather than per-orientation
sprites
New API:
- maps.load_textures(asset_aliases) resolves the current tilemap's
tile atlas-ids to texture-handles via the asset-lib indirection
(asset_aliases[tilemap.asset_pack] -> lib-id -> atlas.json lookup
per tile)
Schema additions:
- tilemap.asset_pack (alias-key, optional)
- tile.texture (atlas-id, optional)
- map.tile_rotations (parallel array, optional)
All existing Phase 1 color-only tilemaps render unchanged.
Pre-existing bug: split_namespaced_id used first-dot-split. For modules
with dotted IDs (e.g. lib-core.maps-test), this misclassified
'<dotted-module-id>.<tilemap-name>' as cross-lib reference.
Replaced with split_local_tilemap() that matches by exact current-
module-id prefix. Module-id can now have arbitrary dots.
Surfaced by P.3.1 reference test-module 'lib-core.maps-test' which
has a dotted module-id.