Commit Graph

30 Commits

Author SHA1 Message Date
Axel Meyer
9a5ef5e832 feat(maps): v3 cells_material field foundation
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>
2026-06-01 21:44:23 +02:00
Axel Meyer
b5621c5d08 fix(maps): v0.5.6 — derive bitmask from cell's own corner paint-tiles
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>
2026-06-01 17:19:26 +02:00
calic
e3b6e38c5a feat(maps): v0.5.5 — cell_material_slot public getter
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.
2026-05-29 09:56:45 +02:00
calic
be6d419061 feat(maps): v0.5.4 — atlas_diffuse_handle + atlas_tile_size_px + atlas_tile_uv
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.
2026-05-29 01:37:38 +02:00
calic
9bd4e5a415 fix(maps): v0.5.3 — refresh atlas_by_alias after load_textures
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.
2026-05-29 01:27:49 +02:00
calic
714ec57010 feat(maps): v0.5.2 — set_vertex / get_vertex public APIs
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.
2026-05-28 23:51:39 +02:00
calic
d0f4d17b65 maps v0.5.1 — opaque-flag + manifest fields + override format
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>
2026-05-28 23:09:24 +02:00
calic
f8994c7ffa maps v0.5.0 — schema-v3 + autotile + override + render-opt
Five sub-slices closed in-session 2026-05-28 against the design at
sporel-meta/docs/design/2026-05-28-autotile-blob-styles-design.md.

0.5.0a — schema-v3 + auto-upgrade
  validate_map_table_v3 + build_map_v3 + serialize_map_v3 +
  upgrade_v2_to_v3 (no-op shape transformation). load() chains
  v1 -> v2 -> v3 transparent. All runtime `m.schema_version`-
  comparisons switched from `== 2`/`~= 2` to `>= 2`/`< 2` so v3
  inherits v2 behaviour for existing code paths. Per-layer optional
  fields (material, vertices, overrides) accepted by validator,
  preserved on round-trip serialize, ignored by renderer until
  0.5.0c/d wire them in.

0.5.0b — multi-layer terrain rendering validated
  Existing LAYER_ORDER_PRE/POST_ENTITIES loop already covered multi-
  layer; sub-slice validated end-to-end against a v3 map with three
  non-empty layers in vagrant-skeleton.

0.5.0c — vertex-painted autotile path
  ~150 LOC autotile section. rotate_bitmask_90cw + mirror_bitmask_h
  + apply_blob_gating helpers. SLOT_LOOKUP[bitmask] -> {slot, rot,
  flip} precomputed at module-init via D4-orbit walk over 14
  canonical patterns. cell_has_material (any-corner rule),
  compute_cell_bitmask_v3 (8-neighbour bitmask + blob-gating),
  atlas_slot_index (slot_NN_* tile-name parser, cached per atlas).
  build_map_v3 exposes atlas_by_alias for O(1) layer.material ->
  atlas lookup. draw_layer branches on layer.vertices+material:
  autotile path computes bitmask, looks up slot, draws with rot+flip
  via draw_sprite_transform. Legacy tiles[] path unchanged. cell_gid
  returns sentinel 1 for vertex-painted material cells so
  is_walkable / blocks_walk / blocks_sight stay transparent.

0.5.0d — sparse override sublayer
  Override map keyed "x:y" -> slot_id (0..13). Override implies
  cell-has-material (restores the 1-cell-isolated case the any-corner
  rule cannot express). Renders canonical slot in default orientation
  (no rot/flip), bypassing bitmask compute. Public APIs:
  set_override / clear_override / get_override with validation
  (v3-only, valid layer, in-bounds, integer 0..13). Internal helpers
  require_v3_layer + check_coords.

0.5.0e — render optimisation
  LAYER_Z + LAYER_ORDER_TOP_DOWN constants. m._layer_has_content
  precomputed in build_map_v3 (per-layer scan: tiles[] non-zero OR
  vertices painted OR overrides set); draw_map_pre/post_entities
  skip empty layers cheaply. m._opaque_ceiling per-cell topmost-
  opaque z-index, lazily built via ensure_opaque_ceiling. Opaque
  heuristic (light): vertex-painted cell opaque iff bitmask resolves
  to slot 13 OR override == slot 13. Legacy tiles[] reads
  atlas.tile.opaque (currently always nil pending atlas-baker E2
  alpha-analysis; safe-conservative default = not opaque). draw_layer
  skips cells where ceiling > current layer z. Writes nil out
  _opaque_ceiling for lazy rebuild.

