// Trees (background layer), acorns, saplings, and waking freezing water. // ---------- Trees (background layer) ---------- // Trunk cells drill into wood chunks; leaves just fall apart. A tree whose trunk // no longer reaches the ground comes down: every trunk cell drops as wood chunks. function cutTree(i, x, y, tier, amt = 1) { const b = bgm[i]; renderDirtyIdx(i); const p = digp[i] + amt * tier.speed * 70 / (1 + HARD[b]); if (p < 255) { digp[i] = p; return false; } digp[i] = 0; bgm[i] = 0; if (b === WOOD && woodRoom(i)) { setCell(i, LWOOD, temp[i], 0); discover(LWOOD, x, y, WOOD, 'dig'); } else if (b === LEAVES && mat[i] === EMPTY && rnd() < 0.03) { setCell(i, ACORN, temp[i], 0); discover(ACORN, x, y, LEAVES, 'dig'); } else { wakeXY(x, y); emit({ t: 'debris', x, y, m: b }); } if (b === WOOD) for (const [dx, dy] of [[1, 0], [-1, 0], [0, -1], [0, 1], [1, -1], [-1, -1]]) { const nx = x + dx, ny = y + dy; if (nx >= 0 && ny >= 0 && nx < W && ny < H && bgm[ny * W + nx]) treeFall(ny * W + nx); } return true; } // A trunk cell is held up by solid ground or soil directly beneath it. function treeHeld(i) { if (i + W >= N) return true; const b = mat[i + W]; return PH[b] === PH_SOLID || SOIL[b] === 1; } // Base cells of every tree (the lowest trunk cells). Checked now and then: a tree // whose ground has been dug or washed away comes down. let treeBases = []; function findTreeBases() { treeBases = []; for (let i = 0; i < N - W; i++) if (bgm[i] === WOOD && bgm[i + W] !== WOOD) treeBases.push(i); } function treeSupportTick() { for (let k = treeBases.length - 1; k >= 0; k--) { const i = treeBases[k]; if (bgm[i] !== WOOD) { treeBases.splice(k, 1); continue; } if (!treeHeld(i)) { treeBases.splice(k, 1); treeFall(i); } } } // Make room for a wood chunk where a trunk cell was: air is fine; water there is // pushed to the nearest open cell (the chunk then floats up). function woodRoom(i) { const m = mat[i]; if (m === EMPTY || PH[m] === PH_GAS) return true; if (PH[m] !== PH_LIQUID) return false; const T = temp[i]; mat[i] = EMPTY; depositNear(m, T, i % W, (i / W) | 0, i); // if there is nowhere at all, that bit of water is lost return true; } function treeFall(start) { if (bgm[start] !== WOOD) return; // follow the wood (trunk and branches) to see whether any of it still stands on something const seenT = new Set([start]), st = [start], woodCells = []; let grounded = false, bx0 = W, bx1 = 0, by0 = H, by1 = 0; while (st.length && woodCells.length < 2000) { const i = st.pop(); woodCells.push(i); const x = i % W, y = (i / W) | 0; if (treeHeld(i)) { grounded = true; break; } if (x < bx0) bx0 = x; if (x > bx1) bx1 = x; if (y < by0) by0 = y; if (y > by1) by1 = y; for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { const nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; const j = ny * W + nx; if (bgm[j] === WOOD && !seenT.has(j)) { seenT.add(j); st.push(j); } } } if (grounded) return; // its crown: leaves connected to it, not straying far from its own wood const cells = woodCells.slice(); for (const i of woodCells) st.push(i); bx0 -= 9; bx1 += 9; by0 -= 12; by1 += 4; while (st.length && cells.length < 4000) { const i = st.pop(), x = i % W, y = (i / W) | 0; for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { const nx = x + dx, ny = y + dy; if (nx < bx0 || ny < by0 || nx > bx1 || ny > by1 || nx < 0 || ny < 0 || nx >= W || ny >= H) continue; const j = ny * W + nx; if (bgm[j] === LEAVES && !seenT.has(j)) { seenT.add(j); st.push(j); cells.push(j); } } } let wood = 0; // a felled tree drops a few acorns from its crown const crown = cells.filter(i => bgm[i] === LEAVES && mat[i] === EMPTY); const nAcorn = Math.min(crown.length, 1 + (crown.length > 60 ? 1 : 0) + (rnd() < 0.5 ? 1 : 0)); for (let k = 0; k < nAcorn; k++) { const i = crown[(rnd() * crown.length) | 0]; if (mat[i] === EMPTY) { setCell(i, ACORN, AMB[(i / W) | 0], 0); bgm[i] = 0; discover(ACORN, i % W, (i / W) | 0, LEAVES, 'dig'); } } for (const i of cells) { const x = i % W, y = (i / W) | 0; if (bgm[i] === WOOD && woodRoom(i)) { setCell(i, LWOOD, temp[i], 0); wood++; } else if (bgm[i] && (x + y) % 3 === 0) emit({ t: 'debris', x, y, m: bgm[i] }); bgm[i] = 0; wakeXY(x, y); } if (wood) { discover(LWOOD, cells[0] % W, (cells[0] / W) | 0, WOOD, 'dig'); emit({ t: 'sfx', s: 'decon' }); } } // Acorns resting on soil under open sky sprout after a while; saplings grow // into full trees in the background layer. let saplings = []; // Heat only runs where something is off ambient, so water resting in freezing air // would never be looked at. Wake its area now and then so it can freeze. function coldTick() { for (let y = 0; y < H && AMB[y] < -0.5; y++) { const yw = y * W; for (let x = 0; x < W; x++) if (mat[yw + x] === WATER) { wakeHeatXY(x, y); x |= 31; } } } function plantTick() { for (let x = 0; x < W; x++) { const y = roofY[x]; if (y <= 6 || y >= H - 1) continue; const i = y * W + x; if (mat[i] !== ACORN || (flags[i] & F_BURN)) continue; renderDirtyIdx(i); if (!SOIL[mat[i + W]] || temp[i] < 2 || temp[i] > 45) { life[i] = 0; continue; } let room = true; for (let k = 1; k <= 6 && room; k++) { const j = i - k * W; if (PH[mat[j]] !== PH_EMPTY && PH[mat[j]] !== PH_GAS || bgm[j] === WOOD) room = false; } if (!room) continue; if (++life[i] < 14) continue; setCell(i, EMPTY, AMB[y], 0); saplings.push({ x, y: y + 1, age: 0, th: 8 + ((rnd() * 7) | 0), seed: (rnd() * 1e9) | 0 }); if (!stats.sprouted) { stats.sprouted = 1; emit({ t: 'sprout', x, y }); } } } let treeDraw = null; // cells the sapling being drawn has set, so the next stage can redraw it function setTreeCell(x, y, m) { if (x < 0 || y < 1 || x >= W || y >= H) return; const i = y * W + x; if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return; if (m === LEAVES && bgm[i]) return; bgm[i] = m; renderDirtyIdx(i); if (treeDraw) treeDraw.push(i); } function saplingTick() { for (let k = saplings.length - 1; k >= 0; k--) { const sp = saplings[k], base = sp.y * W + sp.x; // the soil it grows from must still be there if (sp.y >= H || !(SOIL[mat[base]] || PH[mat[base]] === PH_SOLID)) { saplings.splice(k, 1); continue; } if (sp.age > 2 && bgm[base - W] !== WOOD) { saplings.splice(k, 1); continue; } // cut down or burned while young // if any trunk it grew is gone (cut or burned), it stops growing and is left as it is if (sp.wood && sp.wood.some(i => bgm[i] !== WOOD)) { saplings.splice(k, 1); continue; } sp.age++; const s = Math.min(1, sp.age / 180); // wipe the previous stage, then draw this one if (sp.cells) for (const i of sp.cells) if (bgm[i] === LEAVES || (bgm[i] === WOOD && i !== base - W)) { bgm[i] = 0; renderDirtyIdx(i); } treeDraw = sp.cells = []; const th = Math.max(1, Math.round(sp.th * s)), wide = s > 0.5 ? 2 : 1; for (let j = 1; j <= th; j++) for (let dx = 0; dx < wide; dx++) setTreeCell(sp.x + dx, sp.y - j, WOOD); if (s > 0.2) { // the same lumpy crown as wild trees, scaled with age const q = s, h0 = sp.seed; if (s >= 1) for (const side of [-1, 1]) { const by = sp.y - th + 3 + (h0 >> (side > 0 ? 3 : 5)) % 3; for (let j = 1; j <= Math.round(3 * q); j++) setTreeCell(sp.x + (side < 0 ? -j : 1 + j), by - (j >> 1), WOOD); } const blobs = [[0.5, -th - 1, (6 + (h0 % 25) / 10) * q], [-3.5 * q, -th + 1, (4 + (h0 >> 4) % 20 / 10) * q], [4.5 * q, -th + 1, (4 + (h0 >> 8) % 20 / 10) * q], [0.5, -th - 5 * q, (4 + (h0 >> 12) % 20 / 10) * q]]; for (const [bx, by, r] of blobs) { if (r < 1) continue; const cx = sp.x + bx, cy = sp.y + by; for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) { const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85); if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) setTreeCell(xx, yy, LEAVES); } } } treeDraw = null; sp.wood = sp.cells.filter(i => bgm[i] === WOOD); for (const i of sp.wood) if (bgm[i + W] !== WOOD && !treeBases.includes(i)) treeBases.push(i); if (s >= 1) { saplings.splice(k, 1); stats.grown = (stats.grown || 0) + 1; } } } // A flame next to a tree sets that part of it alight: it moves into the // foreground as burning wood or leaves and burns like any other fuel. function igniteTree(j) { const b = bgm[j]; bgm[j] = 0; mat[j] = b; flags[j] = F_BURN; life[j] = FUEL[b]; temp[j] = Math.max(temp[j], IGN[b] + 40); const x = j % W, y = (j / W) | 0; wakeXY(x, y); wakeHeatXY(x, y); }