// Constants const CHUNK_SIZE = 200; const PIXEL_SIZE = 4; const GRAVITY = 0.5; const WATER_SPREAD = 3; const SAND_COLOR = '#e6c588'; const WATER_COLOR = '#4a80f5'; const WALL_COLOR = '#888888'; // Element types const EMPTY = 0; const SAND = 1; const WATER = 2; const WALL = 3; // Global variables let canvas, ctx; let currentTool = SAND; let isDrawing = false; let isDragging = false; let lastMouseX, lastMouseY; let currentMouseX, currentMouseY; let lastFrameTime = 0; let fps = 0; let worldOffsetX = 0; let worldOffsetY = 0; let chunks = new Map(); // Map to store chunks with key "x,y" let debugMode = false; // Initialize the simulation window.onload = function() { canvas = document.getElementById('simulation-canvas'); ctx = canvas.getContext('2d'); // Set canvas size to fill the screen resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Tool selection document.getElementById('sand-btn').addEventListener('click', () => setTool(SAND)); document.getElementById('water-btn').addEventListener('click', () => setTool(WATER)); document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY)); // Navigation controls document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0)); document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0)); document.getElementById('move-up').addEventListener('click', () => moveWorld(0, -CHUNK_SIZE/2)); document.getElementById('move-down').addEventListener('click', () => moveWorld(0, CHUNK_SIZE/2)); document.getElementById('debug-btn').addEventListener('click', toggleDebug); // Drawing events canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); canvas.addEventListener('mouseleave', handleMouseUp); // Touch events for mobile canvas.addEventListener('touchstart', handleTouchStart); canvas.addEventListener('touchmove', handleTouchMove); canvas.addEventListener('touchend', handleMouseUp); // Initialize the first chunk getOrCreateChunk(0, 0); // Start the simulation loop requestAnimationFrame(simulationLoop); }; function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight; } function setTool(tool) { currentTool = tool; document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active')); if (tool === SAND) { document.getElementById('sand-btn').classList.add('active'); } else if (tool === WATER) { document.getElementById('water-btn').classList.add('active'); } else if (tool === EMPTY) { document.getElementById('eraser-btn').classList.add('active'); } } function handleMouseDown(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Right mouse button for dragging the world if (e.button === 2 || e.ctrlKey || e.shiftKey) { isDragging = true; lastMouseX = x; lastMouseY = y; } else { // Left mouse button for drawing isDrawing = true; draw(x, y); } } function handleMouseMove(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Always update current mouse position currentMouseX = x; currentMouseY = y; if (isDragging) { // Calculate how much the mouse has moved const dx = x - lastMouseX; const dy = y - lastMouseY; // Move the world in the opposite direction (divide by pixel size to convert to world coordinates) moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE); // Update the last mouse position lastMouseX = x; lastMouseY = y; } else if (isDrawing) { draw(x, y); } } function handleMouseUp(e) { isDrawing = false; isDragging = false; } function draw(x, y) { if (!isDrawing) return; // Convert screen coordinates to world coordinates const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX; const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY; // Draw a small brush (3x3) for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { setPixel(worldX + dx, worldY + dy, currentTool); } } } function handleTouchStart(e) { e.preventDefault(); // Check if we have multiple touch points (for dragging) if (e.touches.length > 1) { isDragging = true; lastMouseX = e.touches[0].clientX; lastMouseY = e.touches[0].clientY; } else { // Single touch for drawing isDrawing = true; const rect = canvas.getBoundingClientRect(); const x = e.touches[0].clientX - rect.left; const y = e.touches[0].clientY - rect.top; draw(x, y); } } function handleTouchMove(e) { e.preventDefault(); const rect = canvas.getBoundingClientRect(); if (isDragging && e.touches.length > 1) { // Calculate how much the touch has moved const x = e.touches[0].clientX; const y = e.touches[0].clientY; const dx = x - lastMouseX; const dy = y - lastMouseY; // Move the world in the opposite direction moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE); // Update the last touch position lastMouseX = x; lastMouseY = y; } else if (isDrawing) { const x = e.touches[0].clientX - rect.left; const y = e.touches[0].clientY - rect.top; draw(x, y); } } function moveWorld(dx, dy) { worldOffsetX += dx; worldOffsetY += dy; updateCoordinatesDisplay(); } function updateCoordinatesDisplay() { const chunkX = Math.floor(worldOffsetX / CHUNK_SIZE); const chunkY = Math.floor(worldOffsetY / CHUNK_SIZE); document.getElementById('coords').textContent = `Chunk: ${chunkX},${chunkY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`; } function getChunkKey(chunkX, chunkY) { return `${chunkX},${chunkY}`; } function getOrCreateChunk(chunkX, chunkY) { const key = getChunkKey(chunkX, chunkY); if (!chunks.has(key)) { // Create a new chunk with empty pixels const chunkData = new Array(CHUNK_SIZE * CHUNK_SIZE).fill(EMPTY); // Add floor at the bottom of the world (y = 0) if (chunkY === 0) { // Fill the bottom row with walls for (let x = 0; x < CHUNK_SIZE; x++) { chunkData[0 * CHUNK_SIZE + x] = WALL; } } chunks.set(key, chunkData); } return chunks.get(key); } function getChunkCoordinates(worldX, worldY) { const chunkX = Math.floor(worldX / CHUNK_SIZE); const chunkY = Math.floor(worldY / CHUNK_SIZE); const localX = ((worldX % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; const localY = ((worldY % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; return { chunkX, chunkY, localX, localY }; } function setPixel(worldX, worldY, type) { const { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY); const chunk = getOrCreateChunk(chunkX, chunkY); const index = localY * CHUNK_SIZE + localX; chunk[index] = type; } function getPixel(worldX, worldY) { // Special case: floor at the bottom of the world if (worldY === 0) { return WALL; } const { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY); const key = getChunkKey(chunkX, chunkY); if (!chunks.has(key)) { return EMPTY; } const chunk = chunks.get(key); const index = localY * CHUNK_SIZE + localX; return chunk[index]; } function simulationLoop(timestamp) { // Calculate FPS const deltaTime = timestamp - lastFrameTime; lastFrameTime = timestamp; fps = Math.round(1000 / deltaTime); document.getElementById('fps').textContent = `FPS: ${fps}`; // Update physics updatePhysics(); // Render render(); // Continue the loop requestAnimationFrame(simulationLoop); } function updatePhysics() { // Get visible chunks const visibleChunks = getVisibleChunks(); // Process each visible chunk for (const { chunkX, chunkY } of visibleChunks) { const chunk = getOrCreateChunk(chunkX, chunkY); // Process from bottom to top, right to left for correct gravity simulation for (let y = CHUNK_SIZE - 1; y >= 0; y--) { // Alternate direction each row for more natural flow const startX = y % 2 === 0 ? 0 : CHUNK_SIZE - 1; const endX = y % 2 === 0 ? CHUNK_SIZE : -1; const step = y % 2 === 0 ? 1 : -1; for (let x = startX; x !== endX; x += step) { const index = y * CHUNK_SIZE + x; const type = chunk[index]; if (type === EMPTY) continue; const worldX = chunkX * CHUNK_SIZE + x; const worldY = chunkY * CHUNK_SIZE + y; if (type === SAND) { updateSand(worldX, worldY); } else if (type === WATER) { updateWater(worldX, worldY); } } } } } function updateSand(x, y) { // Try to move down if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, SAND); } // Try to move down-left or down-right else if (getPixel(x - 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y + 1, SAND); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, SAND); } // Sand can displace water else if (getPixel(x, y + 1) === WATER) { setPixel(x, y, WATER); setPixel(x, y + 1, SAND); } } function updateWater(x, y) { // Try to move down if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, WATER); } // Try to move down-left or down-right else if (getPixel(x - 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y + 1, WATER); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, WATER); } // Try to spread horizontally else { let moved = false; // Randomly choose direction first const goLeft = Math.random() > 0.5; if (goLeft && getPixel(x - 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y, WATER); moved = true; } else if (!goLeft && getPixel(x + 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y, WATER); moved = true; } // Try the other direction if first failed else if (!goLeft && getPixel(x - 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y, WATER); moved = true; } else if (goLeft && getPixel(x + 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y, WATER); moved = true; } } } function getVisibleChunks() { const visibleChunks = []; // Calculate visible chunk range const startChunkX = Math.floor(worldOffsetX / CHUNK_SIZE) - 1; const endChunkX = Math.ceil((worldOffsetX + canvas.width / PIXEL_SIZE) / CHUNK_SIZE) + 1; const startChunkY = Math.floor(worldOffsetY / CHUNK_SIZE) - 1; const endChunkY = Math.ceil((worldOffsetY + canvas.height / PIXEL_SIZE) / CHUNK_SIZE) + 1; for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) { for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) { visibleChunks.push({ chunkX, chunkY }); } } return visibleChunks; } function toggleDebug() { debugMode = !debugMode; document.getElementById('debug-btn').classList.toggle('active'); } function render() { // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Get visible chunks const visibleChunks = getVisibleChunks(); // Render each visible chunk for (const { chunkX, chunkY } of visibleChunks) { const key = getChunkKey(chunkX, chunkY); if (!chunks.has(key)) continue; const chunk = chunks.get(key); // Calculate screen position of chunk const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE; const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE; // Draw chunk border in debug mode if (debugMode) { ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 1; ctx.strokeRect( screenX, screenY, CHUNK_SIZE * PIXEL_SIZE, CHUNK_SIZE * PIXEL_SIZE ); // Draw chunk coordinates ctx.fillStyle = '#ffffff'; ctx.font = '12px Arial'; ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15); } // Render each pixel in the chunk for (let y = 0; y < CHUNK_SIZE; y++) { for (let x = 0; x < CHUNK_SIZE; x++) { const index = y * CHUNK_SIZE + x; const type = chunk[index]; if (type === EMPTY) continue; // Set color based on type if (type === SAND) { ctx.fillStyle = SAND_COLOR; } else if (type === WATER) { ctx.fillStyle = WATER_COLOR; } else if (type === WALL) { ctx.fillStyle = WALL_COLOR; } // Draw the pixel ctx.fillRect( screenX + x * PIXEL_SIZE, screenY + y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE ); } } } // Draw cursor position in debug mode if (debugMode) { const rect = canvas.getBoundingClientRect(); const mouseX = lastMouseX || 0; const mouseY = lastMouseY || 0; const worldX = Math.floor(mouseX / PIXEL_SIZE) + worldOffsetX; const worldY = Math.floor(mouseY / PIXEL_SIZE) + worldOffsetY; document.getElementById('coords').textContent = `Chunk: ${Math.floor(worldOffsetX / CHUNK_SIZE)},${Math.floor(worldOffsetY / CHUNK_SIZE)} | ` + `Mouse: ${worldX},${worldY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`; } }