Hot loads: visible melt damage, rebuild-over repair, warning glow (t69)
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
"render/world.js",
|
||||
"render/machines.js",
|
||||
"render/overlays.js",
|
||||
"render/screw-heat.js",
|
||||
"ui/audio.js",
|
||||
"ui/state.js",
|
||||
"ui/threads.js",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Persistent scorch and broken casing, or a warning before the 1000°C limit, in both looks.
|
||||
function drawScrewHeat() {
|
||||
const ctx = R.ctx, zp = R.zp, v = viewCells(), s = BS * zp;
|
||||
const x0 = Math.max(0, Math.floor(R.camX / BS)), x1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||
const y0 = Math.max(0, Math.floor(R.camY / BS)), y1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
|
||||
ctx.save();
|
||||
for (let by = y0; by <= y1; by++) for (let bx = x0; bx <= x1; bx++) {
|
||||
const b = by * BW + bx;
|
||||
if (blockP[b] !== BP_SCREW || (SET.fog && fogSeen(bx * BS + 2, by * BS + 2) < 64)) continue;
|
||||
const damage = screwDamageMask(b);
|
||||
if (!damage && screwLoadHeat(b) < 900) continue;
|
||||
const p = worldToScreenPx(bx * BS, by * BS);
|
||||
if (damage) {
|
||||
ctx.fillStyle = 'rgba(32,19,17,0.65)'; ctx.fillRect(p.x, p.y, s, s);
|
||||
// Keep the actual holes visible beneath a jagged fracture, even after cooling.
|
||||
ctx.strokeStyle = '#f18b68'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); ctx.moveTo(p.x, p.y + s * 0.65);
|
||||
ctx.lineTo(p.x + s * 0.4, p.y + s * 0.4); ctx.lineTo(p.x + s * 0.6, p.y + s * 0.65);
|
||||
ctx.lineTo(p.x + s, p.y + s * 0.3); ctx.stroke();
|
||||
ctx.setLineDash([zp, zp]); ctx.strokeRect(p.x, p.y, s, s); ctx.setLineDash([]);
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(255,126,38,0.3)'; ctx.fillRect(p.x, p.y, s, s);
|
||||
ctx.strokeStyle = '#ffb54f'; ctx.lineWidth = Math.max(1, zp * 0.3); ctx.strokeRect(p.x, p.y, s, s);
|
||||
ctx.fillStyle = '#ffdc85'; ctx.fillRect(p.x + s * 0.45, p.y + s * 0.15, zp * 0.4, s * 0.4);
|
||||
ctx.fillRect(p.x + s * 0.45, p.y + s * 0.7, zp * 0.4, zp * 0.4);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -46,8 +46,26 @@ function clearStraySeals() {
|
||||
function wallCells(code, bx, by) {
|
||||
return blockCellList(bx, by).filter(i => !(isSlope(code) || isSlopeBelt(code)) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
|
||||
}
|
||||
// Missing screw cells persist in part[] already; no separate damage state is needed.
|
||||
function screwDamageMask(b) {
|
||||
if (blockP[b] !== BP_SCREW) return 0;
|
||||
const x = (b % BW) * BS, y = ((b / BW) | 0) * BS;
|
||||
let mask = 0;
|
||||
for (let k = 0; k < 16; k++) if (!IS_SCREW[part[(y + (k >> 2)) * W + x + (k & 3)]]) mask |= 1 << k;
|
||||
return mask;
|
||||
}
|
||||
function screwLoadHeat(b) {
|
||||
const x = (b % BW) * BS, y = ((b / BW) | 0) * BS;
|
||||
let T = 0;
|
||||
for (let k = 0; k < 16; k++) {
|
||||
const i = (y + (k >> 2)) * W + x + (k & 3);
|
||||
if (IS_SCREW[part[i]] && PH[mat[i]] === PH_POWDER && temp[i] > T) T = temp[i];
|
||||
}
|
||||
return T;
|
||||
}
|
||||
function canPlaceBlock(bx, by, code = BP_WALL) {
|
||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
|
||||
if (code === BP_SCREW && screwDamageMask(by * BW + bx)) return '';
|
||||
if (blockP[by * BW + bx]) return 'Something is already built there';
|
||||
for (const i of blockCellList(bx, by)) {
|
||||
if (mach[i] || (part[i] && slopeSealOwner(i) < 0)) return 'Something is already built there';
|
||||
@@ -66,6 +84,13 @@ function placeBlock(code, v, bx, by, free, extra) {
|
||||
if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R';
|
||||
const cost = pieceCost(code, v);
|
||||
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); }
|
||||
const b = by * BW + bx, damage = code === BP_SCREW ? screwDamageMask(b) : 0;
|
||||
if (damage) {
|
||||
undoNote({ t: 'screw-repair', bx, by, damage, paid: !free });
|
||||
for (const i of blockCellList(bx, by)) { part[i] = SCREW_PARTS[blockV[b] & 7]; markPow(i, true); wakeIdx(i); }
|
||||
stats.placed++;
|
||||
return '';
|
||||
}
|
||||
const cells = isSlope(code) || isSlopeBelt(code) ? wallCells(code, bx, by) : blockCellList(bx, by);
|
||||
if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it
|
||||
for (const i of cells) {
|
||||
@@ -215,6 +240,7 @@ function blockInReach(bx, by) { return inBuildRange(bx * BS + BS / 2, by * BS +
|
||||
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
|
||||
function blockIs(b, code, v, extra) {
|
||||
if (blockP[b] !== code) return false;
|
||||
if (code === BP_SCREW && screwDamageMask(b)) return false;
|
||||
if (isFilter(code)) return (blockV[b] & 128) === filterVariant(code, v) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
|
||||
return blockV[b] === v;
|
||||
}
|
||||
@@ -224,6 +250,11 @@ function placeBlocks(code, v, list, extra, replace) {
|
||||
for (const [bx, by] of list) {
|
||||
if (!blockInReach(bx, by)) { last = 'Out of reach'; continue; }
|
||||
const b = by * BW + bx;
|
||||
if (code === BP_SCREW && screwDamageMask(b)) {
|
||||
const r = placeBlock(code, v, bx, by, false, extra);
|
||||
if (r) { last = r; break; }
|
||||
n++; continue;
|
||||
}
|
||||
if (replace && blockP[b]) {
|
||||
if (blockIs(b, code, v, extra)) continue;
|
||||
if (blockP[b] === code && isFilter(code)) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = filterVariant(code, v); setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
|
||||
|
||||
@@ -106,6 +106,7 @@ function machinesTick() {
|
||||
if (stamp[i] === tickStamp) continue;
|
||||
if (temp[i] > 1000 && rnd() < 0.02) {
|
||||
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
||||
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
|
||||
continue;
|
||||
}
|
||||
const s = netSat(i);
|
||||
|
||||
+14
-4
@@ -13,6 +13,13 @@ function undoRecord(label, fn) {
|
||||
undoRec = rec;
|
||||
try { return fn(); } finally { undoRec = prev; rec.label = typeof label === 'function' ? label(rec) : label; undoPush(rec); }
|
||||
}
|
||||
function undoScrewRepair(bx, by, damage, paid) {
|
||||
if (blockP[by * BW + bx] !== BP_SCREW) return false;
|
||||
const cells = blockCellList(bx, by);
|
||||
for (let j = 0; j < 16; j++) if (damage & (1 << j)) { const i = cells[j]; part[i] = 0; markPow(i, false); wakeIdx(i); }
|
||||
if (paid) refundAt(pieceCost(BP_SCREW, 0), 1, bx * BS + 2, by * BS + 2);
|
||||
return true;
|
||||
}
|
||||
function undoLast() {
|
||||
const rec = undoStack.pop();
|
||||
if (!rec) return 'Nothing to undo.';
|
||||
@@ -21,7 +28,10 @@ function undoLast() {
|
||||
const fail = () => { fails++; };
|
||||
for (let k = rec.ops.length - 1; k >= 0; k--) {
|
||||
const o = rec.ops[k];
|
||||
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
|
||||
if (o.t === 'screw-repair') {
|
||||
if (!undoScrewRepair(o.bx, o.by, o.damage, o.paid)) fail();
|
||||
}
|
||||
else if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
|
||||
else if (o.t === 'block-') { const r = placeBlock(o.code, isFilter(o.code) ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
|
||||
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (isFilter(blockP[b])) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
|
||||
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
|
||||
@@ -30,7 +40,7 @@ function undoLast() {
|
||||
else if (o.t === 'mold-') { const r = placeMoldAt(o.kind, o.bx, o.by, o.wall); if (r) fail(r); }
|
||||
else if (o.t === 'paste') {
|
||||
// ghosts still waiting are cleared; anything already built from them is taken back
|
||||
for (const [bx, by, code] of o.blocks) { const b = by * BW + bx; if (ghostBlocks.has(b)) { ghostBlocks.delete(b); ghostBlocked.delete(b); } else if (blockP[b] === code) removeBlock(bx, by); }
|
||||
for (const [bx, by, code, damage] of o.blocks) { const b = by * BW + bx; if (ghostBlocks.has(b)) { ghostBlocks.delete(b); ghostBlocked.delete(b); } else if (damage) { if (!undoScrewRepair(bx, by, damage, true)) fail(); } else if (blockP[b] === code) removeBlock(bx, by); }
|
||||
for (const g of o.machines) { const k = ghostMachines.indexOf(g); if (k >= 0) ghostMachines.splice(k, 1); else { const mm = machines.find(m => m && m.type === g.type && m.x === g.bx * BS && m.y === g.by * BS); if (mm) removeMachine(mm.id, true, false); } }
|
||||
for (const g of o.molds) { const k = ghostMolds.indexOf(g); if (k >= 0) ghostMolds.splice(k, 1); else { const mo = molds.find(m => m.kind === g.kind && m.x === g.bx * BS && m.y === g.by * BS); if (mo) removeMold(mo); } }
|
||||
}
|
||||
@@ -56,13 +66,13 @@ function ghostTick0() {
|
||||
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
||||
const [b, [code, v, e, swap]] = nx.value;
|
||||
const bx = b % BW, by = (b / BW) | 0;
|
||||
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
|
||||
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v) && !screwDamageMask(b)) {
|
||||
if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
||||
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
||||
}
|
||||
if (!blockInReach(bx, by)) continue;
|
||||
// a replacing ghost takes down what is there (refunded) and builds itself, like a replace in reach
|
||||
if (swap && blockP[b]) {
|
||||
if (swap && blockP[b] && !(code === BP_SCREW && screwDamageMask(b))) {
|
||||
if (blockP[b] === code && isFilter(code)) { blockV[b] = filterVariant(code, v); setFilter(b, e); filtersDirty = true; ghostBlocks.delete(b); ghostBlocked.delete(b); budget--; continue; }
|
||||
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||
removeBlock(bx, by);
|
||||
|
||||
+4
-2
@@ -108,7 +108,7 @@ function dragDirFor(P, a, b) {
|
||||
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
|
||||
}
|
||||
function blockLabel(P, rec) {
|
||||
const put = piecesIn(rec, 'block+'), took = piecesIn(rec, 'block-') + piecesIn(rec, 'filter');
|
||||
const put = piecesIn(rec, 'block+') + piecesIn(rec, 'screw-repair'), took = piecesIn(rec, 'block-') + piecesIn(rec, 'filter');
|
||||
return took ? `replacing ${plural(took, 'piece')}` : `placing ${plural(put, P.name.toLowerCase())}`;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,9 @@ function primaryRelease(shift) {
|
||||
const b = by * BW + bx;
|
||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
|
||||
// held G over a different piece: the ghost replaces it when the bot comes in range
|
||||
ghostBlocks.set(b, GAME.replace && blockP[b] ? [code, v, ex, 1] : ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
|
||||
ghostBlocks.set(b, GAME.replace && blockP[b] ? [code, v, ex, 1] : ex ? [code, v, ex] : [code, v]);
|
||||
const damage = code === BP_SCREW ? screwDamageMask(b) : 0;
|
||||
g.blocks.push(damage ? [bx, by, code, damage] : [bx, by, code]); ghosts++;
|
||||
}
|
||||
if (ghosts) undoNote(g);
|
||||
return res;
|
||||
|
||||
@@ -20,6 +20,7 @@ function drainEvents() {
|
||||
} else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m);
|
||||
else if (e.t === 'suck') spawnSuck(e.x, e.y, e.m);
|
||||
else if (e.t === 'warn') spawnFloater(e.x, e.y, e.text, '#ff8a72');
|
||||
else if (e.t === 'screw_melt') showCard({ kind: 'Heat damage', title: 'Screw conveyor melted', body: ['Loads hotter than 1000°C melt holes in the tube. Grains spill through the holes.', 'Build a screw conveyor over the damaged block to repair it for the price of one piece.'] });
|
||||
else if (e.t === 'sold') spawnFloater(e.x, e.y, '+' + fmtMoney(e.v));
|
||||
else if (e.t === 'order_new') {
|
||||
const o = e.o;
|
||||
|
||||
@@ -134,6 +134,14 @@ function hoverInfo(ax, ay) {
|
||||
}
|
||||
else if (isWall(b) && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} ${isSlope(b) ? 'sloped wall' : 'block'}`;
|
||||
const jb = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
|
||||
if (b === BP_SCREW && !bar) {
|
||||
if (screwDamageMask(jb)) {
|
||||
t1 = 'Screw conveyor · melted'; warn = 'needs rebuilding';
|
||||
t2.push('Melted from carrying a load hotter than 1000°C.', 'Build a screw conveyor over it to repair it for the price of one piece.');
|
||||
} else if (screwLoadHeat(jb) >= 900) {
|
||||
warn = 'near heat limit'; t2.push(`${Math.round(screwLoadHeat(jb))}°C load · melts above 1000°C`);
|
||||
}
|
||||
}
|
||||
if (GAME.infoOverlay && !id && !bar && pieceEnds(blockP[jb], blockV[jb]).length) t2.push(`moving ${(flowRate[jb] || 0).toFixed(1)} grains/s`);
|
||||
for (let end = 0; end < 2 && !id && !bar; end++) {
|
||||
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
|
||||
|
||||
@@ -79,6 +79,7 @@ function frame(now) {
|
||||
if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false);
|
||||
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
|
||||
drawJoinMarks();
|
||||
drawScrewHeat();
|
||||
drawPing();
|
||||
drawHoveredVein();
|
||||
drawPingRecharge();
|
||||
|
||||
Reference in New Issue
Block a user