Compare commits

..

No commits in common. "ebb96846ed3e430eaa8e0841eaa1f410d99556be" and "8ef18f52abb41d3e3a961ac94c4bde7c90a1db95" have entirely different histories.

11 changed files with 136 additions and 897 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
.aider*

View File

@ -17,7 +17,6 @@ const FLOWER_COLORS = ['#FF0000', '#FFFF00', '#FF00FF', '#FFA500', '#FFFFFF', '#
const LEAF_COLOR = '#228B22'; const LEAF_COLOR = '#228B22';
const FIRE_COLORS = ['#FF0000', '#FF3300', '#FF6600', '#FF9900', '#FFCC00', '#FFFF00']; const FIRE_COLORS = ['#FF0000', '#FF3300', '#FF6600', '#FF9900', '#FFCC00', '#FFFF00'];
const LAVA_COLORS = ['#FF0000', '#FF3300', '#FF4500', '#FF6600', '#FF8C00']; const LAVA_COLORS = ['#FF0000', '#FF3300', '#FF4500', '#FF6600', '#FF8C00'];
const RABBIT_COLORS = ['#FFFFFF', '#E0E0E0', '#D3C8B4']; // White, Light Gray, Light Brown
// Color variation functions // Color variation functions
function getRandomColorVariation(baseColor, range) { function getRandomColorVariation(baseColor, range) {
@ -59,7 +58,6 @@ const TREE_SEED = 11;
const LEAF = 12; const LEAF = 12;
const FIRE = 13; const FIRE = 13;
const LAVA = 14; const LAVA = 14;
const RABBIT = 15;
// Flammable materials // Flammable materials
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF]; const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];

View File

@ -4,37 +4,29 @@ function updateSand(x, y) {
if (getPixel(x, y + 1) === EMPTY) { if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, SAND); setPixel(x, y + 1, SAND);
return true;
} }
// Try to move down-left or down-right // Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, SAND); setPixel(x - 1, y + 1, SAND);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SAND); setPixel(x + 1, y + 1, SAND);
return true;
} }
// Sand can displace water // Sand can displace water
else if (getPixel(x, y + 1) === WATER) { else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER); setPixel(x, y, WATER);
setPixel(x, y + 1, SAND); setPixel(x, y + 1, SAND);
return true;
} }
return false;
} }
function updateWater(x, y) { function updateWater(x, y) {
let modified = false;
// Update water color dynamically // Update water color dynamically
const metadata = getMetadata(x, y); const metadata = getMetadata(x, y);
if (metadata) { if (metadata) {
if (metadata.waterColorTimer === undefined) { if (metadata.waterColorTimer === undefined) {
metadata.waterColorTimer = 0; metadata.waterColorTimer = 0;
modified = true;
} }
metadata.waterColorTimer++; metadata.waterColorTimer++;
@ -43,7 +35,6 @@ function updateWater(x, y) {
if (metadata.waterColorTimer > 20 && Math.random() < 0.1) { if (metadata.waterColorTimer > 20 && Math.random() < 0.1) {
metadata.colorIndex = Math.floor(Math.random() * 10); metadata.colorIndex = Math.floor(Math.random() * 10);
metadata.waterColorTimer = 0; metadata.waterColorTimer = 0;
modified = true;
} }
setMetadata(x, y, metadata); setMetadata(x, y, metadata);
@ -54,20 +45,17 @@ function updateWater(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, WATER); setPixel(x, y + 1, WATER);
moveMetadata(x, y, x, y + 1); moveMetadata(x, y, x, y + 1);
return true;
} }
// Try to move down-left or down-right // Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, WATER); setPixel(x - 1, y + 1, WATER);
moveMetadata(x, y, x - 1, y + 1); moveMetadata(x, y, x - 1, y + 1);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, WATER); setPixel(x + 1, y + 1, WATER);
moveMetadata(x, y, x + 1, y + 1); moveMetadata(x, y, x + 1, y + 1);
return true;
} }
// Try to spread horizontally // Try to spread horizontally
else { else {
@ -79,25 +67,21 @@ function updateWater(x, y) {
if (goLeft && getPixel(x - 1, y) === EMPTY) { if (goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER); setPixel(x - 1, y, WATER);
moveMetadata(x, y, x - 1, y); moved = true;
return true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) { } else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER); setPixel(x + 1, y, WATER);
moveMetadata(x, y, x + 1, y); moved = true;
return true;
} }
// Try the other direction if first failed // Try the other direction if first failed
else if (!goLeft && getPixel(x - 1, y) === EMPTY) { else if (!goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER); setPixel(x - 1, y, WATER);
moveMetadata(x, y, x - 1, y); moved = true;
return true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) { } else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER); setPixel(x + 1, y, WATER);
moveMetadata(x, y, x + 1, y); moved = true;
return true;
} }
} }
@ -113,18 +97,15 @@ function updateWater(x, y) {
if (getPixel(x + dir.dx, y + dir.dy) === FIRE) { if (getPixel(x + dir.dx, y + dir.dy) === FIRE) {
setPixel(x + dir.dx, y + dir.dy, EMPTY); setPixel(x + dir.dx, y + dir.dy, EMPTY);
removeMetadata(x + dir.dx, y + dir.dy); removeMetadata(x + dir.dx, y + dir.dy);
return true;
} else if (getPixel(x + dir.dx, y + dir.dy) === LAVA) { } else if (getPixel(x + dir.dx, y + dir.dy) === LAVA) {
// Water turns lava into stone // Water turns lava into stone
setPixel(x + dir.dx, y + dir.dy, STONE); setPixel(x + dir.dx, y + dir.dy, STONE);
removeMetadata(x + dir.dx, y + dir.dy); removeMetadata(x + dir.dx, y + dir.dy);
// Water is consumed in the process // Water is consumed in the process
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
return true; return;
} }
} }
return modified;
} }
function updateDirt(x, y) { function updateDirt(x, y) {
@ -132,30 +113,25 @@ function updateDirt(x, y) {
if (getPixel(x, y + 1) === EMPTY) { if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, DIRT); setPixel(x, y + 1, DIRT);
return true;
} }
// Try to move down-left or down-right // Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, DIRT); setPixel(x - 1, y + 1, DIRT);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, DIRT); setPixel(x + 1, y + 1, DIRT);
return true;
} }
// Dirt can displace water // Dirt can displace water
else if (getPixel(x, y + 1) === WATER) { else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER); setPixel(x, y, WATER);
setPixel(x, y + 1, DIRT); setPixel(x, y + 1, DIRT);
return true;
} }
// Dirt can turn into grass if exposed to air above // Dirt can turn into grass if exposed to air above
if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) { if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) {
setPixel(x, y, GRASS); setPixel(x, y, GRASS);
return true;
} }
// Dirt can randomly spawn seeds if exposed to air above // Dirt can randomly spawn seeds if exposed to air above
@ -164,17 +140,12 @@ function updateDirt(x, y) {
const seedRoll = Math.random(); const seedRoll = Math.random();
if (seedRoll < 0.0002) { // Grass blade seed (most common) if (seedRoll < 0.0002) { // Grass blade seed (most common)
setPixel(x, y - 1, SEED); setPixel(x, y - 1, SEED);
return true;
} else if (seedRoll < 0.00025) { // Flower seed (less common) } else if (seedRoll < 0.00025) { // Flower seed (less common)
setPixel(x, y - 1, SEED); setPixel(x, y - 1, SEED);
// Mark this seed as a flower seed (will be handled in updateSeed) // Mark this seed as a flower seed (will be handled in updateSeed)
setMetadata(x, y - 1, { type: 'flower' }); setMetadata(x, y - 1, { type: 'flower' });
return true;
} else if (seedRoll < 0.00026) { // Tree seed (rare) } else if (seedRoll < 0.00026) { // Tree seed (rare)
setPixel(x, y - 1, TREE_SEED); setPixel(x, y - 1, TREE_SEED);
return true;
} }
} }
return false;
} }

