diff --git a/index.html b/index.html index 29b5dec..dd19a7c 100644 --- a/index.html +++ b/index.html @@ -6,32 +6,32 @@ MEATSLICER - +
- - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/js/input.js b/js/input.js index ea739a4..2cf34a8 100644 --- a/js/input.js +++ b/js/input.js @@ -9,6 +9,10 @@ const Input = { anyKey: false, }; +let _rect = null; + +Input.updateRect = function () { _rect = canvas ? canvas.getBoundingClientRect() : null; }; + function initInput(canvas) { window.addEventListener('keydown', (e) => { const k = e.key.toLowerCase(); @@ -21,7 +25,7 @@ function initInput(canvas) { window.addEventListener('blur', () => { Input.keys = {}; Input.mdown = false; }); function setMouse(e) { - const r = canvas.getBoundingClientRect(); + const r = _rect || (_rect = canvas.getBoundingClientRect()); Input.mx = (e.clientX - r.left) * (W / r.width); Input.my = (e.clientY - r.top) * (H / r.height); } diff --git a/js/main.js b/js/main.js index 3613ae7..4193a23 100644 --- a/js/main.js +++ b/js/main.js @@ -105,6 +105,7 @@ function fitCanvas() { const k = Math.min(window.innerWidth / W, window.innerHeight / H) * 0.97; canvas.style.width = Math.floor(W * k) + 'px'; canvas.style.height = Math.floor(H * k) + 'px'; + if (Input.updateRect) Input.updateRect(); } function resetRun() { diff --git a/js/rooms.js b/js/rooms.js index 96c323e..c95f7ed 100644 --- a/js/rooms.js +++ b/js/rooms.js @@ -94,6 +94,8 @@ function enterRoom(gx, gy) { G.boss = null; G.roomLayer = null; G.roomLayerKey = ''; + G.wallLayer = null; + G.wallLayerKey = ''; G.roomDamaged = false; G.roomEnterT = G.time; G.entryFresh = false; @@ -435,8 +437,7 @@ function drawRoom(ctx) { drawWalls(ctx, r); } -function drawWalls(ctx, r) { - const locked = roomLocked(); +function drawWallBands(target, r) { const a = roomBounds(r); const theme = ROOM_THEMES[r.theme] || ROOM_THEMES.abattoir; const wallPool = theme.wall; @@ -444,43 +445,69 @@ function drawWalls(ctx, r) { const top = a.y0 - WALL, bottom = a.y1 + WALL; function clipBand(dir) { - ctx.beginPath(); + target.beginPath(); if (dir === 'n' || dir === 's') { const y = dir === 'n' ? top : a.y1; if (r.doors[dir]) { - ctx.rect(left, y, a.cx - DOOR_HALF - left, WALL); - ctx.rect(a.cx + DOOR_HALF, y, right - a.cx - DOOR_HALF, WALL); - } else ctx.rect(left, y, right - left, WALL); + target.rect(left, y, a.cx - DOOR_HALF - left, WALL); + target.rect(a.cx + DOOR_HALF, y, right - a.cx - DOOR_HALF, WALL); + } else target.rect(left, y, right - left, WALL); } else { const x = dir === 'w' ? left : a.x1; if (r.doors[dir]) { - ctx.rect(x, top, WALL, a.cy - DOOR_HALF - top); - ctx.rect(x, a.cy + DOOR_HALF, WALL, bottom - a.cy - DOOR_HALF); - } else ctx.rect(x, top, WALL, bottom - top); + target.rect(x, top, WALL, a.cy - DOOR_HALF - top); + target.rect(x, a.cy + DOOR_HALF, WALL, bottom - a.cy - DOOR_HALF); + } else target.rect(x, top, WALL, bottom - top); } - ctx.clip(); + target.clip(); } for (const dir of ['n', 's']) { - ctx.save(); clipBand(dir); + target.save(); clipBand(dir); const y = dir === 'n' ? top + TILE / 2 : a.y1 + TILE / 2; for (let x = left; x < right; x += TILE) { const h = hashTile(Math.floor(x / TILE) + r.gx * 17, (dir === 'n' ? -1 : 11) + r.gy * 19); - Sprites.draw(ctx, wallPool[h % wallPool.length], x + TILE / 2, y, 0, TILE); + Sprites.draw(target, wallPool[h % wallPool.length], x + TILE / 2, y, 0, TILE); } - ctx.restore(); + target.restore(); } for (const dir of ['w', 'e']) { - ctx.save(); clipBand(dir); + target.save(); clipBand(dir); const x = dir === 'w' ? left + TILE / 2 : a.x1 + TILE / 2; for (let y = top; y < bottom; y += TILE) { const h = hashTile((dir === 'w' ? -1 : 16) + r.gx * 17, Math.floor(y / TILE) + r.gy * 19); - Sprites.draw(ctx, wallPool[h % wallPool.length], x, y + TILE / 2, Math.PI / 2, TILE); + Sprites.draw(target, wallPool[h % wallPool.length], x, y + TILE / 2, Math.PI / 2, TILE); } - ctx.restore(); + target.restore(); + } + target.strokeStyle = 'rgba(0,0,0,0.5)'; target.lineWidth = 3; + target.strokeRect(a.x0, a.y0, a.w, a.h); +} + +function drawWalls(ctx, r) { + const locked = roomLocked(); + const a = roomBounds(r); + + // static wall tiles are cached; door sprites are dynamic per frame + const canCache = typeof document !== 'undefined' && document.createElement && G.imagesLoaded; + if (canCache) { + const key = r.gx + ',' + r.gy + ':' + G.floor + ':' + r.shape + ':' + r.theme + ':' + hdScale(); + if (!G.wallLayer || G.wallLayerKey !== key) { + const rs = hdScale(); + const layer = document.createElement('canvas'); + layer.width = (a.x1 + WALL) * rs; + layer.height = (a.y1 + WALL) * rs; + const lctx = layer.getContext('2d'); + lctx.setTransform(rs, 0, 0, rs, 0, 0); + lctx.imageSmoothingEnabled = !G.hdRemaster; + drawWallBands(lctx, r); + G.wallLayer = layer; + G.wallLayerKey = key; + } + ctx.drawImage(G.wallLayer, 0, 0, G.wallLayer.width / hdScale(), G.wallLayer.height / hdScale()); + } else { + drawWallBands(ctx, r); } - ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 3; - ctx.strokeRect(a.x0, a.y0, a.w, a.h); for (const [dir, has] of Object.entries(r.doors)) { if (!has) continue; diff --git a/js/sfxbank.js b/js/sfxbank.js index a0c8541..1b7472c 100644 --- a/js/sfxbank.js +++ b/js/sfxbank.js @@ -111,7 +111,7 @@ const SfxBank = { if (this.buffers.has(name) || this.pending.has(name) || this.failed.has(name)) return; const file = this.FILES[name]; if (!file || !this.ctx || typeof fetch !== 'function') return; - const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=56') + const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=57') .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); }) .then(bytes => this.ctx.decodeAudioData(bytes)) .then(buffer => this.buffers.set(name, this.trim(buffer))) diff --git a/js/sprites.js b/js/sprites.js index 7631cac..43a796a 100644 --- a/js/sprites.js +++ b/js/sprites.js @@ -88,10 +88,10 @@ const Sprites = { const finish = () => { if (--pending === 0) G.imagesLoaded = true; }; img.onload = () => { this.imgs[name] = img; if (typeof img.decode === 'function') img.decode().then(finish, finish); else finish(); }; img.onerror = () => { - if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=56'; return; } + if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=57'; return; } finish(); }; - img.src = hd ? 'assets/hd/' + name + '.webp?v=56' : 'assets/' + name + '.png?v=56'; + img.src = hd ? 'assets/hd/' + name + '.webp?v=57' : 'assets/' + name + '.png?v=57'; } }, @@ -161,6 +161,11 @@ const Sprites = { draw(ctx, name, x, y, rot, targetW, flash, alpha, scaleX, scaleY) { const img = this.imgs[name]; const tw = targetW || 32; + if (img && !rot && scaleX === undefined && scaleY === undefined && alpha === undefined && !flash) { + const dh = img.height * (tw / img.width); + ctx.drawImage(img, x - tw / 2, y - dh / 2, tw, dh); + return; + } ctx.save(); ctx.translate(x, y); if (rot) ctx.rotate(rot); diff --git a/package.json b/package.json index 6e13f3e..d322791 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "meatslicer", "productName": "MeatSlicer", - "version": "0.9.0", + "version": "0.9.1", "private": true, "description": "A Butcher's Descent - clear, harvest, descend.", "main": "desktop/main.js", diff --git a/test/smoke.js b/test/smoke.js index 0a76e33..c33627d 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -216,7 +216,7 @@ 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=56' && urls[1] === 'assets/' + name + '.png?v=56'; + const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=57' && urls[1] === 'assets/' + name + '.png?v=57'; if (ok) hdFallbackOk++; } check('HD loader tries WebP then PNG per sprite', hdFallbackOk === ctx.SPRITE_MANIFEST.length); @@ -241,7 +241,7 @@ 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=56'; + const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=57'; if (ok) sdUrlOk++; } check('SD loader requests one PNG per sprite', sdUrlOk === ctx.SPRITE_MANIFEST.length);