2026-10-22 14:45:19 +00:00
|
|
|
import { grid, cols, rows, currentFrame, incrementFrame } from './state.js';
|
2026-10-22 14:05:51 +00:00
|
|
|
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';
|
2026-10-22 14:45:19 +00:00
|
|
|
import { updateGrass } from './grass.js';
|
2026-10-22 14:05:51 +00:00
|
|
|
import { updateFire } from './fire.js';
|
2026-10-22 14:45:19 +00:00
|
|
|
import { updateLava } from './lava.js';
|
|
|
|
|
import { updateOil } from './oil.js';
|
|
|
|
|
import { updateIce } from './ice.js';
|
|
|
|
|
import { updateSmoke } from './smoke.js';
|
2026-10-22 14:05:51 +00:00
|
|
|
import { updateExplosive, explode } from './explosive.js';
|
|
|
|
|
|
|
|
|
|
export function spawnCell(x, y, type) {
|
2026-10-22 14:45:19 +00:00
|
|
|
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);
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
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;
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
grid[x][y] = createCell(type);
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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
|
2026-10-22 14:05:51 +00:00
|
|
|
};
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function update() {
|
2026-10-22 14:45:19 +00:00
|
|
|
incrementFrame();
|
2026-10-22 14:05:51 +00:00
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
// Iterate bottom-to-top so falling particles don't fall multiple times in 1 frame
|
2026-10-22 14:05:51 +00:00
|
|
|
for (let y = rows - 1; y >= 0; y--) {
|
2026-10-22 14:45:19 +00:00
|
|
|
// 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);
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
function updateCell(x, y, cell) {
|
|
|
|
|
cell.lastUpdated = currentFrame;
|
2026-10-22 14:05:51 +00:00
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
// Tossed particles (kinetic velocity physics from explosions)
|
2026-10-22 14:05:51 +00:00
|
|
|
if (cell.vx || cell.vy) {
|
|
|
|
|
applyVelocity(x, y, cell);
|
|
|
|
|
if (!grid[x][y] || grid[x][y] !== cell) return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const type = cell.type;
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyVelocity(x, y, cell) {
|
|
|
|
|
cell.vx = cell.vx || 0;
|
|
|
|
|
cell.vy = cell.vy || 0;
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
if (cell.type === 'steam' || cell.type === 'smoke') {
|
|
|
|
|
cell.vy -= 0.1;
|
2026-10-22 14:05:51 +00:00
|
|
|
} else {
|
|
|
|
|
cell.vy += config.global.gravity;
|
|
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
cell.vx *= config.global.drag;
|
|
|
|
|
cell.vy *= config.global.drag;
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
let nextX = Math.round(x + cell.vx);
|
|
|
|
|
let nextY = Math.round(y + cell.vy);
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
// Wall bounces
|
|
|
|
|
if (nextX < 0) {
|
|
|
|
|
nextX = 0;
|
|
|
|
|
cell.vx *= -0.5;
|
|
|
|
|
} else if (nextX >= cols) {
|
|
|
|
|
nextX = cols - 1;
|
2026-10-22 14:05:51 +00:00
|
|
|
cell.vx *= -0.5;
|
|
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
if (nextY < 0) {
|
|
|
|
|
nextY = 0;
|
|
|
|
|
cell.vy *= -0.4;
|
|
|
|
|
} else if (nextY >= rows) {
|
|
|
|
|
nextY = rows - 1;
|
2026-10-22 14:05:51 +00:00
|
|
|
cell.vy *= -0.3;
|
|
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
if (nextX >= 0 && nextX < cols && nextY >= 0 && nextY < rows) {
|
|
|
|
|
const neighbor = grid[nextX][nextY];
|
|
|
|
|
if (!neighbor) {
|
|
|
|
|
move(x, y, nextX, nextY);
|
2026-10-22 14:45:19 +00:00
|
|
|
} else if (neighbor.type === 'steam' || neighbor.type === 'smoke') {
|
2026-10-22 14:05:51 +00:00
|
|
|
swap(x, y, nextX, nextY);
|
2026-10-22 14:45:19 +00:00
|
|
|
} 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;
|
2026-10-22 14:05:51 +00:00
|
|
|
} else {
|
|
|
|
|
cell.vx *= -0.3;
|
|
|
|
|
cell.vy *= -0.3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|