// Weather: clouds from steam, rain and snow, and the world water count. // ---------- Weather ---------- // Steam that rises into the open sky is gathered into clouds above where it rose. The clouds // drift slowly with the wind and rain it back out, or snow where the ground is below freezing. // Water is only ever moved around, never made, so the world neither floods nor runs dry. If // some does leave (dropped into the trade dock, say), a shower blows in from beyond the claim // now and then with just enough to make it up. const WBAND = 64; let WS = 0x1b873593 | 0; function wrnd() { WS ^= WS << 13; WS ^= WS >>> 17; WS ^= WS << 5; return (WS >>> 0) * 2.3283064365386963e-10; } let weather = { moist: new Float32Array(1), raining: new Uint8Array(1), shower: null, next: 0, wind: 0.3, seen: false, skyY: 1 }; function resetWeather() { let top = H; for (let x = 0; x < W; x++) if (surfY[x] < top) top = surfY[x]; weather = { moist: new Float32Array(Math.ceil(W / WBAND)), raining: new Uint8Array(Math.ceil(W / WBAND)), shower: null, next: tick + rainGap(), wind: wrnd() < 0.5 ? -0.3 : 0.3, seen: false, skyY: Math.max(1, top - 40), base: 0 }; } function rainGap() { return 3600 * (5 + wrnd() * 7); } // 5 to 12 minutes between checks on the world's water // All the world's water: everything that is water or will be again (ice, snow, steam, clouds, // rain in the air, what is in your bag). weather.base is how much a world started with. function worldWater() { let n = 0; for (let i = 0; i < N; i++) { const m = mat[i]; if (m === WATER || m === ICE || m === SNOW || m === STEAM) n++; } for (let b = 0; b < weather.moist.length; b++) n += weather.moist[b]; for (const f of flyers) if (f.m === WATER || f.m === SNOW) n++; for (const pn of pipeNets) if (pn.m === WATER) n += pn.n; // water in pipes still counts for (const mm of machines) if (mm && mm.type === 'silo' && mm.sm === SNOW) n += mm.sn; // and snow in silos return Math.round(n + bag[WATER] + bag[SNOW]); } function absorbSteam(x) { if (chunkWork) { chunkEvent('moist', x); return; } const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)); weather.moist[b] += 1; } // Send about k drops (snowflakes where it is freezing) down from the clouds over x0..x1; returns how many fell function spawnRain(x0, x1, k) { let airborne = 0; for (const f of flyers) if (f.rain) airborne++; if (airborne >= 2500) return 0; let n = Math.floor(k) + (wrnd() < k - Math.floor(k) ? 1 : 0), made = 0; n = Math.min(n, 2500 - airborne); for (; n > 0; n--) { const x = Math.floor(x0 + wrnd() * (x1 - x0)); if (x < 0 || x >= W) continue; const y = Math.min(weather.skyY, Math.max(1, roofY[x] - 30)); const ground = Math.min(H - 1, roofY[x]), cold = AMB[ground] < 0; flyers.push({ x: x + wrnd(), y: y + wrnd() * 6, vx: weather.wind * (cold ? 0.35 : 0.12), vy: cold ? 0.15 : 0.5, age: 0, m: cold ? SNOW : WATER, T: cold ? Math.min(AMB[ground], -2) : Math.max(4, Math.min(18, AMB[ground])), free: true, run: 0, rain: true }); made++; } return made; } // How hard it is raining over column x, 0 (dry) to 1 (a downpour) function rainAt(x) { let r = 0; const sh = weather.shower; if (sh && x >= sh.x0 - 40 && x <= sh.x1 + 40) r += sh.rate * sh.ramp * (x >= sh.x0 && x <= sh.x1 ? 1 : 0.5); const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)), mo = weather.moist[b]; if (weather.raining[b]) r += Math.min(3, 0.5 + mo / 100); return Math.min(1, r / 2); } function weatherTick() { const w = weather, n = w.moist.length; if (!w.shower && tick >= w.next) { // a shower from beyond the claim only when the world is short of the water it started with const short = w.base ? w.base - worldWater() : 0; if (short > Math.max(60, w.base * 0.03)) { const len = Math.min(W, 160 + wrnd() * 380), x0 = wrnd() * Math.max(1, W - len); w.shower = { x0, x1: x0 + len, rate: 0.6 + wrnd() * 1.6, until: tick + 60 * (40 + Math.floor(wrnd() * 80)), ramp: 0, left: short }; w.wind = (wrnd() - 0.5) * 0.8; emit({ t: 'weather', kind: 'shower', x0: Math.round(x0), x1: Math.round(x0 + len) }); } else w.next = tick + rainGap(); } if (w.shower) { const sh = w.shower; sh.ramp = tick < sh.until ? Math.min(1, sh.ramp + 1 / 300) : sh.ramp - 1 / 300; // fades in and out over 5 s sh.x0 += w.wind * 0.03; sh.x1 += w.wind * 0.03; if (sh.ramp <= 0) { w.shower = null; w.next = tick + rainGap(); } else if (!(sh.left <= 0)) { const made = spawnRain(sh.x0, sh.x1, Math.min(sh.left === undefined ? 1e9 : sh.left, sh.rate * sh.ramp * (sh.x1 - sh.x0) / 300)); if (sh.left !== undefined) { sh.left -= made; if (sh.left <= 0 && sh.until > tick) sh.until = tick; } // made up the shortfall: it passes } } // clouds made of your steam build up, then rain it back out over where they hang and a band // either side, so water boiled off one spot does not all fall back on it for (let b = 0; b < n; b++) { if (!w.raining[b] && w.moist[b] > 150) w.raining[b] = 1; if (w.raining[b]) { w.moist[b] -= spawnRain(Math.max(0, (b - 1) * WBAND), Math.min(W, (b + 2) * WBAND), Math.min(3, 0.5 + w.moist[b] / 100)); if (w.moist[b] < 12) w.raining[b] = 0; } } // and drift a little downwind every second, raining ones at half the pace; at the edge of the // world they pile up instead if (tick % 60 === 0) { const out = new Float32Array(n), d = w.wind > 0 ? 1 : -1, f = Math.min(0.12, Math.abs(w.wind) * 0.1); for (let b = 0; b < n; b++) { const mv = w.moist[b] * f * (w.raining[b] ? 0.5 : 1); out[b] += w.moist[b] - mv; if (b + d >= 0 && b + d < n) out[b + d] += mv; else out[b] += mv; } w.moist = out; } }