Compare commits

...

74 Commits

Author SHA1 Message Date
Kacper Kostka
b82f415f4f txts 2025-04-05 19:17:21 +02:00
Kacper Kostka (aider)
80c243ebf8 feat: Implement player block breaking with 10-pixel range and inventory collection 2025-04-05 19:12:11 +02:00
Kacper Kostka (aider)
d765defa9d feat: Add block breaking, HUD, and inventory system for player 2025-04-05 19:08:28 +02:00
Kacper Kostka
755be2f5a4 refactor: Update player sprite and camera movement parameters 2025-04-05 19:08:24 +02:00
Kacper Kostka (aider)
c5b7f2f224 feat: Implement climbing system and tree collision avoidance for player
This commit adds:
1. Player climbing mechanism for small steps
2. Collision avoidance with trees (WOOD and LEAF pixels)
3. Enhanced animation states for climbing and jumping
4. Improved player movement and collision detection
2025-04-05 18:52:50 +02:00
Kacper Kostka (aider)
bb1e25e753 refactor: Reduce player size by 50% to improve pixel collision 2025-04-05 18:49:31 +02:00
Kacper Kostka
d2ab5094ab refactor: Update player sprite and frame dimensions 2025-04-05 18:49:29 +02:00
Kacper Kostka (aider)
583544840b feat: Add F3 debug mode with collision boxes and FPS display 2025-04-05 18:39:47 +02:00
Kacper Kostka (aider)
cb62097150 fix: Stretch player sprite to match collision box height and remove levitation 2025-04-05 18:36:27 +02:00
Kacper Kostka (aider)
8e2575f6fc fix: Align player collision box and sprite rendering precisely 2025-04-05 18:33:19 +02:00
Kacper Kostka
04295f9f9f refactor: Simplify player animation update logic 2025-04-05 18:33:17 +02:00
Kacper Kostka (aider)
0c8e13d630 feat: Improve player sprite rendering with pixelation and animation fixes 2025-04-05 18:29:24 +02:00
Kacper Kostka (aider)
f592c74412 fix: Adjust player sprite rendering and collision box for proper scaling and alignment 2025-04-05 18:23:38 +02:00
Kacper Kostka (aider)
84e08b397d feat: Update player sprite to 32x32 with improved animation and collision handling 2025-04-05 18:22:39 +02:00
Kacper Kostka
c853738bbf refactor: Update player sprite and adjust movement parameters 2025-04-05 18:22:36 +02:00
Kacper Kostka (aider)
0a13dfc0a3 refactor: Optimize camera movement with chunk-aware smooth tracking 2025-04-05 18:15:32 +02:00
Kacper Kostka
afba547fce fix: Adjust player jump force for more precise movement 2025-04-05 18:13:58 +02:00
Kacper Kostka (aider)
8562c86986 feat: Increase zoom level to show smaller view area when spawning player 2025-04-05 18:08:11 +02:00
Kacper Kostka (aider)
ad90b9320f fix: Optimize player spawn, camera zoom, and performance to reduce lag 2025-04-05 18:06:39 +02:00
Kacper Kostka (aider)
85a96f153f fix: Change PIXEL_SIZE from const to let to allow modification 2025-04-05 18:04:44 +02:00
Kacper Kostka (aider)
db5b49ee7f feat: Add player entity with WASD/space controls and spawn functionality 2025-04-05 18:03:10 +02:00
Kacper Kostka (aider)
d86baa8f99 refactor: Remove cloud rendering from sky function 2025-04-05 17:54:50 +02:00
Kacper Kostka (aider)
f0b00c3ccb feat: Enhance cloud rendering with larger, more spread-out groups of squares 2025-04-05 17:53:55 +02:00
Kacper Kostka (aider)
724c5907a1 feat: Enhance cloud rendering with more spread and noise 2025-04-05 17:51:37 +02:00
Kacper Kostka (aider)
288c4a8772 refactor: Simplify cloud rendering to use fewer random squares 2025-04-05 17:50:51 +02:00
Kacper Kostka (aider)
03b192ae0a refactor: Replace cloud circles with static pixel squares 2025-04-05 17:49:02 +02:00
Kacper Kostka (aider)
da895b11df feat: Enhance cloud rendering with more detailed, larger fluffy clouds 2025-04-05 17:46:22 +02:00
Kacper Kostka (aider)
853da1a61d feat: Brighten sky, reduce cloud size, and remove color animation 2025-04-05 17:44:58 +02:00
Kacper Kostka (aider)
d2a4927577 feat: Add bluish animated sky background with dynamic colors and clouds 2025-04-05 17:43:12 +02:00
Kacper Kostka (aider)
90650fefdd fix: Improve rabbit collision, rotation, and jumping mechanics 2025-04-05 17:26:25 +02:00
Kacper Kostka (aider)
61ee259f6b feat: Add rabbit button to HUD and implement tool selection 2025-04-05 17:24:10 +02:00
Kacper Kostka (aider)
5ac6d205ad feat: Add rabbit spawning logic to input handling 2025-04-05 17:23:35 +02:00
Kacper Kostka (aider)
f1a18f9168 feat: Add rabbit entity with dynamic movement and sprite rendering 2025-04-05 17:20:54 +02:00
Kacper Kostka
f9cb363b37 feat: Add rabbit sprite to game assets 2025-04-05 17:20:51 +02:00
Kacper Kostka (aider)
cf64b6db48 fix: Remove duplicate constants and add null check for updatePhysicsObjects 2025-04-05 17:16:45 +02:00
Kacper Kostka (aider)
20f5036848 feat: Add physics objects with square, circle, and triangle tools 2025-04-05 17:15:40 +02:00
Kacper Kostka (aider)
34dd7e2d62 Based on the changes you've made, the implementation looks complete and well-structured. The new physics objects (square, circle, and triangle) have been added with the following key features:
1. They can be created using the new buttons in the UI
2. They fall due to gravity
3. They rotate while falling
4. They bounce and collide with world elements
5. They come to rest when they stop moving

The changes have been made to the following files:
- `index.html`: Added new buttons and script reference
- `js/constants.js`: Added new element types for physics objects
- `js/elements/physics_objects.js`: New file with physics object implementation
- `js/physics.js`: Added call to update physics objects
- `js/render.js`: Added rendering of physics objects

The implementation looks solid and should work as expected. Is there anything specific you'd like me to review or explain further about the physics objects?
2025-04-05 17:15:08 +02:00
Kacper Kostka (aider)
2067dad1d3 feat: Increase gravity strength by 3x and enhance element fall mechanics 2025-04-05 17:07:03 +02:00
Kacper Kostka
7cabd79d5f Revert "feat: Add rabbit spawning button to HUD and implement rabbit tool"
This reverts commit 032793292f.
2025-04-05 17:04:16 +02:00
Kacper Kostka
349d38c04c Revert "feat: Add rabbit tool and event listener for rabbit button"
This reverts commit bdab2974c6.
2025-04-05 17:04:05 +02:00
Kacper Kostka
b5d1a643bd Revert "fix: Remove duplicate RABBIT declaration and move toggleDebug function"
This reverts commit bcd61b7433.
2025-04-05 17:03:55 +02:00
Kacper Kostka
b7d12114d3 Revert "fix: Resolve canvas reference and duplicate currentTool declaration"
This reverts commit a948fab619.
2025-04-05 16:28:42 +02:00
Kacper Kostka (aider)
a948fab619 fix: Resolve canvas reference and duplicate currentTool declaration 2025-04-05 16:27:42 +02:00
Kacper Kostka (aider)
bcd61b7433 fix: Remove duplicate RABBIT declaration and move toggleDebug function 2025-04-05 16:26:18 +02:00
Kacper Kostka (aider)
bdab2974c6 feat: Add rabbit tool and event listener for rabbit button 2025-04-05 16:24:29 +02:00
Kacper Kostka (aider)
032793292f feat: Add rabbit spawning button to HUD and implement rabbit tool 2025-04-05 16:24:16 +02:00
Kacper Kostka (aider)
ba4fa3eb37 feat: Increase gravity strength for falling elements 2025-04-05 16:17:02 +02:00
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
25 changed files with 2573 additions and 172 deletions

