diff --git a/html/js/render/asciiRenderer.js b/html/js/render/asciiRenderer.js index 62e107d..c08869d 100644 --- a/html/js/render/asciiRenderer.js +++ b/html/js/render/asciiRenderer.js @@ -16,7 +16,8 @@ AsciiRenderer.prototype.render = function (scene) { if (!scene) return; - var cell = this.cell, ctx = this.ctx; + this.lastScene = scene; + var cell = this.cell, ctx = this.ctx, now = Date.now(), hasTorch = false; this.canvas.width = scene.cols * cell; this.canvas.height = scene.rows * cell; @@ -38,14 +39,33 @@ ctx.fillText(walk ? '·' : '#', x * cell + cell / 2, y * cell + cell / 2); } else { var def = scene.legend[scene.grid[y][x]] || { char: '?', color: '#555' }; - ctx.fillStyle = 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]; - if (b < 1) ctx.globalAlpha = b; + 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 + // gently flickers; the glyph itself stays at full brightness (never dimmed). + hasTorch = true; + var seed = x * 7 + y * 13; + var fl = 0.7 + Math.sin(now / 170 + seed) * 0.3; + var rad = cell * (1.9 + Math.sin(now / 240 + seed) * 0.18); + var g = ctx.createRadialGradient(gx, gy, 0, gx, gy, rad); + g.addColorStop(0, 'rgba(255,196,96,' + (0.6 * fl) + ')'); + g.addColorStop(0.45, 'rgba(255,140,44,' + (0.24 * fl) + ')'); + g.addColorStop(1, 'rgba(255,120,30,0)'); + ctx.globalAlpha = 1; + ctx.fillStyle = g; + ctx.fillRect(gx - rad, gy - rad, rad * 2, rad * 2); + ctx.fillStyle = '#ffd9a0'; + ctx.fillText(def.char, gx, gy); + } else { + ctx.fillStyle = 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]; + if (b < 1) ctx.globalAlpha = b; + } + ctx.fillText(def.char, gx, gy); + ctx.globalAlpha = 1; } - ctx.fillText(def.char, x * cell + cell / 2, y * cell + cell / 2); - ctx.globalAlpha = 1; } } } @@ -95,9 +115,29 @@ break; } } + + // Keep a light RAF alive so the torch glows flicker between game updates (~22fps). + this._hasTorches = hasTorch; + if (hasTorch) this._scheduleFlicker(); + }; + + // Re-render on a throttled RAF so torch glows breathe even when the game isn't updating. + AsciiRenderer.prototype._scheduleFlicker = function () { + if (this._flickerRaf || !this._hasTorches || !this.ctx) return; + var self = this; + this._flickerRaf = requestAnimationFrame(function () { + self._flickerRaf = null; + if (!self.ctx || !self._hasTorches || !self.lastScene) return; + var t = Date.now(); + if (self._lastFlick && t - self._lastFlick < 45) { self._scheduleFlicker(); return; } + self._lastFlick = t; + self.render(self.lastScene); + }); }; AsciiRenderer.prototype.destroy = function () { + this._hasTorches = false; + if (this._flickerRaf) { cancelAnimationFrame(this._flickerRaf); this._flickerRaf = null; } if (this.canvas && this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); this.canvas = null; this.ctx = null; diff --git a/html/js/render/isoRenderer.js b/html/js/render/isoRenderer.js index a5a74c8..a0a5d05 100644 --- a/html/js/render/isoRenderer.js +++ b/html/js/render/isoRenderer.js @@ -73,6 +73,17 @@ this.ctx.drawImage(img, cx - w / 2, baseY - h, w, h); }; + // 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) { + var ctx = this.ctx, hw = this.tileW / 2, hh = this.tileH / 2; + ctx.beginPath(); + ctx.moveTo(cx, cy - hh); ctx.lineTo(cx + hw, cy); ctx.lineTo(cx, cy + hh); ctx.lineTo(cx - hw, cy); + ctx.closePath(); + ctx.fillStyle = color; ctx.fill(); + ctx.strokeStyle = 'rgba(0,0,0,0.28)'; ctx.lineWidth = 1; ctx.stroke(); + }; + // 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; @@ -286,6 +297,7 @@ 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.kind !== 'dark') this._diamond(it.sx, it.sy + this.tileH * 0.5, (legend[it.kind] && legend[it.kind].color) || '#3a4048'); } else if (it.type === 'prop') { this._contactShadow(it.sx, it.sy + this.tileH * 1.4, 22, 9); rec = this._load(this._tileUrl(it.kind)); diff --git a/html/js/render/spGameRenderer.js b/html/js/render/spGameRenderer.js index 03cbbcb..ba29703 100644 --- a/html/js/render/spGameRenderer.js +++ b/html/js/render/spGameRenderer.js @@ -75,7 +75,23 @@ if (SP._renderer) return SP._renderer; var host = rkHost(); if (!host || !RK.createRenderer) return null; - SP._renderer = RK.createRenderer(SP.mode(), host, { cell: 24 }); + var mode = SP.mode(); + // 3D needs THREE, lazy-loaded from a CDN. The sync createRenderer() silently returns a Tiled + // renderer when THREE isn't loaded yet ("picked 3D, got original"). Kick off the load and + // re-mount as real 3D once it's ready; meanwhile we render Tiled so the screen isn't blank. + if (mode === '3d' && RK.ensureThree && RK.threeReady && !RK.threeReady()) { + if (!SP._threePending) { + SP._threePending = true; + RK.ensureThree(function (ok) { + SP._threePending = false; + if (ok && SP._live && SP.mode() === '3d') { + SP._destroyRenderer(); + if (SP._lastState) SP.render(SP._lastState, SP._lastOpts); + } + }); + } + } + SP._renderer = RK.createRenderer(mode, host, { cell: 24 }); // The camera owns the canvas transform (centre on the player), so position it absolutely // and let it overflow — do NOT use RK.attachZoom (it fights this transform). if (SP._renderer && SP._renderer.canvas) {