// 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 && (p <= P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); } 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); } function entTick() { for (let k = ents.length - 1; k >= 0; k--) { const e = ents[k]; entMark(e, 0); entCurM = e.m; const ix = e.ix, iy = e.iy; // what is underneath? let belt = 0, bsat = 0, 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) { 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; } } 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 { e.vx = 0; e.x = cx; break; } } while (cy !== ty) { const s = Math.sign(ty - cy); if (entFreeAt(cx, cy + s)) cy += s; else { e.vy = 0; e.y = cy; break; } } if (!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; // 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 (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) + '.'; }