From 263bfb1cf45b9bf840775b0a64cf86f6fb9e11f0 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 14 Jul 2026 14:24:51 -0400 Subject: [PATCH] fix(render): iso/3d face movement dir; 3d crash-proof; brighter ASCII walls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Iso & 3D character faced the wrong way: the SP game never sends player.facing (only the tavern does) so it defaulted to down/SW. Both renderers now INFER facing from the movement delta they already track (explicit e.facing still wins for the tavern). - 3D could break the game: wrap ThreeRenderer construction (createRenderer) and the _animate RAF body in try/catch — a 3D failure now falls back to Tiled / stops the loop instead of throwing. - ASCII walls/floors were near-invisible (legend colours are near-black, tuned for tile fog): use brighter glyph colours for wall/floor in ASCII (features keep their own colours). --- html/js/render/asciiRenderer.js | 10 ++++++++-- html/js/render/isoRenderer.js | 14 +++++++++++--- html/js/render/renderModes.js | 8 +++++++- html/js/render/threeRenderer.js | 16 +++++++++++++--- 4 files changed, 39 insertions(+), 9 deletions(-) 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 () {