View File

@ -2,7 +2,6 @@
let fireUpdateCounter = 0; let fireUpdateCounter = 0;
function updateFire(x, y) { function updateFire(x, y) {
let modified = false;
const metadata = getMetadata(x, y); const metadata = getMetadata(x, y);
if (!metadata) { if (!metadata) {
@ -11,17 +10,15 @@ function updateFire(x, y) {
lifetime: 100 + Math.floor(Math.random() * 100), lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
}); });
return true; return;
} }
// Decrease lifetime // Decrease lifetime
metadata.lifetime--; metadata.lifetime--;
modified = true;
// Randomly change color for flickering effect // Randomly change color for flickering effect
if (Math.random() < 0.2) { if (Math.random() < 0.2) {
metadata.colorIndex = Math.floor(Math.random() * FIRE_COLORS.length); metadata.colorIndex = Math.floor(Math.random() * FIRE_COLORS.length);
modified = true;
} }
// Update metadata // Update metadata
@ -32,7 +29,7 @@ function updateFire(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y - 1, FIRE); setPixel(x, y - 1, FIRE);
moveMetadata(x, y, x, y - 1); moveMetadata(x, y, x, y - 1);
return true; return;
} }
// Fire can also move slightly to the sides // Fire can also move slightly to the sides
@ -42,7 +39,7 @@ function updateFire(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + direction, y - 1, FIRE); setPixel(x + direction, y - 1, FIRE);
moveMetadata(x, y, x + direction, y - 1); moveMetadata(x, y, x + direction, y - 1);
return true; return;
} }
} }
@ -64,7 +61,6 @@ function updateFire(x, y) {
lifetime: 100 + Math.floor(Math.random() * 100), lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
}); });
modified = true;
} }
} }
@ -72,14 +68,10 @@ function updateFire(x, y) {
if (metadata.lifetime <= 0) { if (metadata.lifetime <= 0) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
removeMetadata(x, y); removeMetadata(x, y);
return true;
} }
return modified;
} }
function updateLava(x, y) { function updateLava(x, y) {
let modified = false;
const metadata = getMetadata(x, y); const metadata = getMetadata(x, y);
if (!metadata) { if (!metadata) {
@ -87,13 +79,11 @@ function updateLava(x, y) {
setMetadata(x, y, { setMetadata(x, y, {
colorIndex: Math.floor(Math.random() * LAVA_COLORS.length) colorIndex: Math.floor(Math.random() * LAVA_COLORS.length)
}); });
modified = true;
} else { } else {
// Randomly change color for flowing effect // Randomly change color for flowing effect
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
metadata.colorIndex = Math.floor(Math.random() * LAVA_COLORS.length); metadata.colorIndex = Math.floor(Math.random() * LAVA_COLORS.length);
setMetadata(x, y, metadata); setMetadata(x, y, metadata);
modified = true;
} }
} }
@ -104,20 +94,17 @@ function updateLava(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, LAVA); setPixel(x, y + 1, LAVA);
moveMetadata(x, y, x, y + 1); moveMetadata(x, y, x, y + 1);
return true;
} }
// Try to move down-left or down-right // Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, LAVA); setPixel(x - 1, y + 1, LAVA);
moveMetadata(x, y, x - 1, y + 1); moveMetadata(x, y, x - 1, y + 1);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, LAVA); setPixel(x + 1, y + 1, LAVA);
moveMetadata(x, y, x + 1, y + 1); moveMetadata(x, y, x + 1, y + 1);
return true;
} }
// Try to spread horizontally (slower than water) // Try to spread horizontally (slower than water)
else if (Math.random() < 0.3) { else if (Math.random() < 0.3) {
@ -128,24 +115,20 @@ function updateLava(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y, LAVA); setPixel(x - 1, y, LAVA);
moveMetadata(x, y, x - 1, y); moveMetadata(x, y, x - 1, y);
return true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) { } else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y, LAVA); setPixel(x + 1, y, LAVA);
moveMetadata(x, y, x + 1, y); moveMetadata(x, y, x + 1, y);
return true;
} }
// Try the other direction if first failed // Try the other direction if first failed
else if (!goLeft && getPixel(x - 1, y) === EMPTY) { else if (!goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y, LAVA); setPixel(x - 1, y, LAVA);
moveMetadata(x, y, x - 1, y); moveMetadata(x, y, x - 1, y);
return true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) { } else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y, LAVA); setPixel(x + 1, y, LAVA);
moveMetadata(x, y, x + 1, y); moveMetadata(x, y, x + 1, y);
return true;
} }
} }
} }
@ -168,21 +151,16 @@ function updateLava(x, y) {
lifetime: 100 + Math.floor(Math.random() * 100), lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
}); });
modified = true;
} }
// Lava can melt sand into glass (stone) // Lava can melt sand into glass (stone)
else if (nearbyType === SAND && Math.random() < 0.05) { else if (nearbyType === SAND && Math.random() < 0.05) {
setPixel(x + dir.dx, y + dir.dy, STONE); setPixel(x + dir.dx, y + dir.dy, STONE);
modified = true;
} }
// Lava can burn dirt // Lava can burn dirt
else if (nearbyType === DIRT && Math.random() < 0.02) { else if (nearbyType === DIRT && Math.random() < 0.02) {
setPixel(x + dir.dx, y + dir.dy, EMPTY); setPixel(x + dir.dx, y + dir.dy, EMPTY);
modified = true;
} }
} }
return modified;
} }

View File

