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.