1
.gitignore vendored Normal file
View File

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

View File

@@ -20,7 +20,12 @@
<button id="tree-seed-btn">Tree Seed</button> <button id="tree-seed-btn">Tree Seed</button>
<button id="fire-btn">Fire</button> <button id="fire-btn">Fire</button>
<button id="lava-btn">Lava</button> <button id="lava-btn">Lava</button>
<button id="rabbit-btn">Rabbit</button>
<button id="square-btn">Square</button>
<button id="circle-btn">Circle</button>
<button id="triangle-btn">Triangle</button>
<button id="eraser-btn">Eraser</button> <button id="eraser-btn">Eraser</button>
<button id="spawn-player-btn">Spawn Player</button>
</div> </div>
<div class="navigation"> <div class="navigation">
<button id="move-left"></button> <button id="move-left"></button>
@@ -44,6 +49,10 @@
<script src="js/elements/plants.js"></script> <script src="js/elements/plants.js"></script>
<script src="js/elements/trees.js"></script> <script src="js/elements/trees.js"></script>
<script src="js/elements/fire.js"></script> <script src="js/elements/fire.js"></script>
<script src="js/elements/physics_objects.js"></script>
<script src="js/entities/entity.js"></script>
<script src="js/entities/rabbit.js"></script>
<script src="js/entities/player.js"></script>
<script src="js/render.js"></script> <script src="js/render.js"></script>
<script src="js/input.js"></script> <script src="js/input.js"></script>
<script src="js/physics.js"></script> <script src="js/physics.js"></script>

View File

@@ -1,7 +1,7 @@
// Game constants // Game constants
const CHUNK_SIZE = 200; const CHUNK_SIZE = 200;
const PIXEL_SIZE = 4; let PIXEL_SIZE = 4;
const GRAVITY = 0.5; const GRAVITY = 1.5; // Increased gravity (3x stronger)
const WATER_SPREAD = 3; const WATER_SPREAD = 3;
// Base Colors // Base Colors
@@ -17,6 +17,7 @@ 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) {
@@ -58,6 +59,10 @@ 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;
const SQUARE = 16;
const CIRCLE = 17;
const TRIANGLE = 18;
// 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

@@ -1,32 +1,53 @@
// Basic element behaviors (sand, water, dirt) // Basic element behaviors (sand, water, dirt)
function updateSand(x, y) { function updateSand(x, y) {
// Try to move down // Try to move down with stronger gravity (up to 5 pixels at once)
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 5;
let newY = y;
// Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
}
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, SAND); setPixel(x, newY, 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++;
@@ -35,27 +56,44 @@ 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);
} }
// Try to move down // Try to move down with stronger gravity (up to 4 pixels at once)
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 4;
let newY = y;
// Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
}
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, WATER); setPixel(x, newY, WATER);
moveMetadata(x, y, x, y + 1); moveMetadata(x, y, x, newY);
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 {
@@ -67,21 +105,25 @@ 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);
moved = true; 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, WATER); setPixel(x + 1, y, WATER);
moved = true; 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, WATER); setPixel(x - 1, y, WATER);
moved = true; 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, WATER); setPixel(x + 1, y, WATER);
moved = true; moveMetadata(x, y, x + 1, y);
return true;
} }
} }
@@ -97,41 +139,62 @@ 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; return true;
}
} }
} }
return modified;
}
function updateDirt(x, y) { function updateDirt(x, y) {
// Try to move down // Try to move down with stronger gravity (up to 5 pixels at once)
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 5;
let newY = y;
// Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
}
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY); setPixel(x, y, EMPTY);
setPixel(x, y + 1, DIRT); setPixel(x, newY, 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
@@ -140,12 +203,17 @@ 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,6 +2,7 @@
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) {
@@ -10,15 +11,17 @@ 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; return true;
} }
// 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
@@ -29,7 +32,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; return true;
} }
// Fire can also move slightly to the sides // Fire can also move slightly to the sides
@@ -39,7 +42,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; return true;
} }
} }
@@ -61,6 +64,7 @@ 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;
} }
} }
@@ -68,10 +72,14 @@ 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) {
@@ -79,11 +87,13 @@ 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;
} }
} }
@@ -94,17 +104,20 @@ 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) {
@@ -115,20 +128,24 @@ 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;
} }
} }
} }
@@ -151,16 +168,21 @@ 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

