From 569ec6e5b650865878cb280fade3adf22aa86847 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Tue, 27 Oct 2026 16:47:35 -0400 Subject: [PATCH] Resonance ping: right click with the drill lights up nearby veins through fog (t50) Co-Authored-By: Claude Opus 5.5 --- docs/PROJECT_MAP.md | 19 ++++++++++--------- docs/agent-log.md | 10 ++++++++++ src/render/overlays.js | 36 ++++++++++++++++++++++++++++++++++++ src/sim/drill.js | 16 ++++++++++++++++ src/ui/actions.js | 11 +++++++++++ src/ui/loop.js | 1 + src/ui/notebook.js | 2 +- src/ui/selection-panels.js | 2 +- test/t_drill.js | 17 +++++++++++++++++ 9 files changed, 103 insertions(+), 11 deletions(-) diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index ad7e1c5..ab59aef 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -320,10 +320,10 @@ Tool helpers: reach, brushes, and plain digging. - functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig - variables: BUILD_MAX, buildBox -### src/sim/drill.js (161 lines) +### src/sim/drill.js (177 lines) The resonance drill: tuned pulses that shatter one material. -- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick -- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF +- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick, pingRadius, resonancePing +- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF, PING_VEINS ### src/sim/nature.js (168 lines) Trees (background layer), acorns, saplings, and waking freezing water. @@ -412,9 +412,10 @@ Scale colour without changing the smooth-look class. Machine overlays: fans, engines, silo and the rest, drawn on top of the world. - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks -### src/render/overlays.js (285 lines) +### src/render/overlays.js (321 lines) Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles. -- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles +- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles +- variables: PING_SPEED, PING_FADE ### src/render/smooth.js (99 lines) Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution. @@ -512,10 +513,10 @@ Keyboard and mouse listeners. - functions: isTyping - variables: canvas -### src/ui/actions.js (243 lines) +### src/ui/actions.js (254 lines) What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge -- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick -- variables: ovShown +- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, firePing, toolTick +- variables: ovShown, PING_RECHARGE ### src/ui/gamepad.js (60 lines) Gamepad input. @@ -545,7 +546,7 @@ Shared world colours, minimap and full map. - functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap - variables: MAP_S, MAP -### src/ui/loop.js (137 lines) +### src/ui/loop.js (138 lines) The frame loop and boot. - functions: scanVisible, frame, boot - variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT diff --git a/docs/agent-log.md b/docs/agent-log.md index 3faa6f7..6f35053 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,16 @@ What agents have done, newest first. Add an entry for every piece of work: the d note or request it was for, what changed, the files touched, the tests run, and anything left open or not verified. Keep each entry short; the git history has the details. +## 2026-10-27: t50 resonance ping (Claude Code) + +- Right click with the drill: `firePing` (`src/ui/actions.js`) calls `resonancePing` (`src/sim/drill.js`), + which lists natural vein cells (coal seam and the six ore veins) within 3x the bit's reach. + `drawPing` (`src/render/overlays.js`) draws an echo ring and the veins inside it over fog, from + a one-time offscreen image (one clipped drawImage per frame), fading over 4.5 s. 3 s recharge. +- Files: `src/sim/drill.js`, `src/ui/actions.js`, `src/render/overlays.js`, `src/ui/loop.js`, + `src/ui/notebook.js`, `src/ui/selection-panels.js`, `test/t_drill.js`, `docs/PROJECT_MAP.md`. +- Tests: 4 checks in `test/t_drill.js`; `python3 test/run_all.py --browser` all passed. + ## 2026-10-27: t64 build range about the screen; out-of-range replace ghosts (Claude Code) - `setBuildRange`/`inBuildRange` in `src/sim/tools.js`: the UI sets the view box each frame diff --git a/src/render/overlays.js b/src/render/overlays.js index a5f2833..76796ff 100644 --- a/src/render/overlays.js +++ b/src/render/overlays.js @@ -109,6 +109,42 @@ function drawFlowRates() { } ctx.restore(); } +// The resonance ping's echo: a ring spreading from the bot, the veins it found lighting up inside +// it, then fading once it has gone out. The veins are drawn once into a small image, so each +// frame is one clipped drawImage however many there are. Drawn over fog, so unexplored rock shows. +const PING_SPEED = 160, PING_FADE = 4500; +function pingImage(p) { + const size = p.r * 2 + 1, c = document.createElement('canvas'); c.width = c.height = size; + const g = c.getContext('2d'), img = g.createImageData(size, size), d = img.data, ox = Math.floor(p.x - p.r), oy = Math.floor(p.y - p.r); + for (const i of p.cells) { + const x = i % W - ox, y = ((i / W) | 0) - oy; + if (x < 0 || y < 0 || x >= size || y >= size) continue; + const k = (y * size + x) * 4, m = mat[i]; + d[k] = Math.min(255, COLR[m] + 90); d[k + 1] = Math.min(255, COLG[m] + 90); d[k + 2] = Math.min(255, COLB[m] + 90); d[k + 3] = 220; + } + g.putImageData(img, 0, 0); + return { c, ox, oy }; +} +function drawPing() { + const p = GAME.ping; + if (!p) return; + const ms = performance.now() - p.t0, front = ms / 1000 * PING_SPEED, done = p.r / PING_SPEED * 1000; + if (ms > done + PING_FADE) { GAME.ping = null; return; } + if (!p.img) p.img = pingImage(p); + const ctx = R.ctx, zp = R.zp, c = worldToScreenPx(p.x, p.y), o = worldToScreenPx(p.img.ox, p.img.oy); + ctx.save(); + ctx.beginPath(); ctx.arc(c.x, c.y, Math.min(front, p.r + 1) * zp, 0, Math.PI * 2); ctx.clip(); + ctx.globalAlpha = ms < done ? 1 : Math.max(0, 1 - (ms - done) / PING_FADE); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(p.img.c, o.x, o.y, p.img.c.width * zp, p.img.c.height * zp); + ctx.restore(); + if (front < p.r) { + ctx.save(); + ctx.strokeStyle = `rgba(160,225,255,${0.85 * (1 - front / p.r)})`; ctx.lineWidth = Math.max(2, zp * 0.8); + ctx.beginPath(); ctx.arc(c.x, c.y, front * zp, 0, Math.PI * 2); ctx.stroke(); + ctx.restore(); + } +} // Outline a run of blocks: strong while it is being edited, faint when pointed at function drawRunOutline(run, strong) { if (!run || !run.length) return; diff --git a/src/sim/drill.js b/src/sim/drill.js index b521711..3b421b1 100644 --- a/src/sim/drill.js +++ b/src/sim/drill.js @@ -159,3 +159,19 @@ function pulseTick() { } drillPulses = drillPulses.filter(p => p.r < p.range + 3); } +// Resonance ping (t50): an echo from the bot that finds ore veins in the rock around it, under +// fog too. Reach grows with the bit. Only reads the world; the UI draws the echo. +const PING_VEINS = new Uint8Array(NMAT); for (const m of [COALV, CUV, FEV, SNV, ZNV, AGV, AUV]) PING_VEINS[m] = 1; +function pingRadius() { return BIT_TIERS[player.bitTier].reach * 3; } +// Natural vein cells within r of (cx, cy), and the materials found. +function resonancePing(cx, cy, r = pingRadius()) { + const cells = [], found = new Set(), r2 = r * r; + const x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(W - 1, Math.ceil(cx + r)); + const y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(H - 1, Math.ceil(cy + r)); + for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { + const dx = x - cx, dy = y - cy, i = y * W + x, m = mat[i]; + if (dx * dx + dy * dy > r2 || !PING_VEINS[m] || !(flags[i] & F_NAT)) continue; + cells.push(i); found.add(m); + } + return { cells, found: [...found], r, x: cx, y: cy }; +} diff --git a/src/ui/actions.js b/src/ui/actions.js index c4c7ace..ea5b9b6 100644 --- a/src/ui/actions.js +++ b/src/ui/actions.js @@ -197,9 +197,20 @@ function secondaryPress(shift) { if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; } else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; } else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; } + else if (t === 'drill') { firePing(); GAME.rmbConsumed = true; } else { GAME.rmbConsumed = false; if (t === 'build') { endRmbRecord(); GAME.rmbRec = { label: '', ops: [] }; } } } +// Right click with the drill: a resonance ping. The echo is drawn by drawPing (overlays.js). +const PING_RECHARGE = 180; +function firePing() { + if (GAME.ping && tick >= GAME.ping.tick && tick - GAME.ping.tick < PING_RECHARGE) { setStatus('The ping is still ringing; try again in a moment.'); playSfx('error'); return; } + const p = resonancePing(bot.x + bot.w / 2, bot.y + bot.h / 2); + GAME.ping = Object.assign(p, { tick, t0: performance.now() }); + playSfx('pulse', 2); + const names = p.found.map(m => MD[m].name.toLowerCase()); + setStatus(names.length ? `Ping: ${names.slice(0, 3).join(', ')}${names.length > 3 ? ' and more' : ''} nearby.` : `Ping: no veins within ${Math.round(p.r / BS)} blocks.`, 'info'); +} // continuous actions, once per simulation tick function toolTick() { const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); diff --git a/src/ui/loop.js b/src/ui/loop.js index f4c8c69..fe98ccb 100644 --- a/src/ui/loop.js +++ b/src/ui/loop.js @@ -73,6 +73,7 @@ function frame(now) { if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false); if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); } drawJoinMarks(); + drawPing(); drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb); drawFloaters(); drawRain(); diff --git a/src/ui/notebook.js b/src/ui/notebook.js index da3305a..acc1e9d 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -57,7 +57,7 @@ function renderNotebook() { } function controlsHtml() { const kb = [ - ['Dev console (marks the world)', 'Backtick (`)'], ['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'], + ['Dev console (marks the world)', 'Backtick (`)'], ['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'], ['Resonance ping: light up nearby veins (Drill)', 'Right mouse'], ['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall: cycle shape · flip diagonal drag', 'T'], ['Wall or mold material menu (Build)', 'F'], ['Pick up selection or what you click (Pneumatic nozzle)', 'Left drag · Shift: everything'], ['Select (Pneumatic nozzle)', 'Click a bag name · Q/E'], ['Add to selection (Pneumatic nozzle)', 'Ctrl+click a bag name'], ['Show everything found (Pneumatic nozzle)', 'Hold Ctrl in the bag'], ['Clear selection (Pneumatic nozzle)', 'Clear in the bag'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'], ['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and screw conveyor directions, grains per second', 'I'], ['Bot lamp', 'L'], diff --git a/src/ui/selection-panels.js b/src/ui/selection-panels.js index 1aef2d6..0b22eba 100644 --- a/src/ui/selection-panels.js +++ b/src/ui/selection-panels.js @@ -20,7 +20,7 @@ function renderSelCard() { const pw = PULSE_W[GAME.drillW], tn = GAME.drillTune; h = `
QResonance drill${pw.name} pulseE
${esc(b.name.toLowerCase())}hardness ≤ ${b.hard}reach ${drillReach(GAME.drillW)}${tn ? `tuned: ${esc(MD[tn].name.toLowerCase())} · R` : 'untuned · R tunes'}
-
Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. Q/E width: narrow bores a long tunnel fast, wide clears a big face up close. R over a material tunes to it, so only that shatters. Built blocks are never harmed.
`; +
Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. Q/E width: narrow bores a long tunnel fast, wide clears a big face up close. R over a material tunes to it, so only that shatters. Right click pings: veins nearby light up, even unexplored. Built blocks are never harmed.
`; } else if (t === 'build') { // the build tool in general; the piece itself is described in the menu's hover popup const P = piece(); diff --git a/test/t_drill.js b/test/t_drill.js index 1cba0ec..bcfef6d 100644 --- a/test/t_drill.js +++ b/test/t_drill.js @@ -171,4 +171,21 @@ check('a mold and what sets inside it stay safe from the drill', run(`(() => { for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) if (crumbly(i)) return false; return true; })()`)); +// Resonance ping (t50): finds natural veins within reach of the bit, under fog too, and +// nothing else. +flatWorld(run, 240, 160, 150); +const ping = run(`(() => { + player.bitTier = 0; const cx = 100, cy = 100, r = pingRadius(); + setCell(cy * W + cx + 10, CUV, 20, F_NAT); setCell(cy * W + cx - 20, FEV, 20, F_NAT); + setCell(cy * W + cx + r + 5, AUV, 20, F_NAT); // out of reach + setCell((cy + 5) * W + cx, CUV, 20, 0); // not natural (set loose) + setCell((cy - 5) * W + cx, STONE, 20, F_NAT); // not a vein + const p = resonancePing(cx, cy); + player.bitTier = BIT_TIERS.length - 1; const far = resonancePing(cx, cy); + return { n: p.cells.length, found: p.found.map(m => MD[m].name), r, farFound: far.found.map(m => MD[m].name), farR: far.r }; +})()`); +check('a ping finds natural veins in reach', ping.n === 2 && ping.found.includes('Copper ore vein') && ping.found.includes('Iron ore vein'), JSON.stringify(ping)); +check('it skips rock, loose ore and veins out of reach', !ping.found.includes('Gold ore vein') && ping.n === 2, JSON.stringify(ping)); +check('a better bit pings farther', ping.farR > ping.r && ping.farFound.includes('Gold ore vein'), JSON.stringify(ping)); +check('a ping never changes the world', run('(() => { const h0 = mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); resonancePing(100, 100); return h0 === mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); })()')); done();