SandGame/physics.js

255 lines
7.4 KiB
JavaScript
Raw Normal View History

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';
import { updateGrass } from './grass.js';
2026-10-22 14:05:51 +00:00
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';
2026-10-22 14:05:51 +00:00
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);
2026-10-22 14:05:51 +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
}
}
}
grid[x][y] = createCell(type);
2026-10-22 14:05:51 +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
};
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() {
incrementFrame();
2026-10-22 14:05:51 +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--) {
// 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
}
}
}
}
function updateCell(x, y, cell) {
cell.lastUpdated = currentFrame;
2026-10-22 14:05:51 +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;
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;
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:05:51 +00:00
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;
}
2026-10-22 14:05:51 +00:00
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;
2026-10-22 14:05:51 +00:00
cell.vx *= -0.5;
}
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: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);
} else if (neighbor.type === 'steam' || neighbor.type === 'smoke') {
2026-10-22 14:05:51 +00:00
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;
2026-10-22 14:05:51 +00:00
} else {
cell.vx *= -0.3;
cell.vy *= -0.3;
}
}
}