Compare commits

..

27 Commits

Author SHA1 Message Date
Kacper Kostka
ebb96846ed Create .gitignore 2025-04-05 15:59:45 +02:00
Kacper Kostka (aider)
60c2db558f feat: Add rabbit element with jumping and movement behaviors
This commit introduces a new rabbit element with the following features:
- Jumping mechanics with variable height
- Random direction changes
- Falling and basic ground interaction
- Color variations
- Avoidance of water

The changes include:
- Added RABBIT constant to element types
- Created rabbit.js with updateRabbit function
- Added RABBIT_COLORS to constants
- Implemented basic rabbit movement and behavior logic
2025-04-05 15:57:54 +02:00
Kacper Kostka (aider)
bb45c9fba8 refactor: Optimize rendering with chunk canvas caching and performance improvements 2025-04-05 15:49:13 +02:00
Kacper Kostka (aider)
d87105baad feat: Ensure stone layer chunks always render and remain visible 2025-04-05 15:45:13 +02:00
Kacper Kostka (aider)
883c3d9a08 feat: Improve stone layer visibility and rendering 2025-04-05 15:40:27 +02:00
Kacper Kostka (aider)
15fb106246 fix: Add missing dirtyChunks variable and update rendering logic 2025-04-05 15:37:55 +02:00
Kacper Kostka (aider)
60a1757ab1 feat: Optimize rendering and physics with dirty chunk tracking and adaptive update rates
This commit introduces several performance optimizations:
- Implement chunk-based dirty rendering
- Add adaptive physics update rates
- Return modification status from element update functions
- Reduce unnecessary rendering and physics calculations
- Track world movement for efficient re-rendering

The key changes include:
1. Adding `dirtyChunks` and `worldMoved` tracking
2. Modifying element update functions to return modification status
3. Implementing adaptive physics update rates based on FPS
4. Skipping rendering for unchanged chunks
5. Reducing computational overhead in physics and rendering loops

These optimizations should significantly improve the simulation's performance, especially with large numbers of elements.
2025-04-05 15:35:19 +02:00
Kacper Kostka (aider)
1369822dc9 refactor: Add visibility check for chunk processing in physics and render 2025-04-05 15:27:10 +02:00
Kacper Kostka (aider)
4c96226d05 perf: Optimize chunk rendering and generation by skipping off-screen chunks 2025-04-05 15:26:49 +02:00
Kacper Kostka (aider)
c7735b8578 fix: Ensure consistent terrain generation for all chunks at y=0 2025-04-05 15:24:32 +02:00
Kacper Kostka (aider)
d34a695bb8 feat: Prevent tree seeds from spawning near water 2025-04-05 15:21:34 +02:00
Kacper Kostka (aider)
9c49af57cb refactor: Reverse noisy transition between sand and stone layers 2025-04-05 15:19:53 +02:00
Kacper Kostka (aider)
206e9eb5cc feat: Add noisy transition between stone and sand layers 2025-04-05 15:17:38 +02:00
Kacper Kostka (aider)
908c819441 refactor: Remove floor generation and related code from world generation 2025-04-05 15:16:21 +02:00
Kacper Kostka (aider)
7aa0c1785b feat: Add generateChunksAroundPlayer function to world.js 2025-04-05 15:14:35 +02:00
Kacper Kostka (aider)
b85ae017b8 feat: Move stone layer generation from y = -1 to y = 1 2025-04-05 15:14:14 +02:00
Kacper Kostka (aider)
eab3a9b790 refactor: Remove floor generation for second chunk at world bottom 2025-04-05 15:12:10 +02:00
Kacper Kostka (aider)
d5f46d94c3 feat: Add stone layer beneath terrain generation 2025-04-05 15:08:40 +02:00
Kacper Kostka (aider)
ec7f8f9522 feat: Spawn tree seeds 3 pixels above surface for better growth 2025-04-05 15:06:49 +02:00
Kacper Kostka (aider)
0b96238226 feat: Implement minimum 5-pixel spacing for tree seed generation 2025-04-05 15:05:51 +02:00
Kacper Kostka (aider)
170b8d0a85 refactor: Reduce tree seed generation frequency and spacing 2025-04-05 15:03:57 +02:00
Kacper Kostka (aider)
4716cbe692 feat: Add natural tree and flower seed generation on grass terrain 2025-04-05 15:02:59 +02:00
Kacper Kostka (aider)
1584600d0b feat: Enhance grass generation with more coverage and variety 2025-04-05 15:00:12 +02:00
Kacper Kostka (aider)
7806b39a51 feat: Enhance terrain generation with more grass and water variety 2025-04-05 14:57:01 +02:00
Kacper Kostka (aider)
076f21f9ca feat: Enhance terrain generation with noisy sand and dynamic grass coverage 2025-04-05 14:54:17 +02:00
Kacper Kostka (aider)
a31f401378 feat: Enhance terrain generation with dynamic chunk spreading and seeded randomness 2025-04-05 14:53:15 +02:00
Kacper Kostka (aider)
c35f6cd448 feat: Add procedural generation for first chunk with sand hills, water, and grass 2025-04-05 14:50:47 +02:00
11 changed files with 897 additions and 136 deletions

