mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 22:58:32 +02:00
The migration that makes the pack ladder pay off IN the dungeon, not just the tavern.
The SP game now draws via RK (Tiled/ASCII/Iso/3D + unlocked packs) instead of the
legacy ROT display.
- Adapter: sceneFromGameState now consumes the CLIENT state shape (sparse {y:{x:v}}
tile maps, not just server arrays) via maxDims(). Verified + unit-tested.
- Bug fix: RK.sceneFromGameState was never actually exposed (only RK.scene.*), so
matchClient + tavern spectator were silently no-op-ing. Aliased onto RK.
- Bridge (spGameRenderer.js, RK.SPGame): mounts an RK renderer in a dedicated host
inside #game-display, toggles the legacy ROT canvases, zoom-enabled. Game._draw
GameScreen prefers it and FALLS BACK to RenderEngine on any failure, so the live
game can never blank. ROT still draws the splash/win/lose text screens.
- Picker (spRenderPicker.js): homepage mode + pack selector, gated by entitlements
(free = Tiled/Original + ASCII; ladder unlocks Iso/3D/packs), rebuilds on identity.
- index.html loads the render-kit renderers + the bridge/picker.
Bundled with the catalog ladder (migration 028): free Original bare pack, ladder
1/5/10/20/40/50. Deploying makes the free tavern bare (by design) AND the dungeon
render-kit-ed. v1 renders the whole explored level + zoom (player-centered camera is
the next polish). Suite 62/435 green; adapter smoke-verified.
238 lines
11 KiB
JavaScript
238 lines
11 KiB
JavaScript
// SinglePlayerAvatar stores the selected identity for the legacy single-player page and manages
|
|
// the overlay canvas used by old ROT.js screens. It delegates visual decisions to RK.avatarVisuals.
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
var DEFAULT_APPEARANCE = { avatar: 'default', tint: 'none', equipment: { body: 'none', head: 'none', shield: 'none', weapon: 'none' } };
|
|
|
|
function normalize(input) {
|
|
if (root.RK && root.RK.normalizeAppearance) return root.RK.normalizeAppearance(input || DEFAULT_APPEARANCE);
|
|
return DEFAULT_APPEARANCE;
|
|
}
|
|
|
|
function requestRedraw() {
|
|
if (root.Game && root.Game._gameActive && root.Game._drawGameScreen) {
|
|
root.Game._drawGameScreen();
|
|
} else if (root.ScreenManager && root.DisplayManager && root.DisplayManager.ensureDisplay && root.DisplayManager.ensureDisplay()) {
|
|
if (root.ScreenManager.isShowingWaitingScreen && root.ScreenManager.isShowingWaitingScreen() && root.ScreenManager.drawWaitingScreen) {
|
|
root.ScreenManager.drawWaitingScreen(true);
|
|
} else if (root.ScreenManager.drawWelcomeScreen) {
|
|
root.ScreenManager.drawWelcomeScreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
var SinglePlayerAvatar = {
|
|
_appearance: null,
|
|
_overlay: null,
|
|
_ctx: null,
|
|
_lastWorld: null,
|
|
_facing: 'down',
|
|
_loading: {},
|
|
_initialized: false,
|
|
|
|
init: function () {
|
|
if (this._initialized) return;
|
|
this._initialized = true;
|
|
var identity = root.RK && root.RK.loadIdentity ? root.RK.loadIdentity() : null;
|
|
this.setAppearance(identity && identity.appearance ? identity.appearance : DEFAULT_APPEARANCE, { save: false, redraw: false });
|
|
this._bindButton();
|
|
this.updateButton();
|
|
},
|
|
|
|
_bindButton: function () {
|
|
var self = this;
|
|
var btn = root.document && root.document.getElementById('characterButton');
|
|
if (!btn || btn._rkBound) return;
|
|
btn._rkBound = true;
|
|
btn.addEventListener('click', function () {
|
|
if (!root.RK || !root.RK.openCustomize) return;
|
|
root.RK.openCustomize(self._appearance || DEFAULT_APPEARANCE, function (appearance) {
|
|
var normalized = normalize(appearance);
|
|
self.setAppearance(normalized, { save: true, redraw: true });
|
|
if (root.socket && root.socket.emit) {
|
|
root.socket.emit('identity:update', { appearance: normalized });
|
|
}
|
|
}, { projection: 'topdown' });
|
|
});
|
|
},
|
|
|
|
setAppearance: function (appearance, opts) {
|
|
opts = opts || {};
|
|
this._appearance = normalize(appearance);
|
|
if (opts.save !== false && root.RK && root.RK.saveIdentity) root.RK.saveIdentity(this._appearance);
|
|
this.updateButton();
|
|
if (opts.redraw !== false) requestRedraw();
|
|
},
|
|
|
|
applyIdentity: function (data) {
|
|
if (!data) return;
|
|
if (data.entitlements && root.RK && root.RK.setEntitlementSnapshot) {
|
|
root.RK.setEntitlementSnapshot(data.entitlements);
|
|
if (root.RK.rebuildRenderPicker) root.RK.rebuildRenderPicker();
|
|
}
|
|
if (data.appearance) this.setAppearance(data.appearance, { save: true, redraw: true });
|
|
else this.updateButton();
|
|
},
|
|
|
|
applyEntitlements: function (data) {
|
|
if (data && root.RK && root.RK.setEntitlementSnapshot) {
|
|
root.RK.setEntitlementSnapshot(data);
|
|
this.updateButton();
|
|
}
|
|
},
|
|
|
|
updateButton: function () {
|
|
var btn = root.document && root.document.getElementById('characterButton');
|
|
var label = root.document && root.document.getElementById('characterButtonLabel');
|
|
if (!btn || !label) return;
|
|
if (!root.RK || !root.RK.appearance) {
|
|
label.textContent = 'CHARACTER';
|
|
return;
|
|
}
|
|
var info = root.RK.appearance(this._appearance || DEFAULT_APPEARANCE);
|
|
// Show the chosen character's name, but keep it clearly a customize control (the leading
|
|
// 🎭 + this title) so it doesn't read like a mode/role button.
|
|
label.textContent = (info && info.label ? info.label : 'CHARACTER').toUpperCase();
|
|
btn.title = 'Create / customize your character'
|
|
+ (info && info.label ? ' — currently ' + info.label : '')
|
|
+ (info && info.premium ? ' (premium)' : '');
|
|
},
|
|
|
|
_baseCanvas: function () {
|
|
if (!root.DisplayManager || !root.DisplayManager.getDisplay()) return null;
|
|
var display = root.DisplayManager.getDisplay();
|
|
return display && display.getContainer ? display.getContainer() : null;
|
|
},
|
|
|
|
_syncOverlay: function () {
|
|
var base = this._baseCanvas();
|
|
var host = root.document && root.document.getElementById('game-display');
|
|
if (!base || !host) return null;
|
|
|
|
if (!this._overlay) {
|
|
this._overlay = root.document.createElement('canvas');
|
|
this._overlay.className = 'single-player-avatar-overlay';
|
|
this._overlay.style.position = 'absolute';
|
|
this._overlay.style.pointerEvents = 'none';
|
|
this._overlay.style.zIndex = '6';
|
|
this._overlay.style.imageRendering = 'pixelated';
|
|
host.appendChild(this._overlay);
|
|
this._ctx = this._overlay.getContext('2d');
|
|
}
|
|
|
|
if (this._overlay.width !== base.width) this._overlay.width = base.width;
|
|
if (this._overlay.height !== base.height) this._overlay.height = base.height;
|
|
this._overlay.style.left = base.offsetLeft + 'px';
|
|
this._overlay.style.top = base.offsetTop + 'px';
|
|
this._overlay.style.width = base.offsetWidth + 'px';
|
|
this._overlay.style.height = base.offsetHeight + 'px';
|
|
this._overlay.style.display = 'block';
|
|
return this._ctx;
|
|
},
|
|
|
|
clearOverlay: function () {
|
|
if (this._ctx && this._overlay) this._ctx.clearRect(0, 0, this._overlay.width, this._overlay.height);
|
|
},
|
|
|
|
currentAppearance: function () {
|
|
return this._appearance || normalize(DEFAULT_APPEARANCE);
|
|
},
|
|
|
|
resolveVisual: function (opts) {
|
|
opts = opts || {};
|
|
if (!root.RK || !root.RK.avatarVisuals) {
|
|
return { appearance: this.currentAppearance(), kind: 'legacy', fallbackTile: opts.fallbackTile || '@' };
|
|
}
|
|
return root.RK.avatarVisuals.resolve(this.currentAppearance(), opts);
|
|
},
|
|
|
|
_ensureVisual: function (visual) {
|
|
if (!root.RK || !root.RK.avatarVisuals || !visual || !visual.canvas) return false;
|
|
return root.RK.avatarVisuals.ensureCanvasReady(visual, requestRedraw);
|
|
},
|
|
|
|
canDrawPlayer: function () {
|
|
return this._ensureVisual(this.resolveVisual({ projection: 'legacy-rot', context: 'dungeon-player' }));
|
|
},
|
|
|
|
_updateFacing: function (player) {
|
|
if (!player) return this._facing;
|
|
if (this._lastWorld) {
|
|
var dx = player.x - this._lastWorld.x;
|
|
var dy = player.y - this._lastWorld.y;
|
|
if (dx < 0) this._facing = 'left';
|
|
else if (dx > 0) this._facing = 'right';
|
|
else if (dy < 0) this._facing = 'up';
|
|
else if (dy > 0) this._facing = 'down';
|
|
}
|
|
this._lastWorld = { x: player.x, y: player.y };
|
|
return this._facing;
|
|
},
|
|
|
|
drawPlayer: function (gameState, viewport) {
|
|
var visual = this.resolveVisual({ projection: 'legacy-rot', context: 'dungeon-player' });
|
|
if (!gameState || !gameState.player || !viewport || !this._ensureVisual(visual)) {
|
|
this.clearOverlay();
|
|
return false;
|
|
}
|
|
var ctx = this._syncOverlay();
|
|
if (!ctx) return false;
|
|
|
|
this.clearOverlay();
|
|
var player = gameState.player;
|
|
var facing = this._updateFacing(player);
|
|
var drew = root.RK.avatarVisuals.drawTopdownWorld(ctx, visual, {
|
|
id: 'single-player',
|
|
x: player.x,
|
|
y: player.y,
|
|
facing: facing
|
|
}, viewport, { onReady: requestRedraw });
|
|
// After the avatar is drawn, sync the overlay FX layer to the same base
|
|
// canvas and let its warm torch light track the player cell. Fully guarded:
|
|
// no-op when the FX module is absent.
|
|
this._syncFX(viewport);
|
|
return drew;
|
|
},
|
|
|
|
// Attach/sync the FX overlay to the same base canvas used by the avatar overlay
|
|
// (mirrors _syncOverlay's sizing) and drive the torch light at the player cell.
|
|
_syncFX: function (viewport) {
|
|
if (!root.FX || !viewport) return;
|
|
var base = this._baseCanvas();
|
|
if (!base) return;
|
|
try {
|
|
root.FX.attach(base);
|
|
var cell = viewport.cell || (root.options && root.options.tileWidth) || 32;
|
|
var px = viewport.screenX * cell + cell / 2;
|
|
var py = viewport.screenY * cell + cell / 2;
|
|
root.FX.renderLighting(px, py, cell);
|
|
} catch (_) { /* FX is purely additive — never let it break the avatar */ }
|
|
},
|
|
|
|
drawLegendIcon: function (tileX, tileY, opts) {
|
|
opts = opts || {};
|
|
var visual = this.resolveVisual({ projection: 'legacy-rot', context: 'legend', fallbackTile: opts.fallbackTile || '@2' });
|
|
if (!this._ensureVisual(visual)) return false;
|
|
var ctx = this._syncOverlay();
|
|
if (!ctx || !root.RK || !root.RK.avatarVisuals) return false;
|
|
var cell = opts.cell || (root.options && root.options.tileWidth) || 32;
|
|
var cols = opts.cols || 2;
|
|
return root.RK.avatarVisuals.drawIcon(ctx, visual, {
|
|
x: tileX * cell,
|
|
y: tileY * cell,
|
|
w: cols * cell,
|
|
h: cell
|
|
}, {
|
|
context: 'legend',
|
|
scale: 0.78,
|
|
offsetY: Math.max(1, Math.round(cell * 0.08)),
|
|
onReady: requestRedraw
|
|
});
|
|
}
|
|
};
|
|
|
|
root.SinglePlayerAvatar = SinglePlayerAvatar;
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = SinglePlayerAvatar;
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|