// The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. // Material the vacuum would start on at the cursor: a bar, else the nearest loose grain. function vacuumTargetAt(cx, cy, r) { for (const e of entsInCircle(cx, cy, Math.max(1, r))) return e.m; let best = 0, bd = 1e9; forBrush(cx, cy, Math.max(1, r), (x, y, i) => { const m = mat[i], ph = PH[m]; if (ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) { const d = (x - cx) * (x - cx) + (y - cy) * (y - cy); if (d < bd) { bd = d; best = m; } } }); return best; } // Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true. function toolVacuum(cx, cy, r, pick) { let got = 0, full = false, budget = 48; for (const e of entsInCircle(cx, cy, r)) { if (!pick(e.m) || !inReach(e.ix + 2, e.iy + 1)) continue; if (bagTotal >= bagCap()) { full = true; break; } bagAdd(e.m, 1); removeEnt(e); got++; emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m }); } if (!full) forBrush(cx, cy, r, (x, y, i) => { const m = mat[i], ph = PH[m]; if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) return; if (!pick(m)) return; if (part[i] >= P_TU && part[i] <= P_TR) return; if (bagTotal >= bagCap()) { full = true; return false; } bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++; if (got & 1) emit({ t: 'suck', x, y, m }); if (--budget <= 0) return false; }); return { got, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' }; } // Blow: grains leave the bot's nozzle and fly to the cursor, where they drop. // Liquids need the Flask. Bars are thrown, one at a time. function nozzle(tx, ty) { const cx = bot.x + bot.w / 2, cy = bot.y + bot.h / 2; let dx = tx - cx, dy = ty - cy; const d = Math.hypot(dx, dy) || 1; const reach = Math.min(d, REACH - 1); dx /= d; dy /= d; return { ox: cx + dx * 3, oy: cy + dy * 3, dx, dy, dist: Math.max(1, reach - 3) }; } // mix: optional list of materials blown out in turn, one grain each (an even mix). let blowRR = 0; function toolBlow(cx, cy, r, m, mix) { let pickM = null; if (mix && mix.length) { const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask)); if (!avail.length) return { placed: 0, status: 'Tick materials in your bag to mix.' }; pickM = () => { for (let k = 0; k < avail.length; k++) { const c = avail[(blowRR++) % avail.length]; if (bag[c] > 0) return c; } return 0; }; m = avail[0]; } if (!m) return { placed: 0, status: 'Pick what to blow out: Q/E, or click it in your bag.' }; if (bag[m] <= 0) return { placed: 0, status: `No ${MD[m].name.toLowerCase()} left.` }; const ph = PH[m]; if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' }; const nz = nozzle(cx + 0.5, cy + 0.5); if (ph === PH_ITEM) { if (tick % 10 !== 0) return { placed: 0, status: '' }; // a short lob: aim so the bar lands near the cursor const T = Math.max(10, Math.min(34, nz.dist / 2.2)); const tx = nz.ox + nz.dx * nz.dist, ty = nz.oy + nz.dy * nz.dist; let e = null; for (let k = 0; k < 6 && !e; k++) { const sx = nz.ox + nz.dx * k * 2 - 2, sy = nz.oy + nz.dy * k * 2 - 1; if (entFreeAt(Math.round(sx), Math.round(sy))) e = spawnBar(m, sx, sy, (tx - sx - 2) / T, (ty - sy - 1) / T - 0.5 * 0.07 * T); } if (!e) return { placed: 0, status: 'No room to throw a bar from here.' }; bagTake(m, 1); return { placed: 1, status: '' }; } if (flyers.length > 3000) return { placed: 0, status: '' }; // nozzle 0 is a trickle for careful work, the widest a torrent const rate = 1 + r + ((r * r) >> 1); const n = pickM ? rate : Math.min(bag[m], rate); let placed = 0; for (let k = 0; k < n; k++) { const gm = pickM ? pickM() : m; if (!gm) break; // aim each grain at a spot in the brush around the cursor, on an arc that passes // through it; past that point gravity carries it on until it hits something const a = rnd() * Math.PI * 2, rr = Math.sqrt(rnd()) * (r + 0.5); const tx = cx + 0.5 + Math.cos(a) * rr, ty = cy + 0.5 + Math.sin(a) * rr; const ox = nz.ox + (rnd() - 0.5), oy = nz.oy + (rnd() - 0.5); const dx = tx - ox, dy = ty - oy, d = Math.hypot(dx, dy) || 1; // air drag (FLY_Q) and gravity (FLY_G) act every tick; pick the launch speed that // reaches the target after T ticks const T = Math.max(3, Math.min(60, Math.round(d / (2.0 + rnd() * 0.4)))), S = (1 - FLY_Q ** T) / (1 - FLY_Q), G = FLY_G * (T - S) / (1 - FLY_Q); flyers.push({ x: ox, y: oy, vx: dx / S, vy: (dy - G) / S, age: 0, m: gm, T: bagTemp(gm), free: false, run: 0, bag: true }); bagTake(gm, 1); placed++; } return { placed, status: '' }; } const FLY_G = 0.06, FLY_Q = 0.93; // Can a flying grain of m be in cell j? function flyOpen(m, j) { if (occ[j] || mach[j] && mat[j] === HULL) return false; const p = part[j]; if (p === P_GRATE && PH[m] === PH_POWDER) return false; if (p === P_INTAKE) return false; if (p === P_FILTER && PH[m] === PH_POWDER && !filterPassAt(m, j)) return false; const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS; } // Put a grain into the nearest open cell around (x, y), at or above where it stopped. It never // jumps down: below could be the far side of a floor, a grate or a filter (into a mold, say). function depositNear(m, T, x, y, skip) { x = Math.max(0, Math.min(W - 1, x)); y = Math.max(0, Math.min(H - 1, y)); for (let r = 0; r <= 24; r++) { for (let dy = -r; dy <= 0; dy++) for (let dx = -r; dx <= r; dx++) { if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue; const xx = x + dx, yy = y + dy; if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue; const j = yy * W + xx; if (j !== skip && flyOpen(m, j)) { setCell(j, m, T, 0); return true; } } } return false; } // What comes out of the bag is at room temperature, except what would melt at it: snow stays frozen, // and molten things come out hot enough to pour. function bagTemp(m) { return PH[m] === PH_LIQUID ? Math.max(LIQT[m], 20) : MELT[m] !== 0 && MELT[m] < 20 ? MELT[m] - 6 : 20; } function landFlyer(f, x, y) { // settle into the nearest open cell at or above where it stopped for (let k = 0; k < 8; k++) { const yy = y - k; if (yy < 0) break; const j = yy * W + x; if (flyOpen(f.m, j)) { setCell(j, f.m, f.T, 0); return; } } if (!depositNear(f.m, f.T, x, y)) { if (f.rain) absorbSteam(x); else bagAdd(f.m, 1, true); } // nowhere to go: back into the bag (rain: back into the clouds) } function flyTick() { for (let k = flyers.length - 1; k >= 0; k--) { const f = flyers[k]; const steps = Math.max(1, Math.ceil(Math.max(Math.abs(f.vx), Math.abs(f.vy)))); const sx = f.vx / steps, sy = f.vy / steps; let px = Math.floor(f.x), py = Math.floor(f.y), done = false; for (let s = 0; s < steps; s++) { const nx = f.x + sx, ny = f.y + sy, cx = Math.floor(nx), cy = Math.floor(ny); if (cx < 0 || cx >= W) { f.vx = 0; continue; } // the world's edge stops it; it drops from there if (cy >= H) { landFlyer(f, px, H - 1); done = true; break; } if (cy < 0) { f.x = nx; f.y = ny; continue; } const open = flyOpen(f.m, cy * W + cx); if (!f.free) { // still inside whatever the bot is flying through: pass until the first open cell // a blocked nozzle spills: the grain backs up into the nearest open space if (open) f.free = true; else if (++f.run > 14) { if (!depositNear(f.m, f.T, cx, cy)) bagAdd(f.m, 1, true); done = true; break; } } else if (!open) { landFlyer(f, px, py); done = true; break; } f.x = nx; f.y = ny; px = cx; py = cy; } if (done) { flyers[k] = flyers[flyers.length - 1]; flyers.pop(); continue; } f.vx *= FLY_Q; f.vy = f.vy * FLY_Q + FLY_G; if (++f.age > 900) { landFlyer(f, Math.max(0, Math.min(W - 1, px)), Math.max(0, py)); flyers[k] = flyers[flyers.length - 1]; flyers.pop(); } } } // The flame heats what it touches, and licks a few grains into a loose pile, so fuel // buried just under ore still catches if air reaches it (through a grate, say). // Returns { any: anything got hotter, fuel: there is fuel within the flame's reach }. function toolIgnite(cx, cy) { let any = false, fuel = false; forBrush(cx, cy, 4, (x, y, i) => { const dx = x - cx, dy = y - cy, near = dx * dx + dy * dy <= 2.5; if (IGN[mat[i]]) fuel = true; if (!near && !(IGN[mat[i]] && dy >= 0)) return; const top = near ? 640 : 520; if (temp[i] < top) { temp[i] = Math.min(top, temp[i] + (near ? 26 : 12)); any = true; } wakeHeatXY(x, y); }); return { any, fuel }; }