289 lines
15 KiB
JavaScript
289 lines
15 KiB
JavaScript
// Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
|
|
// ---------- Bars (loose objects) ----------
|
|
// A bar is 4x2 grains. It falls, rests on things, rides belts, and melts back
|
|
// into molten metal if it gets hot enough. While it sits somewhere, grains treat
|
|
// its cells as solid (occ).
|
|
const BAR_W = 4, BAR_H = 2;
|
|
let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through
|
|
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (IS_SCREW[p] || p === P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
|
|
function entSupportCount(x, y) {
|
|
if (y + BAR_H >= H) return BAR_W;
|
|
let n = 0;
|
|
for (let dx = 0; dx < BAR_W; dx++) if (entSolidCell((y + BAR_H) * W + x + dx)) n++;
|
|
return n;
|
|
}
|
|
function entFreeAt(x, y) {
|
|
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
|
|
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) if (entSolidCell((y + dy) * W + x + dx)) return false;
|
|
return true;
|
|
}
|
|
function entMark(e, on) {
|
|
const x = e.ix, y = e.iy;
|
|
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) {
|
|
const xx = x + dx, yy = y + dy;
|
|
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
|
|
occ[yy * W + xx] = on ? 1 : 0;
|
|
if (!on) wakeXY(xx, yy);
|
|
}
|
|
}
|
|
function spawnBar(m, x, y, vx, vy, T) {
|
|
if (ents.length >= 4000 || !m) return null;
|
|
let ix = Math.round(x), iy = Math.round(y);
|
|
// find a free spot near the requested one
|
|
let ok = entFreeAt(ix, iy);
|
|
for (let k = 1; !ok && k < 12; k++) { if (entFreeAt(ix, iy - k)) { iy -= k; ok = true; } }
|
|
if (!ok) return null;
|
|
const e = { m, x: ix, y: iy, ix, iy, vx: vx || 0, vy: vy || 0, T: T === undefined ? AMB[Math.max(0, Math.min(H - 1, iy))] : T, rest: 0 };
|
|
ents.push(e); entMark(e, 1);
|
|
return e;
|
|
}
|
|
function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) ents.splice(k, 1); }
|
|
// No transient screw state: even a loaded bar derives its ride from its eight cells.
|
|
function entScrewAt(x, y) {
|
|
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0;
|
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
|
const p = part[yy * W + xx]; if (!IS_SCREW[p]) return 0;
|
|
}
|
|
return part[(y + 1) * W + x + 1];
|
|
}
|
|
function entScrewFree(x, y, screwOnly = false) {
|
|
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
|
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
|
const j = yy * W + xx, p = part[j], screw = IS_SCREW[p];
|
|
// gas and liquid don't stop a bar, in a screw or in the open
|
|
if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (screwOnly ? !screw : !screw && entSolidCell(j))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
// Check the swept rectangle too: a snap must never jump through a grain or another bar.
|
|
function entScrewMove(e, x, y, screwOnly = false, mouth = false) {
|
|
if (!entScrewFree(x, y, screwOnly)) return false;
|
|
if (mouth) {
|
|
// Go around the receiving corner, checking both legs before moving.
|
|
const acrossY = y > e.iy ? e.iy : y, downX = y > e.iy ? x : e.ix;
|
|
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entScrewFree(downX, yy)) return false;
|
|
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entScrewFree(xx, acrossY)) return false;
|
|
} else for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
|
|
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entScrewFree(xx, yy)) return false;
|
|
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
|
|
}
|
|
function entScrewLane(y, iy) { return Math.max(y & ~3, Math.min((y & ~3) + 2, iy)); }
|
|
function entScrewEdge(e, dx, dy, k) {
|
|
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
|
|
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
|
|
}
|
|
function entScrewEnter(e, dx, dy, mouth = false) {
|
|
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
|
const i = entScrewEdge(e, dx, dy, k);
|
|
if (i < 0) continue;
|
|
const xx = i % W, yy = (i / W) | 0;
|
|
const p = part[i]; if (!IS_SCREW[p] || (!dx && SCREW_DX[p]) || dx * SCREW_DX[p] + dy * SCREW_DY[p] < 0) continue;
|
|
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
|
|
const x = (xx & ~3), y = entScrewLane(yy, e.iy);
|
|
if (entScrewMove(e, x, SCREW_DX[p] || dy ? y : e.iy, true, mouth)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
function entScrewTick(e, p) {
|
|
e.vx = e.vy = 0; e.x = e.ix; e.y = e.iy;
|
|
const ci = (e.iy + 1) * W + e.ix + 1;
|
|
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
|
|
const dx = SCREW_DX[p], dy = SCREW_DY[p];
|
|
if (dx && dy) { entDiagonalScrewTick(e, p); return; }
|
|
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
|
|
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
|
const i = entScrewEdge(e, dx, dy, k);
|
|
if (i < 0) return;
|
|
const xx = i % W, yy = (i / W) | 0;
|
|
const q = part[i];
|
|
if (IS_SCREW[q] && q !== p) { next = q; ax = xx; ay = yy; break; }
|
|
}
|
|
if (next) {
|
|
if (SCREW_DX[next] === -dx && SCREW_DY[next] === -dy) return;
|
|
x = ax & ~3; y = entScrewLane(ay, e.iy);
|
|
if (entScrewMove(e, x, y, true)) stats.moved++;
|
|
return;
|
|
}
|
|
if (entScrewAt(x, y)) { if (entScrewMove(e, x, y, true)) stats.moved++; return; }
|
|
// Clear the mouth in one move so the next tick can derive ordinary falling from position.
|
|
x = e.ix + dx * BAR_W; y = e.iy + dy * BAR_H;
|
|
if (!entScrewFree(x, y) || entScrewAt(x, y)) return;
|
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
|
if (entScrewMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
|
}
|
|
// A horizontal bar stays whole: one diagonal block per four screw beats.
|
|
function entDiagonalScrewTick(e, p) {
|
|
if (tick & 7) return;
|
|
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
|
|
for (const [ox, oy] of [[dx, dy], [dx, 0], [0, dy]]) {
|
|
const x = bx + ox * BS, y = by + oy * BS + lane, q = entScrewAt(x, y);
|
|
if (!q || (ox && oy && (q < P_SCREW_UR || q > P_SCREW_UL || SCREW_DX[q] !== dx || SCREW_DY[q] !== dy))) continue;
|
|
if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return;
|
|
if (entScrewMove(e, x, y, true)) stats.moved++;
|
|
return;
|
|
}
|
|
const x = bx + dx * BS;
|
|
let y = by + dy * BS + lane;
|
|
const belt = y >= 0 && y < H ? mat[y * W + x] : EMPTY;
|
|
if (dy > 0 && (belt === BELTL || belt === BELTR)) y = (y & ~3) - BAR_H;
|
|
if (!entScrewFree(x, y)) return;
|
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
|
if (entScrewMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
|
|
}
|
|
// Bars stay level; their highest contact follows the face one grain per belt beat.
|
|
function entSlopeBeltTick(e) {
|
|
const bottom = e.iy + BAR_H;
|
|
if (bottom >= H) return false;
|
|
let d = 0, slope = false, screen = false;
|
|
for (let x = e.ix; x < e.ix + BAR_W; x++) {
|
|
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
|
|
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
|
|
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
|
|
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
|
}
|
|
}
|
|
if (!d) return false;
|
|
let face = H;
|
|
for (let x = e.ix + d; x < e.ix + d + BAR_W; x++) for (let dy = -1; dy <= 1; dy++) {
|
|
const y = bottom + dy;
|
|
if (x < 0 || x >= W || y < 0 || y >= H) continue;
|
|
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
|
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
|
|
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
|
|
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
|
face = Math.min(face, y);
|
|
}
|
|
if (!slope) return false;
|
|
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
|
|
if (screen ? tick % SCREEN_NUDGE_TICKS : tick & 1) return true;
|
|
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
|
|
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
|
|
else if (!entScrewEnter(e, d, 0)) entScrewEnter(e, d, Math.sign(y - e.iy), true);
|
|
return true;
|
|
}
|
|
function entTick() {
|
|
for (let k = ents.length - 1; k >= 0; k--) {
|
|
const e = ents[k];
|
|
entMark(e, 0);
|
|
entCurM = e.m;
|
|
let ix = e.ix; const iy = e.iy;
|
|
const screw = entScrewAt(ix, iy);
|
|
if (screw) entScrewTick(e, screw);
|
|
else if (!entSlopeBeltTick(e)) {
|
|
// what is underneath?
|
|
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
|
|
if (!support) {
|
|
const yb = iy + BAR_H;
|
|
for (let dx = 0; dx < BAR_W; dx++) {
|
|
const x = ix + dx; if (x < 0 || x >= W) continue;
|
|
const j = yb * W + x, m = mat[j];
|
|
if (m === BELTL || m === BELTR || part[j] === P_FILTER) transport = true;
|
|
if (m === BELTL || m === BELTR) { const s = netSat(j); if (s > bsat) { bsat = s; belt = m === BELTR ? 1 : -1; } }
|
|
else if (part[j] === P_FILTER && !filterPassAt(e.m, j) && (yb & 3) === 0) { bsat = 1; belt = blockV[(yb >> 2) * BW + (x >> 2)] & 128 ? 1 : -1; }
|
|
if (entSolidCell(j)) support = true;
|
|
}
|
|
}
|
|
if (support && !transport && e.vy === 0 && Math.abs(e.vx) < 0.05) {
|
|
const n = entSupportCount(ix, iy);
|
|
if (n < 2) {
|
|
let left = 0, right = 0;
|
|
for (let dx = 0; dx < BAR_W; dx++) if (!entSolidCell((iy + BAR_H) * W + ix + dx)) {
|
|
if (dx < BAR_W / 2) left++; else right++;
|
|
}
|
|
const dir = left > right ? -1 : right > left ? 1 : ((ix + iy) & 1) ? 1 : -1;
|
|
if (entFreeAt(ix + dir, iy)) ix += dir;
|
|
} else if (ix & 1) {
|
|
for (let dir = -1; dir <= 1; dir += 2) if (entFreeAt(ix + dir, iy) && entSupportCount(ix + dir, iy) >= 2) { ix += dir; break; }
|
|
}
|
|
if (ix !== e.ix) { e.x = e.ix = ix; support = entSupportCount(ix, iy) > 0; }
|
|
}
|
|
const ci = Math.min(H - 1, iy + 1) * W + Math.min(W - 1, ix + 1);
|
|
const inLiq = PH[mat[ci]] === PH_LIQUID;
|
|
if (support) { if (e.vy > 0) e.vy = 0; e.vx = belt ? belt * 0.5 * bsat : e.vx * 0.55; }
|
|
else { e.vy = Math.min(inLiq ? 0.35 : 1.3, e.vy + 0.07); e.vx *= inLiq ? 0.8 : 0.985; }
|
|
if (belt && bsat > 0) stats.moved += 0.05;
|
|
e.x += e.vx; e.y += e.vy;
|
|
// step toward the new rounded position one cell at a time
|
|
let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy;
|
|
while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else if (belt && entScrewEnter(e, s, 0)) { cx = e.ix; cy = e.iy; e.x = cx; e.y = cy; break; } else { e.vx = 0; e.x = cx; break; } }
|
|
while (cy !== ty) { const s = Math.sign(ty - cy); if (entScrewAt(cx, cy)) break; if (entFreeAt(cx, cy + s)) cy += s; else if (entScrewEnter(e, 0, s)) { cx = e.ix; cy = e.iy; break; } else { e.vy = 0; e.y = cy; break; } }
|
|
if (support && !belt && entScrewEnter(e, 0, 1)) { cx = e.ix; cy = e.iy; }
|
|
if (!entScrewAt(cx, cy) && !entFreeAt(cx, cy)) {
|
|
// something moved into us (a grain fell in): try to step up out of it
|
|
let freed = false;
|
|
for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; }
|
|
if (!freed) e.y = cy;
|
|
}
|
|
e.ix = cx; e.iy = cy;
|
|
}
|
|
if (rackIntake(e)) { ents.splice(k, 1); continue; }
|
|
const cx = e.ix, cy = e.iy;
|
|
// heat: the bar tends toward the temperature around it
|
|
const T = temp[Math.min(H - 1, cy + 1) * W + Math.min(W - 1, cx + 1)];
|
|
e.T += (T - e.T) * 0.04;
|
|
if (FIRETO[e.m] && e.T >= 880 && rnd() < 0.004 + (e.T - 880) * 0.00012) {
|
|
e.m = FIRETO[e.m]; discover(e.m, e.ix, e.iy, GREENB, 'fire', 880);
|
|
}
|
|
if (e.T >= MELT[e.m]) {
|
|
// melts back into 8 grains of molten metal if they fit here
|
|
let fits = true;
|
|
for (let dy = 0; dy < BAR_H && fits; dy++) for (let dx = 0; dx < BAR_W; dx++) { const j = (cy + dy) * W + cx + dx; if (PH[mat[j]] === PH_SOLID || PH[mat[j]] === PH_POWDER) { fits = false; break; } }
|
|
if (fits) {
|
|
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) setCell((cy + dy) * W + cx + dx, MELTTO[e.m], e.T, 0);
|
|
ents.splice(k, 1);
|
|
discover(MELTTO[e.m], cx, cy);
|
|
continue;
|
|
}
|
|
}
|
|
entMark(e, 1);
|
|
}
|
|
entCurM = 0;
|
|
}
|
|
function entsInCircle(cx, cy, r) {
|
|
const out = [];
|
|
for (const e of ents) {
|
|
const ex = e.ix + BAR_W / 2, ey = e.iy + BAR_H / 2;
|
|
if ((ex - cx) * (ex - cx) + (ey - cy) * (ey - cy) <= (r + 2.5) * (r + 2.5)) out.push(e);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------- Bag ----------
|
|
function bagAdd(m, n, force) {
|
|
const room = force ? n : Math.max(0, bagCap() - bagTotal);
|
|
const k = Math.min(n, room);
|
|
bag[m] += k; bagTotal += k;
|
|
if (k > 0 && m < NMAT) discover(m, undefined, undefined, 0, 'pickup');
|
|
return k;
|
|
}
|
|
function bagTake(m, n) { const k = Math.min(n, bag[m]); bag[m] -= k; bagTotal -= k; return k; }
|
|
// Give back what fits in the bag; the rest drops into the world near (x, y):
|
|
// grains as loose material, bars as bar objects. Counts what dropped.
|
|
let droppedLast = 0;
|
|
function giveOrDrop(m, n, x, y) {
|
|
if (n <= 0) return;
|
|
if (m === CASH) { money += n; return; }
|
|
const k = bagAdd(m, n, false);
|
|
let rest = n - k;
|
|
if (!rest) return;
|
|
droppedLast += rest;
|
|
if (PH[m] === PH_ITEM) {
|
|
for (let j = 0; j < rest; j++) if (!spawnBar(m, x - 2 + (j % 3) * 2, y - 2 - j, (rnd() - 0.5) * 0.4, -0.3)) bagAdd(m, 1, true);
|
|
return;
|
|
}
|
|
for (let r = 0; r <= 12 && rest > 0; r++) for (let dy = -r; dy <= r && rest > 0; dy++) for (let dx = -r; dx <= r && rest > 0; dx++) {
|
|
if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
|
|
const xx = Math.round(x) + dx, yy = Math.round(y) + dy;
|
|
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
|
|
const j = yy * W + xx;
|
|
if (mat[j] === EMPTY && !occ[j] && !part[j] && !mach[j]) { setCell(j, m, AMB[yy], 0); rest--; }
|
|
}
|
|
if (rest > 0) bagAdd(m, rest, true);
|
|
}
|
|
function refundAt(cost, mult, x, y) { for (const [m, n] of cost) giveOrDrop(m, n * mult, x, y); }
|
|
function canAfford(cost, mult) { for (const [m, n] of cost) if ((m === CASH ? money : bag[m]) < n * mult) return false; return true; }
|
|
function payCost(cost, mult) { for (const [m, n] of cost) { if (m === CASH) money -= n * mult; else bagTake(m, n * mult); } }
|
|
function fmtMoney(v) { return '$' + Math.round(v).toLocaleString('en-US'); }
|
|
function costText(cost, mult) { return cost.map(([m, n]) => { const k = n * (mult || 1); return m === CASH ? fmtMoney(k) : `${k} ${MD[m].name.toLowerCase()}${PH[m] === PH_ITEM && k !== 1 ? 's' : ''}`; }).join(' + '); }
|
|
function needText(cost, mult) { const c = cost.find(([m, n]) => (m === CASH ? money : bag[m]) < n * (mult || 1)); return c && c[0] === CASH ? `Costs ${fmtMoney(c[1] * (mult || 1))}; you have ${fmtMoney(money)}.` : 'Not enough ' + costText(c ? [c] : cost, mult) + '.'; }
|