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.
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||