Files
deepgrain-foundry/src/sim/vacuum.js
T

166 lines
8.5 KiB
JavaScript

// 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 };
}