mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
fix: fog-of-war entities, multi-level descend feedback, camera centering, fewer levels
- FOG: sceneFromGameState only emits exit/entrance/treasure/items once their cell is EXPLORED, and the monster only while IN VIEW — no more exit stairs leaking through the fog. - DESCEND FEEDBACK: reaching a non-final exit descends (multi-level) and used to silently dump the player at a new entrance (read as a bug). Server now sends depth/maxDepth on game_event; the client shows "Stairs down! Level N of M — keep going". Also lowered level counts (WOW 4->2, BTC 8->3, XMR 2->1) — clearing 4-8 levels in one block was unwinnable; kept modest until proper multi-level calibration + UX. - CAMERA: the host filled the very-tall #game-display so centering put the player low; it's now a bounded viewport (min(100%,78vh)) centered in #game-display, so the player stays on-screen-centered. Suite 62/435 green.
This commit is contained in:
@@ -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($('<li class="status" style="color:#ffcc00; font-weight:bold;">')
|
||||
.text('⬇️ Stairs down! ' + lvl + 'keep going — escape before the block!'));
|
||||
} else if (data.event === 'treasure_found') {
|
||||
$m.append($('<li class="status" style="color:#fbbf24; font-weight:bold;">').text('💰 Treasure secured!'));
|
||||
} else return;
|
||||
if (typeof UI !== 'undefined' && UI.scrollChat) UI.scrollChat();
|
||||
});
|
||||
socket.on('queue_cancelled', this.onQueueCancelled);
|
||||
|
||||
// Payment/Address handlers
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user