diff --git a/html/js/core/game.js b/html/js/core/game.js
index 84fb646..07d6e7d 100644
--- a/html/js/core/game.js
+++ b/html/js/core/game.js
@@ -288,17 +288,27 @@ var Game = {
},
_drawGameScreen: function() {
- // The render-kit dungeon path (RK.SPGame) is DISABLED until it has a player-centered camera
- // + real monster/entity sprites — without those it rendered the whole level tiny in a corner
- // with a red-dot monster. Until then the single-player game uses the proven legacy renderer.
- if (window.RK && RK.SPGame && RK.SPGame.hide) RK.SPGame.hide();
- if (!this._ensureDisplay()) return;
const gameState = GameState.getGameStateForRender();
- RenderEngine.drawGameScreen(gameState);
- // JUICE: keep the FX overlay aligned to the base canvas and feed it the player's screen-space
- // position so its torch lighting tracks the camera.
- if (window.FX) {
- try { this._fxSyncLighting(); } catch (e) { /* best-effort */ }
+ // Render the dungeon through the render kit (Tiled / ASCII / Iso / 3D + unlocked packs) with
+ // a player-centered camera + goblin monster sprite. The legacy RenderEngine remains a HARD
+ // fallback (used only if the kit is unavailable or a render throws), so the game never blanks.
+ var rk = false;
+ if (window.RK && RK.SPGame && RK.SPGame.available()) {
+ RK.SPGame.show();
+ rk = RK.SPGame.render(gameState, {
+ cryptoType: (window.ScreenManager && ScreenManager._cryptoType) || undefined,
+ playerAppearance: (window.SinglePlayerAvatar && SinglePlayerAvatar.currentAppearance)
+ ? SinglePlayerAvatar.currentAppearance() : null,
+ isSpectating: this._isSpectating
+ });
+ }
+ if (!rk) {
+ if (window.RK && RK.SPGame) RK.SPGame.hide();
+ if (!this._ensureDisplay()) return;
+ RenderEngine.drawGameScreen(gameState);
+ if (window.FX) {
+ try { this._fxSyncLighting(); } catch (e) { /* best-effort */ }
+ }
}
},
diff --git a/html/js/render/isoRenderer.js b/html/js/render/isoRenderer.js
index 392a752..2392be0 100644
--- a/html/js/render/isoRenderer.js
+++ b/html/js/render/isoRenderer.js
@@ -268,10 +268,12 @@
}
}
}
+ this.focusPoint = null;
for (var i = 0; i < scene.entities.length; i++) {
var e = scene.entities[i];
p = this._project(e.x, e.y, originX, originY);
items.push({ type: 'entity', e: e, sx: p.x, sy: p.y, depth: e.x + e.y + 0.55 });
+ if (e.you || e.kind === 'player') this.focusPoint = { x: p.x, y: p.y + this.tileH }; // SP camera target
}
items.sort(function (a, b) { return a.depth === b.depth ? (a.y || 0) - (b.y || 0) : a.depth - b.depth; });
diff --git a/html/js/render/spGameRenderer.js b/html/js/render/spGameRenderer.js
index e255736..7f47456 100644
--- a/html/js/render/spGameRenderer.js
+++ b/html/js/render/spGameRenderer.js
@@ -16,7 +16,9 @@
function doc() { return root.document; }
function gameDisplay() { return doc() && doc().getElementById('game-display'); }
- // A dedicated child host so the RK canvas doesn't collide with the ROT canvas / overlays.
+ // A dedicated, SELF-SIZING child host so the RK canvas doesn't collide with the ROT canvas and
+ // doesn't depend on the ROT canvas for its dimensions (it's an aspect-ratio viewport the camera
+ // transform is clipped to).
function rkHost() {
var gd = gameDisplay();
if (!gd) return null;
@@ -24,7 +26,9 @@
if (!host) {
host = doc().createElement('div');
host.id = 'rk-game-host';
- host.style.display = 'none';
+ host.style.cssText = 'display:none; position:relative; width:100%; max-width:900px;' +
+ ' aspect-ratio:16/10; margin:0 auto; overflow:hidden; background:#0a0c0f;' +
+ ' border:1px solid #2a313a; border-radius:6px; touch-action:none; cursor:grab;';
gd.appendChild(host);
}
return host;
@@ -67,10 +71,39 @@
var host = rkHost();
if (!host || !RK.createRenderer) return null;
SP._renderer = RK.createRenderer(SP.mode(), host, { cell: 24 });
- if (RK.attachZoom) { try { RK.attachZoom(host); } catch (_) {} }
+ // 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) {
+ var c = SP._renderer.canvas;
+ c.style.position = 'absolute'; c.style.top = '0'; c.style.left = '0';
+ c.style.maxWidth = 'none'; c.style.imageRendering = 'pixelated';
+ }
+ if (!host._rkZoomBound) {
+ host._rkZoomBound = true;
+ host.addEventListener('wheel', function (ev) {
+ ev.preventDefault();
+ SP._zoom = Math.max(0.6, Math.min(4, (SP._zoom || 1.7) * Math.exp(-ev.deltaY * 0.0015)));
+ SP._applyCamera();
+ }, { passive: false });
+ }
return SP._renderer;
};
+ // Centre the (whole-scene) canvas on the player via a CSS transform, clipped to the host —
+ // renderer-agnostic (works for tiled / iso / 3d, each of which reports its own focusPoint).
+ SP._applyCamera = function () {
+ var r = SP._renderer;
+ var host = doc() && doc().getElementById('rk-game-host');
+ if (!r || !r.canvas || !host) return;
+ var scale = SP._zoom || 1.7;
+ var fp = r.focusPoint;
+ r.canvas.style.transformOrigin = '0 0';
+ if (!fp) { r.canvas.style.transform = 'scale(' + scale + ')'; return; }
+ var w = host.clientWidth || 640, h = host.clientHeight || 400;
+ var tx = w / 2 - fp.x * scale, ty = h / 2 - fp.y * scale;
+ r.canvas.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + scale + ')';
+ };
+
function toggleLegacy(hide) {
var gd = gameDisplay();
if (!gd) return;
@@ -117,6 +150,7 @@
isSpectating: opts.isSpectating
});
r.render(scene);
+ SP._applyCamera();
return true;
} catch (e) {
if (root.console) console.warn('SPGame render failed; falling back to legacy:', e && e.message);
diff --git a/html/js/render/tileRenderer.js b/html/js/render/tileRenderer.js
index 436c25a..ccf9574 100644
--- a/html/js/render/tileRenderer.js
+++ b/html/js/render/tileRenderer.js
@@ -203,10 +203,17 @@
}
continue;
}
- // Dungeon monster.
+ // Dungeon monster — draw the Goblin character sprite (same path the tavern draws
+ // characters through), falling back to a red orb only if the sprite is unavailable.
if (e.kind === 'monster') {
+ drawEntityShadow(ctx, e.x * cell + cell / 2, e.y * cell + cell * 0.9, cell * 0.32, cell * 0.13);
+ var mAvatar = e.avatar || 'char-goblin';
+ if (window.RK && RK.avatarVisuals && RK.avatarVisuals.drawTopdownWorld) {
+ var mv = RK.avatarVisuals.resolve({ avatar: mAvatar }, { projection: 'topdown', context: 'dungeon', entity: e });
+ if (RK.avatarVisuals.drawTopdownWorld(ctx, mv, e, { screenX: e.x, screenY: e.y, cell: cell }, { now: now })) continue;
+ }
+ if (window.RK && RK.drawCharCanvas && RK.drawCharCanvas(ctx, { x: e.x, y: e.y, avatar: mAvatar }, cell, now)) continue;
var mx = e.x * cell + cell / 2, my = e.y * cell + cell / 2, mr = cell * 0.34;
- drawEntityShadow(ctx, mx, e.y * cell + cell * 0.9, cell * 0.32, cell * 0.13);
ctx.beginPath();
ctx.arc(mx, my, mr, 0, Math.PI * 2);
ctx.fillStyle = e.color || '#f85149';
@@ -267,6 +274,10 @@
// Snapshot the base scene, then composite the torch light + vignette on top, and keep
// an animation loop running so the light flickers and embers drift between renders.
+ // Where the player is on this canvas — the SP camera centers the view on it.
+ var _fp = findPlayer(scene);
+ this.focusPoint = _fp ? { x: (_fp.x + 0.5) * cell, y: (_fp.y + 0.5) * cell } : null;
+
this._lastScene = scene;
this._collectEmitters(scene);
this._snapshot();
diff --git a/html/js/ui/spRenderPicker.js b/html/js/ui/spRenderPicker.js
index e1b063a..02205e9 100644
--- a/html/js/ui/spRenderPicker.js
+++ b/html/js/ui/spRenderPicker.js
@@ -30,9 +30,8 @@
doc().head.appendChild(s);
}
- // Disabled until the single-player render-kit path (RK.SPGame) has a camera + entity sprites.
- // Keeping the module wired so re-enabling is a one-line flip.
- var ENABLED = false;
+ // Re-enabled now that the SP render kit has a player-centered camera + goblin sprite.
+ var ENABLED = true;
function build() {
var h = host();