fix(render): iso/3d face movement dir; 3d crash-proof; brighter ASCII walls

- 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).
This commit is contained in:
jwinterm
2026-07-14 14:24:51 -04:00
parent 11732531d2
commit 263bfb1cf4
4 changed files with 39 additions and 9 deletions
+8 -2
View File
@@ -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];
+11 -3
View File
@@ -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 };
+7 -1
View File
@@ -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');
+13 -3
View File
@@ -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 () {