@@ -0,0 +1,265 @@
// Physics objects (square, circle, triangle)
// Constants are already defined in constants.js
// Physics object properties
const PHYSICS_OBJECT_COLORS = ['#FF5733', '#33FF57', '#3357FF', '#F3FF33', '#FF33F3'];
// Physics constants
const GRAVITY_ACCELERATION = 0.2;
const BOUNCE_FACTOR = 0.7;
const FRICTION = 0.98;
const ROTATION_SPEED = 0.05;
// Store physics objects
const physicsObjects = [];
// Physics object class
class PhysicsObject {
constructor(type, x, y, size) {
this.type = type;
this.x = x;
this.y = y;
this.size = size || 10;
this.vx = 0;
this.vy = 0;
this.rotation = 0;
this.angularVelocity = (Math.random() - 0.5) * ROTATION_SPEED;
this.color = PHYSICS_OBJECT_COLORS[Math.floor(Math.random() * PHYSICS_OBJECT_COLORS.length)];
this.isStatic = false;
this.lastUpdate = performance.now();
}
update() {
const now = performance.now();
const deltaTime = Math.min(50, now - this.lastUpdate); // Cap at 50ms to prevent huge jumps
this.lastUpdate = now;
if (this.isStatic) return false;
// Apply gravity
this.vy += GRAVITY_ACCELERATION;
// Calculate new position
const newX = this.x + this.vx;
const newY = this.y + this.vy;
// Check for collisions with world elements
const collisionResult = this.checkCollisions(newX, newY);
if (collisionResult.collision) {
// Handle collision response
if (collisionResult.horizontal) {
this.vx *= -BOUNCE_FACTOR;
}
if (collisionResult.vertical) {
this.vy *= -BOUNCE_FACTOR;
}
// Apply friction
this.vx *= FRICTION;
this.vy *= FRICTION;
// If object is almost stopped, make it static
if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1 && Math.abs(this.angularVelocity) < 0.01) {
this.isStatic = true;
}
} else {
// No collision, update position
this.x = newX;
this.y = newY;
}
// Update rotation
this.rotation += this.angularVelocity;
this.angularVelocity *= FRICTION;
return true;
}
checkCollisions(newX, newY) {
const result = {
collision: false,
horizontal: false,
vertical: false
};
// Check points around the object based on its type
const checkPoints = this.getCollisionCheckPoints(newX, newY);
for (const point of checkPoints) {
const pixel = getPixel(Math.floor(point.x), Math.floor(point.y));
if (pixel !== EMPTY && pixel !== WATER &&
pixel !== SQUARE && pixel !== CIRCLE && pixel !== TRIANGLE) {
result.collision = true;
// Determine collision direction
if (point.type === 'horizontal') {
result.horizontal = true;
} else if (point.type === 'vertical') {
result.vertical = true;
} else {
// Corner collision, check both directions
result.horizontal = true;
result.vertical = true;
}
}
}
return result;
}
getCollisionCheckPoints(x, y) {
const points = [];
const halfSize = this.size / 2;
if (this.type === SQUARE) {
// For a square, check corners and edges
const corners = [
{ x: x - halfSize, y: y - halfSize },
{ x: x + halfSize, y: y - halfSize },
{ x: x - halfSize, y: y + halfSize },
{ x: x + halfSize, y: y + halfSize }
];
// Add rotated corners
for (const corner of corners) {
const rotatedCorner = this.rotatePoint(corner.x, corner.y, x, y, this.rotation);
points.push({ x: rotatedCorner.x, y: rotatedCorner.y, type: 'corner' });
}
// Add edge midpoints
points.push({ x: x, y: y - halfSize, type: 'vertical' });
points.push({ x: x, y: y + halfSize, type: 'vertical' });
points.push({ x: x - halfSize, y: y, type: 'horizontal' });
points.push({ x: x + halfSize, y: y, type: 'horizontal' });
} else if (this.type === CIRCLE) {
// For a circle, check points around the circumference
const numPoints = 12;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
points.push({
x: x + Math.cos(angle) * halfSize,
y: y + Math.sin(angle) * halfSize,
type: angle < Math.PI / 4 || angle > Math.PI * 7/4 || (angle > Math.PI * 3/4 && angle < Math.PI * 5/4) ? 'horizontal' : 'vertical'
});
}
} else if (this.type === TRIANGLE) {
// For a triangle, check vertices and edges
const vertices = [
{ x: x, y: y - halfSize },
{ x: x - halfSize, y: y + halfSize },
{ x: x + halfSize, y: y + halfSize }
];
// Add rotated vertices
for (const vertex of vertices) {
const rotatedVertex = this.rotatePoint(vertex.x, vertex.y, x, y, this.rotation);
points.push({ x: rotatedVertex.x, y: rotatedVertex.y, type: 'corner' });
}
// Add edge midpoints
const midpoints = [
{ x: (vertices[0].x + vertices[1].x) / 2, y: (vertices[0].y + vertices[1].y) / 2, type: 'edge' },
{ x: (vertices[1].x + vertices[2].x) / 2, y: (vertices[1].y + vertices[2].y) / 2, type: 'horizontal' },
{ x: (vertices[2].x + vertices[0].x) / 2, y: (vertices[2].y + vertices[0].y) / 2, type: 'edge' }
];
for (const midpoint of midpoints) {
const rotatedMidpoint = this.rotatePoint(midpoint.x, midpoint.y, x, y, this.rotation);
points.push({ x: rotatedMidpoint.x, y: rotatedMidpoint.y, type: midpoint.type });
}
}
return points;
}
rotatePoint(px, py, cx, cy, angle) {
const s = Math.sin(angle);
const c = Math.cos(angle);
// Translate point back to origin
px -= cx;
py -= cy;
// Rotate point
const xnew = px * c - py * s;
const ynew = px * s + py * c;
// Translate point back
return {
x: xnew + cx,
y: ynew + cy
};
}
render(ctx, offsetX, offsetY) {
const screenX = (this.x - offsetX) * PIXEL_SIZE;
const screenY = (this.y - offsetY) * PIXEL_SIZE;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
// Draw collision box in debug mode
if (debugMode) {
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
// Draw a circle for the collision radius
ctx.beginPath();
ctx.arc(0, 0, this.size * PIXEL_SIZE / 2, 0, Math.PI * 2);
ctx.stroke();
// Draw a dot at the center
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
// Restore original fill color
ctx.fillStyle = this.color;
}
if (this.type === SQUARE) {
const halfSize = this.size / 2 * PIXEL_SIZE;
ctx.fillRect(-halfSize, -halfSize, this.size * PIXEL_SIZE, this.size * PIXEL_SIZE);
} else if (this.type === CIRCLE) {
ctx.beginPath();
ctx.arc(0, 0, this.size / 2 * PIXEL_SIZE, 0, Math.PI * 2);
ctx.fill();
} else if (this.type === TRIANGLE) {
const halfSize = this.size / 2 * PIXEL_SIZE;
ctx.beginPath();
ctx.moveTo(0, -halfSize);
ctx.lineTo(-halfSize, halfSize);
ctx.lineTo(halfSize, halfSize);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
}
function createPhysicsObject(type, x, y, size) {
const obj = new PhysicsObject(type, x, y, size || 10);
physicsObjects.push(obj);
return obj;
}
function updatePhysicsObjects() {
// Update all physics objects
for (let i = physicsObjects.length - 1; i >= 0; i--) {
physicsObjects[i].update();
}
}
function renderPhysicsObjects(ctx, offsetX, offsetY) {
// Render all physics objects
for (const obj of physicsObjects) {
obj.render(ctx, offsetX, offsetY);
}
}

View File

@@ -1,21 +1,39 @@
// Plant element behaviors (grass, seeds, trees) // Plant element behaviors (grass, seeds, trees)
function updateGrass(x, y) { function updateGrass(x, y) {
// Grass behaves like dirt for physics // Grass behaves like dirt for physics with stronger gravity
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 5;
setPixel(x, y, EMPTY); let newY = y;
setPixel(x, y + 1, GRASS);
// Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
} }
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY);
setPixel(x, newY, GRASS);
return true;
}
// 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, 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
@@ -28,35 +46,57 @@ 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) {
// Seeds fall like sand // Seeds fall like sand with stronger gravity
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 5;
setPixel(x, y, EMPTY); let newY = y;
setPixel(x, y + 1, SEED);
moveMetadata(x, y, x, y + 1); // Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
} }
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY);
setPixel(x, newY, SEED);
moveMetadata(x, y, x, newY);
return true;
}
// 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, 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) {
@@ -72,12 +112,16 @@ 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) {
@@ -86,7 +130,7 @@ function updateGrassBlade(x, y) {
if (!metadata) { if (!metadata) {
setMetadata(x, y, { age: 0, height: 1 }); setMetadata(x, y, { age: 0, height: 1 });
return; return true;
} }
// Increment age // Increment age
@@ -99,13 +143,17 @@ 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) {
@@ -119,7 +167,7 @@ function updateFlower(x, y) {
age: 0, age: 0,
height: 1 height: 1
}); });
return; return true;
} }
// Increment age // Increment age
@@ -139,12 +187,14 @@ 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
@@ -154,6 +204,9 @@ 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;
} }

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

