Compare commits
12 Commits
c3e9eda702
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3e8ce64f | ||
|
|
ff27f3caf6 | ||
|
|
52da578e3f | ||
|
|
b68b1d4fba | ||
|
|
c159738870 | ||
|
|
ba2ef2df06 | ||
|
|
aaeb8d3650 | ||
|
|
e082a39596 | ||
|
|
e399f6c6f8 | ||
|
|
8453b1fc8f | ||
|
|
1ee19b4b7f | ||
|
|
394e8faef2 |
21
assets/_sources/_gen.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const { PNG } = require('pngjs');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
function writeSolidRGBA(p, w, h, rgba) {
|
||||||
|
const png = new PNG({ width: w, height: h });
|
||||||
|
for (let i = 0; i < png.data.length; i += 4) {
|
||||||
|
png.data[i + 0] = rgba[0];
|
||||||
|
png.data[i + 1] = rgba[1];
|
||||||
|
png.data[i + 2] = rgba[2];
|
||||||
|
png.data[i + 3] = rgba[3];
|
||||||
|
}
|
||||||
|
fs.writeFileSync(p, PNG.sync.write(png));
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOURCES = path.resolve(__dirname);
|
||||||
|
writeSolidRGBA(path.join(SOURCES, 'demo_tilemap', 'grass_diffuse.png'), 32, 32, [110, 170, 80, 255]);
|
||||||
|
writeSolidRGBA(path.join(SOURCES, 'demo_tilemap', 'stone_diffuse.png'), 32, 32, [120, 120, 120, 255]);
|
||||||
|
writeSolidRGBA(path.join(SOURCES, 'walls_tilemap', 'wall_brick_diffuse.png'), 32, 32, [180, 100, 80, 255]);
|
||||||
|
writeSolidRGBA(path.join(SOURCES, 'walls_tilemap', 'wall_wood_diffuse.png'), 32, 32, [140, 90, 50, 255]);
|
||||||
|
console.log('test-stub sources generated');
|
||||||
BIN
assets/_sources/demo_tilemap/grass_diffuse.png
Normal file
|
After Width: | Height: | Size: 128 B |
BIN
assets/_sources/demo_tilemap/stone_diffuse.png
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
assets/_sources/walls_tilemap/wall_brick_diffuse.png
Normal file
|
After Width: | Height: | Size: 128 B |
BIN
assets/_sources/walls_tilemap/wall_wood_diffuse.png
Normal file
|
After Width: | Height: | Size: 128 B |
33
assets/atlases/demo_tilemap/tiles.atlas.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"atlas_id": "demo_tilemap",
|
||||||
|
"atlas_version": 1,
|
||||||
|
"atlas_size_px": [
|
||||||
|
64,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
"tile_size_px": 32,
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "grass",
|
||||||
|
"walkable": true,
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "stone",
|
||||||
|
"walkable": false,
|
||||||
|
"uv": [
|
||||||
|
32,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
assets/atlases/demo_tilemap/tiles.atlas.lock.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"atlas_id": "demo_tilemap",
|
||||||
|
"bindings": {
|
||||||
|
"grass": 1,
|
||||||
|
"stone": 2
|
||||||
|
},
|
||||||
|
"deleted": [],
|
||||||
|
"next_id": 3
|
||||||
|
}
|
||||||
BIN
assets/atlases/demo_tilemap/tiles.diffuse.atlas.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
assets/atlases/demo_tilemap/tiles.height.atlas.png
Normal file
|
After Width: | Height: | Size: 176 B |
35
assets/atlases/walls_tilemap/tiles.atlas.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"atlas_id": "walls_tilemap",
|
||||||
|
"atlas_version": 1,
|
||||||
|
"atlas_size_px": [
|
||||||
|
64,
|
||||||
|
64
|
||||||
|
],
|
||||||
|
"tile_size_px": 32,
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "wall_brick",
|
||||||
|
"walkable": false,
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"blocks_sight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "wall_wood",
|
||||||
|
"walkable": false,
|
||||||
|
"uv": [
|
||||||
|
32,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"blocks_sight": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
assets/atlases/walls_tilemap/tiles.atlas.lock.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"atlas_id": "walls_tilemap",
|
||||||
|
"bindings": {
|
||||||
|
"wall_brick": 1,
|
||||||
|
"wall_wood": 2
|
||||||
|
},
|
||||||
|
"deleted": [],
|
||||||
|
"next_id": 3
|
||||||
|
}
|
||||||
BIN
assets/atlases/walls_tilemap/tiles.diffuse.atlas.png
Normal file
|
After Width: | Height: | Size: 161 B |
BIN
assets/atlases/walls_tilemap/tiles.height.atlas.png
Normal file
|
After Width: | Height: | Size: 176 B |
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "demo_tilemap",
|
|
||||||
"tile_size": 32,
|
|
||||||
"tiles": [
|
|
||||||
{"id": "grass", "walkable": true, "color": [110, 170, 80]},
|
|
||||||
{"id": "stone", "walkable": false, "color": [120, 120, 120]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "walls_tilemap",
|
|
||||||
"tile_size": 32,
|
|
||||||
"tiles": [
|
|
||||||
{"id": "wall_brick", "walkable": false, "color": [180, 100, 80]},
|
|
||||||
{"id": "wall_wood", "walkable": false, "color": [140, 90, 50]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
318
init.lua
@@ -8,6 +8,13 @@ local maps = require("lib-core.maps")
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
function M.run_tests(ctx)
|
function M.run_tests(ctx)
|
||||||
|
-- Asset-aliases for texture-loading (test-libs lack manifest asset_aliases;
|
||||||
|
-- hard-code the test-lib itself as the asset provider for its own atlases).
|
||||||
|
local aliases = {
|
||||||
|
demo_tilemap = "lib-core.maps-test",
|
||||||
|
walls_tilemap = "lib-core.maps-test",
|
||||||
|
}
|
||||||
|
|
||||||
-- ---------- Task 1: GID encode/decode ----------
|
-- ---------- Task 1: GID encode/decode ----------
|
||||||
engine.test.equals(maps.encode_gid(0, 0, 0), 0, "encode_gid(0,0,0) = 0 (empty sentinel)")
|
engine.test.equals(maps.encode_gid(0, 0, 0), 0, "encode_gid(0,0,0) = 0 (empty sentinel)")
|
||||||
engine.test.equals(maps.encode_gid(0, 12, 0), 192, "encode_gid(0,12,0) = 192 (= 12<<4)")
|
engine.test.equals(maps.encode_gid(0, 12, 0), 192, "encode_gid(0,12,0) = 192 (= 12<<4)")
|
||||||
@@ -101,6 +108,7 @@ function M.run_tests(ctx)
|
|||||||
local v2_id = maps.load("maps/demo_v2.map.json")
|
local v2_id = maps.load("maps/demo_v2.map.json")
|
||||||
engine.test.equals(v2_id, "demo_v2", "v2 map loaded with id 'demo_v2'")
|
engine.test.equals(v2_id, "demo_v2", "v2 map loaded with id 'demo_v2'")
|
||||||
maps.set_current(v2_id)
|
maps.set_current(v2_id)
|
||||||
|
maps.load_textures(aliases)
|
||||||
local v2_size = maps.size()
|
local v2_size = maps.size()
|
||||||
engine.test.equals(v2_size.w, 4, "v2 map width = 4")
|
engine.test.equals(v2_size.w, 4, "v2 map width = 4")
|
||||||
engine.test.equals(v2_size.h, 4, "v2 map height = 4")
|
engine.test.equals(v2_size.h, 4, "v2 map height = 4")
|
||||||
@@ -139,11 +147,11 @@ function M.run_tests(ctx)
|
|||||||
-- tile_at_layer returns tile-record from the correct atlas
|
-- tile_at_layer returns tile-record from the correct atlas
|
||||||
local surface_tile = maps.tile_at_layer("surface", 1, 1)
|
local surface_tile = maps.tile_at_layer("surface", 1, 1)
|
||||||
engine.test.assert(surface_tile ~= nil, "surface (1,1) returns tile")
|
engine.test.assert(surface_tile ~= nil, "surface (1,1) returns tile")
|
||||||
engine.test.equals(surface_tile.id, "grass", "surface (1,1) = grass")
|
engine.test.equals(surface_tile.name, "grass", "surface (1,1) name = grass")
|
||||||
|
|
||||||
local wall_tile = maps.tile_at_layer("wall", 0, 0)
|
local wall_tile = maps.tile_at_layer("wall", 0, 0)
|
||||||
engine.test.assert(wall_tile ~= nil, "wall (0,0) returns tile")
|
engine.test.assert(wall_tile ~= nil, "wall (0,0) returns tile")
|
||||||
engine.test.equals(wall_tile.id, "wall_brick", "wall (0,0) = wall_brick (from walls_tilemap)")
|
engine.test.equals(wall_tile.name, "wall_brick", "wall (0,0) name = wall_brick (from walls_tilemap)")
|
||||||
|
|
||||||
-- Empty cell returns nil
|
-- Empty cell returns nil
|
||||||
local empty_top = maps.tile_at_layer("topsurface", 0, 0)
|
local empty_top = maps.tile_at_layer("topsurface", 0, 0)
|
||||||
@@ -154,7 +162,7 @@ function M.run_tests(ctx)
|
|||||||
|
|
||||||
-- Legacy tile_at backwards-compat: queries surface layer
|
-- Legacy tile_at backwards-compat: queries surface layer
|
||||||
local legacy = maps.tile_at(1, 1)
|
local legacy = maps.tile_at(1, 1)
|
||||||
engine.test.equals(legacy.id, "grass", "legacy tile_at(x,y) queries surface layer")
|
engine.test.equals(legacy.name, "grass", "legacy tile_at(x,y) queries surface layer")
|
||||||
|
|
||||||
-- ---------- Task 7: Gameplay queries ----------
|
-- ---------- Task 7: Gameplay queries ----------
|
||||||
-- Surface-only cells (inside, no wall) → walkable
|
-- Surface-only cells (inside, no wall) → walkable
|
||||||
@@ -205,36 +213,30 @@ function M.run_tests(ctx)
|
|||||||
local v1_id = maps.load("maps/demo.map.json")
|
local v1_id = maps.load("maps/demo.map.json")
|
||||||
engine.test.equals(v1_id, "demo", "v1 demo map loaded")
|
engine.test.equals(v1_id, "demo", "v1 demo map loaded")
|
||||||
maps.set_current(v1_id)
|
maps.set_current(v1_id)
|
||||||
|
maps.load_textures(aliases)
|
||||||
engine.test.equals(maps.atlas_count(), 1, "v1 auto-upgrade has 1 atlas")
|
engine.test.equals(maps.atlas_count(), 1, "v1 auto-upgrade has 1 atlas")
|
||||||
engine.test.equals(maps.atlas_id_at(0), "demo_tilemap", "v1 atlas[0] = demo_tilemap")
|
engine.test.equals(maps.atlas_id_at(0), "demo_tilemap", "v1 atlas[0] = demo_tilemap")
|
||||||
engine.test.assert(maps.has_layer("surface"), "v1 auto-upgrade has surface layer")
|
engine.test.assert(maps.has_layer("surface"), "v1 auto-upgrade has surface layer")
|
||||||
engine.test.assert(not maps.has_layer("wall"), "v1 has no wall layer")
|
engine.test.assert(not maps.has_layer("wall"), "v1 has no wall layer")
|
||||||
|
|
||||||
-- Walkability via v2-API should match v1-API result
|
-- Walkability via v2-API should match v1-API result (atlas records now loaded)
|
||||||
engine.test.assert(maps.is_walkable(5, 5), "v1 (5,5) walkable (was grass in v1 test)")
|
engine.test.assert(maps.is_walkable(5, 5), "v1 (5,5) walkable (was grass in v1 test)")
|
||||||
engine.test.assert(not maps.is_walkable(0, 0), "v1 (0,0) not walkable (was stone in v1 test)")
|
engine.test.assert(not maps.is_walkable(0, 0), "v1 (0,0) not walkable (was stone in v1 test)")
|
||||||
|
|
||||||
-- Restore current to v2 map for subsequent tests if any
|
-- Geometry assertions (demo map, already current after load_textures block)
|
||||||
maps.set_current("demo_v2")
|
|
||||||
|
|
||||||
-- Setup (legacy demo assertions, now using already-loaded demo from Task 9 above)
|
|
||||||
local demo_map = "demo"
|
|
||||||
maps.set_current(demo_map)
|
|
||||||
|
|
||||||
-- Geometry assertions
|
|
||||||
local size = maps.size()
|
local size = maps.size()
|
||||||
engine.test.equals(size.w, 16, "map width = 16")
|
engine.test.equals(size.w, 16, "map width = 16")
|
||||||
engine.test.equals(size.h, 16, "map height = 16")
|
engine.test.equals(size.h, 16, "map height = 16")
|
||||||
engine.test.equals(maps.tile_size(), 32, "tile_size = 32")
|
engine.test.equals(maps.tile_size(), 32, "tile_size = 32")
|
||||||
|
|
||||||
-- Tile lookup + walkability
|
-- Tile lookup + walkability (post-M.2: tile.id is int, tile.name is string)
|
||||||
engine.test.equals(maps.tile_at(0, 0).id, "stone", "tile_at(0,0) is stone")
|
engine.test.equals(maps.tile_at(0, 0).name, "stone", "tile_at(0,0) name = stone")
|
||||||
engine.test.equals(maps.tile_at(5, 5).id, "grass", "tile_at(5,5) is grass")
|
engine.test.equals(maps.tile_at(5, 5).name, "grass", "tile_at(5,5) name = grass")
|
||||||
engine.test.assert(not maps.is_walkable(0, 0), "stone border non-walkable")
|
engine.test.assert(not maps.is_walkable(0, 0), "stone border non-walkable")
|
||||||
engine.test.assert(maps.is_walkable(5, 5), "grass interior walkable")
|
engine.test.assert(maps.is_walkable(5, 5), "grass interior walkable")
|
||||||
|
|
||||||
-- Arity-flex tile_at: 3-arg with nil map_id falls back to current_map (P.2.1)
|
-- Arity-flex tile_at: 3-arg with nil map_id falls back to current_map (P.2.1)
|
||||||
engine.test.equals(maps.tile_at(nil, 5, 5).id, "grass", "tile_at 3-arg nil-fallback")
|
engine.test.equals(maps.tile_at(nil, 5, 5).name, "grass", "tile_at 3-arg nil-fallback name = grass")
|
||||||
|
|
||||||
-- Out-of-bounds returns nil
|
-- Out-of-bounds returns nil
|
||||||
engine.test.equals(maps.tile_at(-1, 0), nil, "tile_at OOB negative tx returns nil")
|
engine.test.equals(maps.tile_at(-1, 0), nil, "tile_at OOB negative tx returns nil")
|
||||||
@@ -244,6 +246,290 @@ function M.run_tests(ctx)
|
|||||||
local list = maps.list()
|
local list = maps.list()
|
||||||
engine.test.equals(#list, 2, "maps.list returns 2 registered maps")
|
engine.test.equals(#list, 2, "maps.list returns 2 registered maps")
|
||||||
|
|
||||||
|
-- ---------- v0.4.0: Write APIs — set_cell_gid ----------
|
||||||
|
-- Build a fresh v2 map for write tests (separate from earlier load-based tests).
|
||||||
|
-- maps.create only handles the v1 path (requires tilemap_table); use maps.load
|
||||||
|
-- with a fixture file for the v2 path instead.
|
||||||
|
local write_id = maps.load("maps/write_test.map.json")
|
||||||
|
maps.set_current(write_id)
|
||||||
|
|
||||||
|
-- Happy path
|
||||||
|
local gid_painted = maps.encode_gid(0, 1, 0)
|
||||||
|
maps.set_cell_gid("surface", 1, 2, gid_painted)
|
||||||
|
engine.test.equals(maps.cell_gid("surface", 1, 2), gid_painted,
|
||||||
|
"set_cell_gid writes the cell that cell_gid reads back")
|
||||||
|
|
||||||
|
-- Bounds-error: x >= w
|
||||||
|
local ok1, err1 = pcall(function() maps.set_cell_gid("surface", 4, 0, gid_painted) end)
|
||||||
|
engine.test.assert(not ok1, "set_cell_gid rejects x out of bounds")
|
||||||
|
engine.test.assert(err1 and err1:find("bounds"), "set_cell_gid bounds-error mentions 'bounds'")
|
||||||
|
|
||||||
|
-- Bounds-error: negative coord
|
||||||
|
local ok2 = pcall(function() maps.set_cell_gid("surface", -1, 0, gid_painted) end)
|
||||||
|
engine.test.assert(not ok2, "set_cell_gid rejects negative x")
|
||||||
|
|
||||||
|
-- Unknown-layer error
|
||||||
|
local ok3, err3 = pcall(function() maps.set_cell_gid("bogus_layer", 0, 0, gid_painted) end)
|
||||||
|
engine.test.assert(not ok3, "set_cell_gid rejects unknown layer")
|
||||||
|
engine.test.assert(err3 and err3:find("layer"), "unknown-layer error mentions 'layer'")
|
||||||
|
|
||||||
|
-- On-demand layer allocation
|
||||||
|
engine.test.assert(maps.cell_gid("wall", 0, 0) == 0,
|
||||||
|
"wall layer empty before any write")
|
||||||
|
maps.set_cell_gid("wall", 2, 2, gid_painted)
|
||||||
|
engine.test.equals(maps.cell_gid("wall", 2, 2), gid_painted,
|
||||||
|
"set_cell_gid auto-allocates the wall layer")
|
||||||
|
engine.test.equals(maps.cell_gid("wall", 0, 0), 0,
|
||||||
|
"auto-allocated layer initialised to all-zero except the written cell")
|
||||||
|
|
||||||
|
-- ---------- v0.4.0: set_roof ----------
|
||||||
|
-- Roof should be nil before any write. No direct cell_roof reader exists yet
|
||||||
|
-- (out of scope for v0.4.0). Verify behaviourally via JSON round-trip in Task 3.
|
||||||
|
|
||||||
|
-- Happy path: set roof cells, no assertion on read since no public reader
|
||||||
|
maps.set_roof(1, 1, 1)
|
||||||
|
maps.set_roof(1, 1, 0) -- toggle back
|
||||||
|
maps.set_roof(2, 2, 1) -- leave this one set for Task 3's round-trip test
|
||||||
|
|
||||||
|
-- Value validation
|
||||||
|
local ok_v, err_v = pcall(function() maps.set_roof(0, 0, 2) end)
|
||||||
|
engine.test.assert(not ok_v, "set_roof rejects value=2")
|
||||||
|
engine.test.assert(err_v and err_v:find("0.*1"), "set_roof error mentions 0 or 1")
|
||||||
|
|
||||||
|
-- Bounds rejection
|
||||||
|
local ok_b = pcall(function() maps.set_roof(4, 0, 1) end)
|
||||||
|
engine.test.assert(not ok_b, "set_roof rejects x out of bounds")
|
||||||
|
|
||||||
|
-- ---------- v0.4.0: save_to_disk ----------
|
||||||
|
-- Map state from Task 1+2: surface(1,2) = gid_painted, wall(2,2) = gid_painted,
|
||||||
|
-- roof(2,2) = 1. Save and re-load to verify round-trip.
|
||||||
|
|
||||||
|
-- Re-establish deterministic state (defensive — earlier asserts may have toggled).
|
||||||
|
maps.set_cell_gid("surface", 1, 2, gid_painted)
|
||||||
|
maps.set_cell_gid("wall", 2, 2, gid_painted)
|
||||||
|
maps.set_roof(2, 2, 1)
|
||||||
|
|
||||||
|
-- Pick a temp path. Use TEMP env var for Windows compatibility; fall back to
|
||||||
|
-- os.tmpname() on POSIX (where it returns a writable path).
|
||||||
|
local tmp_dir = os.getenv("TEMP") or os.getenv("TMP") or "/tmp"
|
||||||
|
-- Unique suffix via os.clock() + os.time() to avoid collisions.
|
||||||
|
local tmp_suffix = string.format("sporel_maps_test_%d_%d", os.time(), math.floor(os.clock() * 1e6) % 1000000)
|
||||||
|
local out_path = tmp_dir .. "/" .. tmp_suffix .. "_1.json"
|
||||||
|
local out_path_2 = tmp_dir .. "/" .. tmp_suffix .. "_2.json"
|
||||||
|
local rl_path = tmp_dir .. "/" .. tmp_suffix .. "_rl.json"
|
||||||
|
maps.save_to_disk("write_test", out_path)
|
||||||
|
|
||||||
|
-- Re-read raw bytes for byte-determinism test
|
||||||
|
local f1 = io.open(out_path, "r")
|
||||||
|
engine.test.assert(f1 ~= nil, "save_to_disk wrote a readable file")
|
||||||
|
local bytes_1 = f1:read("*a")
|
||||||
|
f1:close()
|
||||||
|
|
||||||
|
-- Save a second time; bytes must match exactly
|
||||||
|
maps.save_to_disk("write_test", out_path_2)
|
||||||
|
local f2 = io.open(out_path_2, "r")
|
||||||
|
local bytes_2 = f2:read("*a")
|
||||||
|
f2:close()
|
||||||
|
engine.test.equals(bytes_1, bytes_2,
|
||||||
|
"save_to_disk is byte-deterministic across calls")
|
||||||
|
|
||||||
|
-- Verify JSON has expected keys
|
||||||
|
engine.test.assert(bytes_1:find('"schema_version"') ~= nil, "saved file has schema_version key")
|
||||||
|
engine.test.assert(bytes_1:find('"surface"') ~= nil, "saved file has surface key")
|
||||||
|
engine.test.assert(bytes_1:find('"wall"') ~= nil, "saved file has wall key (auto-allocated)")
|
||||||
|
engine.test.assert(bytes_1:find('"roof"') ~= nil, "saved file has roof key (set in Task 2)")
|
||||||
|
|
||||||
|
-- Re-load (with patched id to avoid registry collision).
|
||||||
|
-- engine.asset.load_json only accepts relative (sandboxed) paths, so write
|
||||||
|
-- the round-trip file into the maps-test lib's own maps/ directory where
|
||||||
|
-- the asset resolver can find it by relative path.
|
||||||
|
local lib_dir = engine.module.dir_of("lib-core.maps-test")
|
||||||
|
local rewritten = bytes_1:gsub('"id"%s*:%s*"write_test"', '"id":"write_test_reloaded"')
|
||||||
|
local abs_rl_path = lib_dir .. "/maps/write_test_reloaded.map.json"
|
||||||
|
local rf = io.open(abs_rl_path, "w"); rf:write(rewritten); rf:close()
|
||||||
|
maps.load("maps/write_test_reloaded.map.json")
|
||||||
|
|
||||||
|
engine.test.equals(maps.cell_gid("surface", 1, 2, "write_test_reloaded"), gid_painted,
|
||||||
|
"save round-trip preserves surface cell")
|
||||||
|
engine.test.equals(maps.cell_gid("wall", 2, 2, "write_test_reloaded"), gid_painted,
|
||||||
|
"save round-trip preserves wall cell")
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
os.remove(out_path); os.remove(out_path_2); os.remove(abs_rl_path)
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- 0.5.7: Cell-Tile material API tests (maps_t1)
|
||||||
|
-- =====================================================================
|
||||||
|
-- Reload the write_test map for a clean slate.
|
||||||
|
maps.set_current("write_test")
|
||||||
|
|
||||||
|
-- maps_t1: round-trip set/get
|
||||||
|
maps.set_cell_material("surface", 1, 1, true)
|
||||||
|
engine.test.equals(maps.get_cell_material("surface", 1, 1), true,
|
||||||
|
"maps_t1: set_cell_material then get_cell_material round-trips")
|
||||||
|
|
||||||
|
maps.set_cell_material("surface", 1, 1, false)
|
||||||
|
engine.test.equals(maps.get_cell_material("surface", 1, 1), false,
|
||||||
|
"maps_t1: set_cell_material(false) clears the flag")
|
||||||
|
|
||||||
|
-- Out-of-bounds rejection
|
||||||
|
local ok_oob, err_oob = pcall(function()
|
||||||
|
maps.set_cell_material("surface", -1, 0, true)
|
||||||
|
end)
|
||||||
|
engine.test.assert(not ok_oob,
|
||||||
|
"maps_t1: set_cell_material rejects negative x")
|
||||||
|
engine.test.assert(err_oob and err_oob:find("bounds"),
|
||||||
|
"maps_t1: bounds-error mentions 'bounds'")
|
||||||
|
|
||||||
|
-- Unknown-layer rejection
|
||||||
|
local ok_lay = pcall(function()
|
||||||
|
maps.set_cell_material("bogus_layer", 0, 0, true)
|
||||||
|
end)
|
||||||
|
engine.test.assert(not ok_lay,
|
||||||
|
"maps_t1: set_cell_material rejects unknown layer")
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- maps_t2-t7: Cell-Tile slot-resolution proofs
|
||||||
|
-- =====================================================================
|
||||||
|
-- write_test is 4x4. Use v2_size for w/h reference (already declared earlier).
|
||||||
|
|
||||||
|
-- maps_t2: cell_material_slot returns a non-nil record for a
|
||||||
|
-- cells_material cell.
|
||||||
|
-- Clear the surface layer's cells_material first.
|
||||||
|
for cy = 0, v2_size.h - 1 do
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, cy, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
maps.set_cell_material("surface", 1, 1, true)
|
||||||
|
local rec = maps.cell_material_slot("surface", 1, 1)
|
||||||
|
engine.test.assert(rec ~= nil,
|
||||||
|
"maps_t2: cell_material_slot returns non-nil for cell-tile material")
|
||||||
|
|
||||||
|
-- maps_t3: isolated slot reachable for the first time.
|
||||||
|
-- Clear cells_material AND vertices, place an isolated paint at (2, 2).
|
||||||
|
for cy = 0, v2_size.h - 1 do
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, cy, false)
|
||||||
|
maps.set_vertex("surface", cx, cy, false)
|
||||||
|
end
|
||||||
|
maps.set_vertex("surface", v2_size.w, cy, false)
|
||||||
|
end
|
||||||
|
for cx = 0, v2_size.w do maps.set_vertex("surface", cx, v2_size.h, false) end
|
||||||
|
maps.set_cell_material("surface", 2, 2, true)
|
||||||
|
local r3 = maps.cell_material_slot("surface", 2, 2)
|
||||||
|
engine.test.equals(r3.slot, 0, "maps_t3: slot 0 isolated reachable for first time")
|
||||||
|
engine.test.equals(r3.rot, 0, "maps_t3: isolated rot=0")
|
||||||
|
|
||||||
|
-- maps_t4: two adjacent cells → two end-caps facing each other.
|
||||||
|
maps.set_cell_material("surface", 1, 2, true)
|
||||||
|
maps.set_cell_material("surface", 2, 2, true)
|
||||||
|
local r4a = maps.cell_material_slot("surface", 1, 2)
|
||||||
|
local r4b = maps.cell_material_slot("surface", 2, 2)
|
||||||
|
engine.test.equals(r4a.slot, 1, "maps_t4: left cell is end (slot 1)")
|
||||||
|
engine.test.equals(r4a.rot, 1, "maps_t4: left end points east (rot 1)")
|
||||||
|
engine.test.equals(r4b.slot, 1, "maps_t4: right cell is end (slot 1)")
|
||||||
|
engine.test.equals(r4b.rot, 3, "maps_t4: right end points west (rot 3)")
|
||||||
|
|
||||||
|
-- maps_t5: 3-cell line → middle is straight.
|
||||||
|
maps.set_cell_material("surface", 0, 2, true)
|
||||||
|
-- already painted (1,2) and (2,2) from t4
|
||||||
|
local r5 = maps.cell_material_slot("surface", 1, 2)
|
||||||
|
engine.test.equals(r5.slot, 4, "maps_t5: middle of 3-cell line is straight (slot 4)")
|
||||||
|
-- rot may be 1 or 3 (horizontal canonical); accept both
|
||||||
|
engine.test.assert(r5.rot == 1 or r5.rot == 3,
|
||||||
|
"maps_t5: straight is horizontal (rot 1 or 3)")
|
||||||
|
|
||||||
|
-- Reset surface for t6.
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, 2, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- maps_t6: T-junction at (1, 2) with arms W (0, 2), E (2, 2), S (1, 3).
|
||||||
|
-- write_test is 4x4 so (1, 3) is the last row.
|
||||||
|
maps.set_cell_material("surface", 0, 2, true)
|
||||||
|
maps.set_cell_material("surface", 1, 2, true)
|
||||||
|
maps.set_cell_material("surface", 2, 2, true)
|
||||||
|
maps.set_cell_material("surface", 1, 3, true)
|
||||||
|
local r6 = maps.cell_material_slot("surface", 1, 2)
|
||||||
|
engine.test.equals(r6.slot, 5, "maps_t6: T-junction is tee_open (slot 5)")
|
||||||
|
|
||||||
|
-- Reset for t7.
|
||||||
|
for cy = 0, v2_size.h - 1 do
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, cy, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- maps_t7: 4-cell "+" at (1, 1) with N (1, 0), E (2, 1), S (1, 2), W (0, 1).
|
||||||
|
maps.set_cell_material("surface", 1, 1, true)
|
||||||
|
maps.set_cell_material("surface", 1, 0, true)
|
||||||
|
maps.set_cell_material("surface", 2, 1, true)
|
||||||
|
maps.set_cell_material("surface", 1, 2, true)
|
||||||
|
maps.set_cell_material("surface", 0, 1, true)
|
||||||
|
local r7 = maps.cell_material_slot("surface", 1, 1)
|
||||||
|
engine.test.equals(r7.slot, 8, "maps_t7: cross is cross_open (slot 8) — second previously-unreachable slot reachable")
|
||||||
|
|
||||||
|
-- Reset surface fully so later tests (if any added) start clean.
|
||||||
|
for cy = 0, v2_size.h - 1 do
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, cy, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- maps_t8: cells_material round-trips through save_to_disk + reload
|
||||||
|
-- =====================================================================
|
||||||
|
-- Use a unique temp path so we don't collide with the earlier write_test save.
|
||||||
|
local cm_path = tmp_dir .. "/" .. tmp_suffix .. "_cm.json"
|
||||||
|
local lib_dir2 = engine.module.dir_of("lib-core.maps-test")
|
||||||
|
local cm_rl_abs = lib_dir2 .. "/maps/write_test_cm_reloaded.map.json"
|
||||||
|
|
||||||
|
maps.set_current("write_test")
|
||||||
|
maps.set_cell_material("surface", 2, 1, true)
|
||||||
|
maps.save_to_disk("write_test", cm_path)
|
||||||
|
|
||||||
|
local f = io.open(cm_path, "r")
|
||||||
|
local cm_bytes = f:read("*a"); f:close()
|
||||||
|
engine.test.assert(cm_bytes:find('"cells_material"') ~= nil,
|
||||||
|
"maps_t8: cells_material appears in serialized JSON when set")
|
||||||
|
|
||||||
|
-- Patch id + relocate so the load resolver can find it.
|
||||||
|
local cm_rewritten = cm_bytes:gsub('"id"%s*:%s*"write_test"',
|
||||||
|
'"id":"write_test_cm_reloaded"')
|
||||||
|
local rf = io.open(cm_rl_abs, "w"); rf:write(cm_rewritten); rf:close()
|
||||||
|
maps.load("maps/write_test_cm_reloaded.map.json")
|
||||||
|
engine.test.equals(maps.get_cell_material("surface", 2, 1, "write_test_cm_reloaded"), true,
|
||||||
|
"maps_t8: cells_material round-trips through save+load")
|
||||||
|
|
||||||
|
-- Cleanup
|
||||||
|
os.remove(cm_path); os.remove(cm_rl_abs)
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- maps_t9: byte-determinism regression — a map with no cells_material
|
||||||
|
-- writes byte-identical output through the 0.5.7 serializer as it did
|
||||||
|
-- under 0.5.6.
|
||||||
|
-- =====================================================================
|
||||||
|
-- Reset surface cells_material so the save contains no cell-tile data.
|
||||||
|
maps.set_current("write_test")
|
||||||
|
for cy = 0, v2_size.h - 1 do
|
||||||
|
for cx = 0, v2_size.w - 1 do
|
||||||
|
maps.set_cell_material("surface", cx, cy, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local bytes_a_path = tmp_dir .. "/" .. tmp_suffix .. "_a.json"
|
||||||
|
local bytes_b_path = tmp_dir .. "/" .. tmp_suffix .. "_b.json"
|
||||||
|
maps.save_to_disk("write_test", bytes_a_path)
|
||||||
|
maps.save_to_disk("write_test", bytes_b_path)
|
||||||
|
local fa = io.open(bytes_a_path, "r"); local bytes_a = fa:read("*a"); fa:close()
|
||||||
|
local fb = io.open(bytes_b_path, "r"); local bytes_b = fb:read("*a"); fb:close()
|
||||||
|
engine.test.equals(bytes_a, bytes_b,
|
||||||
|
"maps_t9: byte-deterministic save with no cells_material content")
|
||||||
|
engine.test.assert(bytes_a:find('"cells_material"') == nil,
|
||||||
|
"maps_t9: cells_material key absent when all entries are zero")
|
||||||
|
os.remove(bytes_a_path); os.remove(bytes_b_path)
|
||||||
|
|
||||||
engine.exit(engine.test.failures())
|
engine.exit(engine.test.failures())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "lib-core.maps-test",
|
"id": "lib-core.maps-test",
|
||||||
"role": "test",
|
"role": "test",
|
||||||
"version": "0.2.0",
|
"version": "0.5.5",
|
||||||
"api_min": "0.1",
|
"api_min": "0.1",
|
||||||
"deps": [
|
"deps": [
|
||||||
{"id": "lib-core.maps", "version": "0.2.0"}
|
{"id": "lib-core.maps", "version": "0.5.7"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
24
maps/write_test.map.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 3,
|
||||||
|
"id": "write_test",
|
||||||
|
"size": { "w": 4, "h": 4 },
|
||||||
|
"atlases": ["demo_tilemap"],
|
||||||
|
"layers": {
|
||||||
|
"surface": {
|
||||||
|
"material": "demo_tilemap",
|
||||||
|
"vertices": [
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0
|
||||||
|
],
|
||||||
|
"tiles": [
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
scripts/bake.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Re-bake the stub atlases used by maps-test fixtures.
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
BAKER=~/Projects/Sporel/sporel-tool-atlas-baker/bin/atlas-baker.js
|
||||||
|
|
||||||
|
node "$BAKER" \
|
||||||
|
--in "$LIB_DIR/assets/_sources/demo_tilemap" \
|
||||||
|
--out "$LIB_DIR/assets/atlases/demo_tilemap" \
|
||||||
|
--atlas-id demo_tilemap \
|
||||||
|
--tile-size 32
|
||||||
|
|
||||||
|
node "$BAKER" \
|
||||||
|
--in "$LIB_DIR/assets/_sources/walls_tilemap" \
|
||||||
|
--out "$LIB_DIR/assets/atlases/walls_tilemap" \
|
||||||
|
--atlas-id walls_tilemap \
|
||||||
|
--tile-size 32 \
|
||||||
|
--blocks-sight-pattern '^wall_'
|
||||||