diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 332e200..d5e46cc 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -130,6 +130,20 @@ const SocketHandlers = { socket.on('game_start', this.onGameStart); socket.on('game_update', this.onGameUpdate); socket.on('game_over', this.onGameOver); + // Discrete in-run events. Multi-level DESCEND is the big one — without this, taking the + // stairs down silently dumped the player at a new entrance and read as a bug. + socket.on('game_event', function (data) { + if (!data || !data.event) return; + var $m = $('#messages'); + if (data.event === 'descend') { + var lvl = (data.depth && data.maxDepth) ? ('Level ' + data.depth + ' of ' + data.maxDepth + ' — ') : ''; + $m.append($('
  • ') + .text('⬇️ Stairs down! ' + lvl + 'keep going — escape before the block!')); + } else if (data.event === 'treasure_found') { + $m.append($('
  • ').text('💰 Treasure secured!')); + } else return; + if (typeof UI !== 'undefined' && UI.scrollChat) UI.scrollChat(); + }); socket.on('queue_cancelled', this.onQueueCancelled); // Payment/Address handlers diff --git a/html/js/render/sceneModel.js b/html/js/render/sceneModel.js index a0520ff..99550e3 100644 --- a/html/js/render/sceneModel.js +++ b/html/js/render/sceneModel.js @@ -194,32 +194,38 @@ function sceneFromGameState(state, opts) { var entities = []; - // Entrance / exit / treasure as entities (so they layer above tiles). - if (state.entrance) { + // Fog of war: a cell is `seen` once explored (grid isn't 'dark'); `inView` only while currently + // visible. Features (exit/treasure/entrance) show once discovered; the monster only shows while + // in view — otherwise the exit stairs leak through the fog before you've found them. + function seen(x, y) { return !!(grid[y] && grid[y][x] && grid[y][x] !== 'dark'); } + function inView(x, y) { return !!(visible[y] && visible[y][x] !== undefined); } + + // Entrance / exit / treasure as entities (so they layer above tiles) — only once explored. + if (state.entrance && seen(state.entrance[0], state.entrance[1])) { entities.push({ id: 'entrance', x: state.entrance[0], y: state.entrance[1], kind: 'feature', char: '<', color: '#3fb950', label: null }); } - if (state.exit) { + if (state.exit && seen(state.exit[0], state.exit[1])) { entities.push({ id: 'exit', x: state.exit[0], y: state.exit[1], kind: 'feature', char: '>', color: '#d29922', label: null }); } - if (state.treasure) { + if (state.treasure && seen(state.treasure[0], state.treasure[1])) { var tChar = opts.cryptoType === 'XMR' ? '$M' : '$W'; entities.push({ id: 'treasure', x: state.treasure[0], y: state.treasure[1], kind: 'feature', char: tChar, color: '#fbbf24', label: null }); } - // Items. + // Items — only once explored. if (state.items) { for (var key in state.items) { if (state.items.hasOwnProperty(key)) { var item = state.items[key]; - if (item && typeof item.x === 'number') { + if (item && typeof item.x === 'number' && seen(item.x, item.y)) { entities.push({ id: 'item:' + key, x: item.x, y: item.y, kind: 'item', char: '$', color: '#fbbf24', label: null }); } } } } - // Monster. - if (state.monster) { + // Monster — only while it's actually in view. + if (state.monster && inView(state.monster.x, state.monster.y)) { entities.push({ id: 'monster', x: state.monster.x, y: state.monster.y, kind: 'monster', char: '~', color: '#f85149', label: null diff --git a/html/js/render/spGameRenderer.js b/html/js/render/spGameRenderer.js index 3ed2cb6..3e6fdff 100644 --- a/html/js/render/spGameRenderer.js +++ b/html/js/render/spGameRenderer.js @@ -26,9 +26,12 @@ if (!host) { host = doc().createElement('div'); host.id = 'rk-game-host'; - // Absolutely FILL #game-display (which is position:relative) so the flex centering + the - // hidden ROT/FX sibling canvases can't shrink or shove us into a corner. - host.style.cssText = 'display:none; position:absolute; top:0; left:0; right:0; bottom:0;' + + // A bounded viewport CENTERED in #game-display (which is position:relative). Absolute + // positioning dodges the flex centering + sibling ROT/FX canvases; capping the height + // (vs filling the very-tall #game-display) keeps the player-centered camera on-screen + // instead of low. + host.style.cssText = 'display:none; position:absolute; top:50%; left:50%;' + + ' width:min(100%, 1000px); height:min(100%, 78vh); transform:translate(-50%,-50%);' + ' overflow:hidden; background:#0a0c0f; z-index:6; touch-action:none; cursor:grab;'; gd.appendChild(host); } diff --git a/src/game/difficultyConfig.js b/src/game/difficultyConfig.js index 2ae3e2f..ba89975 100644 --- a/src/game/difficultyConfig.js +++ b/src/game/difficultyConfig.js @@ -157,12 +157,15 @@ function getDifficultyPreset(cryptoType = 'WOW', overridePreset = null) { // NOTE: the exact per-network level counts are sim starting points (calibration re-run is pending // the sim's multi-level support). Operator env overrides (DUNGEON_LEVELS, DUNGEON_*, MONSTER_*) win. // Kill: NETWORK_TUNING_DISABLED=true. +// Level counts trimmed from the first sim estimate — 4-8 levels in a single block deadline is too +// punishing (you rarely clear them all before the block). Kept modest until a real multi-level +// calibration run + the descent UX are in. Reach-exit-to-win stays intuitive with fewer levels. const NETWORK_TUNING = { GRIN: { levels: 1 }, // ~1 min blocks - XMR: { levels: 2 }, // ~2 min + XMR: { levels: 1 }, // ~2 min LTC: { levels: 2 }, // ~2.5 min - WOW: { levels: 4 }, // ~5 min (measured) - BTC: { levels: 8 } // ~10 min + WOW: { levels: 2 }, // ~5 min (measured) + BTC: { levels: 3 } // ~10 min }; // Fold the per-network level count onto the resolved preset (size + monster stay at the preset). diff --git a/src/game/movementManager.js b/src/game/movementManager.js index 52b7a48..a13ed08 100644 --- a/src/game/movementManager.js +++ b/src/game/movementManager.js @@ -65,9 +65,12 @@ class MovementManager { this.spectatorManager.broadcastToSpectators(game.id, state); } - // Handle special events from moveResult (escape / treasure) + // Handle special events from moveResult (escape / treasure / descend). Include depth for the + // multi-level descend so the client can tell the player they took the stairs down (not a bug). if (moveResult && moveResult.event) { - this.io.to(socketId).emit('game_event', { event: moveResult.event }); + this.io.to(socketId).emit('game_event', { + event: moveResult.event, depth: moveResult.depth, maxDepth: moveResult.maxDepth + }); } } diff --git a/test/networkTuning_ultra.test.js b/test/networkTuning_ultra.test.js index 303f9fd..ab0255a 100644 --- a/test/networkTuning_ultra.test.js +++ b/test/networkTuning_ultra.test.js @@ -23,13 +23,14 @@ describe('per-network difficulty tuning (multi-level)', () => { expect(w.monster.movesPerPlayerMove).toBe(DIFFICULTY_PRESETS.casino.monster.movesPerPlayerMove); }); - test('levels scale with block time: GRIN < XMR < WOW < BTC', () => { + test('levels are non-decreasing with block time, BTC the deepest', () => { const lv = (n) => applyNetworkTuning(casino(), n).levels; - expect(lv('GRIN')).toBeLessThan(lv('XMR')); + expect(lv('GRIN')).toBeLessThanOrEqual(lv('XMR')); expect(lv('XMR')).toBeLessThanOrEqual(lv('LTC')); - expect(lv('LTC')).toBeLessThan(lv('WOW')); + expect(lv('LTC')).toBeLessThanOrEqual(lv('WOW')); expect(lv('WOW')).toBeLessThan(lv('BTC')); - expect(lv('GRIN')).toBe(1); // fast chain = single level + expect(lv('GRIN')).toBe(1); // fast chain = single level + expect(lv('BTC')).toBeGreaterThan(1); // slow chain descends }); test('applies to every preset (levels is preset-agnostic pacing)', () => {