// 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'; const DIRT_COLOR = '#8B4513'; const STONE_COLOR = '#A9A9A9'; const GRASS_COLOR = '#7CFC00'; const WOOD_COLOR = '#8B5A2B'; const SEED_COLOR = '#654321'; const FLOWER_COLORS = ['#FF0000', '#FFFF00', '#FF00FF', '#FFA500', '#FFFFFF', '#00FFFF']; const LEAF_COLOR = '#228B22'; const FIRE_COLORS = ['#FF0000', '#FF3300', '#FF6600', '#FF9900', '#FFCC00', '#FFFF00']; const LAVA_COLORS = ['#FF0000', '#FF3300', '#FF4500', '#FF6600', '#FF8C00']; // Element types const EMPTY = 0; const SAND = 1; const WATER = 2; const WALL = 3; const DIRT = 4; const STONE = 5; const GRASS = 6; const WOOD = 7; const SEED = 8; const GRASS_BLADE = 9; const FLOWER = 10; const TREE_SEED = 11; const LEAF = 12; const FIRE = 13; const LAVA = 14; // Flammable materials const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF]; // 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; // Start at Chunk: 0,0 let worldOffsetXBeforeDrag = 0; let worldOffsetYBeforeDrag = 0; let chunks = new Map(); // Map to store chunks with key "x,y" let metadata = new Map(); // Map to store metadata for pixels let debugMode = false; let fireUpdateCounter = 0; // 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('dirt-btn').addEventListener('click', () => setTool(DIRT)); document.getElementById('stone-btn').addEventListener('click', () => setTool(STONE)); document.getElementById('grass-btn').addEventListener('click', () => setTool(GRASS)); document.getElementById('wood-btn').addEventListener('click', () => setTool(WOOD)); document.getElementById('seed-btn').addEventListener('click', () => setTool(SEED)); document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED)); document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE)); document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA)); 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 === DIRT) { document.getElementById('dirt-btn').classList.add('active'); } else if (tool === STONE) { document.getElementById('stone-btn').classList.add('active'); } else if (tool === GRASS) { document.getElementById('grass-btn').classList.add('active'); } else if (tool === WOOD) { document.getElementById('wood-btn').classList.add('active'); } else if (tool === SEED) { document.getElementById('seed-btn').classList.add('active'); } else if (tool === TREE_SEED) { document.getElementById('tree-seed-btn').classList.add('active'); } else if (tool === FIRE) { document.getElementById('fire-btn').classList.add('active'); } else if (tool === LAVA) { document.getElementById('lava-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; worldOffsetXBeforeDrag = worldOffsetX; worldOffsetYBeforeDrag = worldOffsetY; } 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; if (isDragging) { // Calculate the total movement during this drag const totalDragX = worldOffsetX - worldOffsetXBeforeDrag; const totalDragY = worldOffsetY - worldOffsetYBeforeDrag; if (debugMode) { console.log(`Drag completed: ${totalDragX}, ${totalDragY}`); } } 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++) { const pixelX = worldX + dx; const pixelY = worldY + dy; // Special handling for fire - only set fire to flammable materials if (currentTool === FIRE) { const currentPixel = getPixel(pixelX, pixelY); if (FLAMMABLE_MATERIALS.includes(currentPixel)) { setPixel(pixelX, pixelY, FIRE); setMetadata(pixelX, pixelY, { lifetime: 100 + Math.floor(Math.random() * 100), colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) }); } } else { setPixel(pixelX, pixelY, currentTool); // Add metadata for special types if (currentTool === SEED) { setMetadata(pixelX, pixelY, { type: 'regular' }); } else if (currentTool === FLOWER) { setMetadata(pixelX, pixelY, { type: 'flower', color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)], age: 0, height: 1 }); } } } } } 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; worldOffsetXBeforeDrag = worldOffsetX; worldOffsetYBeforeDrag = worldOffsetY; } 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; currentMouseX = x; currentMouseY = y; 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; currentMouseX = x; currentMouseY = y; 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 and y = 1) if (chunkY === 0 || chunkY === 1) { // Fill the bottom row with walls for (let x = 0; x < CHUNK_SIZE; x++) { chunkData[(CHUNK_SIZE - 1) * 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 (first two chunks) const floorChunkY = Math.floor(worldY / CHUNK_SIZE); if (worldY % CHUNK_SIZE === CHUNK_SIZE - 1 && (floorChunkY === 0 || floorChunkY === 1)) { 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]; } // Metadata functions to store additional information about pixels function setMetadata(worldX, worldY, data) { const key = `${worldX},${worldY}`; metadata.set(key, data); } function getMetadata(worldX, worldY) { const key = `${worldX},${worldY}`; return metadata.get(key); } function removeMetadata(worldX, worldY) { const key = `${worldX},${worldY}`; metadata.delete(key); } // Move metadata when a pixel moves function moveMetadata(fromX, fromY, toX, toY) { const data = getMetadata(fromX, fromY); if (data) { setMetadata(toX, toY, data); removeMetadata(fromX, fromY); } } 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(); // Increment fire update counter fireUpdateCounter++; // 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 || type === STONE || type === WOOD || type === LEAF) 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); } else if (type === DIRT) { updateDirt(worldX, worldY); } else if (type === GRASS) { updateGrass(worldX, worldY); } else if (type === SEED) { updateSeed(worldX, worldY); } else if (type === GRASS_BLADE) { updateGrassBlade(worldX, worldY); } else if (type === FLOWER) { updateFlower(worldX, worldY); } else if (type === TREE_SEED) { updateTreeSeed(worldX, worldY); } else if (type === FIRE) { updateFire(worldX, worldY); } else if (type === LAVA) { updateLava(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 updateDirt(x, y) { // Try to move down if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, DIRT); } // 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, DIRT); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, DIRT); } // Dirt can displace water else if (getPixel(x, y + 1) === WATER) { setPixel(x, y, WATER); setPixel(x, y + 1, DIRT); } // Dirt can turn into grass if exposed to air above if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) { setPixel(x, y, GRASS); } // Dirt can randomly spawn seeds if exposed to air above if (getPixel(x, y - 1) === EMPTY) { // Spawn different types of seeds with different probabilities const seedRoll = Math.random(); if (seedRoll < 0.0002) { // Grass blade seed (most common) setPixel(x, y - 1, SEED); } else if (seedRoll < 0.00025) { // Flower seed (less common) setPixel(x, y - 1, SEED); // Mark this seed as a flower seed (will be handled in updateSeed) setMetadata(x, y - 1, { type: 'flower' }); } else if (seedRoll < 0.00026) { // Tree seed (rare) setPixel(x, y - 1, TREE_SEED); } } } function updateGrass(x, y) { // Grass behaves like dirt for physics if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, GRASS); } else if (getPixel(x - 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y + 1, GRASS); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, GRASS); } else if (getPixel(x, y + 1) === WATER) { setPixel(x, y, WATER); setPixel(x, y + 1, GRASS); } // Grass can spread to nearby dirt if (Math.random() < 0.0005) { const directions = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1} ]; const dir = directions[Math.floor(Math.random() * directions.length)]; if (getPixel(x + dir.dx, y + dir.dy) === DIRT) { setPixel(x + dir.dx, y + dir.dy, GRASS); } } // Grass dies if covered (no air above) if (getPixel(x, y - 1) !== EMPTY && Math.random() < 0.05) { setPixel(x, y, DIRT); } } // New functions for plant growth function updateSeed(x, y) { // Seeds fall like sand if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, SEED); moveMetadata(x, y, x, y + 1); } else if (getPixel(x - 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y + 1, SEED); moveMetadata(x, y, x - 1, y + 1); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, SEED); moveMetadata(x, y, x + 1, y + 1); } // Seeds can float on water else if (getPixel(x, y + 1) === WATER) { // Just float, don't do anything } // If seed is on dirt or grass, it can germinate else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) { const metadata = getMetadata(x, y); // Check if this is a flower seed if (metadata && metadata.type === 'flower') { setPixel(x, y, FLOWER); // Set a random flower color setMetadata(x, y, { type: 'flower', color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)], age: 0, height: 1 }); } else { // Regular seed becomes a grass blade setPixel(x, y, GRASS_BLADE); setMetadata(x, y, { age: 0, height: 1 }); } } } function updateGrassBlade(x, y) { // Grass blades are static once grown const metadata = getMetadata(x, y); if (!metadata) { setMetadata(x, y, { age: 0, height: 1 }); return; } // Increment age metadata.age++; setMetadata(x, y, metadata); // Grass blades can grow taller up to a limit if (metadata.age % 200 === 0 && metadata.height < 3 && getPixel(x, y - 1) === EMPTY) { setPixel(x, y - 1, GRASS_BLADE); setMetadata(x, y - 1, { age: 0, height: metadata.height + 1 }); metadata.isTop = false; setMetadata(x, y, metadata); } // Grass blades die if covered by something other than another grass blade if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== GRASS_BLADE && Math.random() < 0.01) { setPixel(x, y, EMPTY); removeMetadata(x, y); } } function updateFlower(x, y) { // Flowers are similar to grass blades but with a colored top const metadata = getMetadata(x, y); if (!metadata) { setMetadata(x, y, { type: 'flower', color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)], age: 0, height: 1 }); return; } // Increment age metadata.age++; setMetadata(x, y, metadata); // Flowers can grow taller up to a limit (2x bigger) if (metadata.age % 300 === 0 && metadata.height < 8 && getPixel(x, y - 1) === EMPTY) { // If this is the top of the flower, make it a stem and put a new flower on top setPixel(x, y - 1, FLOWER); setMetadata(x, y - 1, { type: 'flower', color: metadata.color, age: 0, height: metadata.height + 1, isTop: true }); metadata.isTop = false; setMetadata(x, y, metadata); } // Flowers die if covered if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== FLOWER && Math.random() < 0.01) { setPixel(x, y, EMPTY); removeMetadata(x, y); } // Flowers can drop seeds occasionally if (metadata.isTop && Math.random() < 0.0001) { const directions = [-1, 1]; const dir = directions[Math.floor(Math.random() * directions.length)]; if (getPixel(x + dir, y) === EMPTY) { setPixel(x + dir, y, SEED); setMetadata(x + dir, y, { type: 'flower' }); } } } function updateTreeSeed(x, y) { // Tree seeds fall like other seeds if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, TREE_SEED); moveMetadata(x, y, x, y + 1); } else if (getPixel(x - 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y + 1, TREE_SEED); moveMetadata(x, y, x - 1, y + 1); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, TREE_SEED); moveMetadata(x, y, x + 1, y + 1); } // Seeds can float on water else if (getPixel(x, y + 1) === WATER) { // Just float, don't do anything } // If seed is on dirt or grass, it can grow into a tree else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) { // Start growing a tree growTree(x, y); } } function growTree(x, y) { // Replace the seed with the trunk setPixel(x, y, WOOD); // Determine tree height (50-80 blocks, 10x bigger) const treeHeight = 50 + Math.floor(Math.random() * 31); // Grow the trunk upward for (let i = 1; i < treeHeight; i++) { if (getPixel(x, y - i) === EMPTY) { setPixel(x, y - i, WOOD); } else { break; // Stop if we hit something } } // Add leaves at the top (10x bigger radius) addLeaves(x, y - treeHeight + 1, 20 + Math.floor(Math.random() * 10)); // Add some branches addBranches(x, y, treeHeight); } function addBranches(x, y, treeHeight) { // Add 2-4 branches at different heights const numBranches = 2 + Math.floor(Math.random() * 3); for (let i = 0; i < numBranches; i++) { // Position branch at different heights along the trunk const branchY = y - Math.floor(treeHeight * (0.3 + 0.4 * i / numBranches)); // Choose left or right direction const direction = Math.random() > 0.5 ? 1 : -1; // Branch length (10-15 blocks) const branchLength = 10 + Math.floor(Math.random() * 6); // Create the branch for (let j = 1; j <= branchLength; j++) { // Branch goes out horizontally with some upward angle const branchX = x + (j * direction); const upwardAngle = Math.floor(j * 0.3); if (getPixel(branchX, branchY - upwardAngle) === EMPTY) { setPixel(branchX, branchY - upwardAngle, WOOD); } else { break; // Stop if we hit something } // Add small leaf clusters at the end of branches if (j === branchLength) { addLeaves(branchX, branchY - upwardAngle, 8 + Math.floor(Math.random() * 4)); } } } } function addLeaves(x, y, radius) { // Add a cluster of leaves around the point for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { // Skip the exact center (trunk position) if (dx === 0 && dy === 0) continue; // Make it more circular by checking distance const distance = Math.sqrt(dx*dx + dy*dy); if (distance <= radius) { // Random chance to place a leaf based on distance from center // More dense leaves for larger trees const density = radius > 10 ? 0.8 : 0.6; if (Math.random() < (1 - distance/radius/density)) { if (getPixel(x + dx, y + dy) === EMPTY) { setPixel(x + dx, y + dy, LEAF); } } } } } } 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; } } // Water extinguishes fire and turns lava into stone const directions = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} ]; for (const dir of directions) { if (getPixel(x + dir.dx, y + dir.dy) === FIRE) { setPixel(x + dir.dx, y + dir.dy, EMPTY); removeMetadata(x + dir.dx, y + dir.dy); } else if (getPixel(x + dir.dx, y + dir.dy) === LAVA) { // Water turns lava into stone setPixel(x + dir.dx, y + dir.dy, STONE); removeMetadata(x + dir.dx, y + dir.dy); // Water is consumed in the process setPixel(x, y, EMPTY); return; } } } // Add fire update function function updateFire(x, y) { const metadata = getMetadata(x, y); if (!metadata) { // Initialize metadata if it doesn't exist setMetadata(x, y, { lifetime: 100 + Math.floor(Math.random() * 100), colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) }); return; } // Decrease lifetime metadata.lifetime--; // Randomly change color for flickering effect if (Math.random() < 0.2) { metadata.colorIndex = Math.floor(Math.random() * FIRE_COLORS.length); } // Update metadata setMetadata(x, y, metadata); // Fire rises upward occasionally if (Math.random() < 0.3 && getPixel(x, y - 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y - 1, FIRE); moveMetadata(x, y, x, y - 1); return; } // Fire can also move slightly to the sides if (Math.random() < 0.1) { const direction = Math.random() > 0.5 ? 1 : -1; if (getPixel(x + direction, y - 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + direction, y - 1, FIRE); moveMetadata(x, y, x + direction, y - 1); return; } } // Fire spreads to nearby flammable materials (less frequently to reduce performance impact) if (fireUpdateCounter % 3 === 0 && Math.random() < 0.3) { const directions = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} ]; const dir = directions[Math.floor(Math.random() * directions.length)]; const nearbyType = getPixel(x + dir.dx, y + dir.dy); if (FLAMMABLE_MATERIALS.includes(nearbyType)) { setPixel(x + dir.dx, y + dir.dy, FIRE); setMetadata(x + dir.dx, y + dir.dy, { lifetime: 100 + Math.floor(Math.random() * 100), colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) }); } } // Fire burns out after its lifetime if (metadata.lifetime <= 0) { setPixel(x, y, EMPTY); removeMetadata(x, y); } } // Add lava update function function updateLava(x, y) { const metadata = getMetadata(x, y); if (!metadata) { // Initialize metadata if it doesn't exist setMetadata(x, y, { colorIndex: Math.floor(Math.random() * LAVA_COLORS.length) }); } else { // Randomly change color for flowing effect if (Math.random() < 0.1) { metadata.colorIndex = Math.floor(Math.random() * LAVA_COLORS.length); setMetadata(x, y, metadata); } } // Lava moves slower than water if (Math.random() < 0.7) { // Try to move down if (getPixel(x, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x, y + 1, LAVA); moveMetadata(x, y, x, y + 1); } // 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, LAVA); moveMetadata(x, y, x - 1, y + 1); } else if (getPixel(x + 1, y + 1) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y + 1, LAVA); moveMetadata(x, y, x + 1, y + 1); } // Try to spread horizontally (slower than water) else if (Math.random() < 0.3) { // 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, LAVA); moveMetadata(x, y, x - 1, y); } else if (!goLeft && getPixel(x + 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y, LAVA); moveMetadata(x, y, x + 1, y); } // Try the other direction if first failed else if (!goLeft && getPixel(x - 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x - 1, y, LAVA); moveMetadata(x, y, x - 1, y); } else if (goLeft && getPixel(x + 1, y) === EMPTY) { setPixel(x, y, EMPTY); setPixel(x + 1, y, LAVA); moveMetadata(x, y, x + 1, y); } } } // Lava sets nearby flammable materials on fire const directions = [ {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, {dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} ]; for (const dir of directions) { const nearbyType = getPixel(x + dir.dx, y + dir.dy); // Set flammable materials on fire if (FLAMMABLE_MATERIALS.includes(nearbyType)) { setPixel(x + dir.dx, y + dir.dy, FIRE); setMetadata(x + dir.dx, y + dir.dy, { lifetime: 100 + Math.floor(Math.random() * 100), colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) }); } // Lava can melt sand into glass (stone) else if (nearbyType === SAND && Math.random() < 0.05) { setPixel(x + dir.dx, y + dir.dy, STONE); } // Lava can burn dirt else if (nearbyType === DIRT && Math.random() < 0.02) { setPixel(x + dir.dx, y + dir.dy, EMPTY); } } } 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; } else if (type === DIRT) { ctx.fillStyle = DIRT_COLOR; } else if (type === STONE) { ctx.fillStyle = STONE_COLOR; } else if (type === GRASS) { ctx.fillStyle = GRASS_COLOR; } else if (type === WOOD) { ctx.fillStyle = WOOD_COLOR; } else if (type === SEED) { ctx.fillStyle = SEED_COLOR; } else if (type === GRASS_BLADE) { ctx.fillStyle = GRASS_COLOR; } else if (type === FLOWER) { // Get flower color from metadata or use a default const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); ctx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0]; } else if (type === TREE_SEED) { ctx.fillStyle = SEED_COLOR; } else if (type === LEAF) { ctx.fillStyle = LEAF_COLOR; } else if (type === FIRE) { // Get fire color from metadata const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata ? metadata.colorIndex : 0; ctx.fillStyle = FIRE_COLORS[colorIndex]; } else if (type === LAVA) { // Get lava color from metadata const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata ? metadata.colorIndex : 0; ctx.fillStyle = LAVA_COLORS[colorIndex]; } // Draw the pixel ctx.fillRect( screenX + x * PIXEL_SIZE, screenY + y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE ); } } } // Draw cursor position and update debug info if (currentMouseX !== undefined && currentMouseY !== undefined) { const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX; const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY; // Update coordinates display in debug mode if (debugMode) { 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)}`; // Draw cursor outline const cursorScreenX = (worldX - worldOffsetX) * PIXEL_SIZE; const cursorScreenY = (worldY - worldOffsetY) * PIXEL_SIZE; ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 2; ctx.strokeRect( cursorScreenX - PIXEL_SIZE, cursorScreenY - PIXEL_SIZE, PIXEL_SIZE * 3, PIXEL_SIZE * 3 ); // Draw a dot at the exact mouse position ctx.fillStyle = '#ff0000'; ctx.beginPath(); ctx.arc(currentMouseX, currentMouseY, 3, 0, Math.PI * 2); ctx.fill(); } } }