Resonance ping: right click with the drill lights up nearby veins through fog (t50)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-9
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -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'],
|
||||
|
||||
@@ -20,7 +20,7 @@ function renderSelCard() {
|
||||
const pw = PULSE_W[GAME.drillW], tn = GAME.drillTune;
|
||||
h = `<div class="row cycle"><kbd>Q</kbd><span class="name grow">Resonance drill</span><span class="chip hot">${pw.name} pulse</span><kbd>E</kbd></div>
|
||||
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">${esc(b.name.toLowerCase())}</span><span class="chip">hardness ≤ ${b.hard}</span><span class="chip">reach ${drillReach(GAME.drillW)}</span>${tn ? `<span class="chip ok"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(tn)}"></span>tuned: ${esc(MD[tn].name.toLowerCase())} · R</span>` : '<span class="chip">untuned · R tunes</span>'}</div>
|
||||
<div class="sub">Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. <kbd>Q</kbd>/<kbd>E</kbd> width: narrow bores a long tunnel fast, wide clears a big face up close. <kbd>R</kbd> over a material tunes to it, so only that shatters. Built blocks are never harmed.</div>`;
|
||||
<div class="sub">Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. <kbd>Q</kbd>/<kbd>E</kbd> width: narrow bores a long tunnel fast, wide clears a big face up close. <kbd>R</kbd> over a material tunes to it, so only that shatters. Right click pings: veins nearby light up, even unexplored. Built blocks are never harmed.</div>`;
|
||||
} else if (t === 'build') {
|
||||
// the build tool in general; the piece itself is described in the menu's hover popup
|
||||
const P = piece();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user