@@ -1,30 +1,51 @@
// Tree element behaviors // Tree element behaviors
function updateTreeSeed(x, y) { function updateTreeSeed(x, y) {
// Tree seeds fall like other seeds // Tree seeds fall like other seeds with stronger gravity
if (getPixel(x, y + 1) === EMPTY) { let maxFall = 5;
setPixel(x, y, EMPTY); let newY = y;
setPixel(x, y + 1, TREE_SEED);
moveMetadata(x, y, x, y + 1); // Check how far down we can fall
for (let i = 1; i <= maxFall; i++) {
if (getPixel(x, y + i) === EMPTY) {
newY = y + i;
} else {
break;
} }
}
if (newY > y) {
// Fall straight down as far as possible
setPixel(x, y, EMPTY);
setPixel(x, newY, TREE_SEED);
moveMetadata(x, y, x, newY);
return true;
}
// 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, 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) {

207
js/entities/entity.js Normal file
View File

@@ -0,0 +1,207 @@
// Base entity system
const ENTITY_TYPES = {
RABBIT: 'rabbit',
PLAYER: 'player'
};
// Store all entities
const entities = [];
// Base Entity class
class Entity {
constructor(type, x, y, options = {}) {
this.type = type;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.width = options.width || 10;
this.height = options.height || 10;
this.rotation = 0;
this.sprite = null;
this.flipped = false;
this.isStatic = false;
this.lastUpdate = performance.now();
this.id = Entity.nextId++;
}
static nextId = 1;
update() {
// Override in subclasses
return false;
}
render(ctx, offsetX, offsetY) {
// Default rendering - override in subclasses
const screenX = (this.x - offsetX) * PIXEL_SIZE;
const screenY = (this.y - offsetY) * PIXEL_SIZE;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(this.rotation);
if (this.sprite && this.sprite.complete) {
const width = this.width * PIXEL_SIZE;
const height = this.height * PIXEL_SIZE;
if (this.flipped) {
ctx.scale(-1, 1);
ctx.drawImage(this.sprite, -width/2, -height/2, width, height);
} else {
ctx.drawImage(this.sprite, -width/2, -height/2, width, height);
}
} else {
// Fallback if sprite not loaded
ctx.fillStyle = '#FF00FF';
ctx.fillRect(
-this.width/2 * PIXEL_SIZE,
-this.height/2 * PIXEL_SIZE,
this.width * PIXEL_SIZE,
this.height * PIXEL_SIZE
);
}
// Draw collision box in debug mode
if (debugMode) {
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
ctx.strokeRect(
-this.width/2 * PIXEL_SIZE,
-this.height/2 * PIXEL_SIZE,
this.width * PIXEL_SIZE,
this.height * PIXEL_SIZE
);
// Draw a dot at the entity's center
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
checkCollisions(newX, newY) {
const result = {
collision: false,
horizontal: false,
vertical: false,
ground: false
};
// Check points around the entity
const halfWidth = this.width / 2;
const halfHeight = this.height / 2;
// Check bottom points for ground collision - check multiple points along the bottom
const numBottomPoints = 5;
let groundCollision = false;
// For player entity, adjust the collision detection to match sprite feet position
const yOffset = this.type === ENTITY_TYPES.PLAYER ? 2 : 0;
for (let i = 0; i < numBottomPoints; i++) {
const ratio = i / (numBottomPoints - 1);
const bottomX = newX - halfWidth + (2 * halfWidth * ratio);
const bottomY = newY + halfHeight + yOffset;
if (this.isPixelSolid(bottomX, bottomY)) {
groundCollision = true;
break;
}
}
if (groundCollision) {
result.collision = true;
result.vertical = true;
result.ground = true;
}
// Check side points for horizontal collision
// For player entity, adjust the collision detection to match sprite position
const yAdjust = this.type === ENTITY_TYPES.PLAYER ? -1 : 0;
const leftMiddle = { x: newX - halfWidth, y: newY + yAdjust };
const rightMiddle = { x: newX + halfWidth, y: newY + yAdjust };
if (this.isPixelSolid(leftMiddle.x, leftMiddle.y)) {
result.collision = true;
result.horizontal = true;
}
if (this.isPixelSolid(rightMiddle.x, rightMiddle.y)) {
result.collision = true;
result.horizontal = true;
}
// Check top for ceiling collision
const topMiddle = { x: newX, y: newY - halfHeight + (this.type === ENTITY_TYPES.PLAYER ? -2 : 0) };
if (this.isPixelSolid(topMiddle.x, topMiddle.y)) {
result.collision = true;
result.vertical = true;
}
return result;
}
isPixelSolid(x, y) {
// Use ceiling for y coordinate to better detect ground below
const pixel = getPixel(Math.floor(x), Math.ceil(y));
// For player entity, don't collide with trees (WOOD and LEAF)
if (this.type === ENTITY_TYPES.PLAYER) {
return pixel !== EMPTY &&
pixel !== WATER &&
pixel !== FIRE &&
pixel !== SQUARE &&
pixel !== CIRCLE &&
pixel !== TRIANGLE &&
pixel !== WOOD &&
pixel !== LEAF;
}
// For other entities, use the original collision detection
return pixel !== EMPTY &&
pixel !== WATER &&
pixel !== FIRE &&
pixel !== SQUARE &&
pixel !== CIRCLE &&
pixel !== TRIANGLE;
}
}
// Function to create and register an entity
function createEntity(type, x, y, options = {}) {
let entity;
switch(type) {
case ENTITY_TYPES.RABBIT:
entity = new Rabbit(x, y, options);
break;
case ENTITY_TYPES.PLAYER:
entity = new Player(x, y, options);
break;
default:
console.error(`Unknown entity type: ${type}`);
return null;
}
entities.push(entity);
return entity;
}
// Update all entities
function updateEntities() {
for (let i = entities.length - 1; i >= 0; i--) {
entities[i].update();
}
}
// Render all entities
function renderEntities(ctx, offsetX, offsetY) {
for (const entity of entities) {
entity.render(ctx, offsetX, offsetY);
}
}

543
js/entities/player.js Normal file
View File

@@ -0,0 +1,543 @@
// Player entity class
class Player extends Entity {
constructor(x, y, options = {}) {
super(ENTITY_TYPES.PLAYER, x, y, {
width: 3, // 50% smaller collision box width
height: 6, // 50% smaller collision box height
...options
});
// Load player sprite
this.sprite = new Image();
this.sprite.src = 'sprites/player.png';
// Movement properties
this.moveSpeed = 0.03;
this.jumpForce = -0.2;
this.gravity = 0.02;
this.maxVelocity = 0.5;
this.friction = 0.9;
// State tracking
this.isJumping = false;
this.direction = 1; // 1 = right, -1 = left
this.lastUpdate = performance.now();
this.lastDirection = 1; // Track last direction to prevent unnecessary flipping
this.isClimbing = false; // Track climbing state
// Player stats
this.maxHealth = 100;
this.health = 100;
this.breakingPower = 1;
this.breakingRange = 10; // Increased from 3 to 10 pixels
this.isBreaking = false;
this.breakingCooldown = 0;
this.breakingCooldownMax = 10;
// Inventory
this.inventory = {
sand: 0,
water: 0,
dirt: 0,
stone: 0,
wood: 0,
grass: 0,
seed: 0
};
// Animation properties
this.frameWidth = 32;
this.frameHeight = 30;
this.frameCount = 4;
this.currentFrame = 0;
this.animationSpeed = 150; // ms per frame
this.lastFrameUpdate = 0;
this.isMoving = false;
this.animationTimer = 0; // Consistent timer for animation
}
update() {
const now = performance.now();
const deltaTime = Math.min(50, now - this.lastUpdate);
this.lastUpdate = now;
// Apply gravity
this.vy += this.gravity;
// Cap velocity
if (this.vx > this.maxVelocity) this.vx = this.maxVelocity;
if (this.vx < -this.maxVelocity) this.vx = -this.maxVelocity;
if (this.vy > this.maxVelocity * 2) this.vy = this.maxVelocity * 2;
// Apply friction when not actively moving
if (!this.isMoving) {
this.vx *= this.friction;
}
// Calculate new position
let newX = this.x + this.vx * deltaTime;
let newY = this.y + this.vy * deltaTime;
// Check for collisions
const collisionResult = this.checkCollisions(newX, newY);
if (collisionResult.collision) {
if (collisionResult.horizontal) {
// Try to climb up if there's a 1-pixel step
if (this.tryClimbing(newX, newY)) {
// Successfully climbed, continue with adjusted position
newY -= 1; // Move up one pixel to climb
} else {
// Can't climb, stop horizontal movement
newX = this.x;
this.vx = 0;
}
}
if (collisionResult.vertical) {
if (this.vy > 0) {
this.isJumping = false;
}
newY = this.y;
this.vy = 0;
}
}
// Update position
this.x = newX;
this.y = newY;
// Update breaking cooldown
if (this.breakingCooldown > 0) {
this.breakingCooldown--;
}
// Handle breaking action
if (this.isBreaking && this.breakingCooldown <= 0) {
this.breakBlock();
this.breakingCooldown = this.breakingCooldownMax;
}
// Update animation
this.updateAnimation(deltaTime);
// Center camera on player
this.centerCamera();
// Update HUD
this.updateHUD();
return true;
}
updateAnimation(deltaTime) {
// Update animation timer consistently
this.animationTimer += deltaTime;
// Only update direction when it actually changes to prevent flipping
if (Math.abs(this.vx) > 0.005) {
const newDirection = this.vx > 0 ? 1 : -1;
if (newDirection !== this.lastDirection) {
this.direction = newDirection;
this.lastDirection = newDirection;
}
}
}
render(ctx, offsetX, offsetY) {
const screenX = (this.x - offsetX) * PIXEL_SIZE;
const screenY = (this.y - offsetY) * PIXEL_SIZE;
if (this.sprite && this.sprite.complete) {
// Set pixelated rendering (nearest neighbor)
ctx.imageSmoothingEnabled = false;
ctx.save();
ctx.translate(screenX, screenY);
// Use 50% smaller dimensions for the sprite
const spriteDisplayWidth = 12 * (PIXEL_SIZE / 2); // 50% smaller sprite
const spriteDisplayHeight = 12 * (PIXEL_SIZE / 2); // 50% smaller sprite
// Flip horizontally based on direction
if (this.direction < 0) {
ctx.scale(-1, 1);
}
// Draw the correct sprite frame
// Center the sprite on the entity position, with y-offset to align feet with collision box
// Stretch the sprite vertically to match the collision box height
ctx.drawImage(
this.sprite,
this.currentFrame * this.frameWidth, 0,
this.frameWidth, this.frameHeight,
-spriteDisplayWidth / 2, -spriteDisplayHeight / 2, // Remove the negative offset that caused levitation
spriteDisplayWidth, spriteDisplayHeight * 1.2 // Stretch sprite vertically by 20% to match collision box
);
ctx.restore();
// Reset image smoothing for other rendering
ctx.imageSmoothingEnabled = true;
// Draw collision box in debug mode
if (debugMode) {
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
ctx.strokeRect(
screenX - this.width * PIXEL_SIZE / 2,
screenY - this.height * PIXEL_SIZE / 2,
this.width * PIXEL_SIZE,
this.height * PIXEL_SIZE
);
// Also draw sprite boundary in debug mode
ctx.strokeStyle = '#ff00ff';
ctx.lineWidth = 1;
ctx.strokeRect(
screenX - spriteDisplayWidth / 2,
screenY - spriteDisplayHeight / 2, // Match the updated sprite drawing position
spriteDisplayWidth,
spriteDisplayHeight * 1.2 // Match the stretched sprite height
);
// Draw a dot at the entity's exact position
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(screenX, screenY, 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
moveLeft() {
this.vx = -this.moveSpeed;
this.direction = -1;
this.isMoving = true;
}
moveRight() {
this.vx = this.moveSpeed;
this.direction = 1;
this.isMoving = true;
}
moveUp() {
this.vy = -this.moveSpeed;
this.isMoving = true;
}
moveDown() {
this.vy = this.moveSpeed;
this.isMoving = true;
}
stopMoving() {
this.isMoving = false;
}
jump() {
if (!this.isJumping) {
this.vy = this.jumpForce;
this.isJumping = true;
}
}
startBreaking() {
this.isBreaking = true;
}
stopBreaking() {
this.isBreaking = false;
}
breakBlock() {
// Get mouse position in world coordinates
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
// Calculate distance from player to target block
const distance = Math.sqrt(
Math.pow(worldX - this.x, 2) +
Math.pow(worldY - this.y, 2)
);
// Only break blocks within range
if (distance <= this.breakingRange) {
// Get the block type at that position
const blockType = getPixel(worldX, worldY);
// Only break non-empty blocks that aren't special entities
if (blockType !== EMPTY &&
blockType !== WATER &&
blockType !== FIRE &&
blockType !== SQUARE &&
blockType !== CIRCLE &&
blockType !== TRIANGLE) {
// Add to inventory based on block type
this.addToInventory(blockType);
// Replace with empty space
setPixel(worldX, worldY, EMPTY);
// Create a breaking effect (particles)
this.createBreakingEffect(worldX, worldY, blockType);
}
}
}
addToInventory(blockType) {
// Map block type to inventory item
switch(blockType) {
case SAND:
this.inventory.sand++;
break;
case DIRT:
this.inventory.dirt++;
break;
case STONE:
this.inventory.stone++;
break;
case GRASS:
this.inventory.grass++;
break;
case WOOD:
this.inventory.wood++;
break;
case SEED:
case TREE_SEED:
this.inventory.seed++;
break;
}
}
createBreakingEffect(x, y, blockType) {
// Create a simple particle effect at the breaking location
// This could be expanded with a proper particle system
const numParticles = 5;
// For now, we'll just create a visual feedback by setting nearby pixels
// to a different color briefly, then clearing them
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
// Skip if the pixel is not empty
if (getPixel(x + dx, y + dy) !== EMPTY) continue;
// Set a temporary pixel
setPixel(x + dx, y + dy, EMPTY);
// Mark the chunk as dirty for rendering
const { chunkX, chunkY } = getChunkCoordinates(x + dx, y + dy);
const key = getChunkKey(chunkX, chunkY);
dirtyChunks.add(key);
}
}
}
updateHUD() {
// Get or create the HUD container
let hudContainer = document.getElementById('player-hud');
if (!hudContainer) {
hudContainer = document.createElement('div');
hudContainer.id = 'player-hud';
hudContainer.style.position = 'fixed';
hudContainer.style.bottom = '10px';
hudContainer.style.left = '10px';
hudContainer.style.width = '300px';
hudContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
hudContainer.style.color = 'white';
hudContainer.style.padding = '10px';
hudContainer.style.borderRadius = '5px';
hudContainer.style.fontFamily = 'Arial, sans-serif';
hudContainer.style.zIndex = '1000';
document.body.appendChild(hudContainer);
// Create health bar container
const healthBarContainer = document.createElement('div');
healthBarContainer.id = 'health-bar-container';
healthBarContainer.style.width = '100%';
healthBarContainer.style.height = '20px';
healthBarContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
healthBarContainer.style.marginBottom = '10px';
healthBarContainer.style.borderRadius = '3px';
// Create health bar
const healthBar = document.createElement('div');
healthBar.id = 'health-bar';
healthBar.style.width = '100%';
healthBar.style.height = '100%';
healthBar.style.backgroundColor = '#4CAF50';
healthBar.style.borderRadius = '3px';
healthBar.style.transition = 'width 0.3s';
healthBarContainer.appendChild(healthBar);
hudContainer.appendChild(healthBarContainer);
// Create inventory container
const inventoryContainer = document.createElement('div');
inventoryContainer.id = 'inventory-container';
inventoryContainer.style.display = 'grid';
inventoryContainer.style.gridTemplateColumns = 'repeat(7, 1fr)';
inventoryContainer.style.gap = '5px';
// Create inventory slots
const inventoryItems = ['sand', 'dirt', 'stone', 'grass', 'wood', 'water', 'seed'];
inventoryItems.forEach(item => {
const slot = document.createElement('div');
slot.id = `inventory-${item}`;
slot.className = 'inventory-slot';
slot.style.width = '30px';
slot.style.height = '30px';
slot.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
slot.style.borderRadius = '3px';
slot.style.display = 'flex';
slot.style.flexDirection = 'column';
slot.style.alignItems = 'center';
slot.style.justifyContent = 'center';
slot.style.fontSize = '10px';
slot.style.position = 'relative';
// Create item icon
const icon = document.createElement('div');
icon.style.width = '20px';
icon.style.height = '20px';
icon.style.borderRadius = '3px';
// Set color based on item type
switch(item) {
case 'sand': icon.style.backgroundColor = '#e6c588'; break;
case 'dirt': icon.style.backgroundColor = '#8B4513'; break;
case 'stone': icon.style.backgroundColor = '#A9A9A9'; break;
case 'grass': icon.style.backgroundColor = '#7CFC00'; break;
case 'wood': icon.style.backgroundColor = '#8B5A2B'; break;
case 'water': icon.style.backgroundColor = '#4a80f5'; break;
case 'seed': icon.style.backgroundColor = '#654321'; break;
}
// Create count label
const count = document.createElement('div');
count.id = `${item}-count`;
count.style.position = 'absolute';
count.style.bottom = '2px';
count.style.right = '2px';
count.style.fontSize = '8px';
count.style.fontWeight = 'bold';
count.textContent = '0';
slot.appendChild(icon);
slot.appendChild(count);
inventoryContainer.appendChild(slot);
});
hudContainer.appendChild(inventoryContainer);
// Create controls help text
const controlsHelp = document.createElement('div');
controlsHelp.style.marginTop = '10px';
controlsHelp.style.fontSize = '10px';
controlsHelp.style.color = '#aaa';
controlsHelp.innerHTML = 'Controls: A/D - Move, W/Space - Jump, E - Break blocks';
hudContainer.appendChild(controlsHelp);
}
// Update health bar
const healthBar = document.getElementById('health-bar');
if (healthBar) {
const healthPercent = (this.health / this.maxHealth) * 100;
healthBar.style.width = `${healthPercent}%`;
// Change color based on health
if (healthPercent > 60) {
healthBar.style.backgroundColor = '#4CAF50'; // Green
} else if (healthPercent > 30) {
healthBar.style.backgroundColor = '#FFC107'; // Yellow
} else {
healthBar.style.backgroundColor = '#F44336'; // Red
}
}
// Update inventory counts
for (const [item, count] of Object.entries(this.inventory)) {
const countElement = document.getElementById(`${item}-count`);
if (countElement) {
countElement.textContent = count;
}
}
}
// Try to climb up a small step
tryClimbing(newX, newY) {
const halfWidth = this.width / 2;
// Check if there's a solid pixel in front of the player
const frontX = newX + (this.direction * halfWidth);
const frontY = newY;
// Check if there's a solid pixel at the current level
if (this.isPixelSolid(frontX, frontY)) {
// Check if there's empty space one pixel above
if (!this.isPixelSolid(frontX, frontY - 1) &&
!this.isPixelSolid(this.x, this.y - 1)) {
// Check if there's ground to stand on after climbing
if (this.isPixelSolid(frontX, frontY + 1)) {
this.isClimbing = true;
return true;
}
}
}
this.isClimbing = false;
return false;
}
centerCamera() {
// Get current camera center in world coordinates
const cameraWidth = canvas.width / PIXEL_SIZE;
const cameraHeight = canvas.height / PIXEL_SIZE;
const cameraCenterX = worldOffsetX + cameraWidth / 2;
const cameraCenterY = worldOffsetY + cameraHeight / 2;
// Calculate distance from player to camera center
const distanceX = Math.abs(this.x - cameraCenterX);
const distanceY = Math.abs(this.y - cameraCenterY);
// Define thresholds for camera movement (percentage of screen size)
const thresholdX = cameraWidth * 0.2; // Move when player is 30% away from center
const thresholdY = cameraHeight * 0.2;
// Only move camera when player gets close to the edge of current view
let needsUpdate = false;
if (distanceX > thresholdX) {
// Calculate target position with chunk-based snapping
const chunkSize = CHUNK_SIZE;
const playerChunkX = Math.floor(this.x / chunkSize);
const targetX = this.x - (canvas.width / PIXEL_SIZE / 2);
// Smooth transition to the target position
worldOffsetX += (targetX - worldOffsetX) * 0.2;
needsUpdate = true;
}
if (distanceY > thresholdY) {
// Calculate target position with chunk-based snapping
const chunkSize = CHUNK_SIZE;
const playerChunkY = Math.floor(this.y / chunkSize);
const targetY = this.y - (canvas.height / PIXEL_SIZE / 2);
// Smooth transition to the target position
worldOffsetY += (targetY - worldOffsetY) * 0.1;
needsUpdate = true;
}
// Only mark world as moved if we actually updated the camera
if (needsUpdate) {
worldMoved = true;
}
}
}

148
js/entities/rabbit.js Normal file
View File

@@ -0,0 +1,148 @@
// Rabbit entity
class Rabbit extends Entity {
constructor(x, y, options = {}) {
super(ENTITY_TYPES.RABBIT, x, y, {
width: 6, // Smaller size for rabbit
height: 6,
...options
});
// Load rabbit sprite
this.sprite = new Image();
this.sprite.src = 'sprites/rabbit.png';
// Rabbit specific properties
this.jumpCooldown = 0;
this.jumpForce = -3.5;
this.moveSpeed = 0.8;
this.direction = Math.random() > 0.5 ? 1 : -1; // 1 for right, -1 for left
this.isJumping = false;
this.thinkTimer = 0;
this.actionDuration = 0;
this.currentAction = 'idle';
// Apply gravity
this.gravity = 0.2;
}
update() {
const now = performance.now();
const deltaTime = Math.min(50, now - this.lastUpdate);
this.lastUpdate = now;
// Apply gravity
this.vy += this.gravity;
// Calculate new position
let newX = this.x + this.vx;
let newY = this.y + this.vy;
// Check for collisions
const collisionResult = this.checkCollisions(newX, newY);
if (collisionResult.collision) {
if (collisionResult.horizontal) {
// Hit a wall, reverse direction
this.direction *= -1;
this.vx = this.moveSpeed * this.direction;
newX = this.x; // Don't move horizontally this frame
}
if (collisionResult.vertical) {
if (collisionResult.ground) {
// Landed on ground
this.vy = 0;
this.isJumping = false;
// Find exact ground position
while (this.isPixelSolid(this.x, newY)) {
newY--;
}
newY = Math.floor(newY) + 0.5; // Position just above ground (reduced from 0.99)
} else {
// Hit ceiling
this.vy = 0;
newY = this.y;
}
}
}
// Update position
this.x = newX;
this.y = newY;
// Update jump cooldown
if (this.jumpCooldown > 0) {
this.jumpCooldown--;
}
// AI behavior
this.thinkTimer++;
if (this.thinkTimer >= 30) { // Think every 30 frames
this.thinkTimer = 0;
this.think();
}
// Update action duration
if (this.actionDuration > 0) {
this.actionDuration--;
} else if (this.currentAction !== 'idle') {
this.currentAction = 'idle';
this.vx = 0;
}
// Update sprite direction but only flip the sprite, not rotate it
this.flipped = this.direction < 0;
// Only apply rotation when jumping
if (this.isJumping) {
this.rotation = this.direction < 0 ? -0.2 : 0.2;
} else {
this.rotation = 0;
}
return true;
}
think() {
// Only make decisions when on the ground and not already in an action
if (!this.isJumping && this.actionDuration <= 0) {
const decision = Math.random();
if (decision < 0.5) { // Increased from 0.2 to 0.5 for more frequent jumping
// Jump
this.jump();
} else if (decision < 0.8) { // Adjusted range
// Move
this.move();
} else {
// Idle
this.idle();
}
}
}
jump() {
if (!this.isJumping && this.jumpCooldown <= 0) {
this.vy = this.jumpForce;
this.vx = this.moveSpeed * this.direction;
this.isJumping = true;
this.jumpCooldown = 20;
this.currentAction = 'jump';
this.actionDuration = 30;
}
}
move() {
this.direction = Math.random() > 0.5 ? 1 : -1;
this.vx = this.moveSpeed * this.direction;
this.currentAction = 'move';
this.actionDuration = 60 + Math.floor(Math.random() * 60);
}
idle() {
this.vx = 0;
this.currentAction = 'idle';
this.actionDuration = 30 + Math.floor(Math.random() * 30);
}
}

View File

@@ -4,6 +4,58 @@ let isDragging = false;
let lastMouseX, lastMouseY; let lastMouseX, lastMouseY;
let currentMouseX, currentMouseY; let currentMouseX, currentMouseY;
// Keyboard state tracking
const keyState = {};
let player = null;
// Handle keyboard input for player movement
window.addEventListener('keydown', (e) => {
keyState[e.code] = true;
// Toggle debug mode with F3
if (e.code === 'F3') {
toggleDebug();
e.preventDefault();
}
// Start breaking blocks with E key or left mouse button
if (e.code === 'KeyE' && player) {
player.startBreaking();
}
// Prevent default behavior for game control keys
if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space', 'KeyE'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keyState[e.code] = false;
// Stop breaking blocks when E key is released
if (e.code === 'KeyE' && player) {
player.stopBreaking();
}
});
function updatePlayerMovement() {
if (!player) return;
// Reset movement flag
player.stopMoving();
// Handle movement
if (keyState['KeyA']) {
player.moveLeft();
}
if (keyState['KeyD']) {
player.moveRight();
}
if (keyState['KeyW'] || keyState['Space']) {
player.jump();
}
}
function setTool(tool) { function setTool(tool) {
currentTool = tool; currentTool = tool;
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
@@ -28,6 +80,14 @@ function setTool(tool) {
document.getElementById('fire-btn').classList.add('active'); document.getElementById('fire-btn').classList.add('active');
} else if (tool === LAVA) { } else if (tool === LAVA) {
document.getElementById('lava-btn').classList.add('active'); document.getElementById('lava-btn').classList.add('active');
} else if (tool === RABBIT) {
document.getElementById('rabbit-btn').classList.add('active');
} else if (tool === SQUARE) {
document.getElementById('square-btn').classList.add('active');
} else if (tool === CIRCLE) {
document.getElementById('circle-btn').classList.add('active');
} else if (tool === TRIANGLE) {
document.getElementById('triangle-btn').classList.add('active');
} else if (tool === EMPTY) { } else if (tool === EMPTY) {
document.getElementById('eraser-btn').classList.add('active'); document.getElementById('eraser-btn').classList.add('active');
} }
@@ -46,11 +106,17 @@ function handleMouseDown(e) {
worldOffsetXBeforeDrag = worldOffsetX; worldOffsetXBeforeDrag = worldOffsetX;
worldOffsetYBeforeDrag = worldOffsetY; worldOffsetYBeforeDrag = worldOffsetY;
} else { } else {
// Left mouse button for drawing // Left mouse button for drawing or breaking blocks
if (player) {
// If player exists, start breaking blocks
player.startBreaking();
} else {
// Otherwise use normal drawing
isDrawing = true; isDrawing = true;
draw(x, y); draw(x, y);
} }
} }
}
function handleMouseMove(e) { function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@@ -89,6 +155,11 @@ function handleMouseUp(e) {
} }
} }
isDragging = false; isDragging = false;
// Stop breaking blocks if player exists
if (player) {
player.stopBreaking();
}
} }
function draw(x, y) { function draw(x, y) {
@@ -98,6 +169,14 @@ function draw(x, y) {
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX; const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY; const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY;
// Special handling for physics objects
if (currentTool === SQUARE || currentTool === CIRCLE || currentTool === TRIANGLE) {
// Create a physics object at the cursor position
const size = 10; // Default size
createPhysicsObject(currentTool, worldX, worldY, size);
return;
}
// Draw a small brush (3x3) // Draw a small brush (3x3)
for (let dy = -1; dy <= 1; dy++) { for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) { for (let dx = -1; dx <= 1; dx++) {
@@ -114,6 +193,11 @@ function draw(x, y) {
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length) colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
}); });
} }
}
// Special handling for rabbits - create rabbit entity
else if (currentTool === RABBIT) {
createEntity(ENTITY_TYPES.RABBIT, pixelX, pixelY);
return; // Only create one rabbit per click
} else { } else {
setPixel(pixelX, pixelY, currentTool); setPixel(pixelX, pixelY, currentTool);
@@ -185,4 +269,26 @@ function handleTouchMove(e) {
function toggleDebug() { function toggleDebug() {
debugMode = !debugMode; debugMode = !debugMode;
document.getElementById('debug-btn').classList.toggle('active'); document.getElementById('debug-btn').classList.toggle('active');
// Update UI to show debug mode is active
if (debugMode) {
// Show a temporary notification
const notification = document.createElement('div');
notification.textContent = 'Debug Mode: ON';
notification.style.position = 'fixed';
notification.style.top = '10px';
notification.style.left = '50%';
notification.style.transform = 'translateX(-50%)';
notification.style.backgroundColor = 'rgba(0, 255, 0, 0.7)';
notification.style.color = 'white';
notification.style.padding = '10px 20px';
notification.style.borderRadius = '5px';
notification.style.zIndex = '1000';
document.body.appendChild(notification);
// Remove notification after 2 seconds
setTimeout(() => {
document.body.removeChild(notification);
}, 2000);
}
} }

View File

@@ -5,6 +5,15 @@ let lastFrameTime = 0;
let fps = 0; let fps = 0;
let debugMode = false; let debugMode = false;
// Sky background variables
const SKY_COLORS = [
'#4a90e2', // Brighter blue
'#74b9ff', // Light blue
'#81ecec', // Very light blue/cyan
];
let skyAnimationTime = 0;
let skyAnimationSpeed = 0.0005; // Controls how fast the sky position animates (not color)
// Initialize the simulation // Initialize the simulation
window.onload = function() { window.onload = function() {
canvas = document.getElementById('simulation-canvas'); canvas = document.getElementById('simulation-canvas');
@@ -25,8 +34,15 @@ window.onload = function() {
document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED)); document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED));
document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE)); document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE));
document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA)); document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA));
document.getElementById('rabbit-btn').addEventListener('click', () => setTool(RABBIT));
document.getElementById('square-btn').addEventListener('click', () => setTool(SQUARE));
document.getElementById('circle-btn').addEventListener('click', () => setTool(CIRCLE));
document.getElementById('triangle-btn').addEventListener('click', () => setTool(TRIANGLE));
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY)); document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
// Add player spawn button
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
// Navigation controls // Navigation controls
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0)); document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0)); document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0));
@@ -47,10 +63,25 @@ 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
requestAnimationFrame(simulationLoop); requestAnimationFrame(simulationLoop);
// Initialize physics variables
window.physicsUpdateRate = 16; // ms between physics updates
window.lastPhysicsTime = 0;
window.fireUpdateCounter = 0;
}; };
function resizeCanvas() { function resizeCanvas() {
@@ -58,6 +89,50 @@ function resizeCanvas() {
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight; canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
} }
// Function to spawn player
function spawnPlayer() {
// Hide HUD elements
document.querySelector('.controls').style.display = 'none';
// Resize canvas to full screen first
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Set zoom level to 50% more zoomed in (from default 4 to 6)
PIXEL_SIZE = 6;
// Create player at specified coordinates
// Position adjusted for proper sprite alignment with smaller sprite
player = createEntity(ENTITY_TYPES.PLAYER, 229, 40); // Adjusted Y position for smaller player
// Focus camera on player
worldOffsetX = player.x - (canvas.width / PIXEL_SIZE / 2);
worldOffsetY = player.y - (canvas.height / PIXEL_SIZE / 2);
worldMoved = true;
// Clear chunk cache to force redraw at new zoom level
chunkCanvasCache.clear();
// Remove the event listener for the spawn button to prevent multiple spawns
document.getElementById('spawn-player-btn').removeEventListener('click', spawnPlayer);
// Create CSS for player HUD
const style = document.createElement('style');
style.textContent = `
#player-hud {
transition: opacity 0.3s;
}
.inventory-slot {
transition: transform 0.1s;
}
.inventory-slot:hover {
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.3) !important;
}
`;
document.head.appendChild(style);
}
function simulationLoop(timestamp) { function simulationLoop(timestamp) {
// Calculate FPS // Calculate FPS
const deltaTime = timestamp - lastFrameTime; const deltaTime = timestamp - lastFrameTime;
@@ -65,12 +140,54 @@ 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 // Update player movement if player exists
updatePhysics(); if (player) {
updatePlayerMovement();
}
// Render // Update physics with timestamp for rate limiting
updatePhysics(timestamp);
// Render - skip rendering if FPS is too low to prevent death spiral
if (fps > 10 || timestamp % 3 < 1) {
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,5 +1,22 @@
// Physics simulation functions // 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;
// Update physics objects
if (typeof updatePhysicsObjects === 'function') {
updatePhysicsObjects();
}
// Update entities
if (typeof updateEntities === 'function') {
updateEntities();
}
// Get visible chunks // Get visible chunks
const visibleChunks = getVisibleChunks(); const visibleChunks = getVisibleChunks();
@@ -7,8 +24,12 @@ function updatePhysics() {
fireUpdateCounter++; fireUpdateCounter++;
// Process each visible chunk // 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); 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--) {
@@ -26,28 +47,42 @@ function updatePhysics() {
const worldX = chunkX * CHUNK_SIZE + x; const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y; const worldY = chunkY * CHUNK_SIZE + y;
if (type === SAND) { // Use a lookup table for faster element updates
updateSand(worldX, worldY); const updateFunctions = {
} else if (type === WATER) { [SAND]: updateSand,
updateWater(worldX, worldY); [WATER]: updateWater,
} else if (type === DIRT) { [DIRT]: updateDirt,
updateDirt(worldX, worldY); [GRASS]: updateGrass,
} else if (type === GRASS) { [SEED]: updateSeed,
updateGrass(worldX, worldY); [GRASS_BLADE]: updateGrassBlade,
} else if (type === SEED) { [FLOWER]: updateFlower,
updateSeed(worldX, worldY); [TREE_SEED]: updateTreeSeed,
} else if (type === GRASS_BLADE) { [FIRE]: updateFire,
updateGrassBlade(worldX, worldY); [LAVA]: updateLava
} else if (type === FLOWER) { };
updateFlower(worldX, worldY);
} else if (type === TREE_SEED) { const updateFunction = updateFunctions[type];
updateTreeSeed(worldX, worldY); if (updateFunction) {
} else if (type === FIRE) { const wasModified = updateFunction(worldX, worldY);
updateFire(worldX, worldY); if (wasModified) {
} else if (type === LAVA) { chunkModified = true;
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,23 +1,53 @@
// 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);
// Get visible chunks // Set pixelated rendering for the entire canvas
const visibleChunks = getVisibleChunks(); ctx.imageSmoothingEnabled = false;
// Draw animated sky background
renderSky();
// Display FPS in debug mode
if (debugMode) {
displayDebugInfo();
}
// Get visible chunks - limit the number of chunks processed per frame
const visibleChunks = getVisibleChunks().slice(0, 20);
// Render each visible chunk // 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); 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';
@@ -35,83 +65,30 @@ function render() {
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15); ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
} }
// Render each pixel in the chunk // Remove this chunk from the dirty list after rendering
for (let y = 0; y < CHUNK_SIZE; y++) { if (dirtyChunks.has(key)) {
for (let x = 0; x < CHUNK_SIZE; x++) { dirtyChunks.delete(key);
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;
// Update cloud position animation only (not colors or shapes)
skyAnimationTime += skyAnimationSpeed;
if (skyAnimationTime > 1) skyAnimationTime -= 1;
// Render physics objects
renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY);
// Render entities
if (typeof renderEntities === 'function') {
renderEntities(ctx, worldOffsetX, worldOffsetY);
}
// Render breaking range indicator if player is breaking
if (player && player.isBreaking) {
renderBreakingIndicator(ctx, worldOffsetX, worldOffsetY);
} }
// Draw cursor position and update debug info // Draw cursor position and update debug info
@@ -146,3 +123,216 @@ function render() {
} }
} }
} }
// Render the animated sky background
function renderSky() {
// Create a gradient with fixed colors (no animation of colors)
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
// Use fixed color positions for a brighter, static sky
gradient.addColorStop(0, SKY_COLORS[0]);
gradient.addColorStop(0.5, SKY_COLORS[1]);
gradient.addColorStop(1, SKY_COLORS[2]);
// Fill the background with the gradient
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add a subtle horizon line to separate sky from world
const horizonGradient = ctx.createLinearGradient(0, canvas.height * 0.4, 0, canvas.height * 0.6);
horizonGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
horizonGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
horizonGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = horizonGradient;
ctx.fillRect(0, canvas.height * 0.4, canvas.width, canvas.height * 0.2);
}
// Display debug information
function displayDebugInfo() {
ctx.save();
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(10, 10, 200, 80);
ctx.font = '14px monospace';
ctx.fillStyle = 'white';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// Display FPS
ctx.fillText(`FPS: ${fps}`, 20, 20);
// Display world coordinates
ctx.fillText(`World: ${Math.floor(worldOffsetX)}, ${Math.floor(worldOffsetY)}`, 20, 40);
// Display chunk coordinates
const chunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
const chunkY = Math.floor(worldOffsetY / CHUNK_SIZE);
ctx.fillText(`Chunk: ${chunkX}, ${chunkY}`, 20, 60);
ctx.restore();
}
// Render breaking indicator for player
function renderBreakingIndicator(ctx, offsetX, offsetY) {
// Get mouse position in world coordinates
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
// Calculate distance from player to target block
const distance = Math.sqrt(
Math.pow(worldX - player.x, 2) +
Math.pow(worldY - player.y, 2)
);
// Convert to screen coordinates
const screenX = (worldX - offsetX) * PIXEL_SIZE;
const screenY = (worldY - offsetY) * PIXEL_SIZE;
const playerScreenX = (player.x - offsetX) * PIXEL_SIZE;
const playerScreenY = (player.y - offsetY) * PIXEL_SIZE;
// Draw breaking range circle
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(
playerScreenX,
playerScreenY,
player.breakingRange * PIXEL_SIZE,
0,
Math.PI * 2
);
ctx.stroke();
// Draw target indicator
if (distance <= player.breakingRange) {
ctx.strokeStyle = '#ff0000';
} else {
ctx.strokeStyle = '#888888'; // Gray when out of range
}
ctx.lineWidth = 2;
ctx.strokeRect(
screenX - PIXEL_SIZE/2,
screenY - PIXEL_SIZE/2,
PIXEL_SIZE,
PIXEL_SIZE
);
// Draw line from player to target point
ctx.beginPath();
ctx.moveTo(playerScreenX, playerScreenY);
ctx.lineTo(screenX, screenY);
ctx.stroke();
}
// 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 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();
} }
@@ -33,12 +40,41 @@ 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);
// Add floor at the bottom of the world (y = 0 and y = 1) // Fill chunk at y = 1 with stone, but create a noisy transition at the top
if (chunkY === 0 || chunkY === 1) { if (chunkY === 1) {
// Fill the bottom row with walls // 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++) { 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); chunks.set(key, chunkData);
@@ -47,6 +83,302 @@ 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);
@@ -61,8 +393,13 @@ 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
if (chunk[index] !== type) {
chunk[index] = type; chunk[index] = type;
// Mark chunk as dirty for rendering
dirtyChunks.add(getChunkKey(chunkX, chunkY));
// Assign random color index for natural elements // Assign random color index for natural elements
if (type === DIRT || type === GRASS || type === STONE || type === WOOD || type === LEAF) { 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);
@@ -72,15 +409,26 @@ function setPixel(worldX, worldY, type) {
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, 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);
@@ -116,23 +464,110 @@ 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 // Calculate visible chunk range (chunks that might be visible on screen)
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++) {
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; 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);
}
}
}
}

