Pneumatic nozzle: air streaks while sucking and blowing (t25)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+5
-5
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)')
|
||||
|
||||
Reference in New Issue
Block a user