@ -4,22 +4,18 @@ function updateGrass(x, y) {
if (getPixel(x, y + 1) === EMPTY) { if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, GRASS); setPixel(x, y + 1, GRASS);
return true;
} }
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, GRASS); setPixel(x - 1, y + 1, GRASS);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, GRASS); setPixel(x + 1, y + 1, GRASS);
return true;
} }
else if (getPixel(x, y + 1) === WATER) { else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER); setPixel(x, y, WATER);
setPixel(x, y + 1, GRASS); setPixel(x, y + 1, GRASS);
return true;
} }
// Grass can spread to nearby dirt // Grass can spread to nearby dirt
@ -32,17 +28,13 @@ function updateGrass(x, y) {
const dir = directions[Math.floor(Math.random() * directions.length)]; const dir = directions[Math.floor(Math.random() * directions.length)];
if (getPixel(x + dir.dx, y + dir.dy) === DIRT) { if (getPixel(x + dir.dx, y + dir.dy) === DIRT) {
setPixel(x + dir.dx, y + dir.dy, GRASS); setPixel(x + dir.dx, y + dir.dy, GRASS);
return true;
} }
} }
// Grass dies if covered (no air above) // Grass dies if covered (no air above)
if (getPixel(x, y - 1) !== EMPTY && Math.random() < 0.05) { if (getPixel(x, y - 1) !== EMPTY && Math.random() < 0.05) {
setPixel(x, y, DIRT); setPixel(x, y, DIRT);
return true;
} }
return false;
} }
function updateSeed(x, y) { function updateSeed(x, y) {
@ -51,24 +43,20 @@ function updateSeed(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, SEED); setPixel(x, y + 1, SEED);
moveMetadata(x, y, x, y + 1); moveMetadata(x, y, x, y + 1);
return true;
} }
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, SEED); setPixel(x - 1, y + 1, SEED);
moveMetadata(x, y, x - 1, y + 1); moveMetadata(x, y, x - 1, y + 1);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SEED); setPixel(x + 1, y + 1, SEED);
moveMetadata(x, y, x + 1, y + 1); moveMetadata(x, y, x + 1, y + 1);
return true;
} }
// Seeds can float on water // Seeds can float on water
else if (getPixel(x, y + 1) === WATER) { else if (getPixel(x, y + 1) === WATER) {
// Just float, don't do anything // Just float, don't do anything
return false;
} }
// If seed is on dirt or grass, it can germinate // If seed is on dirt or grass, it can germinate
else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) { else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) {
@ -84,16 +72,12 @@ function updateSeed(x, y) {
age: 0, age: 0,
height: 1 height: 1
}); });
return true;
} else { } else {
// Regular seed becomes a grass blade // Regular seed becomes a grass blade
setPixel(x, y, GRASS_BLADE); setPixel(x, y, GRASS_BLADE);
setMetadata(x, y, { age: 0, height: 1 }); setMetadata(x, y, { age: 0, height: 1 });
return true;
} }
} }
return false;
} }
function updateGrassBlade(x, y) { function updateGrassBlade(x, y) {
@ -102,7 +86,7 @@ function updateGrassBlade(x, y) {
if (!metadata) { if (!metadata) {
setMetadata(x, y, { age: 0, height: 1 }); setMetadata(x, y, { age: 0, height: 1 });
return true; return;
} }
// Increment age // Increment age
@ -115,17 +99,13 @@ function updateGrassBlade(x, y) {
setMetadata(x, y - 1, { age: 0, height: metadata.height + 1 }); setMetadata(x, y - 1, { age: 0, height: metadata.height + 1 });
metadata.isTop = false; metadata.isTop = false;
setMetadata(x, y, metadata); setMetadata(x, y, metadata);
return true;
} }
// Grass blades die if covered by something other than another grass blade // 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) { if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== GRASS_BLADE && Math.random() < 0.01) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
removeMetadata(x, y); removeMetadata(x, y);
return true;
} }
return false;
} }
function updateFlower(x, y) { function updateFlower(x, y) {
@ -139,7 +119,7 @@ function updateFlower(x, y) {
age: 0, age: 0,
height: 1 height: 1
}); });
return true; return;
} }
// Increment age // Increment age
@ -159,14 +139,12 @@ function updateFlower(x, y) {
}); });
metadata.isTop = false; metadata.isTop = false;
setMetadata(x, y, metadata); setMetadata(x, y, metadata);
return true;
} }
// Flowers die if covered // Flowers die if covered
if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== FLOWER && Math.random() < 0.01) { if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== FLOWER && Math.random() < 0.01) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
removeMetadata(x, y); removeMetadata(x, y);
return true;
} }
// Flowers can drop seeds occasionally // Flowers can drop seeds occasionally
@ -176,9 +154,6 @@ function updateFlower(x, y) {
if (getPixel(x + dir, y) === EMPTY) { if (getPixel(x + dir, y) === EMPTY) {
setPixel(x + dir, y, SEED); setPixel(x + dir, y, SEED);
setMetadata(x + dir, y, { type: 'flower' }); setMetadata(x + dir, y, { type: 'flower' });
return true;
} }
} }
return false;
} }

View File

@ -1,110 +0,0 @@
// Rabbit element behaviors
function updateRabbit(x, y) {
const metadata = getMetadata(x, y) || {};
// Initialize rabbit metadata if it doesn't exist
if (!metadata.initialized) {
metadata.initialized = true;
metadata.jumpCooldown = 0;
metadata.direction = Math.random() > 0.5 ? 1 : -1; // 1 for right, -1 for left
metadata.jumpHeight = 0;
metadata.colorIndex = Math.floor(Math.random() * RABBIT_COLORS.length);
setMetadata(x, y, metadata);
return true;
}
// Update metadata
metadata.jumpCooldown = Math.max(0, metadata.jumpCooldown - 1);
// Check if rabbit is on solid ground
const onGround = getPixel(x, y + 1) !== EMPTY && getPixel(x, y + 1) !== WATER;
// Rabbit falls if there's nothing below
if (!onGround && metadata.jumpHeight <= 0) {
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, RABBIT);
moveMetadata(x, y, x, y + 1);
return true;
}
// Rabbit can swim but prefers not to
else if (getPixel(x, y + 1) === WATER) {
// 50% chance to swim down or stay in place
if (Math.random() < 0.5) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, RABBIT);
moveMetadata(x, y, x, y + 1);
return true;
}
// When in water, try to jump out
metadata.jumpCooldown = 0;
}
}
// Rabbit jumps occasionally when on ground
if (onGround && metadata.jumpCooldown === 0) {
// Start a jump
metadata.jumpHeight = 3 + Math.floor(Math.random() * 2); // Jump 3-4 blocks high
metadata.jumpCooldown = 30 + Math.floor(Math.random() * 50); // Wait 30-80 frames before next jump
// Randomly change direction sometimes
if (Math.random() < 0.3) {
metadata.direction = -metadata.direction;
}
setMetadata(x, y, metadata);
}
// Execute jump if jump height is positive
if (metadata.jumpHeight > 0) {
// Move up
if (getPixel(x, y - 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y - 1, RABBIT);
moveMetadata(x, y, x, y - 1);
metadata.jumpHeight--;
setMetadata(x, y - 1, metadata);
return true;
}
// If can't move up, try moving diagonally up in current direction
else if (getPixel(x + metadata.direction, y - 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + metadata.direction, y - 1, RABBIT);
moveMetadata(x, y, x + metadata.direction, y - 1);
metadata.jumpHeight--;
setMetadata(x + metadata.direction, y - 1, metadata);
return true;
}
// If can't move diagonally up, try moving horizontally
else if (getPixel(x + metadata.direction, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + metadata.direction, y, RABBIT);
moveMetadata(x, y, x + metadata.direction, y);
metadata.jumpHeight = 0; // End jump if blocked
setMetadata(x + metadata.direction, y, metadata);
return true;
}
// If completely blocked, end jump
else {
metadata.jumpHeight = 0;
setMetadata(x, y, metadata);
}
}
// Move horizontally when not jumping
else if (metadata.jumpCooldown > 0 && metadata.jumpCooldown < 15 && onGround) {
// Hop horizontally between jumps
if (getPixel(x + metadata.direction, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + metadata.direction, y, RABBIT);
moveMetadata(x, y, x + metadata.direction, y);
return true;
}
// If blocked, try to change direction
else {
metadata.direction = -metadata.direction;
setMetadata(x, y, metadata);
}
}
return false;
}