BIN
sprites/citizen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

BIN
sprites/farnel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
sprites/pingwin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
sprites/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

BIN
sprites/postac.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
sprites/purplin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
sprites/rabbit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -39,6 +39,18 @@ body {
background-color: #ff9800; background-color: #ff9800;
} }
#spawn-player-btn {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 10px 15px;
margin-left: 10px;
}
#spawn-player-btn:hover {
background-color: #45a049;
}
.navigation { .navigation {
display: flex; display: flex;
} }
@@ -63,3 +75,57 @@ body {
background-color: #000; background-color: #000;
cursor: crosshair; cursor: crosshair;
} }
#player-hud {
position: fixed;
bottom: 10px;
left: 10px;
width: 300px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
font-family: Arial, sans-serif;
z-index: 1000;
}
#health-bar-container {
width: 100%;
height: 20px;
background-color: rgba(255, 255, 255, 0.3);
margin-bottom: 10px;
border-radius: 3px;
}
#health-bar {
width: 100%;
height: 100%;
background-color: #4CAF50;
border-radius: 3px;
transition: width 0.3s;
}
#inventory-container {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}
.inventory-slot {
width: 30px;
height: 30px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 10px;
position: relative;
transition: transform 0.1s;
}
.inventory-slot:hover {
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.3);
}