diff --git a/index.html b/index.html index aa47ebd..71b02d6 100644 --- a/index.html +++ b/index.html @@ -37,6 +37,17 @@ - + + + + + + + + + + + + diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 0000000..01f5de9 --- /dev/null +++ b/js/constants.js @@ -0,0 +1,39 @@ +// Game constants +const CHUNK_SIZE = 200; +const PIXEL_SIZE = 4; +const GRAVITY = 0.5; +const WATER_SPREAD = 3; + +// Colors +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]; diff --git a/js/elements/basic.js b/js/elements/basic.js new file mode 100644 index 0000000..748200b --- /dev/null +++ b/js/elements/basic.js @@ -0,0 +1,130 @@ +// Basic element behaviors (sand, water, dirt) +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; + } + } + + // 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; + } + } +} + +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); + } + } +} diff --git a/js/elements/fire.js b/js/elements/fire.js new file mode 100644 index 0000000..4f7caec --- /dev/null +++ b/js/elements/fire.js @@ -0,0 +1,166 @@ +// Fire and lava element behaviors +let fireUpdateCounter = 0; + +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); + } +} + +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); + } + } +} diff --git a/js/elements/plants.js b/js/elements/plants.js new file mode 100644 index 0000000..85bfa3f --- /dev/null +++ b/js/elements/plants.js @@ -0,0 +1,159 @@ +// Plant element behaviors (grass, seeds, trees) +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); + } +} + +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' }); + } + } +} diff --git a/js/elements/trees.js b/js/elements/trees.js new file mode 100644 index 0000000..d5c9997 --- /dev/null +++ b/js/elements/trees.js @@ -0,0 +1,108 @@ +// Tree element behaviors +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); + } + } + } + } + } +} diff --git a/js/input.js b/js/input.js new file mode 100644 index 0000000..20d277e --- /dev/null +++ b/js/input.js @@ -0,0 +1,188 @@ +// Input handling functions +let isDrawing = false; +let isDragging = false; +let lastMouseX, lastMouseY; +let currentMouseX, currentMouseY; + +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 toggleDebug() { + debugMode = !debugMode; + document.getElementById('debug-btn').classList.toggle('active'); +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..9244e12 --- /dev/null +++ b/js/main.js @@ -0,0 +1,76 @@ +// Main game variables and initialization +let canvas, ctx; +let currentTool = SAND; +let lastFrameTime = 0; +let fps = 0; +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('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 and generate terrain around it + getOrCreateChunk(0, 0); + generateChunksAroundPlayer(); + + // Start the simulation loop + requestAnimationFrame(simulationLoop); +}; + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight; +} + +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); +} diff --git a/js/physics.js b/js/physics.js new file mode 100644 index 0000000..fccf83e --- /dev/null +++ b/js/physics.js @@ -0,0 +1,53 @@ +// Physics simulation functions +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); + } + } + } + } +} diff --git a/js/render.js b/js/render.js new file mode 100644 index 0000000..5040a7a --- /dev/null +++ b/js/render.js @@ -0,0 +1,127 @@ +// Rendering functions +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(); + } + } +} diff --git a/js/terrain.js b/js/terrain.js new file mode 100644 index 0000000..ef9d4dd --- /dev/null +++ b/js/terrain.js @@ -0,0 +1,176 @@ +// Terrain generation functions +function generateChunksAroundPlayer() { + const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE); + const centerChunkY = Math.floor(worldOffsetY / CHUNK_SIZE); + const radius = 3; // Generate chunks within 3 chunks of the player + + // Generate chunks in a square around the player + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + const chunkX = centerChunkX + dx; + const chunkY = centerChunkY + dy; + + // Only generate terrain for chunks at or above y=0 + if (chunkY >= 0) { + getOrCreateChunk(chunkX, chunkY); + } + } + } +} + +function generateTerrain(chunkX, chunkY, chunkData) { + // Use a seeded random number generator based on chunk coordinates + const seed = chunkX * 10000 + chunkY; + const random = createSeededRandom(seed); + + // Generate base terrain (hills) + generateHills(chunkX, chunkY, chunkData, random); + + // Add lakes + if (random() < 0.3) { // 30% chance for a lake in a chunk + generateLake(chunkX, chunkY, chunkData, random); + } + + // Add stone formations + if (random() < 0.4) { // 40% chance for stone formations + generateStoneFormation(chunkX, chunkY, chunkData, random); + } + + // Add vegetation (seeds and trees) + addVegetation(chunkX, chunkY, chunkData, random); +} + +function createSeededRandom(seed) { + // Simple seeded random function + return function() { + seed = (seed * 9301 + 49297) % 233280; + return seed / 233280; + }; +} + +function generateHills(chunkX, chunkY, chunkData, random) { + // Generate a height map for this chunk using simplex-like noise + const heightMap = new Array(CHUNK_SIZE).fill(0); + + // Base height (higher for chunks further from origin) + const baseHeight = Math.max(0, 50 - Math.sqrt(chunkX*chunkX + chunkY*chunkY) * 5); + + // Generate a smooth height map + for (let x = 0; x < CHUNK_SIZE; x++) { + // Use multiple frequencies for more natural looking terrain + const noise1 = Math.sin(x * 0.02 + chunkX * CHUNK_SIZE * 0.02 + random() * 10) * 15; + const noise2 = Math.sin(x * 0.05 + chunkX * CHUNK_SIZE * 0.05 + random() * 10) * 7; + const noise3 = Math.sin(x * 0.1 + chunkX * CHUNK_SIZE * 0.1 + random() * 10) * 3; + + // Combine noise at different frequencies + heightMap[x] = Math.floor(baseHeight + noise1 + noise2 + noise3); + + // Ensure height is positive + heightMap[x] = Math.max(0, heightMap[x]); + } + + // Fill the terrain based on the height map + for (let x = 0; x < CHUNK_SIZE; x++) { + const height = heightMap[x]; + + for (let y = CHUNK_SIZE - 1; y >= 0; y--) { + const worldY = chunkY * CHUNK_SIZE + y; + const depth = CHUNK_SIZE - 1 - y; + + if (depth < height) { + const index = y * CHUNK_SIZE + x; + + // Top layer is grass + if (depth === 0) { + chunkData[index] = GRASS; + } + // Next few layers are dirt + else if (depth < 5) { + chunkData[index] = DIRT; + } + // Deeper layers are stone + else { + chunkData[index] = STONE; + } + } + } + } +} + +function generateLake(chunkX, chunkY, chunkData, random) { + // Lake parameters + const lakeX = Math.floor(random() * (CHUNK_SIZE - 60)) + 30; + const lakeY = Math.floor(random() * (CHUNK_SIZE - 60)) + 30; + const lakeWidth = Math.floor(random() * 40) + 20; + const lakeHeight = Math.floor(random() * 20) + 10; + + // Create an elliptical lake + for (let y = 0; y < CHUNK_SIZE; y++) { + for (let x = 0; x < CHUNK_SIZE; x++) { + // Calculate distance from lake center (normalized to create an ellipse) + const dx = (x - lakeX) / lakeWidth; + const dy = (y - lakeY) / lakeHeight; + const distance = Math.sqrt(dx*dx + dy*dy); + + if (distance < 1) { + const index = y * CHUNK_SIZE + x; + + // Water in the center + if (distance < 0.8) { + chunkData[index] = WATER; + } + // Sand around the edges + else { + chunkData[index] = SAND; + } + } + } + } +} + +function generateStoneFormation(chunkX, chunkY, chunkData, random) { + // Stone formation parameters + const formationX = Math.floor(random() * (CHUNK_SIZE - 40)) + 20; + const formationWidth = Math.floor(random() * 30) + 10; + const formationHeight = Math.floor(random() * 40) + 20; + + // Create a stone hill/mountain + for (let x = formationX - formationWidth; x < formationX + formationWidth; x++) { + if (x < 0 || x >= CHUNK_SIZE) continue; + + // Calculate height at this x position (higher in the middle) + const dx = (x - formationX) / formationWidth; + const height = Math.floor(formationHeight * (1 - dx*dx)); + + for (let y = CHUNK_SIZE - 1; y >= CHUNK_SIZE - height; y--) { + if (y < 0 || y >= CHUNK_SIZE) continue; + + const index = y * CHUNK_SIZE + x; + chunkData[index] = STONE; + } + } +} + +function addVegetation(chunkX, chunkY, chunkData, random) { + // Add vegetation on grass + for (let y = 0; y < CHUNK_SIZE; y++) { + for (let x = 0; x < CHUNK_SIZE; x++) { + const index = y * CHUNK_SIZE + x; + + // Only add vegetation on grass + if (chunkData[index] === GRASS) { + // Check if there's empty space above + if (y > 0 && chunkData[(y-1) * CHUNK_SIZE + x] === EMPTY) { + // Random chance to add different types of vegetation + const roll = random(); + + if (roll < 0.01) { // 1% chance for a tree seed + chunkData[(y-1) * CHUNK_SIZE + x] = TREE_SEED; + } else if (roll < 0.05) { // 4% chance for a regular seed + chunkData[(y-1) * CHUNK_SIZE + x] = SEED; + } + } + } + } + } +} diff --git a/js/world.js b/js/world.js new file mode 100644 index 0000000..6e6aa38 --- /dev/null +++ b/js/world.js @@ -0,0 +1,128 @@ +// World management functions +let worldOffsetX = 0; +let worldOffsetY = 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 generatedChunks = new Set(); // Set to track which chunks have been generated + +function moveWorld(dx, dy) { + worldOffsetX += dx; + worldOffsetY += dy; + updateCoordinatesDisplay(); + + // Generate terrain for chunks around the current view + generateChunksAroundPlayer(); +} + +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 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; +} diff --git a/script.js b/script.js index 43802ee..0a2a75b 100644 --- a/script.js +++ b/script.js @@ -1,1350 +1 @@ -// 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; -let generatedChunks = new Set(); // Set to track which chunks have been generated - -// 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 and generate terrain around it - getOrCreateChunk(0, 0); - generateChunksAroundPlayer(); - - // 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(); - - // Generate terrain for chunks around the current view - generateChunksAroundPlayer(); -} - -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'); -} - -// Terrain generation functions -function generateChunksAroundPlayer() { - const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE); - const centerChunkY = Math.floor(worldOffsetY / CHUNK_SIZE); - const radius = 3; // Generate chunks within 3 chunks of the player - - // Generate chunks in a square around the player - for (let dy = -radius; dy <= radius; dy++) { - for (let dx = -radius; dx <= radius; dx++) { - const chunkX = centerChunkX + dx; - const chunkY = centerChunkY + dy; - - // Only generate terrain for chunks at or above y=0 - if (chunkY >= 0) { - getOrCreateChunk(chunkX, chunkY); - } - } - } -} - -function generateTerrain(chunkX, chunkY, chunkData) { - // Use a seeded random number generator based on chunk coordinates - const seed = chunkX * 10000 + chunkY; - const random = createSeededRandom(seed); - - // Generate base terrain (hills) - generateHills(chunkX, chunkY, chunkData, random); - - // Add lakes - if (random() < 0.3) { // 30% chance for a lake in a chunk - generateLake(chunkX, chunkY, chunkData, random); - } - - // Add stone formations - if (random() < 0.4) { // 40% chance for stone formations - generateStoneFormation(chunkX, chunkY, chunkData, random); - } - - // Add vegetation (seeds and trees) - addVegetation(chunkX, chunkY, chunkData, random); -} - -function createSeededRandom(seed) { - // Simple seeded random function - return function() { - seed = (seed * 9301 + 49297) % 233280; - return seed / 233280; - }; -} - -function generateHills(chunkX, chunkY, chunkData, random) { - // Generate a height map for this chunk using simplex-like noise - const heightMap = new Array(CHUNK_SIZE).fill(0); - - // Base height (higher for chunks further from origin) - const baseHeight = Math.max(0, 50 - Math.sqrt(chunkX*chunkX + chunkY*chunkY) * 5); - - // Generate a smooth height map - for (let x = 0; x < CHUNK_SIZE; x++) { - // Use multiple frequencies for more natural looking terrain - const noise1 = Math.sin(x * 0.02 + chunkX * CHUNK_SIZE * 0.02 + random() * 10) * 15; - const noise2 = Math.sin(x * 0.05 + chunkX * CHUNK_SIZE * 0.05 + random() * 10) * 7; - const noise3 = Math.sin(x * 0.1 + chunkX * CHUNK_SIZE * 0.1 + random() * 10) * 3; - - // Combine noise at different frequencies - heightMap[x] = Math.floor(baseHeight + noise1 + noise2 + noise3); - - // Ensure height is positive - heightMap[x] = Math.max(0, heightMap[x]); - } - - // Fill the terrain based on the height map - for (let x = 0; x < CHUNK_SIZE; x++) { - const height = heightMap[x]; - - for (let y = CHUNK_SIZE - 1; y >= 0; y--) { - const worldY = chunkY * CHUNK_SIZE + y; - const depth = CHUNK_SIZE - 1 - y; - - if (depth < height) { - const index = y * CHUNK_SIZE + x; - - // Top layer is grass - if (depth === 0) { - chunkData[index] = GRASS; - } - // Next few layers are dirt - else if (depth < 5) { - chunkData[index] = DIRT; - } - // Deeper layers are stone - else { - chunkData[index] = STONE; - } - } - } - } -} - -function generateLake(chunkX, chunkY, chunkData, random) { - // Lake parameters - const lakeX = Math.floor(random() * (CHUNK_SIZE - 60)) + 30; - const lakeY = Math.floor(random() * (CHUNK_SIZE - 60)) + 30; - const lakeWidth = Math.floor(random() * 40) + 20; - const lakeHeight = Math.floor(random() * 20) + 10; - - // Create an elliptical lake - for (let y = 0; y < CHUNK_SIZE; y++) { - for (let x = 0; x < CHUNK_SIZE; x++) { - // Calculate distance from lake center (normalized to create an ellipse) - const dx = (x - lakeX) / lakeWidth; - const dy = (y - lakeY) / lakeHeight; - const distance = Math.sqrt(dx*dx + dy*dy); - - if (distance < 1) { - const index = y * CHUNK_SIZE + x; - - // Water in the center - if (distance < 0.8) { - chunkData[index] = WATER; - } - // Sand around the edges - else { - chunkData[index] = SAND; - } - } - } - } -} - -function generateStoneFormation(chunkX, chunkY, chunkData, random) { - // Stone formation parameters - const formationX = Math.floor(random() * (CHUNK_SIZE - 40)) + 20; - const formationWidth = Math.floor(random() * 30) + 10; - const formationHeight = Math.floor(random() * 40) + 20; - - // Create a stone hill/mountain - for (let x = formationX - formationWidth; x < formationX + formationWidth; x++) { - if (x < 0 || x >= CHUNK_SIZE) continue; - - // Calculate height at this x position (higher in the middle) - const dx = (x - formationX) / formationWidth; - const height = Math.floor(formationHeight * (1 - dx*dx)); - - for (let y = CHUNK_SIZE - 1; y >= CHUNK_SIZE - height; y--) { - if (y < 0 || y >= CHUNK_SIZE) continue; - - const index = y * CHUNK_SIZE + x; - chunkData[index] = STONE; - } - } -} - -function addVegetation(chunkX, chunkY, chunkData, random) { - // Add vegetation on grass - for (let y = 0; y < CHUNK_SIZE; y++) { - for (let x = 0; x < CHUNK_SIZE; x++) { - const index = y * CHUNK_SIZE + x; - - // Only add vegetation on grass - if (chunkData[index] === GRASS) { - // Check if there's empty space above - if (y > 0 && chunkData[(y-1) * CHUNK_SIZE + x] === EMPTY) { - // Random chance to add different types of vegetation - const roll = random(); - - if (roll < 0.01) { // 1% chance for a tree seed - chunkData[(y-1) * CHUNK_SIZE + x] = TREE_SEED; - } else if (roll < 0.05) { // 4% chance for a regular seed - chunkData[(y-1) * CHUNK_SIZE + x] = SEED; - } - } - } - } - } -} - -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(); - } - } -} +// This file has been replaced by modular files in the js/ directory