View File

@ -5,33 +5,26 @@ function updateTreeSeed(x, y) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, TREE_SEED); setPixel(x, y + 1, TREE_SEED);
moveMetadata(x, y, x, y + 1); moveMetadata(x, y, x, y + 1);
return true;
} }
else if (getPixel(x - 1, y + 1) === EMPTY) { else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, TREE_SEED); setPixel(x - 1, y + 1, TREE_SEED);
moveMetadata(x, y, x - 1, y + 1); moveMetadata(x, y, x - 1, y + 1);
return true;
} }
else if (getPixel(x + 1, y + 1) === EMPTY) { else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, TREE_SEED); setPixel(x + 1, y + 1, TREE_SEED);
moveMetadata(x, y, x + 1, y + 1); moveMetadata(x, y, x + 1, y + 1);
return true;
} }
// Seeds can float on water // Seeds can float on water
else if (getPixel(x, y + 1) === WATER) { else if (getPixel(x, y + 1) === WATER) {
// Just float, don't do anything // Just float, don't do anything
return false;
} }
// If seed is on dirt or grass, it can grow into a tree // 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) { else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) {
// Start growing a tree // Start growing a tree
growTree(x, y); growTree(x, y);
return true;
} }
return false;
} }
function growTree(x, y) { function growTree(x, y) {

View File

@ -47,16 +47,6 @@ window.onload = function() {
// Initialize the first chunk and generate terrain around it // Initialize the first chunk and generate terrain around it
getOrCreateChunk(0, 0); getOrCreateChunk(0, 0);
// Explicitly create and mark the stone layer as dirty
for (let dx = -5; dx <= 5; dx++) {
const chunkX = dx;
const chunkY = 1; // Stone layer
const key = getChunkKey(chunkX, chunkY);
getOrCreateChunk(chunkX, chunkY);
dirtyChunks.add(key);
}
generateChunksAroundPlayer(); generateChunksAroundPlayer();
// Start the simulation loop // Start the simulation loop
@ -75,47 +65,12 @@ function simulationLoop(timestamp) {
fps = Math.round(1000 / deltaTime); fps = Math.round(1000 / deltaTime);
document.getElementById('fps').textContent = `FPS: ${fps}`; document.getElementById('fps').textContent = `FPS: ${fps}`;
// Update physics with timestamp for rate limiting // Update physics
updatePhysics(timestamp); updatePhysics();
// Render // Render
render(); render();
// Memory management: Clean up chunk cache for chunks that are far away
if (timestamp % 5000 < 16) { // Run every ~5 seconds
cleanupChunkCache();
}
// Continue the loop // Continue the loop
requestAnimationFrame(simulationLoop); requestAnimationFrame(simulationLoop);
} }
// Clean up chunk cache to prevent memory leaks
function cleanupChunkCache() {
if (!chunkCanvasCache) return;
const visibleChunks = getVisibleChunks();
const visibleKeys = new Set();
// Get all visible chunk keys
for (const { chunkX, chunkY } of visibleChunks) {
visibleKeys.add(getChunkKey(chunkX, chunkY));
}
// Remove cached canvases for chunks that are far from view
for (const key of chunkCanvasCache.keys()) {
if (!visibleKeys.has(key)) {
// Keep stone layer chunks in cache longer
if (key.split(',')[1] === '1') {
// Only remove if it's really far away
const [chunkX, chunkY] = key.split(',').map(Number);
const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
if (Math.abs(chunkX - centerChunkX) > 10) {
chunkCanvasCache.delete(key);
}
} else {
chunkCanvasCache.delete(key);
}
}
}
}

View File

@ -1,12 +1,5 @@
// Physics simulation functions // Physics simulation functions
function updatePhysics(timestamp) { function updatePhysics() {
// Check if we should update physics based on the update rate
if (timestamp && lastPhysicsTime && timestamp - lastPhysicsTime < physicsUpdateRate) {
return;
}
lastPhysicsTime = timestamp || 0;
// Get visible chunks // Get visible chunks
const visibleChunks = getVisibleChunks(); const visibleChunks = getVisibleChunks();
@ -14,12 +7,8 @@ function updatePhysics(timestamp) {
fireUpdateCounter++; fireUpdateCounter++;
// Process each visible chunk // Process each visible chunk
for (const { chunkX, chunkY, isVisible } of visibleChunks) { for (const { chunkX, chunkY } of visibleChunks) {
// Skip physics calculations for chunks that are not visible
if (!isVisible) continue;
const chunk = getOrCreateChunk(chunkX, chunkY); const chunk = getOrCreateChunk(chunkX, chunkY);
let chunkModified = false;
// Process from bottom to top, right to left for correct gravity simulation // Process from bottom to top, right to left for correct gravity simulation
for (let y = CHUNK_SIZE - 1; y >= 0; y--) { for (let y = CHUNK_SIZE - 1; y >= 0; y--) {
@ -37,42 +26,28 @@ function updatePhysics(timestamp) {
const worldX = chunkX * CHUNK_SIZE + x; const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y; const worldY = chunkY * CHUNK_SIZE + y;
// Use a lookup table for faster element updates if (type === SAND) {
const updateFunctions = { updateSand(worldX, worldY);
[SAND]: updateSand, } else if (type === WATER) {
[WATER]: updateWater, updateWater(worldX, worldY);
[DIRT]: updateDirt, } else if (type === DIRT) {
[GRASS]: updateGrass, updateDirt(worldX, worldY);
[SEED]: updateSeed, } else if (type === GRASS) {
[GRASS_BLADE]: updateGrassBlade, updateGrass(worldX, worldY);
[FLOWER]: updateFlower, } else if (type === SEED) {
[TREE_SEED]: updateTreeSeed, updateSeed(worldX, worldY);
[FIRE]: updateFire, } else if (type === GRASS_BLADE) {
[LAVA]: updateLava updateGrassBlade(worldX, worldY);
}; } else if (type === FLOWER) {
updateFlower(worldX, worldY);
const updateFunction = updateFunctions[type]; } else if (type === TREE_SEED) {
if (updateFunction) { updateTreeSeed(worldX, worldY);
const wasModified = updateFunction(worldX, worldY); } else if (type === FIRE) {
if (wasModified) { updateFire(worldX, worldY);
chunkModified = true; } else if (type === LAVA) {
} updateLava(worldX, worldY);
} }
} }
} }
// Mark chunk as dirty if it was modified
if (chunkModified) {
dirtyChunks.add(getChunkKey(chunkX, chunkY));
}
}
// Adaptive physics rate based on FPS
if (fps < 30 && physicsUpdateRate < 32) {
// If FPS is low, update physics less frequently
physicsUpdateRate = Math.min(32, physicsUpdateRate + 2);
} else if (fps > 50 && physicsUpdateRate > 16) {
// If FPS is high, update physics more frequently
physicsUpdateRate = Math.max(16, physicsUpdateRate - 2);
} }
} }

View File

@ -1,7 +1,4 @@
// Rendering functions // Rendering functions
// Cache for rendered chunks
let chunkCanvasCache = new Map();
function render() { function render() {
// Clear the canvas // Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -10,33 +7,17 @@ function render() {
const visibleChunks = getVisibleChunks(); const visibleChunks = getVisibleChunks();
// Render each visible chunk // Render each visible chunk
for (const { chunkX, chunkY, isVisible } of visibleChunks) { for (const { chunkX, chunkY } of visibleChunks) {
// Skip rendering for chunks that are not visible
if (!isVisible) continue;
const key = getChunkKey(chunkX, chunkY); const key = getChunkKey(chunkX, chunkY);
if (!chunks.has(key)) continue; if (!chunks.has(key)) continue;
const chunk = chunks.get(key);
// Calculate screen position of chunk // Calculate screen position of chunk
const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE; const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE;
const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE; const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE;
// Check if we need to render this chunk
const needsRender = dirtyChunks.has(key) || worldMoved || !chunkCanvasCache.has(key);
// Always render the chunk if it's in the stone layer (for visibility)
// or if it needs rendering
if (needsRender) {
renderChunkToCache(chunkX, chunkY, key);
}
// Draw the cached chunk to the main canvas
const cachedCanvas = chunkCanvasCache.get(key);
if (cachedCanvas) {
ctx.drawImage(cachedCanvas, screenX, screenY);
}
// Draw chunk border in debug mode // Draw chunk border in debug mode
if (debugMode) { if (debugMode) {
ctx.strokeStyle = '#ff0000'; ctx.strokeStyle = '#ff0000';
@ -54,15 +35,85 @@ function render() {
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15); ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
} }
// Remove this chunk from the dirty list after rendering // Render each pixel in the chunk
if (dirtyChunks.has(key)) { for (let y = 0; y < CHUNK_SIZE; y++) {
dirtyChunks.delete(key); 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) {
// Get water color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = WATER_COLORS[colorIndex];
} else if (type === WALL) {
ctx.fillStyle = WALL_COLOR;
} else if (type === DIRT) {
// Get dirt color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = DIRT_COLORS[colorIndex];
} else if (type === STONE) {
// Get stone color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = STONE_COLORS[colorIndex];
} else if (type === GRASS) {
// Get grass color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = GRASS_COLORS[colorIndex];
} else if (type === WOOD) {
// Get wood color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = WOOD_COLORS[colorIndex];
} else if (type === SEED) {
ctx.fillStyle = SEED_COLOR;
} else if (type === GRASS_BLADE) {
// Use the same color variation as grass
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = GRASS_COLORS[colorIndex];
} 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) {
// Get leaf color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
ctx.fillStyle = LEAF_COLORS[colorIndex];
} 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
);
}
} }
} }
// Reset world moved flag after rendering
worldMoved = false;
// Draw cursor position and update debug info // Draw cursor position and update debug info
if (currentMouseX !== undefined && currentMouseY !== undefined) { if (currentMouseX !== undefined && currentMouseY !== undefined) {
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX; const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
@ -95,114 +146,3 @@ function render() {
} }
} }
} }
// Render a chunk to an offscreen canvas and cache it
function renderChunkToCache(chunkX, chunkY, key) {
const chunk = chunks.get(key);
// Create a new canvas for this chunk if it doesn't exist
if (!chunkCanvasCache.has(key)) {
const chunkCanvas = document.createElement('canvas');
chunkCanvas.width = CHUNK_SIZE * PIXEL_SIZE;
chunkCanvas.height = CHUNK_SIZE * PIXEL_SIZE;
chunkCanvasCache.set(key, chunkCanvas);
}
const chunkCanvas = chunkCanvasCache.get(key);
const chunkCtx = chunkCanvas.getContext('2d');
// Clear the chunk canvas
chunkCtx.clearRect(0, 0, chunkCanvas.width, chunkCanvas.height);
// 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];
// Always render stone layer even if it's not directly visible
if (type === EMPTY && chunkY !== 1) continue;
// For the stone layer (chunkY = 1), render a faint background even for empty spaces
if (type === EMPTY && chunkY === 1) {
// Use a very faint gray for empty spaces in the stone layer
chunkCtx.fillStyle = 'rgba(100, 100, 100, 0.2)';
chunkCtx.fillRect(
x * PIXEL_SIZE,
y * PIXEL_SIZE,
PIXEL_SIZE,
PIXEL_SIZE
);
continue;
}
// Set color based on type
if (type === SAND) {
chunkCtx.fillStyle = SAND_COLOR;
} else if (type === WATER) {
// Get water color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = WATER_COLORS[colorIndex];
} else if (type === WALL) {
chunkCtx.fillStyle = WALL_COLOR;
} else if (type === DIRT) {
// Get dirt color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = DIRT_COLORS[colorIndex];
} else if (type === STONE) {
// Get stone color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = STONE_COLORS[colorIndex];
} else if (type === GRASS) {
// Get grass color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = GRASS_COLORS[colorIndex];
} else if (type === WOOD) {
// Get wood color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = WOOD_COLORS[colorIndex];
} else if (type === SEED) {
chunkCtx.fillStyle = SEED_COLOR;
} else if (type === GRASS_BLADE) {
// Use the same color variation as grass
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = GRASS_COLORS[colorIndex];
} else if (type === FLOWER) {
// Get flower color from metadata or use a default
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
chunkCtx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0];
} else if (type === TREE_SEED) {
chunkCtx.fillStyle = SEED_COLOR;
} else if (type === LEAF) {
// Get leaf color from metadata with variation
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
chunkCtx.fillStyle = LEAF_COLORS[colorIndex];
} 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;
chunkCtx.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;
chunkCtx.fillStyle = LAVA_COLORS[colorIndex];
}
// Draw the pixel
chunkCtx.fillRect(
x * PIXEL_SIZE,
y * PIXEL_SIZE,
PIXEL_SIZE,
PIXEL_SIZE
);
}
}
}

