diff --git a/html/js/render/isoRenderer.js b/html/js/render/isoRenderer.js
index db29f74..a87371b 100644
--- a/html/js/render/isoRenderer.js
+++ b/html/js/render/isoRenderer.js
@@ -73,6 +73,27 @@
this.ctx.drawImage(img, cx - w / 2, baseY - h, w, h);
};
+ // Map a dungeon FEATURE entity (entrance/exit/treasure/item) to a pack tile: stairs for the
+ // entrance/exit, a chest for treasure/items. Null if the pack has no fitting tile (→ glyph).
+ IsoRenderer.prototype._featureTileUrl = function (e) {
+ var tiles = this.assets.tiles || {};
+ var c = e.char;
+ if (c === '<' || c === '>') return tiles.stairs || null;
+ if (c === '$' || c === '$W' || c === '$M') return tiles.chestClosed || tiles.chest || null;
+ return null;
+ };
+
+ // Draw a text glyph centered at a projected cell (feature fallback when no pack tile fits).
+ IsoRenderer.prototype._glyph = function (cx, cy, ch, color) {
+ var ctx = this.ctx;
+ ctx.save();
+ ctx.font = 'bold ' + Math.round(this.tileH * 0.95) + "px monospace";
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = color || '#fff';
+ ctx.fillText(ch, cx, cy);
+ ctx.restore();
+ };
+
// A flat iso diamond in the legend colour — the placeholder a tile draws while its pack image
// loads (or if an image is missing), so the view shows the dungeon shape instead of going black.
IsoRenderer.prototype._diamond = function (cx, cy, color) {
@@ -325,12 +346,25 @@
RK.fx.fire(ctx, fcx, fgy, fscale, now, (it.x * 7 + it.y * 13) % 97);
}
} else if (it.type === 'entity') {
+ // Dungeon FEATURES/ITEMS (entrance/exit/treasure/items) are tiles/glyphs, NOT avatars.
+ // The iso renderer is shared with the tavern (where every entity is an avatar); without
+ // this branch the entrance was drawn as a male character — the "copy of the player at
+ // the start" the map origin showed.
+ if (it.e.kind === 'feature' || it.e.kind === 'item') {
+ var furl = this._featureTileUrl(it.e);
+ var frec = furl && this._load(furl);
+ if (frec && frec.ready) this._drawImage(frec.img, it.sx, it.sy + this.tileH, this.imageW, this.imageH);
+ else this._glyph(it.sx, it.sy + this.tileH * 0.4, it.e.char || '?', it.e.color || '#fff');
+ continue;
+ }
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) {
var ch = frame.character || this.assets.character || {};
- var tint = this._skinTintFor(frame.visual);
+ // Monsters use the same character art — tint them RED so they don't read as a
+ // second player following you around.
+ var tint = it.e.kind === 'monster' ? '#f85149' : this._skinTintFor(frame.visual);
this._drawSpriteImage(this._tintedImage(rec.img, tint), it.sx, it.sy + this.tileH * 0.95, ch.imageH || 92);
} else {
ctx.beginPath();
diff --git a/html/js/render/sceneModel.js b/html/js/render/sceneModel.js
index ce63972..cac54af 100644
--- a/html/js/render/sceneModel.js
+++ b/html/js/render/sceneModel.js
@@ -251,7 +251,9 @@ function sceneFromGameState(state, opts) {
var playerEntity = {
id: 'player', x: state.player.x, y: state.player.y,
kind: 'player', char: '@', color: '#9aa4b2',
- facing: state.player.facing || 'down', label: null,
+ // No bogus default: the SP server doesn't send facing, so leave it null and let the iso/3D
+ // renderers INFER facing from movement. A hardcoded 'down' made them always face down/SW.
+ facing: state.player.facing || null, label: null,
you: !state.isSpectating
};
// Attach the player's appearance if available (for render-kit character rendering).