Add map inspector that emits a human-readable stats report

Counts set vs empty cells per layer, lists the top-10 most frequent
non-empty GIDs with decoded components, and reports roof coverage.
When an atlas registry is supplied, atlas-id+tile-name annotations
are added; without one the report still works using indices only.
This commit is contained in:
Axel Meyer
2026-05-23 16:55:56 +02:00
parent f516e2acda
commit 530bceaddd
4 changed files with 193 additions and 0 deletions

77
src/inspector.js Normal file
View File

@@ -0,0 +1,77 @@
'use strict';
const { decodeGid } = require('./gid');
function tileLabel(atlasIdx, tileId, atlasIdsByIndex, registry) {
const atlasId = atlasIdsByIndex[atlasIdx];
const reg = atlasId ? registry[atlasId] : null;
const name = reg ? reg.tilesById[tileId] : null;
const components = `atlas=${atlasIdx} tile=${tileId}`;
if (atlasId && name) return `${components.padEnd(20)} (${atlasId}:${name})`;
if (atlasId) return `${components.padEnd(20)} (${atlasId}:?)`;
return components;
}
function atlasResolutionMarker(atlasId, registry) {
if (!registry || Object.keys(registry).length === 0) return '(no registry)';
if (registry[atlasId]) return '(resolved)';
return '(not found)';
}
function inspectMap(map, registry) {
const lines = [];
const reg = registry || {};
const atlasIdsByIndex = map.atlases || [];
lines.push(`Map: ${map.id} (schema_version=${map.schema_version}, size=${map.size.w}x${map.size.h})`);
lines.push('');
lines.push('Atlases:');
if (atlasIdsByIndex.length === 0) {
lines.push(' (none declared)');
} else {
atlasIdsByIndex.forEach((aid, i) => {
lines.push(` [${i}] ${aid.padEnd(40)} ${atlasResolutionMarker(aid, reg)}`);
});
}
lines.push('');
lines.push('Layers:');
const layerEntries = Object.entries(map.layers || {});
if (layerEntries.length === 0) {
lines.push(' (no layers)');
} else {
for (const [layerName, layer] of layerEntries) {
const tiles = layer.tiles || [];
let set = 0, empty = 0;
const counts = new Map();
for (const gid of tiles) {
if (gid === 0) { empty++; continue; }
set++;
counts.set(gid, (counts.get(gid) || 0) + 1);
}
lines.push(` ${layerName.padEnd(13)} ${String(set).padStart(3)} cells set, ${String(empty).padStart(3)} empty`);
if (counts.size > 0) {
lines.push(' top tiles:');
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
for (const [gid, n] of top) {
const { atlas, tile } = decodeGid(gid);
lines.push(` ${tileLabel(atlas, tile, atlasIdsByIndex, reg)} × ${n}`);
}
}
}
}
lines.push('');
const roof = map.roof;
if (Array.isArray(roof)) {
const flagged = roof.reduce((a, v) => a + (v ? 1 : 0), 0);
lines.push(`Roof: ${flagged} cells flagged`);
} else {
lines.push('Roof: (none)');
}
return lines.join('\n') + '\n';
}
module.exports = { inspectMap };