feat(render): composite object tiles over a base tile (pipeline) + bigger cat

The black boxes around furniture/windows were transparent object tiles drawn
straight onto the dark canvas. Fix at the pipeline level, not as a tavern one-off:

- tileRenderer: a tile kind whose legend declares `over` (the ground it sits on)
  now draws that base tile FIRST, then the object on top — so the object's
  transparent areas show the floor/wall. Works for any scene, atlas or flat-colour
  fallback. Wall-grounding shadow strip now only applies to real walls, not objects.
- sceneModel TAVERN_LEGEND: declares over='floor' for bar/keg/shelf/table/chair/
  barrel/crate/rug and over='wall' for windows.
- catSprites: the cat occupies ~40% of its 50x50 frame, so scale to ~2x the cell
  and centre it — it now reads about tile-sized instead of tiny.

Client-only. Suite 57 suites / 413 tests green.
This commit is contained in:
jwinterm
2026-07-11 07:07:55 -04:00
parent be34bd2b2d
commit cdd379c5e4
3 changed files with 32 additions and 14 deletions
+4 -2
View File
@@ -27,9 +27,11 @@
if (!im.complete || !im.naturalWidth) return false;
var frame = Math.floor((now || 0) / FRAME_MS) % FRAMES;
var sx = frame * FW;
var size = cell * 1.25; // a touch bigger than a tile
// The cat occupies only ~40% of its 50x50 frame (lots of transparent padding), so scale
// the frame to ~2x the cell and centre it — the visible cat then reads about tile-sized.
var size = cell * 2.0;
var dx = e.x * cell + (cell - size) / 2;
var dy = e.y * cell + cell - size + cell * 0.12;
var dy = e.y * cell + (cell - size) / 2 + cell * 0.2; // centred, nudged down onto the tile
ctx.save();
ctx.imageSmoothingEnabled = false;
if (e.facing === 'left') {
+13 -10
View File
@@ -7,19 +7,22 @@
'use strict';
// Tile kinds and how each renderer treats them (glyph for ASCII, colour for tiled/fancy).
// `over` names the ground tile an object sits on, so the renderer composites the object over it
// (its transparent areas show the floor/wall, not the dark canvas). Ground tiles (wall/floor/rug)
// have no `over` — they fill the cell.
var TAVERN_LEGEND = {
wall: { char: '#', color: '#3a3f4b', solid: true },
window: { char: 'W', color: '#4a6a8a', solid: true },
window: { char: 'W', color: '#4a6a8a', solid: true, over: 'wall' },
floor: { char: '·', color: '#6b4d33', solid: false }, // warm wood, not the old dark green
rug: { char: '=', color: '#7a3b39', solid: false },
bar: { char: '=', color: '#6a4a2a', solid: true },
keg: { char: 'k', color: '#5a4028', solid: true },
shelf: { char: 'h', color: '#4a3a28', solid: true },
table: { char: 'T', color: '#6a4a2a', solid: true },
chair: { char: 'c', color: '#5a4028', solid: false }, // walkable — stand on it to "sit"
barrel: { char: 'B', color: '#5a4028', solid: true },
crate: { char: 'C', color: '#7a5a38', solid: true },
door: { char: 'D', color: '#3fb950', solid: false }
rug: { char: 'r', color: '#7a3b39', solid: false, over: 'floor' },
bar: { char: '=', color: '#6a4a2a', solid: true, over: 'floor' },
keg: { char: 'k', color: '#5a4028', solid: true, over: 'floor' },
shelf: { char: 'h', color: '#4a3a28', solid: true, over: 'floor' },
table: { char: 'T', color: '#6a4a2a', solid: true, over: 'floor' },
chair: { char: 'c', color: '#5a4028', solid: false, over: 'floor' }, // walkable — stand on it to "sit"
barrel: { char: 'B', color: '#5a4028', solid: true, over: 'floor' },
crate: { char: 'C', color: '#7a5a38', solid: true, over: 'floor' },
door: { char: 'D', color: '#3fb950', solid: false, over: 'floor' }
};
// Layout character -> tile kind.
+15 -2
View File
@@ -107,9 +107,18 @@
var def = scene.legend[kind] || { color: '#333' };
var coord = useAtlas ? pickVariant(tmap[kind], x, y) : null;
if (coord) {
// Object tiles (furniture, windows, torches, treasure…) are transparent
// around the object. Legend `over` names the ground they sit on (floor/wall);
// draw that first so the transparency shows the room, not the dark canvas.
// Pipeline-level: any scene can declare it, not a tavern one-off.
if (def.over && tmap[def.over]) {
var base = pickVariant(tmap[def.over], x, y);
if (base) atlas.draw(ctx, base[0], base[1], px, py, cell);
}
atlas.draw(ctx, coord[0], coord[1], px, py, cell);
// Dark bottom-edge strip grounds a solid wall against the floor below.
if (def.solid) {
// Dark bottom-edge strip grounds a solid WALL against the floor below (not
// objects, which already sit on their own base tile).
if (def.solid && !def.over) {
var strip = Math.max(2, cell * 0.16);
var sg = ctx.createLinearGradient(0, py + cell - strip, 0, py + cell);
sg.addColorStop(0, 'rgba(0,0,0,0)');
@@ -121,6 +130,10 @@
continue;
}
// Flat-colour fallback for any unmapped kind (or atlas not yet loaded).
if (def.over && scene.legend[def.over]) {
ctx.fillStyle = scene.legend[def.over].color; // base colour under the object
ctx.fillRect(px, py, cell, cell);
}
ctx.fillStyle = def.color;
ctx.fillRect(px, py, cell, cell);
ctx.strokeStyle = 'rgba(0,0,0,0.25)';