diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f359630..2d33330 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -235,6 +235,12 @@ above, including: the bag; Ctrl+click a bag name adds it to the blow mix and Ctrl+right-click to pick-up; the menu is one line per material because the first version was too large. +- **Air streaks for the nozzle (t25, 2026-10-24).** On top of the grains that fly in and out, the + owner chose air streaks: while sucking, short teal streaks swirl in toward the ring; while + blowing, amber streaks run from the bot to the cursor and spread to the brush width. Drawn only + (time-based, no random numbers, so the simulation is unchanged), only in reach, and off with + "Sparks and debris" or in unexplored fog. + - **The Vacuum is the Pneumatic nozzle (t26, 2026-10-24).** The owner's choice of name, since the tool blows as much as it sucks. It shows as the tool card's title row, with the nozzle width as a chip, and in the hotbar tooltip and all player text ("the nozzle" after the first mention). Code diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 780033c..a16a14f 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -366,8 +366,8 @@ Audio: chiptune effects and generative music that changes with depth. Everything - functions: auInit, auVolumes, rateOk, tone, noiseBurst, sfxPlace, playSfx, auAmbient, mtof, zoneForDepth, scaleNote, musicScheduler - variables: AU, SFX, SFX_NEAR, SFX_FAR, ZONES -### src/ui/state.js (67 lines) -Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line. +### src/ui/state.js (75 lines) +Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and tool status. - functions: loadSettings, saveSettings, el, esc, rgbOf, fmtT, toast, setStatus - variables: SET_DEFAULT, SET, TOOLS, ICONS, BAG_ORDER, GAME, keys, hudDirty, $ @@ -404,7 +404,7 @@ The build menu along the bottom: categories, piece tiles and their icons. - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu -### src/ui/selection.js (243 lines) +### src/ui/selection.js (242 lines) The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel. - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - variables: lastBagKey @@ -428,9 +428,9 @@ Gamepad input. - functions: padPressed, pollPad - variables: PAD -### src/ui/previews.js (334 lines) +### src/ui/previews.js (371 lines) Previews drawn under the cursor: build ghosts, drill aim and pulses, selections, paste. -- functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawPreviews, drawTag, drawSelection, drawPastePreview +- functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawAirStreaks, drawPreviews, drawTag, drawSelection, drawPastePreview - variables: CRACK_N, CRACK_LV, SEL_COL ### src/ui/events.js (42 lines) diff --git a/docs/agent-log.md b/docs/agent-log.md index f053e69..982ed48 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,15 @@ 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-24: t25: air streaks for the Pneumatic nozzle (Claude Code, Codex) + +- `drawAirStreaks` in `src/ui/previews.js`: 12 teal streaks spiral into the ring while sucking, 10 + amber streaks run from the nozzle to the cursor in a cone while blowing. `performance.now()` + only, no `rnd()`; skipped with particles off or in fog. `test/play.py`: drawing both doesn't + change the sim RNG. Also commits the `docs/PROJECT_MAP.md` refresh missed in t27/t26. +- Tests: `run_all.py --browser` passed (play.py 240; t_smooth's half-resolution flake at load 9 + passed on rerun). Screenshots `shots/t25-{suck,blow}.png`. + ## 2026-10-24: t26: the Vacuum is renamed the Pneumatic nozzle (Claude Code, Codex) - `TOOLS[1].name`, a title row in the tool card ("Pneumatic nozzle" plus a `width N` chip, moved diff --git a/src/ui/previews.js b/src/ui/previews.js index 58edfb9..dd29de3 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -121,6 +121,42 @@ function drawDrillAim(ax, ay) { if (GAME.drillTune) { const sz = Math.max(6, zp * 1.4); ctx.fillStyle = rgbOf(GAME.drillTune); ctx.fillRect(p.x + k * 1.8, p.y - k * 1.8 - sz, sz, sz); ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.strokeRect(p.x + k * 1.8 + 0.5, p.y - k * 1.8 - sz + 0.5, sz - 1, sz - 1); } ctx.restore(); } +// Air swirls into the pickup ring or spreads from the nozzle toward the cursor. +function drawAirStreaks(ax, ay, p, r, blowing) { + if (!SET.particles || (SET.fog && fogSeen(ax, ay) < 64)) return; + const ctx = R.ctx, zp = R.zp, now = performance.now(); + const cx = p.x + zp / 2, cy = p.y + zp / 2, ring = (r + 0.5) * zp; + ctx.save(); ctx.lineWidth = Math.max(1, zp * 0.2); ctx.lineCap = 'round'; + if (blowing) { + const bx = bot.x + bot.w / 2, by = bot.y + bot.h / 2; + const dx = ax + 0.5 - bx, dy = ay + 0.5 - by, d = Math.hypot(dx, dy) || 1; + const o = worldToScreenPx(bx + dx / d * 3, by + dy / d * 3); + const px = cx - o.x, py = cy - o.y, len = Math.hypot(px, py) || 1; + const ux = px / len, uy = py / len; + for (let i = 0; i < 10; i++) { + const phase = (now * 0.0016 + i * 0.618) % 1; + const side = ((i * 0.37) % 1 - 0.5) * 2 * ring * phase; + const x = o.x + px * phase - uy * side, y = o.y + py * phase + ux * side; + ctx.strokeStyle = `rgba(255,210,122,${Math.sin(phase * Math.PI) * 0.55})`; + ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + ux * 1.6 * zp, y + uy * 1.6 * zp); ctx.stroke(); + } + } else { + const travel = ring * 0.65 + 3 * zp; + for (let i = 0; i < 12; i++) { + const phase = (now * 0.0012 + i * 0.618) % 1; + const dist = ring + 3 * zp - travel * phase; + const angle = i * Math.PI * 2 / 12 + now * 0.0004 + phase * 1.2; + const co = Math.cos(angle), si = Math.sin(angle); + const x = cx + co * dist, y = cy + si * dist; + const vx = -travel * co - dist * (1.2 + 1 / 3) * si; + const vy = -travel * si + dist * (1.2 + 1 / 3) * co; + const scale = 1.2 * zp / Math.hypot(vx, vy); + ctx.strokeStyle = `rgba(92,186,166,${Math.sin(phase * Math.PI) * 0.5})`; + ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + vx * scale, y + vy * scale); ctx.stroke(); + } + } + ctx.restore(); +} function drawPreviews() { const ctx = R.ctx, zp = R.zp; const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); @@ -161,6 +197,7 @@ function drawPreviews() { ctx.strokeStyle = blowing ? 'rgba(255,210,122,0.22)' : 'rgba(92,186,166,0.22)'; ctx.lineWidth = Math.max(1, zp * (r + 0.5) * 0.6); ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(p.x + zp / 2, p.y + zp / 2); ctx.stroke(); } + if (reachOk && (blowing || GAME.lmb)) drawAirStreaks(ax, ay, p, r, blowing); } } else if (t === 'build') { const P = piece(), b = aimBlock(); diff --git a/tasks/t25.md b/tasks/t25.md new file mode 100644 index 0000000..cab0ecb --- /dev/null +++ b/tasks/t25.md @@ -0,0 +1,21 @@ +--- +id: t25 +status: Review +order: 2 +priority: P3 +created: 2026-10-24 +updated: 2026-10-24T19:27:18Z +title: visual effect for vac/blow tool +report: | + (agent) While sucking, short teal streaks of air swirl in toward the nozzle ring; while blowing, + amber streaks run from the bot toward the cursor and widen to the nozzle width. They're on top + of the grains already flying in and out, and they turn off with "Sparks and debris" in + settings. Please check whether they're too faint, too busy or too fast. +--- + +## Subtasks + +- [ ] add visual effects for suck and blow +- [ ] +- [x] While sucking, short teal streaks of air swirl in toward the nozzle ring +- [x] While blowing, amber streaks push out from the bot toward the cursor and spread into a cone diff --git a/test/play.py b/test/play.py index 367f764..139ebe9 100644 --- a/test/play.py +++ b/test/play.py @@ -321,6 +321,28 @@ with sync_playwright() as p: pg.mouse.wheel(0, 100); settle(300) refill('bag.fill(0)') + # t25: draw both nozzle effects without advancing the simulation RNG. + check('nozzle air streak previews draw and preserve the simulation RNG', ev("""(() => { + const aim = GAME.aim, filter = GAME.vacFilter, blow = GAME.blow, consumed = GAME.rmbConsumed; + const particles = SET.particles, fog = SET.fog, coal = bag[COAL]; + try { + SET.particles = true; SET.fog = false; + GAME.aim = { x: Math.floor(bot.x + bot.w / 2 + 16), y: Math.floor(bot.y + bot.h / 2) }; + GAME.vacFilter = -1; GAME.rmbConsumed = false; + bag[COAL] = 10; GAME.blow = new Set([COAL]); + const before = RS; + for (let i = 0; i < 5; i++) { + GAME.lmb = true; GAME.rmb = false; drawPreviews(); + GAME.lmb = false; GAME.rmb = true; drawPreviews(); + } + return toolId() === 'vacuum' && RS === before; + } finally { + GAME.lmb = false; GAME.rmb = false; + GAME.aim = aim; GAME.vacFilter = filter; GAME.blow = blow; GAME.rmbConsumed = consumed; + SET.particles = particles; SET.fog = fog; bag[COAL] = coal; + } + })()""")) + # 3. right button blows the selection; exhausted materials stay selected refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120') ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')