feat(lock-sprite): stable alias-set + source-file persistence across re-bakes
This commit is contained in:
49
src/lock-sprite.js
Normal file
49
src/lock-sprite.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// src/lock-sprite.js
|
||||||
|
// Sprite-Atlas-Lock: stable alias-set + source-file mapping across
|
||||||
|
// re-bakes. New aliases append; removed aliases surface in `removed[]`
|
||||||
|
// so the caller can loud-warn.
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
function loadSpriteLock(lockPath) {
|
||||||
|
if (!lockPath || !fs.existsSync(lockPath)) return null;
|
||||||
|
const raw = fs.readFileSync(lockPath, 'utf8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSpriteLock(existing, sources) {
|
||||||
|
const sourceMap = {};
|
||||||
|
const currentAliases = [];
|
||||||
|
for (const s of sources) {
|
||||||
|
sourceMap[s.alias] = s.sourceFile;
|
||||||
|
currentAliases.push(s.alias);
|
||||||
|
}
|
||||||
|
let mergedAliases;
|
||||||
|
let removed = [];
|
||||||
|
if (existing) {
|
||||||
|
const incoming = new Set(currentAliases);
|
||||||
|
// Preserve existing order; append new
|
||||||
|
mergedAliases = existing.aliases.filter(a => incoming.has(a));
|
||||||
|
for (const a of currentAliases) {
|
||||||
|
if (!existing.aliases.includes(a)) mergedAliases.push(a);
|
||||||
|
}
|
||||||
|
removed = existing.aliases.filter(a => !incoming.has(a));
|
||||||
|
} else {
|
||||||
|
mergedAliases = currentAliases;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
merged: {
|
||||||
|
atlas_id: existing ? existing.atlas_id : null,
|
||||||
|
version: 1,
|
||||||
|
aliases: mergedAliases,
|
||||||
|
sources: sourceMap,
|
||||||
|
},
|
||||||
|
removed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeSpriteLock(lock) {
|
||||||
|
return JSON.stringify(lock, null, 2) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadSpriteLock, mergeSpriteLock, serializeSpriteLock };
|
||||||
80
tests/lock-sprite.test.js
Normal file
80
tests/lock-sprite.test.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const { loadSpriteLock, mergeSpriteLock, serializeSpriteLock } = require('../src/lock-sprite');
|
||||||
|
|
||||||
|
test('loadSpriteLock: missing file -> null', () => {
|
||||||
|
const result = loadSpriteLock('/nonexistent/lock.json');
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadSpriteLock: reads valid lock', () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lock-test-'));
|
||||||
|
const lockPath = path.join(tmp, 'lock.json');
|
||||||
|
fs.writeFileSync(lockPath, JSON.stringify({
|
||||||
|
atlas_id: 'tcbasics',
|
||||||
|
version: 1,
|
||||||
|
aliases: ['bed1', 'bench1'],
|
||||||
|
sources: { bed1: 'Bed1.png', bench1: 'Bench1.png' },
|
||||||
|
}));
|
||||||
|
const lock = loadSpriteLock(lockPath);
|
||||||
|
assert.strictEqual(lock.atlas_id, 'tcbasics');
|
||||||
|
assert.deepStrictEqual(lock.aliases, ['bed1', 'bench1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mergeSpriteLock: no existing lock -> all new', () => {
|
||||||
|
const sources = [
|
||||||
|
{ alias: 'bed1', sourceFile: 'Bed1.png' },
|
||||||
|
{ alias: 'bench1',sourceFile: 'Bench1.png' },
|
||||||
|
];
|
||||||
|
const { merged, removed } = mergeSpriteLock(null, sources);
|
||||||
|
assert.deepStrictEqual(merged.aliases, ['bed1', 'bench1']);
|
||||||
|
assert.deepStrictEqual(removed, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mergeSpriteLock: existing alias preserved; new appended', () => {
|
||||||
|
const existing = {
|
||||||
|
atlas_id: 'tcbasics',
|
||||||
|
version: 1,
|
||||||
|
aliases: ['bed1'],
|
||||||
|
sources: { bed1: 'Bed1.png' },
|
||||||
|
};
|
||||||
|
const sources = [
|
||||||
|
{ alias: 'bed1', sourceFile: 'Bed1.png' },
|
||||||
|
{ alias: 'bench1', sourceFile: 'Bench1.png' },
|
||||||
|
];
|
||||||
|
const { merged, removed } = mergeSpriteLock(existing, sources);
|
||||||
|
assert.deepStrictEqual(merged.aliases, ['bed1', 'bench1']);
|
||||||
|
assert.deepStrictEqual(removed, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mergeSpriteLock: removed alias appears in `removed`', () => {
|
||||||
|
const existing = {
|
||||||
|
atlas_id: 'tcbasics',
|
||||||
|
version: 1,
|
||||||
|
aliases: ['bed1', 'bench1'],
|
||||||
|
sources: { bed1: 'Bed1.png', bench1: 'Bench1.png' },
|
||||||
|
};
|
||||||
|
const sources = [
|
||||||
|
{ alias: 'bed1', sourceFile: 'Bed1.png' },
|
||||||
|
];
|
||||||
|
const { merged, removed } = mergeSpriteLock(existing, sources);
|
||||||
|
assert.deepStrictEqual(merged.aliases, ['bed1']);
|
||||||
|
assert.deepStrictEqual(removed, ['bench1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeSpriteLock: stable JSON shape', () => {
|
||||||
|
const lock = {
|
||||||
|
atlas_id: 'tcbasics',
|
||||||
|
version: 1,
|
||||||
|
aliases: ['bed1'],
|
||||||
|
sources: { bed1: 'Bed1.png' },
|
||||||
|
};
|
||||||
|
const json = serializeSpriteLock(lock);
|
||||||
|
const parsed = JSON.parse(json);
|
||||||
|
assert.deepStrictEqual(parsed, lock);
|
||||||
|
// Trailing newline for POSIX-friendliness
|
||||||
|
assert.ok(json.endsWith('\n'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user