1
.gitignore vendored Normal file
View File

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

View File

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

View File

@ -4,29 +4,37 @@ function updateSand(x, y) {
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, SAND);
return true;
}
// 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);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SAND);
return true;
}
// Sand can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, SAND);
return true;
}
return false;
}
function updateWater(x, y) {
let modified = false;
// Update water color dynamically
const metadata = getMetadata(x, y);
if (metadata) {
if (metadata.waterColorTimer === undefined) {
metadata.waterColorTimer = 0;
modified = true;
}
metadata.waterColorTimer++;
@ -35,6 +43,7 @@ function updateWater(x, y) {
if (metadata.waterColorTimer > 20 && Math.random() < 0.1) {
metadata.colorIndex = Math.floor(Math.random() * 10);
metadata.waterColorTimer = 0;
modified = true;
}
setMetadata(x, y, metadata);
@ -45,17 +54,20 @@ function updateWater(x, y) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, WATER);
moveMetadata(x, y, x, y + 1);
return true;
}
// 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);
moveMetadata(x, y, x - 1, y + 1);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, WATER);
moveMetadata(x, y, x + 1, y + 1);
return true;
}
// Try to spread horizontally
else {
@ -67,21 +79,25 @@ function updateWater(x, y) {
if (goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER);
moved = true;
moveMetadata(x, y, x - 1, y);
return true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moved = true;
moveMetadata(x, y, x + 1, y);
return 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;
moveMetadata(x, y, x - 1, y);
return true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moved = true;
moveMetadata(x, y, x + 1, y);
return true;
}
}
@ -97,15 +113,18 @@ function updateWater(x, y) {
if (getPixel(x + dir.dx, y + dir.dy) === FIRE) {
setPixel(x + dir.dx, y + dir.dy, EMPTY);
removeMetadata(x + dir.dx, y + dir.dy);
return true;
} 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;
return true;
}
}
return modified;
}
function updateDirt(x, y) {
@ -113,25 +132,30 @@ function updateDirt(x, y) {
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, DIRT);
return true;
}
// 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);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, DIRT);
return true;
}
// Dirt can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, DIRT);
return true;
}
// Dirt can turn into grass if exposed to air above
if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) {
setPixel(x, y, GRASS);
return true;
}
// Dirt can randomly spawn seeds if exposed to air above
@ -140,12 +164,17 @@ function updateDirt(x, y) {
const seedRoll = Math.random();
if (seedRoll < 0.0002) { // Grass blade seed (most common)
setPixel(x, y - 1, SEED);
return true;
} 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' });
return true;
} else if (seedRoll < 0.00026) { // Tree seed (rare)
setPixel(x, y - 1, TREE_SEED);
return true;
}
}
return false;
}

View File

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

View File

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

110
js/elements/rabbit.js Normal file
View File