manifest.lib bumped 0.4.0 -> 0.5.0. Consumer modules bumped in
their own commits (sporel-lib-core.render, sporel-module-vagrant-
skeleton, sporel-module-map-editor[-test]).

Closure-gate per sub-slice: milestone-check.sh build + ctest +
test-all-modules GREEN; SPOREL_CI=1 vagrant-skeleton smoke rc=0
with 60 render_frame_ok and no magenta-placeholders.

Detailed sub-slice closure logs live in
sporel-meta/docs/superpowers/plans/2026-05-28-map-schema-v3.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:48:47 +02:00
Axel Meyer
d80c0ecb5f Add save_to_disk plus hand-rolled JSON serializer
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.
2026-05-24 00:43:25 +02:00
Axel Meyer
84d1980c18 Add set_roof write API to lib-core.maps
Allocates the roof array on-demand if the map didn't have one,
validates value in {0, 1}, and bounds-checks the target cell.
2026-05-24 00:34:14 +02:00
Axel Meyer
45b1f93363 Add set_cell_gid write API to lib-core.maps
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.
2026-05-24 00:25:06 +02:00
Axel Meyer
b8705c0ec7 Render magenta placeholder when atlas is unloaded
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>
2026-05-22 00:15:00 +02:00
Axel Meyer
e5a350fcb2 Fix load_textures overwriting tile_size with nil for variable-size atlases
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>
2026-05-21 23:28:33 +02:00
Axel Meyer
99d00bd115 Rewrite draw_layer for atlas-handle + uv rendering; bump to 0.3.0
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>
2026-05-21 23:09:02 +02:00
Axel Meyer
8a75aee48a Rewrite load_textures for paired-PNG atlas format
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>
2026-05-21 22:57:12 +02:00
Axel Meyer
6b2dc22227 Document v0.2.0 API and schema-v2 map format
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>
2026-05-21 16:33:27 +02:00
Axel Meyer
caf29b314d Make load_textures iterate atlases for v2 maps
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>
2026-05-21 16:18:34 +02:00
Axel Meyer
8cec7b6812 Iterate layers in z-order with entity-slot split; multi-layer draw_map
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>
2026-05-21 15:54:27 +02:00
Axel Meyer
53c9fe4c0c Add v2 gameplay queries: walkable / blocks_walk / blocks_sight
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>
2026-05-21 15:50:59 +02:00
Axel Meyer
15fd68670e Add tile_at_layer and route legacy tile_at to surface layer
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>
2026-05-21 15:48:07 +02:00
Axel Meyer
880c67fef3 Expose cell_gid query for multi-atlas v2 maps
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>
2026-05-21 15:43:02 +02:00
Axel Meyer
6af36e9c6e Load schema-v2 maps with multi-layer storage and roof field
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>
2026-05-21 15:37:41 +02:00
Axel Meyer
1562ee0068 feat: validate schema-v2 map tables with layer-name whitelist
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>
2026-05-21 15:26:48 +02:00
Axel Meyer
0985141fb9 feat: add v1->v2 schema auto-upgrade for map tables
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>
2026-05-21 15:20:32 +02:00
Axel Meyer
446d37fb0e feat: add packed-u32 GID encoding helpers for schema-v2 maps
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>
2026-05-21 15:15:34 +02:00
Axel Meyer
2b5acb5caf feat: v0.1.2 sprite-mode in draw_map + tile_rotations + load_textures
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.
2026-05-18 03:21:52 +02:00
Axel Meyer
db9eb222cb fix(P.3.1): split_namespaced_id replaced with current-module-prefix match; v0.1.1
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.
2026-05-11 02:50:32 +02:00
Axel Meyer
ecc6796b0d fix(audit-M1): tile_at 3-arg form accepts nil map_id with current_map fallback 2026-05-10 15:46:35 +02:00
Axel Meyer
94c2197d46 feat: P.0.lib.maps single-tile-grid implementation (replaces trivial-stub) 2026-05-09 18:13:59 +02:00
Axel Meyer
5cf3d0c2fb feat: P.0.module.skeleton trivial-stub for lib-core.maps 2026-05-09 15:42:39 +02:00