diff --git a/html/js/render/asciiRenderer.js b/html/js/render/asciiRenderer.js
index c08869d..7145acd 100644
--- a/html/js/render/asciiRenderer.js
+++ b/html/js/render/asciiRenderer.js
@@ -38,7 +38,8 @@
ctx.fillStyle = walk ? '#2f4030' : '#39404c';
ctx.fillText(walk ? '·' : '#', x * cell + cell / 2, y * cell + cell / 2);
} else {
- var def = scene.legend[scene.grid[y][x]] || { char: '?', color: '#555' };
+ var kind = scene.grid[y][x];
+ var def = scene.legend[kind] || { char: '?', color: '#555' };
var gx = x * cell + cell / 2, gy = y * cell + cell / 2;
if (def.fx === 'fire') {
// Torches are ACTUAL light sources: a warm radial glow behind the glyph that
@@ -57,7 +58,12 @@
ctx.fillStyle = '#ffd9a0';
ctx.fillText(def.char, gx, gy);
} else {
- ctx.fillStyle = def.color;
+ // Brighter glyph colours for readability: the dungeon legend colours are
+ // near-black (tuned for TILE fog, not glyphs), which made ASCII walls/floors
+ // almost invisible. Features (entrance/exit/treasure) keep their own colours.
+ ctx.fillStyle = kind === 'wall' ? '#8b95a6'
+ : (kind === 'floor' || kind === 'floor2') ? '#5b6472'
+ : def.color;
// Dungeon lighting: dim tiles in shadow.
if (scene.lightGrid && scene.lightGrid[y] && scene.lightGrid[y][x] != null) {
var b = scene.lightGrid[y][x];
diff --git a/html/js/render/isoRenderer.js b/html/js/render/isoRenderer.js
index a0a5d05..db29f74 100644
--- a/html/js/render/isoRenderer.js
+++ b/html/js/render/isoRenderer.js
@@ -231,10 +231,18 @@
var visual = this._visualFor(e);
if (visual && visual.allowed === false) return null;
var dirs = this.assets.directions || {};
- var facing = (e && e.facing) || 'down';
var key = e.id || 'anon';
- var st = this.last[key] || (this.last[key] = { x: e.x, y: e.y, t: 0 });
- if (st.x !== e.x || st.y !== e.y) { st.x = e.x; st.y = e.y; st.t = now; }
+ var st = this.last[key] || (this.last[key] = { x: e.x, y: e.y, t: 0, facing: 'down' });
+ if (st.x !== e.x || st.y !== e.y) {
+ // Infer facing from the actual movement delta — the SP game doesn't send player.facing
+ // (only the tavern does), so without this the character always faced 'down' (SW) no matter
+ // which way it moved. Explicit e.facing (tavern) still wins below.
+ var ddx = e.x - st.x, ddy = e.y - st.y;
+ if (Math.abs(ddx) >= Math.abs(ddy)) st.facing = ddx > 0 ? 'right' : 'left';
+ else st.facing = ddy > 0 ? 'down' : 'up';
+ st.x = e.x; st.y = e.y; st.t = now;
+ }
+ var facing = (e && e.facing) || st.facing || 'down';
var moving = (now - st.t) < 360;
var ch = dirs[facing] || (visual && visual.character) || (this.assets.character || {});
if (!moving || !ch.run || ch.run.length === 0) return { url: ch.idle, character: ch, visual: visual };
diff --git a/html/js/render/renderModes.js b/html/js/render/renderModes.js
index 1bdff4c..0466d99 100644
--- a/html/js/render/renderModes.js
+++ b/html/js/render/renderModes.js
@@ -51,7 +51,13 @@
if (!RK.canUseMode(mode)) return new RK.TileRenderer(host, opts);
if (mode === 'ascii' && RK.AsciiRenderer) return new RK.AsciiRenderer(host, opts);
if (mode === 'iso' && RK.IsoRenderer) return new RK.IsoRenderer(host, opts);
- if (mode === '3d' && RK.ThreeRenderer && RK.THREE) return new RK.ThreeRenderer(host, opts);
+ if (mode === '3d' && RK.ThreeRenderer && RK.THREE) {
+ try { return new RK.ThreeRenderer(host, opts); }
+ catch (e) {
+ if (root.console) console.warn('3D renderer failed; using tiled:', e && e.message);
+ return new RK.TileRenderer(host, opts);
+ }
+ }
if (mode === 'fancy') {
try {
if (!RK.FancyRenderer) throw new Error('fancy renderer not loaded');
diff --git a/html/js/render/threeRenderer.js b/html/js/render/threeRenderer.js
index 276c970..c0cd5e7 100644
--- a/html/js/render/threeRenderer.js
+++ b/html/js/render/threeRenderer.js
@@ -260,6 +260,7 @@
ThreeRenderer.prototype._animate = function () {
if (!this.renderer) return;
+ try {
var dt = this.clock.getDelta();
var now = Date.now();
for (var i = 0; i < this.mixers.length; i++) this.mixers[i].update(dt);
@@ -280,12 +281,16 @@
o._model.rotation.z = moving ? Math.sin(now / 120) * 0.045 : 0;
}
this._playAction(o, moving ? 'run' : 'idle');
- if (ent.e && ent.e.facing) {
- var r = ent.e.facing === 'up' ? Math.PI : ent.e.facing === 'left' ? -Math.PI / 2 : ent.e.facing === 'right' ? Math.PI / 2 : 0;
+ // Face the way you move. Prefer explicit facing (tavern); else infer from the world delta
+ // (the SP game doesn't send player.facing). world x=grid x, world z=grid y.
+ var face = ent.e && ent.e.facing;
+ if (!face && moving) { face = Math.abs(dx) >= Math.abs(dz) ? (dx > 0 ? 'right' : 'left') : (dz > 0 ? 'down' : 'up'); ent._face = face; }
+ face = face || ent._face;
+ if (face) {
+ var r = face === 'up' ? Math.PI : face === 'left' ? -Math.PI / 2 : face === 'right' ? Math.PI / 2 : 0;
o.rotation.y += (r - o.rotation.y) * 0.18;
}
}
-
// Player-follow camera: keep the fixed iso offset (8,9,8) but re-target the player's world
// position each frame. The player mesh itself lerps, so the camera glides smoothly with it.
this._fitToHost(); // pick up any host resize
@@ -299,6 +304,11 @@
this.camera.lookAt(pcam.position.x, 0, pcam.position.z);
}
this.renderer.render(this.scene, this.camera);
+ } catch (err) {
+ if (root.console) console.warn('3D animate error; stopping 3D loop:', err && err.message);
+ if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
+ return;
+ }
};
ThreeRenderer.prototype.destroy = function () {