@ -0,0 +1,110 @@
// 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,26 +5,33 @@ function updateTreeSeed(x, y) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, TREE_SEED);
moveMetadata(x, y, x, y + 1);
return true;
}
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);
return true;
}
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);
return true;
}
// Seeds can float on water
else if (getPixel(x, y + 1) === WATER) {
// Just float, don't do anything
return false;
}
// 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);
return true;
}
return false;
}
function growTree(x, y) {

View File

@ -47,6 +47,16 @@ window.onload = function() {
// Initialize the first chunk and generate terrain around it
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();
// Start the simulation loop
@ -65,12 +75,47 @@ function simulationLoop(timestamp) {
fps = Math.round(1000 / deltaTime);
document.getElementById('fps').textContent = `FPS: ${fps}`;
// Update physics
updatePhysics();
// Update physics with timestamp for rate limiting
updatePhysics(timestamp);
// 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
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,5 +1,12 @@
// Physics simulation functions
function updatePhysics() {
function updatePhysics(timestamp) {
// Check if we should update physics based on the update rate
if (timestamp && lastPhysicsTime && timestamp - lastPhysicsTime < physicsUpdateRate) {
return;
}
lastPhysicsTime = timestamp || 0;
// Get visible chunks
const visibleChunks = getVisibleChunks();
@ -7,8 +14,12 @@ function updatePhysics() {
fireUpdateCounter++;
// Process each visible chunk
for (const { chunkX, chunkY } of visibleChunks) {
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
// Skip physics calculations for chunks that are not visible
if (!isVisible) continue;
const chunk = getOrCreateChunk(chunkX, chunkY);
let chunkModified = false;
// Process from bottom to top, right to left for correct gravity simulation
for (let y = CHUNK_SIZE - 1; y >= 0; y--) {
@ -26,28 +37,42 @@ function updatePhysics() {
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);
// Use a lookup table for faster element updates
const updateFunctions = {
[SAND]: updateSand,
[WATER]: updateWater,
[DIRT]: updateDirt,
[GRASS]: updateGrass,
[SEED]: updateSeed,
[GRASS_BLADE]: updateGrassBlade,
[FLOWER]: updateFlower,
[TREE_SEED]: updateTreeSeed,
[FIRE]: updateFire,
[LAVA]: updateLava
};
const updateFunction = updateFunctions[type];
if (updateFunction) {
const wasModified = updateFunction(worldX, worldY);
if (wasModified) {
chunkModified = true;
}
}
}
}
// 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,4 +1,7 @@
// Rendering functions
// Cache for rendered chunks
let chunkCanvasCache = new Map();
function render() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -7,17 +10,33 @@ function render() {
const visibleChunks = getVisibleChunks();
// Render each visible chunk
for (const { chunkX, chunkY } of visibleChunks) {
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
// Skip rendering for chunks that are not visible
if (!isVisible) continue;
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;
// 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
if (debugMode) {
ctx.strokeStyle = '#ff0000';
@ -35,84 +54,14 @@ function render() {
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) {
// 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];
// Remove this chunk from the dirty list after rendering
if (dirtyChunks.has(key)) {
dirtyChunks.delete(key);
}
}
// 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
if (currentMouseX !== undefined && currentMouseY !== undefined) {
@ -146,3 +95,114 @@ 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,12 +6,19 @@ 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
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) {
worldOffsetX += dx;
worldOffsetY += dy;
updateCoordinatesDisplay();
// Mark that the world has moved for rendering
worldMoved = true;
// Generate terrain for chunks around the current view
generateChunksAroundPlayer();
}
@ -33,12 +40,41 @@ function getOrCreateChunk(chunkX, chunkY) {
// 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
// Fill chunk at y = 1 with stone, but create a noisy transition at the top
if (chunkY === 1) {
// Use the chunk position as part of the seed for consistent generation
const seed = chunkX * 10000;
const random = createSeededRandom(seed);
for (let y = 0; y < CHUNK_SIZE; y++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
chunkData[(CHUNK_SIZE - 1) * CHUNK_SIZE + x] = WALL;
// 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);
@ -47,6 +83,302 @@ function getOrCreateChunk(chunkX, chunkY) {
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) {
const chunkX = Math.floor(worldX / CHUNK_SIZE);
const chunkY = Math.floor(worldY / CHUNK_SIZE);
@ -61,8 +393,13 @@ function setPixel(worldX, worldY, type) {
const chunk = getOrCreateChunk(chunkX, chunkY);
const index = localY * CHUNK_SIZE + localX;
// Only update if the pixel type is changing
if (chunk[index] !== type) {
chunk[index] = type;
// Mark chunk as dirty for rendering
dirtyChunks.add(getChunkKey(chunkX, chunkY));
// Assign random color index for natural elements
if (type === DIRT || type === GRASS || type === STONE || type === WOOD || type === LEAF) {
const colorIndex = Math.floor(Math.random() * 10);
@ -72,15 +409,26 @@ function setPixel(worldX, worldY, type) {
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) {
// 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);
@ -116,23 +464,110 @@ function moveMetadata(fromX, fromY, toX, toY) {
if (data) {
setMetadata(toX, toY, data);
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() {
const visibleChunks = [];
// Calculate visible chunk range
// Calculate visible chunk range (chunks that might be visible on screen)
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;
// 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 chunkX = startChunkX; chunkX < endChunkX; chunkX++) {
visibleChunks.push({ chunkX, chunkY });
// Calculate chunk boundaries in world coordinates
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;
}
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);
}
}
}
}