From cd701dbf0dbe1cccc9ff87b79d313e030a9c724e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 00:04:47 +0000 Subject: [PATCH] Balefires on the Wilds map; the last unseen pieces looked at - The Wilds map shows each balefire, with a ring round the ones still to light today, so the round can be planned. - A bound boss is now tested in the real dungeon tick: it covers six tenths of the ground, and its full stride again once the binding is off. - The ?testdrive=1 seam gains `manor()` to stand in the Manor's hall. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_014upby61fMmizgg2cMkKvqW --- client/main.js | 15 ++++++++++++++- test/old-ways.test.js | 13 +++++++++++++ test/wilds-finds.test.js | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/client/main.js b/client/main.js index c6fbbdba..2f5350ce 100644 --- a/client/main.js +++ b/client/main.js @@ -3094,7 +3094,7 @@ function onWsMessage(ev) { return; } if (msg.type === 'dungeon_finds') { applyDungeonFinds(msg); showDungeonFindsHud(msg); return; } - if (msg.type === 'wilds_finds') { applyWildsFinds(msg); showWildsFindsHud(msg); return; } + if (msg.type === 'wilds_finds') { wildsFindsState = msg; applyWildsFinds(msg); showWildsFindsHud(msg); return; } if (msg.type === 'trap_warn') { dungeonTrapWarn(msg.trap, msg.ms); setUnlockToast('⚠️ The roof is going. Move!', { always: true }); return; } if (msg.type === 'toast') { if (msg.message) setUnlockToast(msg.message, (msg.level || msg.always) ? { level: msg.level, always: !!msg.always } : undefined); @@ -10403,6 +10403,7 @@ function showDungeonFindsHud(msg) { } // Out in the Wilds the same line counts the day's balefires. let wildsFindsAskedAt = 0; +let wildsFindsState = null; // the day's balefires and searched cairns, as the server last told it (the map marks them) function showWildsFindsHud(msg) { const el = document.getElementById('dungeonFindsHud'); if (!el || !(me && me.room === 'wilds')) return; @@ -17389,6 +17390,16 @@ const { openNotifModal } = _notif; if (typeof WILDS_CAMPFIRES !== 'undefined') { for (const f of WILDS_CAMPFIRES) drawProp(ctx, PX(f.x), PY(f.y) + fireS * 0.2, fireS, 'campfire'); } + // Balefires: a fire for each, and a ring round the ones still to light today, + // so the round can be planned from the map. + if (world2.finds) { + const litToday = new Set((wildsFindsState && wildsFindsState.lit) || []); + for (const f of world2.finds.balefires) { + const fx = PX(f.x), fy = PY(f.y); + drawProp(ctx, fx, fy + fireS * 0.2, fireS * 0.9, 'campfire'); + if (!litToday.has(f.id)) { ctx.save(); ctx.strokeStyle = '#ffb060'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.arc(fx, fy, fireS * 0.75, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } + } + } // NPCs: villager props at their true spots, then names stacked downward so // the tight camp clusters read as a clean list instead of a tangle. if (typeof WILDS_NPCS !== 'undefined') { @@ -20376,6 +20387,8 @@ if (location.search.includes('testdrive=1')) { teleport(x, y) { if (me) { me.x = x; me.y = y; } }, // Turn the camera round you (radians from its usual place behind you), to look at what stands at your back. look(yaw, pitch) { cameraYawOffset = +yaw || 0; if (pitch != null) cameraPitchOffset = +pitch || 0; return true; }, + // Stand in the Manor's hall (the look of it only; the server keeps its own door). + manor() { swapToManorMap(); if (me) { me.room = 'manor'; me.x = MANOR_WORLD.spawn.x; me.y = MANOR_WORLD.spawn.y; } return activeScene === manorScene; }, // Step straight into a town building's interior (the look of it; the server still keeps its own doors). enter(roomId) { if (me && world && world.buildings.some((b) => b.id === roomId)) { enterBuilding(roomId); return true; } return false; }, kiosks() { return OUTDOOR_KIOSKS.map(k => ({ ...k })); }, diff --git a/test/old-ways.test.js b/test/old-ways.test.js index d87f0c61..838d33f2 100644 --- a/test/old-ways.test.js +++ b/test/old-ways.test.js @@ -146,5 +146,18 @@ check('the panel shows why a thing waits, asks before the brownie is clothed, an const dung = fs.readFileSync(path.join(root, 'lib/dungeons.js'), 'utf8'), delve = fs.readFileSync(path.join(root, 'lib/delve.js'), 'utf8'), cottage = fs.readFileSync(path.join(root, 'client/cottage-scene.js'), 'utf8'); check('a bound boss drags its feet in a dungeon and on a Delve floor', /m\.boundUntil > now \? \{ \.\.\.base, speed: base\.speed \* \(m\.boundSlow \|\| 0\.6\) \} : base/.test(dung) && /\(m\.boundUntil > now \? \(m\.boundSlow \|\| 0\.6\) : 1\)/.test(delve) && /findBoss: \(p, range\) => \[\.\.\.dungeonMobs, \.\.\.allDelveMobs\(\)\]\.find\(\(m\) => m\.boss && !m\.dead && m\.room === p\.room/.test(server)); check('her besom leans by the cottage door, and the cream stands on the hearthstone the night it is set', /besom: !!\(key && accounts\[key\] && accounts\[key\]\.oldWays && accounts\[key\]\.oldWays\.besom && accounts\[key\]\.oldWays\.besom\.made\)/.test(server) && /kkStatic\('wd_besom', 96, 'height'\)/.test(cottage) && /kkStatic\('wd_cream', 18, 'fit'\)/.test(cottage) && /wd_besom: 'kk\/home\/besom\.glb', wd_cream: 'kk\/home\/cream_bowl\.glb'/.test(main) && fs.existsSync(path.join(root, 'public/kk/home/besom.glb')) && fs.existsSync(path.join(root, 'public/kk/home/cream_bowl.glb'))); +// a bound boss, in the real dungeon tick: it covers six tenths of the ground it would +{ + const ps = new Map(); + const dmod = require('../lib/dungeons')({ players: ps, send: () => {}, isEvading: () => false, absorbIncomingDamage: (_p, d) => d, noteAttacked: () => {}, DUNGEON_ROOMS: { 1: 'dungeon_t1', 2: 'dungeon_t2', 3: 'dungeon_t3', 4: 'dungeon_t4' }, DUNGEON_SIZE: 2400, dungeonAffix: () => null }); + const b = dmod.dungeonMobs.find((m) => m.boss && m.tier === 1); + ps.set('z', { id: 'z', room: 'dungeon_t1', x: b.spawnX + 150, y: b.spawnY, health: 1e6, isDead: false, ws: null }); + const step = () => { b.x = b.spawnX; b.y = b.spawnY; dmod.tickDungeon(1); return Math.hypot(b.x - b.spawnX, b.y - b.spawnY); }; + const free = step(); + b.boundUntil = Date.now() + 60000; b.boundSlow = D.POPPET.bindSlow; + const bound = step(); + b.boundUntil = 0; + check('a bound boss covers six tenths of the ground in the same time, and its full stride again once the binding is off', free > 10 && Math.abs(bound / free - 0.6) < 0.01 && Math.abs(step() - free) < 0.01, { free, bound }); +} console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0); diff --git a/test/wilds-finds.test.js b/test/wilds-finds.test.js index 5011b641..eebaae93 100644 --- a/test/wilds-finds.test.js +++ b/test/wilds-finds.test.js @@ -47,5 +47,6 @@ check('a cairn over each cache and a brazier on a cairn for each balefire, on le check('a lit balefire burns for you, and the day\'s count stands at the top of the screen in the Wilds', /f\.flame\.visible = lit\.has\(id\)/.test(scene) && /function showWildsFindsHud\(msg\)/.test(main) && /🔥 Balefires/.test(main) && /type: 'wilds_finds_state'/.test(main)); check('the bee garth stands in the town and opens the Old Ways', /function buildBeeGarth\(scene\)/.test(town) && /altarModel\(one, 'tw_skep', 34, 'height'\)/.test(town) && /npc: 'oldways'/.test(town) && /kiosk\.npc === 'oldways'\) \{ if \(!Modals\.isOpen\('oldWaysOpen'\)\) _oldWays\.open\(\); return; \}/.test(main) && fs.existsSync(path.join(root, 'public/kk/home/bee_skep.glb'))); check('you land astride the besom, and it is put away after a few moments', /broom\.name = 'ridingBesom'/.test(main) && /setTimeout\(\(\) => \{ if \(broom\.parent\) broom\.parent\.remove\(broom\); \}, 3500\);/.test(main)); +check('the Wilds map shows each balefire, ringed while it is still to light today', /for \(const f of world2\.finds\.balefires\)/.test(main) && /if \(!litToday\.has\(f\.id\)\)/.test(main) && /wildsFindsState = msg; applyWildsFinds\(msg\);/.test(main)); console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0);