View File

@ -6,19 +6,12 @@ let worldOffsetYBeforeDrag = 0;
let chunks = new Map(); // Map to store chunks with key "x,y" let chunks = new Map(); // Map to store chunks with key "x,y"
let metadata = new Map(); // Map to store metadata for pixels let metadata = new Map(); // Map to store metadata for pixels
let generatedChunks = new Set(); // Set to track which chunks have been generated let generatedChunks = new Set(); // Set to track which chunks have been generated
let dirtyChunks = new Set(); // Set to track which chunks need rendering
let lastPhysicsTime = 0; // Last time physics was updated
let physicsUpdateRate = 16; // Update physics every 16ms (approx 60fps)
let worldMoved = false; // Track if the world has moved for rendering
function moveWorld(dx, dy) { function moveWorld(dx, dy) {
worldOffsetX += dx; worldOffsetX += dx;
worldOffsetY += dy; worldOffsetY += dy;
updateCoordinatesDisplay(); updateCoordinatesDisplay();
// Mark that the world has moved for rendering
worldMoved = true;
// Generate terrain for chunks around the current view // Generate terrain for chunks around the current view
generateChunksAroundPlayer(); generateChunksAroundPlayer();
} }
@ -40,41 +33,12 @@ function getOrCreateChunk(chunkX, chunkY) {
// Create a new chunk with empty pixels // Create a new chunk with empty pixels
const chunkData = new Array(CHUNK_SIZE * CHUNK_SIZE).fill(EMPTY); const chunkData = new Array(CHUNK_SIZE * CHUNK_SIZE).fill(EMPTY);
// Fill chunk at y = 1 with stone, but create a noisy transition at the top // Add floor at the bottom of the world (y = 0 and y = 1)
if (chunkY === 1) { if (chunkY === 0 || chunkY === 1) {
// Use the chunk position as part of the seed for consistent generation // Fill the bottom row with walls
const seed = chunkX * 10000; for (let x = 0; x < CHUNK_SIZE; x++) {
const random = createSeededRandom(seed); chunkData[(CHUNK_SIZE - 1) * CHUNK_SIZE + x] = WALL;
for (let y = 0; y < CHUNK_SIZE; y++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
// Create a noisy transition at the top of the stone layer
if (y < 10) { // Only apply noise to the top 10 rows
// More noise at the top, less as we go down
const noiseThreshold = y / 10; // 0 at the top, 1 at row 10
if (random() > noiseThreshold) {
chunkData[y * CHUNK_SIZE + x] = SAND;
} else {
// Increase stone density to make it more visible
chunkData[y * CHUNK_SIZE + x] = random() < 0.9 ? STONE : EMPTY;
}
} else {
// Below the transition zone, it's all stone
chunkData[y * CHUNK_SIZE + x] = STONE;
}
}
} }
// Mark this chunk as dirty to ensure it gets rendered
dirtyChunks.add(key);
}
// Floor has been removed as it's no longer needed
// Special generation for all chunks at y=0
if (chunkY === 0) {
generateSpecialChunk(chunkData, chunkX, chunkX);
} }
chunks.set(key, chunkData); chunks.set(key, chunkData);
@ -83,302 +47,6 @@ function getOrCreateChunk(chunkX, chunkY) {
return chunks.get(key); return chunks.get(key);
} }
// Generate special terrain for chunks near the player
function generateSpecialChunk(chunkData, chunkX, playerChunkX) {
// 1. Create a base layer of sand above the floor
const floorY = CHUNK_SIZE - 1;
const baseHeight = 10; // Base height of sand
// Use the chunk position as part of the seed for consistent generation
const seed = chunkX * 10000;
const random = createSeededRandom(seed);
// Create two random hill points
const hill1X = Math.floor(CHUNK_SIZE * (0.2 + random() * 0.2));
const hill2X = Math.floor(CHUNK_SIZE * (0.6 + random() * 0.2));
const hill1Height = baseHeight + Math.floor(random() * 10) + 5; // 5-15 blocks higher
const hill2Height = baseHeight + Math.floor(random() * 10) + 5;
// Generate height map for sand
const heightMap = new Array(CHUNK_SIZE).fill(0);
// Calculate heights based on distance from the two hills
for (let x = 0; x < CHUNK_SIZE; x++) {
// Distance from each hill (using a simple distance function)
const dist1 = Math.abs(x - hill1X);
const dist2 = Math.abs(x - hill2X);
// Height contribution from each hill (inverse to distance)
const h1 = hill1Height * Math.max(0, 1 - dist1 / (CHUNK_SIZE * 0.3));
const h2 = hill2Height * Math.max(0, 1 - dist2 / (CHUNK_SIZE * 0.3));
// Take the maximum height contribution
heightMap[x] = Math.floor(baseHeight + Math.max(h1, h2));
// Add some variation based on distance from player's chunk
const distanceFromPlayer = Math.abs(chunkX - playerChunkX);
if (distanceFromPlayer > 0) {
// Make terrain more extreme as we move away from player
const factor = 1 + (distanceFromPlayer * 0.2);
heightMap[x] = Math.floor(heightMap[x] * factor);
}
}
// Find the lowest points for water
let minHeight = Math.min(...heightMap);
// Place sand according to the height map with noise
for (let x = 0; x < CHUNK_SIZE; x++) {
const height = heightMap[x];
// Add more noise to the height
const noiseHeight = height + Math.floor(random() * 5) - 2;
for (let y = floorY - noiseHeight; y < floorY; y++) {
chunkData[y * CHUNK_SIZE + x] = SAND;
}
// 3. Add grass with significantly more coverage and noise
// Increase grass probability for more coverage - now almost guaranteed
const grassProbability = (height - baseHeight) / (hill1Height - baseHeight);
if (random() < grassProbability * 0.3 + 0.7) { // Minimum 70% chance, up to 100%
// Add grass on top
chunkData[(floorY - noiseHeight) * CHUNK_SIZE + x] = GRASS;
// Much more frequently add patches of grass on the sides
if (random() < 0.8) { // Increased from 0.5
// Add grass to the left if possible
if (x > 0 && chunkData[(floorY - noiseHeight) * CHUNK_SIZE + (x-1)] === SAND) {
chunkData[(floorY - noiseHeight) * CHUNK_SIZE + (x-1)] = GRASS;
}
}
if (random() < 0.8) { // Increased from 0.5
// Add grass to the right if possible
if (x < CHUNK_SIZE-1 && chunkData[(floorY - noiseHeight) * CHUNK_SIZE + (x+1)] === SAND) {
chunkData[(floorY - noiseHeight) * CHUNK_SIZE + (x+1)] = GRASS;
}
}
// More frequently add grass patches below the top
if (random() < 0.6 && noiseHeight > 2) { // Increased from 0.3
const patchDepth = Math.floor(random() * 5) + 2; // Increased max depth and minimum
for (let d = 1; d <= patchDepth; d++) {
if (floorY - noiseHeight + d < floorY) {
chunkData[(floorY - noiseHeight + d) * CHUNK_SIZE + x] = GRASS;
}
}
}
// More frequently add grass clusters
if (random() < 0.5) { // Increased from 0.2
// Add a larger cluster of grass
for (let dy = -2; dy <= 1; dy++) { // Increased vertical range
for (let dx = -2; dx <= 2; dx++) { // Increased horizontal range
const nx = x + dx;
const ny = floorY - noiseHeight + dy;
if (nx >= 0 && nx < CHUNK_SIZE && ny >= 0 && ny < CHUNK_SIZE &&
(chunkData[ny * CHUNK_SIZE + nx] === SAND || chunkData[ny * CHUNK_SIZE + nx] === EMPTY)) {
// Higher chance to place grass closer to center
if (Math.abs(dx) + Math.abs(dy) <= 2 || random() < 0.7) {
chunkData[ny * CHUNK_SIZE + nx] = GRASS;
}
}
}
}
}
// Sometimes add grass "islands" on top of sand
if (random() < 0.15 && noiseHeight > 4) {
// Add a small patch of grass above the surface
const islandHeight = Math.floor(random() * 2) + 1;
for (let d = 1; d <= islandHeight; d++) {
const ny = floorY - noiseHeight - d;
if (ny >= 0) {
chunkData[ny * CHUNK_SIZE + x] = GRASS;
}
}
}
// Randomly spawn tree seeds on grass (reduced frequency)
if (random() < 0.03) { // Reduced from 8% to 3% chance for a tree seed on grass
const seedY = floorY - noiseHeight - 1; // Position above the grass
// Check if there are any existing tree seeds within 5 pixels
let hasSeedNearby = false;
for (let checkY = Math.max(0, seedY - 5); checkY <= Math.min(CHUNK_SIZE - 1, seedY + 5); checkY++) {
for (let checkX = Math.max(0, x - 5); checkX <= Math.min(CHUNK_SIZE - 1, x + 5); checkX++) {
if (chunkData[checkY * CHUNK_SIZE + checkX] === TREE_SEED) {
hasSeedNearby = true;
break;
}
}
if (hasSeedNearby) break;
}
// Check if there's water below or nearby
let hasWaterBelow = false;
for (let checkY = floorY - noiseHeight + 1; checkY < Math.min(CHUNK_SIZE, floorY - noiseHeight + 5); checkY++) {
if (chunkData[checkY * CHUNK_SIZE + x] === WATER) {
hasWaterBelow = true;
break;
}
}
// Only place the seed if there are no other seeds nearby and no water below
// Place seed 3 pixels above the surface instead of just 1
const elevatedSeedY = floorY - noiseHeight - 3; // 3 pixels above the grass
if (!hasSeedNearby && !hasWaterBelow && elevatedSeedY >= 0 && chunkData[(floorY - noiseHeight) * CHUNK_SIZE + x] === GRASS) {
chunkData[elevatedSeedY * CHUNK_SIZE + x] = TREE_SEED;
// Add metadata for the tree seed
const seedMetadata = {
age: Math.floor(random() * 50), // Random initial age
growthStage: 0,
type: 'oak' // Default tree type
};
// We'll set the metadata when the chunk is actually created
}
}
}
}
// 2. Add water in more areas with greater depth
for (let x = 0; x < CHUNK_SIZE; x++) {
const height = heightMap[x];
// Add water where the height is close to the minimum (increased threshold)
if (height <= minHeight + 4) { // Increased from +2 to +4
// Add more layers of water
const waterDepth = 5; // Increased from 3 to 5
for (let d = 0; d < waterDepth; d++) {
const y = floorY - height - d - 1;
if (y >= 0) {
chunkData[y * CHUNK_SIZE + x] = WATER;
}
}
}
// Sometimes add small water pools in random depressions
if (random() < 0.1 && height <= minHeight + 8 && height > minHeight + 4) {
// Add a small pool of water
const poolDepth = Math.floor(random() * 2) + 1;
for (let d = 0; d < poolDepth; d++) {
const y = floorY - height - d - 1;
if (y >= 0) {
chunkData[y * CHUNK_SIZE + x] = WATER;
}
}
}
}
// Add some connected water channels between pools
for (let x = 1; x < CHUNK_SIZE - 1; x++) {
// Check if there's water to the left and right but not at this position
const y = floorY - heightMap[x] - 1;
const leftHasWater = x > 0 && chunkData[y * CHUNK_SIZE + (x-1)] === WATER;
const rightHasWater = x < CHUNK_SIZE-1 && chunkData[y * CHUNK_SIZE + (x+1)] === WATER;
if (leftHasWater && rightHasWater && chunkData[y * CHUNK_SIZE + x] !== WATER) {
if (random() < 0.7) { // 70% chance to connect water bodies
chunkData[y * CHUNK_SIZE + x] = WATER;
}
}
}
// Add some random elements based on the chunk position
if (random() < 0.3) {
// Add a small tree or plant cluster
const plantX = Math.floor(random() * CHUNK_SIZE);
const plantY = floorY - heightMap[plantX] - 1;
if (plantY > 0 && chunkData[plantY * CHUNK_SIZE + plantX] === GRASS) {
// Add a small tree
for (let i = 0; i < 3; i++) {
if (plantY - i > 0) {
chunkData[(plantY - i) * CHUNK_SIZE + plantX] = WOOD;
}
}
// Add some leaves
for (let dy = -2; dy <= 0; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const leafX = plantX + dx;
const leafY = plantY - 3 + dy;
if (leafX >= 0 && leafX < CHUNK_SIZE && leafY >= 0 &&
Math.abs(dx) + Math.abs(dy) < 3) {
chunkData[leafY * CHUNK_SIZE + leafX] = LEAF;
}
}
}
}
}
// Add additional tree seeds scattered throughout the terrain with minimum spacing
const treePositions = []; // Track positions of placed tree seeds
for (let x = 0; x < CHUNK_SIZE; x += 15 + Math.floor(random() * 20)) { // Increased spacing from 5-15 to 15-35
const height = heightMap[x];
const surfaceY = floorY - height;
// Only place seeds on grass
if (chunkData[surfaceY * CHUNK_SIZE + x] === GRASS) {
// Reduced from 25% to 10% chance for a tree seed at each valid position
if (random() < 0.1) {
const seedY = surfaceY - 3; // Position 3 pixels above the grass instead of just 1
// Check if this position is at least 5 pixels away from any existing tree seed
let tooClose = false;
for (const pos of treePositions) {
const distance = Math.abs(x - pos.x) + Math.abs(seedY - pos.y); // Manhattan distance
if (distance < 5) {
tooClose = true;
break;
}
}
// Check if there's water below or nearby
let hasWaterBelow = false;
for (let checkY = surfaceY + 1; checkY < Math.min(CHUNK_SIZE, surfaceY + 5); checkY++) {
if (chunkData[checkY * CHUNK_SIZE + x] === WATER) {
hasWaterBelow = true;
break;
}
}
// Only place the seed if it's not too close to another seed and no water below
if (!tooClose && !hasWaterBelow && seedY >= 0) {
chunkData[seedY * CHUNK_SIZE + x] = TREE_SEED;
treePositions.push({ x, y: seedY });
}
}
}
}
// Add some flower seeds in clusters near grass
for (let i = 0; i < 3; i++) { // Create a few flower clusters
const clusterX = Math.floor(random() * CHUNK_SIZE);
const clusterY = floorY - heightMap[clusterX];
if (chunkData[clusterY * CHUNK_SIZE + clusterX] === GRASS) {
// Create a small cluster of flower seeds
for (let dy = -1; dy <= 0; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const seedX = clusterX + dx;
const seedY = clusterY + dy - 1; // Above the grass
if (seedX >= 0 && seedX < CHUNK_SIZE && seedY >= 0 &&
random() < 0.6 && // 60% chance for each position in the cluster
chunkData[(seedY+1) * CHUNK_SIZE + seedX] === GRASS) {
chunkData[seedY * CHUNK_SIZE + seedX] = SEED;
}
}
}
}
}
}
function getChunkCoordinates(worldX, worldY) { function getChunkCoordinates(worldX, worldY) {
const chunkX = Math.floor(worldX / CHUNK_SIZE); const chunkX = Math.floor(worldX / CHUNK_SIZE);
const chunkY = Math.floor(worldY / CHUNK_SIZE); const chunkY = Math.floor(worldY / CHUNK_SIZE);
@ -393,42 +61,26 @@ function setPixel(worldX, worldY, type) {
const chunk = getOrCreateChunk(chunkX, chunkY); const chunk = getOrCreateChunk(chunkX, chunkY);
const index = localY * CHUNK_SIZE + localX; const index = localY * CHUNK_SIZE + localX;
// Only update if the pixel type is changing chunk[index] = type;
if (chunk[index] !== type) {
chunk[index] = type; // Assign random color index for natural elements
if (type === DIRT || type === GRASS || type === STONE || type === WOOD || type === LEAF) {
// Mark chunk as dirty for rendering const colorIndex = Math.floor(Math.random() * 10);
dirtyChunks.add(getChunkKey(chunkX, chunkY)); setMetadata(worldX, worldY, { ...getMetadata(worldX, worldY) || {}, colorIndex });
}
// Assign random color index for natural elements else if (type === WATER) {
if (type === DIRT || type === GRASS || type === STONE || type === WOOD || type === LEAF) { const colorIndex = Math.floor(Math.random() * 10);
const colorIndex = Math.floor(Math.random() * 10); setMetadata(worldX, worldY, { ...getMetadata(worldX, worldY) || {}, colorIndex, waterColorTimer: 0 });
setMetadata(worldX, worldY, { ...getMetadata(worldX, worldY) || {}, colorIndex });
}
else if (type === WATER) {
const colorIndex = Math.floor(Math.random() * 10);
setMetadata(worldX, worldY, { ...getMetadata(worldX, worldY) || {}, colorIndex, waterColorTimer: 0 });
}
else if (type === TREE_SEED) {
// Initialize tree seed metadata
setMetadata(worldX, worldY, {
age: Math.floor(Math.random() * 50), // Random initial age
growthStage: 0,
type: Math.random() < 0.8 ? 'oak' : 'pine' // 80% oak, 20% pine
});
}
else if (type === SEED) {
// Initialize flower seed metadata
setMetadata(worldX, worldY, {
age: Math.floor(Math.random() * 30),
growthStage: 0,
flowerType: Math.floor(Math.random() * 5) // Different flower types
});
}
} }
} }
function getPixel(worldX, worldY) { 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 { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY);
const key = getChunkKey(chunkX, chunkY); const key = getChunkKey(chunkX, chunkY);
@ -464,110 +116,23 @@ function moveMetadata(fromX, fromY, toX, toY) {
if (data) { if (data) {
setMetadata(toX, toY, data); setMetadata(toX, toY, data);
removeMetadata(fromX, fromY); removeMetadata(fromX, fromY);
// Mark chunks as dirty for rendering
const { chunkX: fromChunkX, chunkY: fromChunkY } = getChunkCoordinates(fromX, fromY);
const { chunkX: toChunkX, chunkY: toChunkY } = getChunkCoordinates(toX, toY);
dirtyChunks.add(getChunkKey(fromChunkX, fromChunkY));
dirtyChunks.add(getChunkKey(toChunkX, toChunkY));
} }
} }
function getVisibleChunks() { function getVisibleChunks() {
const visibleChunks = []; const visibleChunks = [];
// Calculate visible chunk range (chunks that might be visible on screen) // Calculate visible chunk range
const startChunkX = Math.floor(worldOffsetX / CHUNK_SIZE) - 1; const startChunkX = Math.floor(worldOffsetX / CHUNK_SIZE) - 1;
const endChunkX = Math.ceil((worldOffsetX + canvas.width / PIXEL_SIZE) / CHUNK_SIZE) + 1; const endChunkX = Math.ceil((worldOffsetX + canvas.width / PIXEL_SIZE) / CHUNK_SIZE) + 1;
const startChunkY = Math.floor(worldOffsetY / CHUNK_SIZE) - 1; const startChunkY = Math.floor(worldOffsetY / CHUNK_SIZE) - 1;
const endChunkY = Math.ceil((worldOffsetY + canvas.height / PIXEL_SIZE) / CHUNK_SIZE) + 1; const endChunkY = Math.ceil((worldOffsetY + canvas.height / PIXEL_SIZE) / CHUNK_SIZE) + 1;
// Calculate the exact visible area in world coordinates
const visibleStartX = worldOffsetX;
const visibleEndX = worldOffsetX + canvas.width / PIXEL_SIZE;
const visibleStartY = worldOffsetY;
const visibleEndY = worldOffsetY + canvas.height / PIXEL_SIZE;
for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) { for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) {
for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) { for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) {
// Calculate chunk boundaries in world coordinates visibleChunks.push({ chunkX, chunkY });
const chunkWorldStartX = chunkX * CHUNK_SIZE;
const chunkWorldEndX = (chunkX + 1) * CHUNK_SIZE;
const chunkWorldStartY = chunkY * CHUNK_SIZE;
const chunkWorldEndY = (chunkY + 1) * CHUNK_SIZE;
// Check if this chunk is actually visible in the viewport
const isVisible = !(
chunkWorldEndX < visibleStartX ||
chunkWorldStartX > visibleEndX ||
chunkWorldEndY < visibleStartY ||
chunkWorldStartY > visibleEndY
);
visibleChunks.push({ chunkX, chunkY, isVisible });
} }
} }
return visibleChunks; return visibleChunks;
} }
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
// Get visible chunks to prioritize their generation
const visibleChunks = getVisibleChunks();
const visibleChunkKeys = new Set(visibleChunks.map(chunk => getChunkKey(chunk.chunkX, chunk.chunkY)));
// Always generate the stone layer at y = 1 for visible chunks first
for (let dx = -radius; dx <= radius; dx++) {
const chunkX = centerChunkX + dx;
const chunkY = 1; // The chunk at y = 1 (moved from y = -1)
const key = getChunkKey(chunkX, chunkY);
// Always generate stone layer chunks
const isNewChunk = !chunks.has(key);
getOrCreateChunk(chunkX, chunkY);
// Mark as dirty only if it's a new chunk
if (isNewChunk) {
dirtyChunks.add(key);
}
}
// Generate visible chunks first
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
if (isVisible) {
getOrCreateChunk(chunkX, chunkY);
}
}
// Then generate non-visible chunks within the radius (with lower priority)
// Always generate the stone layer at y = 1 for remaining chunks
for (let dx = -radius; dx <= radius; dx++) {
const chunkX = centerChunkX + dx;
const chunkY = 1;
const key = getChunkKey(chunkX, chunkY);
// Skip if already generated
if (!visibleChunkKeys.has(key)) {
getOrCreateChunk(chunkX, chunkY);
}
}
// Generate remaining 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;
const key = getChunkKey(chunkX, chunkY);
// Skip if already generated
if (!visibleChunkKeys.has(key)) {
getOrCreateChunk(chunkX, chunkY);
}
}
}
}