release: bump to 0.8.0 (HD Remaster) with docs, cache bust, and smoke coverage

Cache-bust v=54 -> v=55 across index.html and sfxbank.js, bump the game
version to 0.8.0, document the HD Remaster setting in gameplay.md and the
changelog, and extend the smoke suite with 4x sheet-geometry bounds checks,
HD loader fallback URL assertions, and toggle persistence/reload checks.
This commit is contained in:
Quintine committed 2026-08-12 10:11:23 +10:00
1 parent 928232ce9d
commit 6b4e7091a9
6 files changed
+145 -27

No files matched your search

+108 -1
View File
@@ -31,11 +31,14 @@ const canvasStub = {
getBoundingClientRect: () => ({ left: 0, top: 0, width: 960, height: 640 }),
};
const stored = {};
let reloadCalls = 0;
const sandbox = {
console,
performance: { now: () => simTime },
requestAnimationFrame: () => {}, // we drive update() manually
localStorage: { getItem: () => null, setItem: () => {} },
localStorage: { getItem: k => stored[k] ?? null, setItem: (k, v) => { stored[k] = String(v); } },
location: { reload() { reloadCalls++; } },
document: { getElementById: () => canvasStub },
Image: class { constructor() { this.width = 32; this.height = 32; } set src(v) { this.onerror && this.onerror(); } },
Audio: class {
@@ -153,6 +156,110 @@ delete ctx.Sprites.imgs.player_legs_sheet;
check('legs strip addresses all 8 stride frames', legDraws === 8);
check('legs strip reads stay in bounds', legOutOfBounds === 0);
check('legs strip rotates smoothly at runtime', legRotations.length === 8 && legRotations.every(angle => Math.abs(angle - Math.PI / 3) < 0.001));
console.log('== HD sheets address 4x atlases in bounds ==');
let hdSheetDraws = 0, hdOutOfBounds = 0;
const hdActorCtx = fakeCtx();
hdActorCtx.drawImage = (img, sx, sy, sw, sh) => {
hdSheetDraws++;
if (sx < 0 || sy < 0 || sx + sw > img.width || sy + sh > img.height) hdOutOfBounds++;
};
const hdSheetCases = [
['player', 3072, 11136, 96],
['enemy_shambler', 2048, 7424, 64],
['boss_bonesaw', 4096, 14848, 128],
];
for (const [name, width, height, targetW] of hdSheetCases) {
ctx.Sprites.imgs[name + '_sheet'] = { width, height };
for (let dir = 0; dir < 8; dir++) {
for (const action of Object.keys(ctx.ACTOR_ANIMS)) {
ctx.Sprites.actor(hdActorCtx, name, 0, 0, dir * Math.PI / 4, action, 0.2, targetW);
}
}
delete ctx.Sprites.imgs[name + '_sheet'];
}
check('HD sheets address 4x atlases in bounds', hdSheetDraws === 120 && hdOutOfBounds === 0);
let hdLegDraws = 0, hdLegOutOfBounds = 0;
const hdLegRotations = [];
const hdLegCtx = fakeCtx();
hdLegCtx.rotate = angle => hdLegRotations.push(angle);
hdLegCtx.drawImage = (img, sx, sy, sw, sh) => {
hdLegDraws++;
if (sx < 0 || sy < 0 || sx + sw > img.width || sy + sh > img.height) hdLegOutOfBounds++;
};
ctx.Sprites.imgs.player_legs_sheet = { width: 3072, height: 384 };
for (let frame = 0; frame < 8; frame++) {
ctx.Sprites.legs(hdLegCtx, 0, 0, Math.PI / 3, frame * Math.PI / 4 + 0.01, 384);
}
delete ctx.Sprites.imgs.player_legs_sheet;
check('HD legs strip addresses 8 stride frames at 4x resolution', hdLegDraws === 8 && hdLegOutOfBounds === 0);
check('HD legs strip rotates smoothly', hdLegRotations.length === 8 && hdLegRotations.every(a => Math.abs(a - Math.PI / 3) < 0.001));
console.log('== HD loader falls back per sprite to SD ==');
const origImage = sandbox.Image;
let attemptedUrls = {};
ctx.G.hdRemaster = true;
sandbox.Image = class extends origImage {
constructor() { super(); }
set src(v) {
const base = v.split('?')[0];
const name = ctx.SPRITE_MANIFEST.find(n => base === 'assets/hd/' + n + '.webp' || base === 'assets/' + n + '.png');
if (name) {
if (!attemptedUrls[name]) attemptedUrls[name] = [];
attemptedUrls[name].push(v);
}
this.onerror && this.onerror();
}
};
ctx.Sprites.load();
check('HD loader loads all images (fallbacks)', ctx.G.imagesLoaded === true);
let hdFallbackOk = 0;
for (const name of ctx.SPRITE_MANIFEST) {
const urls = attemptedUrls[name];
const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=55' && urls[1] === 'assets/' + name + '.png?v=55';
if (ok) hdFallbackOk++;
}
check('HD loader tries WebP then PNG per sprite', hdFallbackOk === ctx.SPRITE_MANIFEST.length);
console.log('== SD loader path (pristine restore) ==');
attemptedUrls = {};
sandbox.Image = class extends origImage {
constructor() { super(); }
set src(v) {
const base = v.split('?')[0];
const name = ctx.SPRITE_MANIFEST.find(n => base === 'assets/hd/' + n + '.webp' || base === 'assets/' + n + '.png');
if (name) {
if (!attemptedUrls[name]) attemptedUrls[name] = [];
attemptedUrls[name].push(v);
}
this.onerror && this.onerror();
}
};
ctx.G.hdRemaster = false;
ctx.Sprites.load();
check('SD loader loads all images', ctx.G.imagesLoaded === true);
let sdUrlOk = 0;
for (const name of ctx.SPRITE_MANIFEST) {
const urls = attemptedUrls[name];
const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=55';
if (ok) sdUrlOk++;
}
check('SD loader requests one PNG per sprite', sdUrlOk === ctx.SPRITE_MANIFEST.length);
sandbox.Image = origImage;
console.log('== HD toggle persists and reloads ==');
ctx.G.mode = 'menu';
const reloadBefore = reloadCalls;
ctx.toggleHdRemaster();
check('HD toggle enables the flag', ctx.G.hdRemaster === true);
check('HD toggle persists to localStorage', stored['meatslicer_hd_remaster'] === '1');
check('HD toggle reloads the page', reloadCalls === reloadBefore + 1);
ctx.toggleHdRemaster();
check('HD toggle disables the flag', ctx.G.hdRemaster === false);
check('HD toggle persists off value', stored['meatslicer_hd_remaster'] === '0');
check('HD toggle reloads again', reloadCalls === reloadBefore + 2);
reloadCalls = 0;
check('every weapon has layered render and muzzle geometry', Object.values(ctx.WEAPONS).every(w =>
Number.isFinite(w.torsoW) && Number.isFinite(w.torsoFwd) && Number.isFinite(w.muzzle) && w.muzzle > 0));
check('every weapon has finite impact punch metadata', Object.values(ctx.WEAPONS).every(w => Number.isFinite(w.punch) && w.punch > 0));