feat(iso): orientation-aware walls + floor variety + juice (shadows/vignette)

Fixes the "thin disconnected slabs" on the top-left/bottom-right walls and juices
the iso scene:
- Wall facing: iso walls have an orientation. A run along grid-x uses the _S
  rotation, a run along grid-y uses the perpendicular _W (I viewed the tiles:
  _S faces SW, _W faces SE — the two camera-facing rotations), and true corners
  use the corner tile. The renderer picks per-cell from neighbours. Enclosing
  walls now read as continuous faces on all four edges. Same logic gives
  windows/doors a Y rotation.
- Floor variety: deterministic per-cell hash sprinkles planksBroken into wood
  floors and stoneTile/stoneUneven into stone — no more flat identical sea.
- Juice: soft contact shadows ground every prop + character, and a warm radial
  vignette lights the room instead of it looking pasted onto black.
- Tavern map: barrels/crates line the side walls (clear of windows/spawn, via
  the wall-safe put()) so the lobby reads lived-in in every render mode.

Suite 58/419 green.
This commit is contained in:
jwinterm
2026-07-11 15:11:26 -04:00
parent be8594282b
commit 4d673748df
15 changed files with 99 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+18 -5
View File
@@ -293,11 +293,24 @@
floor2: 'assets/kenney/iso-dungeon/tiles/stone_S.png', // stone — dungeon
dirt: 'assets/kenney/iso-dungeon/tiles/dirt_S.png',
rug: 'assets/kenney/iso-dungeon/tiles/planks_S.png',
// Architecture (full-height tiles). The rich set has real windows/doors/archways.
wall: 'assets/kenney/iso-dungeon/tiles/stoneWall_S.png',
window: 'assets/kenney/iso-dungeon/tiles/stoneWallWindow_S.png',
door: 'assets/kenney/iso-dungeon/tiles/stoneWallDoorOpen_S.png',
archway:'assets/kenney/iso-dungeon/tiles/stoneWallArchway_S.png',
// Architecture (full-height tiles). Iso walls have a facing: a wall running along the
// grid-x axis needs the _S rotation, one running along grid-y needs the perpendicular
// _W rotation, and true corners get the corner tile. The renderer picks per-cell from
// neighbours, choosing `<kind>Y` for y-running segments (that's what fixes the "thin
// slabs" look on the top-left / bottom-right edges).
wall: 'assets/kenney/iso-dungeon/tiles/stoneWall_S.png',
wallY: 'assets/kenney/iso-dungeon/tiles/stoneWall_W.png',
wallCorner: 'assets/kenney/iso-dungeon/tiles/stoneWallCorner_S.png',
window: 'assets/kenney/iso-dungeon/tiles/stoneWallWindow_S.png',
windowY: 'assets/kenney/iso-dungeon/tiles/stoneWallWindow_W.png',
door: 'assets/kenney/iso-dungeon/tiles/stoneWallDoorOpen_S.png',
doorY: 'assets/kenney/iso-dungeon/tiles/stoneWallDoorOpen_W.png',
archway: 'assets/kenney/iso-dungeon/tiles/stoneWallArchway_S.png',
// Floor variety — the renderer sprinkles these deterministically so the ground isn't a
// flat sea of identical planks.
floorAlt: 'assets/kenney/iso-dungeon/tiles/planksBroken_S.png',
floor2Alt: 'assets/kenney/iso-dungeon/tiles/stoneUneven_S.png',
stoneTile: 'assets/kenney/iso-dungeon/tiles/stoneTile_S.png',
column: 'assets/kenney/iso-dungeon/tiles/stoneColumnWood_S.png',
stairs: 'assets/kenney/iso-dungeon/tiles/stairs_S.png',
// Furniture props (drawn over a floor base) — each kind now has distinct art.
+71
View File
@@ -73,6 +73,34 @@
this.ctx.drawImage(img, cx - w / 2, baseY - h, w, h);
};
// Soft contact shadow grounds props/characters so they don't float on the floor.
IsoRenderer.prototype._contactShadow = function (cx, cy, rx, ry) {
var ctx = this.ctx;
ctx.save();
ctx.globalAlpha = 0.28;
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
};
// Warm radial vignette — an ambient tavern glow that darkens the edges and lifts the centre,
// the "juice" that makes the flat canvas feel lit rather than pasted.
IsoRenderer.prototype._vignette = function () {
var ctx = this.ctx, w = this.canvas.width, h = this.canvas.height;
var g = ctx.createRadialGradient(w / 2, h * 0.46, Math.min(w, h) * 0.18,
w / 2, h * 0.46, Math.max(w, h) * 0.62);
g.addColorStop(0, 'rgba(255, 214, 150, 0.10)');
g.addColorStop(0.55, 'rgba(0, 0, 0, 0)');
g.addColorStop(1, 'rgba(0, 0, 0, 0.42)');
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
ctx.restore();
};
IsoRenderer.prototype._imageBounds = function (img) {
if (!img) return null;
var key = img.src || img._rkBoundsKey || null;
@@ -141,6 +169,44 @@
return tiles[kind] || tiles.fallback || tiles.floor;
};
// Deterministic per-cell hash (no RNG — stable across frames/reloads).
function cellHash(x, y) {
var h = (x * 73856093) ^ (y * 19349663);
h = (h ^ (h >>> 13)) >>> 0;
return h / 4294967295;
}
// Wall-family kinds share edge-run detection so a window/door in a wall counts as wall.
var WALLISH = { wall: 1, window: 1, door: 1, archway: 1 };
IsoRenderer.prototype._isWall = function (scene, x, y) {
if (x < 0 || y < 0 || y >= scene.rows || x >= scene.cols) return false;
return !!WALLISH[scene.grid[y][x]];
};
// Iso walls have a facing. A segment running along grid-x uses the base (_S) rotation; one
// running along grid-y uses the perpendicular (`<kind>Y`, _W) rotation; a true corner (both
// axes present) uses the corner tile. This is what makes enclosing walls read as continuous
// faces instead of thin disconnected slabs on the y-running edges.
IsoRenderer.prototype._wallVariant = function (scene, x, y, kind) {
var tiles = this.assets.tiles || {};
var xRun = this._isWall(scene, x - 1, y) || this._isWall(scene, x + 1, y);
var yRun = this._isWall(scene, x, y - 1) || this._isWall(scene, x, y + 1);
if (xRun && yRun && kind === 'wall' && tiles.wallCorner) return 'wallCorner';
if (yRun && !xRun && tiles[kind + 'Y']) return kind + 'Y';
return kind;
};
// Sprinkle floor variants deterministically so the ground isn't a flat identical sea.
IsoRenderer.prototype._floorVariant = function (kind, x, y) {
var tiles = this.assets.tiles || {};
var r = cellHash(x, y);
if (kind === 'floor' && tiles.floorAlt && r > 0.86) return 'floorAlt';
if (kind === 'floor2' && tiles.stoneTile && r > 0.6) return 'stoneTile';
if (kind === 'floor2' && tiles.floor2Alt && r > 0.82) return 'floor2Alt';
return kind;
};
IsoRenderer.prototype._visualFor = function (e) {
var appearance = (e && e.appearance) || { avatar: (e && e.avatar) || 'default' };
if (root.RK && RK.avatarVisuals && RK.avatarVisuals.resolve) {
@@ -207,12 +273,16 @@
var it = items[i];
if (it.type === 'tile') {
var tileKind = PROP[it.kind] ? 'floor' : it.kind;
if (WALLISH[tileKind]) tileKind = this._wallVariant(scene, it.x, it.y, tileKind);
else tileKind = this._floorVariant(tileKind, it.x, it.y);
var rec = this._load(this._tileUrl(tileKind));
if (rec && rec.ready) this._drawImage(rec.img, it.sx, it.sy + this.tileH, this.imageW, this.imageH);
} else if (it.type === 'prop') {
this._contactShadow(it.sx, it.sy + this.tileH * 1.4, 22, 9);
rec = this._load(this._tileUrl(it.kind));
if (rec && rec.ready) this._drawImage(rec.img, it.sx, it.sy + this.tileH, this.imageW, this.imageH);
} else if (it.type === 'entity') {
this._contactShadow(it.sx, it.sy + this.tileH * 1.15, 18, 7);
var frame = this._charFrame(it.e, now);
rec = this._load(frame && frame.url);
if (rec && rec.ready) {
@@ -240,6 +310,7 @@
}
}
}
this._vignette();
if (this._animating) this._invalidate();
};
+10
View File
@@ -51,6 +51,16 @@ function buildDefaultTavern(cols = 28, rows = 15) {
// Corner decor — barrels and crates.
put(1, 1, 'B'); put(cols - 2, 1, 'C'); put(1, rows - 2, 'C'); put(cols - 2, rows - 2, 'B');
// Barrels & crates lining the side walls (one in from the wall, clear of the windows/spawn) so
// the room reads lived-in instead of a big empty floor. Alternating B/C for material variety.
const wallDecor = [
[1, 5, 'B'], [1, 9, 'C'], [1, 12, 'B'],
[cols - 2, 5, 'C'], [cols - 2, 9, 'B'], [cols - 2, 12, 'C'],
[2, rows - 2, 'B'], [cols - 3, rows - 2, 'C'],
[barX0 - 1, barY + 1, 'B'], [barX1 + 1, barY + 1, 'C'] // a barrel/crate tucked beside each keg
];
for (const [dx, dy, dch] of wallDecor) put(dx, dy, dch);
// Door + spawn pad at the bottom-centre (two spawn tiles for deterministic adjacency tests).
const mid = Math.floor(cols / 2);
set(mid, rows - 1, 'D');