mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-20 15:48:33 +02:00
Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
575 lines
22 KiB
JavaScript
575 lines
22 KiB
JavaScript
// Main game controller. Modules (DisplayManager, ScreenManager, RenderEngine, GameState) hold the
|
|
// implementation; this object wires them together and owns the game lifecycle.
|
|
// Stub socket so emit() is always callable when the real socket has not been created yet.
|
|
if (typeof window !== 'undefined' && !window.socket) {
|
|
window.socket = {
|
|
emit: function(event, data) {}
|
|
};
|
|
}
|
|
|
|
var Game = {
|
|
_screenWidth: 25, // Overridden from options when available
|
|
_screenHeight: 19,
|
|
_inputEnabled: true,
|
|
_messageLog: [],
|
|
_maxLogMessages: 5,
|
|
_unconfirmedPayment: false,
|
|
_unconfirmedPaymentInfo: null,
|
|
_awaitingPayment: false,
|
|
_isSpectating: false, // True when viewing someone else's game
|
|
_gameActive: false, // True when game is running (playing or spectating)
|
|
|
|
init: function() {
|
|
if (typeof options !== 'undefined') {
|
|
this._screenWidth = options.width || 25;
|
|
this._screenHeight = options.height || 19;
|
|
}
|
|
|
|
if (!DisplayManager.init(this._screenWidth, this._screenHeight)) {
|
|
return false;
|
|
}
|
|
|
|
ScreenManager.init(this._screenWidth, this._screenHeight);
|
|
RenderEngine.init(this._screenWidth, this._screenHeight);
|
|
GameState.init();
|
|
|
|
ScreenManager.drawWelcomeScreen();
|
|
|
|
return true;
|
|
},
|
|
|
|
_ensureDisplay: function() {
|
|
return DisplayManager.ensureDisplay();
|
|
},
|
|
|
|
_isFirefox: function() {
|
|
return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|
},
|
|
|
|
startGame: function(playerData, mapData, monsterData, itemData, initialVisibleTiles, lightingData, torchData) {
|
|
if (!this._ensureDisplay()) {
|
|
console.error("Game: Cannot start game, display not ready.");
|
|
return false;
|
|
}
|
|
|
|
DisplayManager.clearDisplay();
|
|
|
|
// Drop cached display glyphs so no welcome-screen artifacts survive into the level.
|
|
if (typeof DisplayManager !== 'undefined' && DisplayManager.forceClearToBlack) {
|
|
DisplayManager.forceClearToBlack();
|
|
}
|
|
|
|
// The welcome screen's block simulation draws on the same display and must be stopped first.
|
|
if (typeof ScreenManager !== 'undefined' && ScreenManager.stopBlockSimulation) {
|
|
ScreenManager.stopBlockSimulation();
|
|
}
|
|
|
|
try {
|
|
GameState.setGameActive(true);
|
|
|
|
// Clear previous game state. _visibleTiles is left alone: initializeMap seeds it from
|
|
// the server's initial visible set.
|
|
GameState._exploredTiles = {};
|
|
GameState._map = {};
|
|
GameState._items = {};
|
|
GameState._monster = null;
|
|
GameState._entrance = null;
|
|
GameState._exit = null;
|
|
GameState._treasure = null;
|
|
|
|
GameState.initializePlayer(playerData);
|
|
|
|
GameState._monster = monsterData;
|
|
GameState._items = itemData;
|
|
GameState._entrance = null;
|
|
GameState._exit = null;
|
|
GameState._treasure = null;
|
|
|
|
if (lightingData) {
|
|
GameState._lighting = lightingData;
|
|
}
|
|
if (torchData) {
|
|
GameState._torches = torchData;
|
|
}
|
|
|
|
GameState.initializeMap(mapData, initialVisibleTiles, this._screenWidth, this._screenHeight);
|
|
|
|
if (!GameState.validatePlayerPosition(this._screenWidth, this._screenHeight)) {
|
|
ScreenManager.drawCenteredText(10, "Error: Map invalid / No player start!");
|
|
return false;
|
|
}
|
|
|
|
GameState.computeFieldOfView();
|
|
|
|
if (!GameState._scheduler) GameState._scheduler = new ROT.Scheduler.Simple();
|
|
else GameState._scheduler.clear();
|
|
if (!GameState._engine) GameState._engine = new ROT.Engine(GameState._scheduler);
|
|
|
|
GameState._scheduler.add(GameState._player, true);
|
|
|
|
if (!GameState._map[GameState._player.y] || GameState._map[GameState._player.y][GameState._player.x] === undefined) {
|
|
console.error(`Player position (${GameState._player.x}, ${GameState._player.y}) is STILL invalid before drawing. Map issue?`);
|
|
ScreenManager.drawCenteredText(10, "Error: Player invalid for draw!");
|
|
return false;
|
|
}
|
|
|
|
this._drawGameScreen();
|
|
|
|
// Starts the FX overlay (ambient embers, flicker, particle loop). No-op without window.FX.
|
|
this._fxStart();
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
console.error("Error during Game.startGame:", err);
|
|
console.error("Error name:", err.name, "Error message:", err.message, "Stack:", err.stack);
|
|
if (err.message && (err.message.includes("_map") || err.message.includes("Cannot read properties of undefined"))) {
|
|
console.error("Detailed error info: Player:", GameState._player, "Map keys:", Object.keys(GameState._map || {}).length);
|
|
}
|
|
try {
|
|
ScreenManager.drawCenteredText(10, "Error starting game!");
|
|
ScreenManager.drawCenteredText(12, "Check console (F12) for details.");
|
|
} catch (drawError) {
|
|
console.error("Failed to draw error message on screen:", drawError);
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// Apply a state update pushed by the server.
|
|
updateGameState: function(data) {
|
|
// Snapshot of the treasure/item count; a drop after the update means a pickup happened.
|
|
const prevTreasureCount = (window.FX) ? this._fxTreasureCells().length : 0;
|
|
|
|
const needsRedraw = GameState.updateGameState(data);
|
|
|
|
if (needsRedraw) {
|
|
this._drawGameScreen();
|
|
}
|
|
|
|
if (window.FX && needsRedraw) {
|
|
try {
|
|
const newTreasureCount = this._fxTreasureCells().length;
|
|
if (newTreasureCount < prevTreasureCount) {
|
|
// An item vanished: gold burst for the pickup.
|
|
const pp = this._playerScreenPixel();
|
|
window.FX.burst(pp.x, pp.y, '#ffd700', 28);
|
|
} else {
|
|
// Proximity effects for server-authoritative moves.
|
|
this._fxOnPlayerMoved();
|
|
}
|
|
} catch (e) { /* FX is best-effort; never break gameplay */ }
|
|
}
|
|
},
|
|
|
|
// Alias kept for handlers that call updateGame.
|
|
updateGame: function(data) {
|
|
return this.updateGameState(data);
|
|
},
|
|
|
|
// Handle the game-over event from the server.
|
|
endGame: function(data) {
|
|
try {
|
|
GameState.setGameActive(false);
|
|
if (data && data.reason === 'monster') {
|
|
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('monster');
|
|
} else if (data && data.reason === 'escaped') {
|
|
if (ScreenManager.drawWinScreen) ScreenManager.drawWinScreen(data.treasure || false);
|
|
} else if (data && data.reason === 'timeout') {
|
|
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('timeout');
|
|
} else {
|
|
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('other');
|
|
}
|
|
|
|
// Outcome FX. The loop stays running so the burst/shake renders; it is torn down on
|
|
// return-to-title below.
|
|
if (window.FX) {
|
|
try {
|
|
const pp = this._playerScreenPixel();
|
|
const escaped = !!(data && data.reason === 'escaped');
|
|
if (escaped) {
|
|
window.FX.burst(pp.x, pp.y, '#ffd700', 40);
|
|
window.FX.flash('rgba(255,215,0,1)', 0.35, 400);
|
|
if (data && data.treasure) {
|
|
window.FX.burst(pp.x, pp.y, '#fbbf24', 24);
|
|
}
|
|
} else {
|
|
// Monster, timeout or other loss.
|
|
window.FX.shake(12, 500);
|
|
window.FX.flash('rgba(255,0,0,1)', 0.4, 450);
|
|
}
|
|
// Gameplay is over, so quiet the ambient torch embers.
|
|
if (window.FX.setAmbient) window.FX.setAmbient(false);
|
|
} catch (e) { /* best-effort */ }
|
|
}
|
|
|
|
const score = data && typeof data.score === 'number' ? data.score : 0;
|
|
const treasure = !!(data && data.treasure);
|
|
|
|
if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) {
|
|
const baseY = Math.floor(ScreenManager._screenHeight / 2) + 5;
|
|
ScreenManager.drawCenteredText(baseY, `Score: ${score}`);
|
|
if (treasure) {
|
|
ScreenManager.drawCenteredText(baseY + 1, 'Treasure secured!');
|
|
}
|
|
}
|
|
|
|
// Restart accepts Enter only after activateDelay, so a keypress in flight at game over
|
|
// cannot skip the outcome screen.
|
|
this._awaitingRestart = false;
|
|
const activateDelay = 800;
|
|
const autoReturnDelay = 10000;
|
|
|
|
setTimeout(() => {
|
|
this._awaitingRestart = true;
|
|
if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) {
|
|
const y = ScreenManager._screenHeight - 2;
|
|
const fullHint = 'Press Enter to return to title';
|
|
const shortHint = 'Press Enter';
|
|
const maxLen = ScreenManager._screenWidth - 2; // Leaves a one-cell margin each side
|
|
const hint = fullHint.length > maxLen ? shortHint : fullHint;
|
|
ScreenManager.drawCenteredText(y, hint);
|
|
}
|
|
}, activateDelay);
|
|
|
|
clearTimeout(this._autoReturnTimer);
|
|
this._autoReturnTimer = setTimeout(() => {
|
|
if (this._awaitingRestart) {
|
|
this._awaitingRestart = false;
|
|
this._fxStop(); // Tears down the FX loop so requestAnimationFrame does not leak
|
|
if (ScreenManager && ScreenManager.drawWelcomeScreen) {
|
|
ScreenManager.drawWelcomeScreen();
|
|
}
|
|
}
|
|
}, autoReturnDelay);
|
|
|
|
// The keydown listener is attached once and gated on _awaitingRestart, so repeated
|
|
// game-overs do not stack handlers.
|
|
if (!this._restartListenerAttached) {
|
|
this._restartListenerAttached = true;
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!this._awaitingRestart) return;
|
|
if (e.key === 'Enter') {
|
|
this._awaitingRestart = false;
|
|
clearTimeout(this._autoReturnTimer);
|
|
this._fxStop(); // Tears down the FX loop so requestAnimationFrame does not leak
|
|
if (ScreenManager && ScreenManager.drawWelcomeScreen) {
|
|
ScreenManager.drawWelcomeScreen();
|
|
}
|
|
const chatInput = document.getElementById('chatInput');
|
|
if (chatInput) chatInput.focus();
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Error handling endGame:', err);
|
|
}
|
|
},
|
|
|
|
clearDisplay: function() {
|
|
DisplayManager.clearDisplay();
|
|
},
|
|
|
|
_drawGameScreen: function() {
|
|
const gameState = GameState.getGameStateForRender();
|
|
// The dungeon renders through the render kit (Tiled / ASCII / Iso / 3D plus unlocked packs)
|
|
// with a player-centered camera. RenderEngine is the fallback path, used only when the kit
|
|
// is unavailable or a render fails, so a live 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 */ }
|
|
}
|
|
}
|
|
},
|
|
|
|
// Screen drawing delegates to ScreenManager.
|
|
_drawWelcomeScreen: function() {
|
|
ScreenManager.drawWelcomeScreen();
|
|
},
|
|
|
|
drawWinScreen: function(hasTreasure) {
|
|
ScreenManager.drawWinScreen(hasTreasure);
|
|
},
|
|
|
|
drawLoseScreen: function(reason) {
|
|
ScreenManager.drawLoseScreen(reason);
|
|
},
|
|
|
|
drawWaitingScreen: function() {
|
|
ScreenManager.drawWaitingScreen();
|
|
if (this._unconfirmedPayment && typeof ScreenManager !== 'undefined' && DisplayManager.ensureDisplay()) {
|
|
const display = DisplayManager.getDisplay();
|
|
const badgeText = 'Unconfirmed (mempool)';
|
|
const xStart = Math.max(1, Math.floor((this._screenWidth - badgeText.length) / 2));
|
|
const y = 1; // First row inside the border
|
|
for (let i = 0; i < badgeText.length; i++) {
|
|
display.draw(xStart + i, y, badgeText[i], 'rgba(180,230,255,0.9)', 'transparent');
|
|
}
|
|
const pulse = 0.4 + Math.sin(Date.now()/300)*0.4;
|
|
display.draw(xStart - 2, y, '*', `rgba(120,200,255,${pulse})`, 'transparent');
|
|
}
|
|
},
|
|
|
|
stopWaitingScreen: function() {
|
|
this._awaitingPayment = false;
|
|
this._unconfirmedPayment = false;
|
|
ScreenManager.stopWaitingScreen();
|
|
},
|
|
|
|
// Display mode switching delegates to DisplayManager.
|
|
switchToAsciiMode: function() {
|
|
DisplayManager.switchToAsciiMode();
|
|
},
|
|
|
|
switchToTileMode: function() {
|
|
DisplayManager.switchToTileMode();
|
|
},
|
|
|
|
// Player movement delegates to GameState.
|
|
movePlayer: function(dx, dy) {
|
|
const moved = GameState.movePlayer(dx, dy, this._screenWidth, this._screenHeight);
|
|
if (moved) {
|
|
this._drawGameScreen();
|
|
if (window.FX) {
|
|
try { this._fxOnPlayerMoved(); } catch (e) { /* best-effort */ }
|
|
}
|
|
}
|
|
},
|
|
|
|
// ------------------------------------------------------------------
|
|
// FX helpers. These only consume the window.FX overlay API and never define it. Every call is
|
|
// guarded, so the game stays fully functional where the FX module is absent (match.html,
|
|
// tavern.html). Coordinates use the same camera-centered mapping as the renderer: the player is
|
|
// always drawn at the center of the view.
|
|
// ------------------------------------------------------------------
|
|
|
|
// The ROT.js display container is the base canvas that FX layers sit above.
|
|
_baseCanvas: function() {
|
|
if (typeof DisplayManager === 'undefined' || !DisplayManager.getDisplay) return null;
|
|
var display = DisplayManager.getDisplay();
|
|
return (display && display.getContainer) ? display.getContainer() : null;
|
|
},
|
|
|
|
// Convert a world cell to its center pixel in base-canvas space.
|
|
_cellToScreenPixel: function(wx, wy) {
|
|
var cell = (window.options && window.options.tileWidth) ? window.options.tileWidth : 32;
|
|
var centerX = Math.floor(this._screenWidth / 2);
|
|
var centerY = Math.floor(this._screenHeight / 2);
|
|
var player = (GameState && GameState._player) ? GameState._player : { x: wx, y: wy };
|
|
var sx = wx - player.x + centerX;
|
|
var sy = wy - player.y + centerY;
|
|
return { x: sx * cell + cell / 2, y: sy * cell + cell / 2, cell: cell };
|
|
},
|
|
|
|
// The player is camera-centered, so this is always the center of the view.
|
|
_playerScreenPixel: function() {
|
|
var p = (GameState && GameState._player) ? GameState._player : { x: 0, y: 0 };
|
|
return this._cellToScreenPixel(p.x, p.y);
|
|
},
|
|
|
|
// Chebyshev (king-move) distance, matching movement on the grid.
|
|
_fxDist: function(ax, ay, bx, by) {
|
|
return Math.max(Math.abs(ax - bx), Math.abs(ay - by));
|
|
},
|
|
|
|
// Collect every treasure/item world cell currently known.
|
|
_fxTreasureCells: function() {
|
|
var cells = [];
|
|
if (typeof GameState === 'undefined') return cells;
|
|
var t = GameState._treasure;
|
|
if (t && typeof t[0] === 'number' && typeof t[1] === 'number') {
|
|
cells.push({ x: t[0], y: t[1] });
|
|
}
|
|
var items = GameState._items;
|
|
if (items) {
|
|
for (var k in items) {
|
|
if (items.hasOwnProperty(k)) {
|
|
var it = items[k];
|
|
if (it && typeof it.x === 'number' && typeof it.y === 'number') {
|
|
cells.push({ x: it.x, y: it.y });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return cells;
|
|
},
|
|
|
|
// Fire proximity/pickup FX for the player's current position.
|
|
_fxOnPlayerMoved: function() {
|
|
if (!window.FX) return;
|
|
var player = (GameState && GameState._player) ? GameState._player : null;
|
|
if (!player) return;
|
|
|
|
var pp = this._playerScreenPixel();
|
|
|
|
// Treasure proximity: on the cell gives a gold burst, within two cells gives a sparkle.
|
|
var cells = this._fxTreasureCells();
|
|
var onTreasure = false, nearTreasure = false;
|
|
for (var i = 0; i < cells.length; i++) {
|
|
var d = this._fxDist(player.x, player.y, cells[i].x, cells[i].y);
|
|
if (d === 0) onTreasure = true;
|
|
else if (d <= 2) nearTreasure = true;
|
|
}
|
|
if (onTreasure && window.FX.burst) {
|
|
window.FX.burst(pp.x, pp.y, '#ffd700', 24);
|
|
} else if (nearTreasure && window.FX.sparkle) {
|
|
window.FX.sparkle(pp.x, pp.y, '#fbbf24');
|
|
}
|
|
|
|
// Monster proximity deliberately triggers no screen flash: the monster is usually within a
|
|
// few tiles and already visible on screen, so a per-step flash would fire almost constantly.
|
|
// FX.flash is reserved for one-shot events such as win and lose.
|
|
},
|
|
|
|
// Attach + start the FX loop for a fresh game.
|
|
_fxStart: function() {
|
|
if (!window.FX) return;
|
|
this._fxLastMonsterFlash = 0;
|
|
try {
|
|
var base = this._baseCanvas();
|
|
if (base && window.FX.attach) window.FX.attach(base);
|
|
if (window.FX.setAmbient) window.FX.setAmbient(true);
|
|
if (window.FX.start) window.FX.start();
|
|
} catch (e) { /* best-effort */ }
|
|
},
|
|
|
|
// Stop and clear the FX loop so requestAnimationFrame does not leak.
|
|
_fxStop: function() {
|
|
if (!window.FX) return;
|
|
try {
|
|
if (window.FX.setAmbient) window.FX.setAmbient(false);
|
|
if (window.FX.stop) window.FX.stop();
|
|
if (window.FX.clear) window.FX.clear();
|
|
} catch (e) { /* best-effort */ }
|
|
},
|
|
|
|
// Keep the overlay aligned and feed it the camera-centered player position.
|
|
_fxSyncLighting: function() {
|
|
if (!window.FX) return;
|
|
var base = this._baseCanvas();
|
|
if (!base) return;
|
|
if (window.FX.syncTo) window.FX.syncTo(base);
|
|
if (window.FX.renderLighting && GameState && GameState._player) {
|
|
var pp = this._playerScreenPixel();
|
|
window.FX.renderLighting(pp.x, pp.y, pp.cell, { torches: GameState._torches || [] });
|
|
}
|
|
},
|
|
|
|
// Debug helpers delegate to GameState.
|
|
debugPrintMap: function() {
|
|
GameState.debugPrintMap(this._screenWidth, this._screenHeight);
|
|
},
|
|
|
|
debugTileMapping: function() {
|
|
GameState.debugTileMapping();
|
|
},
|
|
|
|
// Accessors forwarding to GameState, kept for callers that read Game.* directly.
|
|
get _gameActive() {
|
|
return GameState.isGameActive();
|
|
},
|
|
|
|
set _gameActive(value) {
|
|
GameState.setGameActive(value);
|
|
},
|
|
|
|
get _player() {
|
|
return GameState._player;
|
|
},
|
|
|
|
get _monster() {
|
|
return GameState._monster;
|
|
},
|
|
|
|
get _map() {
|
|
return GameState._map;
|
|
},
|
|
|
|
get _items() {
|
|
return GameState._items;
|
|
},
|
|
|
|
get _visibleTiles() {
|
|
return GameState._visibleTiles;
|
|
},
|
|
|
|
get _exploredTiles() {
|
|
return GameState._exploredTiles;
|
|
}
|
|
};
|
|
|
|
// Payment state helpers
|
|
Game._pendingPaymentDetected = function(info) {
|
|
this._unconfirmedPayment = true;
|
|
this._unconfirmedPaymentInfo = info || {};
|
|
this._awaitingPayment = false; // A transaction exists, so this is no longer awaiting creation
|
|
};
|
|
|
|
Game._pendingPaymentConfirmed = function() {
|
|
this._unconfirmedPayment = false;
|
|
this._unconfirmedPaymentInfo = null;
|
|
this._awaitingPayment = false;
|
|
};
|
|
|
|
Game._paymentRequested = function() {
|
|
this._awaitingPayment = true;
|
|
};
|
|
|
|
// Debug utilities: log to console, to the on-page debug panel, and to the server.
|
|
const GameDebug = {
|
|
log: function(message) {
|
|
console.log(message);
|
|
this.updateDebugDisplay(message);
|
|
this.sendToServer(message);
|
|
},
|
|
|
|
updateDebugDisplay: function(message) {
|
|
const debugContent = document.getElementById('debug-content');
|
|
if (debugContent) {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
debugContent.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
|
// Cap the panel at the ten most recent messages.
|
|
const lines = debugContent.children;
|
|
if (lines.length > 10) {
|
|
debugContent.removeChild(lines[0]);
|
|
}
|
|
const debugDisplay = document.getElementById('debug-display');
|
|
debugDisplay.scrollTop = debugDisplay.scrollHeight;
|
|
}
|
|
},
|
|
|
|
sendToServer: function(message) {
|
|
try {
|
|
fetch('/debug', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ message: message })
|
|
}).catch(err => {
|
|
// Debug reporting is best-effort; an unavailable server must not surface an error.
|
|
});
|
|
} catch (err) {
|
|
// Debug reporting is best-effort.
|
|
}
|
|
}
|
|
};
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.Game = Game;
|
|
window.GameDebug = GameDebug;
|
|
}
|