import { grid, cols, rows, currentFrame, incrementFrame } from './state.js'; import { config } from './config.js'; import { move, swap } from './actions.js'; import { updateDirt } from './dirt.js'; import { updateWater } from './water.js'; import { updateAcid } from './acid.js'; import { updateSteam } from './steam.js'; import { updatePlant } from './plant.js'; import { updateGrass } from './grass.js'; import { updateFire } from './fire.js'; import { updateLava } from './lava.js'; import { updateOil } from './oil.js'; import { updateIce } from './ice.js'; import { updateSmoke } from './smoke.js'; import { updateExplosive, explode } from './explosive.js'; export function spawnCell(x, y, type) { if (x < 0 || x >= cols || y < 0 || y >= rows) return; if (type === 'eraser' || type === 'clear') { grid[x][y] = null; return; } const target = grid[x][y]; // Fire detonates TNT directly if (target && target.type === 'explosive' && (type === 'fire' || type === 'lava')) { if (!config.time.paused) { explode(x, y); } return; } // Displacement of fluids/gases when spawning solids or denser fluids const isDisplaceable = target && (target.type === 'water' || target.type === 'acid' || target.type === 'oil' || target.type === 'steam' || target.type === 'smoke'); if (!target || (isDisplaceable && type !== target.type)) { if (isDisplaceable) { // Push aside fluid to a neighbor cell if available const dxs = [0, 0, -1, 1]; const dys = [-1, 1, 0, 0]; for (let i = 0; i < 4; i++) { const nx = x + dxs[i]; const ny = y + dys[i]; if (nx >= 0 && nx < cols && ny >= 0 && ny < rows && !grid[nx][ny]) { grid[nx][ny] = target; target.lastUpdated = currentFrame; target.vx = (target.vx || 0) + dxs[i] * 1.5; target.vy = (target.vy || 0) + dys[i] * 1.5 - 0.5; break; } } } grid[x][y] = createCell(type); } } function packABGR(r, g, b, a = 255) { return ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF); } function clampColor(val) { return Math.max(0, Math.min(255, Math.round(val))); } export function createCell(type) { if (type === 'eraser' || type === 'clear') return null; // Palette with natural variance: [r, g, b, jitter] const palettes = { sand: [235, 192, 98, 24], dirt: [136, 88, 48, 22], water: [35, 130, 235, 18], lava: [255, 95, 0, 32], oil: [48, 38, 28, 12], ice: [195, 235, 250, 16], plant: [34, 139, 34, 20], grass: [56, 188, 42, 22], fire: [255, 190, 30, 35], rock: [120, 125, 132, 24], wood: [112, 64, 32, 18], explosive: [188, 32, 32, 20], acid: [115, 255, 24, 20], steam: [225, 245, 252, 12], smoke: [72, 75, 82, 18] }; const p = palettes[type] || [220, 220, 220, 15]; const enableNoise = config.visual.colorNoise !== false; const j = enableNoise ? p[3] : 0; const noise = (Math.random() - 0.5) * j; const r = clampColor(p[0] + noise); const g = clampColor(p[1] + noise * 0.9); const b = clampColor(p[2] + noise * 0.8); const u32Color = packABGR(r, g, b, 255); const cell = { type, u32Color, lastUpdated: currentFrame }; if (type === 'smoke') { cell.lifespan = Math.floor(Math.random() * 90) + 40; } else if (type === 'fire') { cell.lifespan = Math.floor(Math.random() * 20) + 10; cell.maxLife = cell.lifespan; } return cell; } export function update() { incrementFrame(); // Iterate bottom-to-top so falling particles don't fall multiple times in 1 frame for (let y = rows - 1; y >= 0; y--) { // Alternate sweep direction every frame for perfectly symmetric fluid/sand piling const sweepLeft = (currentFrame % 2 === 0); for (let i = 0; i < cols; i++) { const x = sweepLeft ? (cols - 1 - i) : i; const cell = grid[x][y]; if (cell && cell.lastUpdated !== currentFrame) { updateCell(x, y, cell); } } } } function updateCell(x, y, cell) { cell.lastUpdated = currentFrame; // Tossed particles (kinetic velocity physics from explosions) if (cell.vx || cell.vy) { applyVelocity(x, y, cell); if (!grid[x][y] || grid[x][y] !== cell) return; } const type = cell.type; switch (type) { case 'sand': case 'dirt': updateDirt(x, y, cell); break; case 'water': updateWater(x, y, cell); break; case 'lava': updateLava(x, y, cell); break; case 'oil': updateOil(x, y, cell); break; case 'ice': updateIce(x, y, cell); break; case 'fire': updateFire(x, y, cell); break; case 'smoke': updateSmoke(x, y, cell); break; case 'steam': updateSteam(x, y, cell); break; case 'acid': updateAcid(x, y, cell); break; case 'plant': updatePlant(x, y, cell); break; case 'grass': updateGrass(x, y, cell); break; case 'rock': updateRock(x, y, cell); break; case 'explosive': updateExplosive(x, y, cell); break; } } function updateRock(x, y, cell) { const below = y + 1; if (below < rows) { const belowCell = grid[x][below]; if (belowCell && (belowCell.type === 'water' || belowCell.type === 'oil' || belowCell.type === 'acid')) { swap(x, y, x, below); } } } function applyVelocity(x, y, cell) { cell.vx = cell.vx || 0; cell.vy = cell.vy || 0; if (cell.type === 'steam' || cell.type === 'smoke') { cell.vy -= 0.1; } else { cell.vy += config.global.gravity; } cell.vx *= config.global.drag; cell.vy *= config.global.drag; // Settle kinetic particles when velocity is low if (Math.abs(cell.vx) < 0.1 && Math.abs(cell.vy) < 0.1) { cell.vx = 0; cell.vy = 0; return; } let nextX = Math.round(x + cell.vx); let nextY = Math.round(y + cell.vy); // Wall bounces if (nextX < 0) { nextX = 0; cell.vx *= -0.5; } else if (nextX >= cols) { nextX = cols - 1; cell.vx *= -0.5; } if (nextY < 0) { nextY = 0; cell.vy *= -0.4; } else if (nextY >= rows) { nextY = rows - 1; cell.vy *= -0.3; } if (nextX >= 0 && nextX < cols && nextY >= 0 && nextY < rows) { const neighbor = grid[nextX][nextY]; if (!neighbor) { move(x, y, nextX, nextY); } else if (neighbor.type === 'steam' || neighbor.type === 'smoke') { swap(x, y, nextX, nextY); } else if (neighbor.type === 'water' || neighbor.type === 'dirt' || neighbor.type === 'sand' || neighbor.type === 'acid' || neighbor.type === 'oil') { neighbor.vx = (neighbor.vx || 0) + cell.vx * 0.6; neighbor.vy = (neighbor.vy || 0) + cell.vy * 0.6; cell.vx *= -0.3; cell.vy *= -0.3; } else { cell.vx *= -0.3; cell.vy *= -0.3; } } }