Compare commits

..

84 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
Kacper Kostka (aider)
8ef18f52ab feat: Add color variations for natural elements and dynamic water colors 2025-04-04 12:22:15 +02:00
Kacper Kostka (aider)
a86acfff3a refactor: Spread game code into modular files for better maintainability
This commit reorganizes the game's source code into multiple files within a `js/` directory, creating a more modular and maintainable structure. The changes include:

- Created separate files for different game components:
  - `constants.js`: Game constants and element types
  - `world.js`: World management functions
  - `terrain.js`: Terrain generation logic
  - `physics.js`: Physics simulation
  - `render.js`: Rendering functions
  - `input.js`: Input handling
  - `main.js`: Main game initialization and loop
  - Element-specific files in `js/elements/`:
    - `basic.js`: Sand, water, dirt behaviors
    - `plants.js`: Grass, seeds, flowers
    - `trees.js`: Tree growth and leaf generation
    - `fire.js`: Fire and lava behaviors

- Updated `index.html` to load modules in the correct order
- Removed the monolithic `script.js`

The modular approach improves code readability, makes future extensions easier, and separates concerns more effectively.
2025-04-04 12:15:30 +02:00
Kacper Kostka (aider)
a5702a210f feat: Implement procedural terrain generation with hills, lakes, and vegetation 2025-04-04 12:05:41 +02:00
Kacper Kostka (aider)
3f4f8bc09c fix: Resolve duplicate identifier by renaming chunkY to floorChunkY 2025-04-04 12:00:26 +02:00
Kacper Kostka (aider)
9cdf7eba78 feat: Set default world position to 0,0 and extend floor to 2 chunks deep 2025-04-04 11:59:29 +02:00
Kacper Kostka (aider)
5ef436498f feat: Move default player position to Chunk: 0,-1 and adjust floor to bottom edge 2025-04-04 11:55:14 +02:00
Kacper Kostka (aider)
c858da4c3e feat: Add lava element with dynamic colors and interactions 2025-04-04 11:48:06 +02:00
Kacper Kostka (aider)
d6d874ab99 feat: Add fire simulation with color variation and flammable material restrictions 2025-04-04 11:42:17 +02:00
Kacper Kostka (aider)
0788b3067d feat: Increase tree and flower sizes for better visibility 2025-04-04 11:33:53 +02:00
Kacper Kostka (aider)
d8e868aad8 feat: Add various seeds that grow on dirt, including grass blades, flowers, and trees 2025-04-04 11:30:47 +02:00
27 changed files with 3873 additions and 617 deletions

1
.gitignore vendored Normal file
View File

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

View File

@@ -16,7 +16,16 @@
<button id="stone-btn">Stone</button> <button id="stone-btn">Stone</button>
<button id="grass-btn">Grass</button> <button id="grass-btn">Grass</button>
<button id="wood-btn">Wood</button> <button id="wood-btn">Wood</button>
<button id="seed-btn">Seed</button>
<button id="tree-seed-btn">Tree Seed</button>
<button id="fire-btn">Fire</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>
@@ -33,6 +42,21 @@
</div> </div>
<canvas id="simulation-canvas"></canvas> <canvas id="simulation-canvas"></canvas>
</div> </div>
<script src="script.js"></script> <!-- Load modules in the correct order -->
<script src="js/constants.js"></script>
<script src="js/world.js"></script>
<script src="js/elements/basic.js"></script>
<script src="js/elements/plants.js"></script>
<script src="js/elements/trees.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/input.js"></script>
<script src="js/physics.js"></script>
<script src="js/terrain.js"></script>
<script src="js/main.js"></script>
</body> </body>
</html> </html>

68
js/constants.js Normal file
View File

@@ -0,0 +1,68 @@
// Game constants
const CHUNK_SIZE = 200;
let PIXEL_SIZE = 4;
const GRAVITY = 1.5; // Increased gravity (3x stronger)
const WATER_SPREAD = 3;
// Base Colors
const SAND_COLOR = '#e6c588';
const WATER_COLOR = '#4a80f5';
const WALL_COLOR = '#888888';
const DIRT_COLOR = '#8B4513';
const STONE_COLOR = '#A9A9A9';
const GRASS_COLOR = '#7CFC00';
const WOOD_COLOR = '#8B5A2B';
const SEED_COLOR = '#654321';
const FLOWER_COLORS = ['#FF0000', '#FFFF00', '#FF00FF', '#FFA500', '#FFFFFF', '#00FFFF'];
const LEAF_COLOR = '#228B22';
const FIRE_COLORS = ['#FF0000', '#FF3300', '#FF6600', '#FF9900', '#FFCC00', '#FFFF00'];
const LAVA_COLORS = ['#FF0000', '#FF3300', '#FF4500', '#FF6600', '#FF8C00'];
const RABBIT_COLORS = ['#FFFFFF', '#E0E0E0', '#D3C8B4']; // White, Light Gray, Light Brown
// Color variation functions
function getRandomColorVariation(baseColor, range) {
// Convert hex to RGB
const r = parseInt(baseColor.slice(1, 3), 16);
const g = parseInt(baseColor.slice(3, 5), 16);
const b = parseInt(baseColor.slice(5, 7), 16);
// Add random variation
const rVar = Math.max(0, Math.min(255, r + Math.floor(Math.random() * range * 2) - range));
const gVar = Math.max(0, Math.min(255, g + Math.floor(Math.random() * range * 2) - range));
const bVar = Math.max(0, Math.min(255, b + Math.floor(Math.random() * range * 2) - range));
// Convert back to hex
return `#${rVar.toString(16).padStart(2, '0')}${gVar.toString(16).padStart(2, '0')}${bVar.toString(16).padStart(2, '0')}`;
}
// Generate color palettes for natural elements
const DIRT_COLORS = Array(10).fill().map(() => getRandomColorVariation(DIRT_COLOR, 15));
const GRASS_COLORS = Array(10).fill().map(() => getRandomColorVariation(GRASS_COLOR, 20));
const STONE_COLORS = Array(10).fill().map(() => getRandomColorVariation(STONE_COLOR, 15));
const WOOD_COLORS = Array(10).fill().map(() => getRandomColorVariation(WOOD_COLOR, 15));
const LEAF_COLORS = Array(10).fill().map(() => getRandomColorVariation(LEAF_COLOR, 25));
const WATER_COLORS = Array(10).fill().map(() => getRandomColorVariation(WATER_COLOR, 20));
// Element types
const EMPTY = 0;
const SAND = 1;
const WATER = 2;
const WALL = 3;
const DIRT = 4;
const STONE = 5;
const GRASS = 6;
const WOOD = 7;
const SEED = 8;
const GRASS_BLADE = 9;
const FLOWER = 10;
const TREE_SEED = 11;
const LEAF = 12;
const FIRE = 13;
const LAVA = 14;
const RABBIT = 15;
const SQUARE = 16;
const CIRCLE = 17;
const TRIANGLE = 18;
// Flammable materials
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];

219
js/elements/basic.js Normal file
View File

@@ -0,0 +1,219 @@
// Basic element behaviors (sand, water, dirt)
function updateSand(x, y) {
// Try to move down with stronger gravity (up to 5 pixels at once)
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, newY, SAND);
return true;
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, SAND);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SAND);
return true;
}
// Sand can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, SAND);
return true;
}
return false;
}
function updateWater(x, y) {
let modified = false;
// Update water color dynamically
const metadata = getMetadata(x, y);
if (metadata) {
if (metadata.waterColorTimer === undefined) {
metadata.waterColorTimer = 0;
modified = true;
}
metadata.waterColorTimer++;
// Change color occasionally
if (metadata.waterColorTimer > 20 && Math.random() < 0.1) {
metadata.colorIndex = Math.floor(Math.random() * 10);
metadata.waterColorTimer = 0;
modified = true;
}
setMetadata(x, y, metadata);
}
// Try to move down with stronger gravity (up to 4 pixels at once)
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, newY, WATER);
moveMetadata(x, y, x, newY);
return true;
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, WATER);
moveMetadata(x, y, x - 1, y + 1);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, WATER);
moveMetadata(x, y, x + 1, y + 1);
return true;
}
// Try to spread horizontally
else {
let moved = false;
// Randomly choose direction first
const goLeft = Math.random() > 0.5;
if (goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER);
moveMetadata(x, y, x - 1, y);
return true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moveMetadata(x, y, x + 1, y);
return true;
}
// Try the other direction if first failed
else if (!goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER);
moveMetadata(x, y, x - 1, y);
return true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moveMetadata(x, y, x + 1, y);
return true;
}
}
// Water extinguishes fire and turns lava into stone
const directions = [
{dx: -1, dy: 0}, {dx: 1, dy: 0},
{dx: 0, dy: -1}, {dx: 0, dy: 1},
{dx: -1, dy: -1}, {dx: 1, dy: -1},
{dx: -1, dy: 1}, {dx: 1, dy: 1}
];
for (const dir of directions) {
if (getPixel(x + dir.dx, y + dir.dy) === FIRE) {
setPixel(x + dir.dx, y + dir.dy, EMPTY);
removeMetadata(x + dir.dx, y + dir.dy);
return true;
} else if (getPixel(x + dir.dx, y + dir.dy) === LAVA) {
// Water turns lava into stone
setPixel(x + dir.dx, y + dir.dy, STONE);
removeMetadata(x + dir.dx, y + dir.dy);
// Water is consumed in the process
setPixel(x, y, EMPTY);
return true;
}
}
return modified;
}
function updateDirt(x, y) {
// Try to move down with stronger gravity (up to 5 pixels at once)
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, newY, DIRT);
return true;
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, DIRT);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, DIRT);
return true;
}
// Dirt can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, DIRT);
return true;
}
// Dirt can turn into grass if exposed to air above
if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) {
setPixel(x, y, GRASS);
return true;
}
// Dirt can randomly spawn seeds if exposed to air above
if (getPixel(x, y - 1) === EMPTY) {
// Spawn different types of seeds with different probabilities
const seedRoll = Math.random();
if (seedRoll < 0.0002) { // Grass blade seed (most common)
setPixel(x, y - 1, SEED);
return true;
} else if (seedRoll < 0.00025) { // Flower seed (less common)
setPixel(x, y - 1, SEED);
// Mark this seed as a flower seed (will be handled in updateSeed)
setMetadata(x, y - 1, { type: 'flower' });
return true;
} else if (seedRoll < 0.00026) { // Tree seed (rare)
setPixel(x, y - 1, TREE_SEED);
return true;
}
}
return false;
}

188
js/elements/fire.js Normal file
View File

@@ -0,0 +1,188 @@
// Fire and lava element behaviors
let fireUpdateCounter = 0;
function updateFire(x, y) {
let modified = false;
const metadata = getMetadata(x, y);
if (!metadata) {
// Initialize metadata if it doesn't exist
setMetadata(x, y, {
lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
});
return true;
}
// Decrease lifetime
metadata.lifetime--;
modified = true;
// Randomly change color for flickering effect
if (Math.random() < 0.2) {
metadata.colorIndex = Math.floor(Math.random() * FIRE_COLORS.length);
modified = true;
}
// Update metadata
setMetadata(x, y, metadata);
// Fire rises upward occasionally
if (Math.random() < 0.3 && getPixel(x, y - 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y - 1, FIRE);
moveMetadata(x, y, x, y - 1);
return true;
}
// Fire can also move slightly to the sides
if (Math.random() < 0.1) {
const direction = Math.random() > 0.5 ? 1 : -1;
if (getPixel(x + direction, y - 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + direction, y - 1, FIRE);
moveMetadata(x, y, x + direction, y - 1);
return true;
}
}
// Fire spreads to nearby flammable materials (less frequently to reduce performance impact)
if (fireUpdateCounter % 3 === 0 && Math.random() < 0.3) {
const directions = [
{dx: -1, dy: 0}, {dx: 1, dy: 0},
{dx: 0, dy: -1}, {dx: 0, dy: 1},
{dx: -1, dy: -1}, {dx: 1, dy: -1},
{dx: -1, dy: 1}, {dx: 1, dy: 1}
];
const dir = directions[Math.floor(Math.random() * directions.length)];
const nearbyType = getPixel(x + dir.dx, y + dir.dy);
if (FLAMMABLE_MATERIALS.includes(nearbyType)) {
setPixel(x + dir.dx, y + dir.dy, FIRE);
setMetadata(x + dir.dx, y + dir.dy, {
lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
});
modified = true;
}
}
// Fire burns out after its lifetime
if (metadata.lifetime <= 0) {
setPixel(x, y, EMPTY);
removeMetadata(x, y);
return true;
}
return modified;
}
function updateLava(x, y) {
let modified = false;
const metadata = getMetadata(x, y);
if (!metadata) {
// Initialize metadata if it doesn't exist
setMetadata(x, y, {
colorIndex: Math.floor(Math.random() * LAVA_COLORS.length)
});
modified = true;
} else {
// Randomly change color for flowing effect
if (Math.random() < 0.1) {
metadata.colorIndex = Math.floor(Math.random() * LAVA_COLORS.length);
setMetadata(x, y, metadata);
modified = true;
}
}
// Lava moves slower than water
if (Math.random() < 0.7) {
// Try to move down
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, LAVA);
moveMetadata(x, y, x, y + 1);
return true;
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, LAVA);
moveMetadata(x, y, x - 1, y + 1);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, LAVA);
moveMetadata(x, y, x + 1, y + 1);
return true;
}
// Try to spread horizontally (slower than water)
else if (Math.random() < 0.3) {
// Randomly choose direction first
const goLeft = Math.random() > 0.5;
if (goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, LAVA);
moveMetadata(x, y, x - 1, y);
return true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, LAVA);
moveMetadata(x, y, x + 1, y);
return true;
}
// Try the other direction if first failed
else if (!goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, LAVA);
moveMetadata(x, y, x - 1, y);
return true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, LAVA);
moveMetadata(x, y, x + 1, y);
return true;
}
}
}
// Lava sets nearby flammable materials on fire
const directions = [
{dx: -1, dy: 0}, {dx: 1, dy: 0},
{dx: 0, dy: -1}, {dx: 0, dy: 1},
{dx: -1, dy: -1}, {dx: 1, dy: -1},
{dx: -1, dy: 1}, {dx: 1, dy: 1}
];
for (const dir of directions) {
const nearbyType = getPixel(x + dir.dx, y + dir.dy);
// Set flammable materials on fire
if (FLAMMABLE_MATERIALS.includes(nearbyType)) {
setPixel(x + dir.dx, y + dir.dy, FIRE);
setMetadata(x + dir.dx, y + dir.dy, {
lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
});
modified = true;
}
// Lava can melt sand into glass (stone)
else if (nearbyType === SAND && Math.random() < 0.05) {
setPixel(x + dir.dx, y + dir.dy, STONE);
modified = true;
}
// Lava can burn dirt
else if (nearbyType === DIRT && Math.random() < 0.02) {
setPixel(x + dir.dx, y + dir.dy, EMPTY);
modified = true;
}
}
return modified;
}

View File

@@ -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);
}
}

212
js/elements/plants.js Normal file
View File

@@ -0,0 +1,212 @@
// Plant element behaviors (grass, seeds, trees)
function updateGrass(x, y) {
// Grass behaves like dirt for physics with stronger gravity
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, newY, GRASS);
return true;
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, GRASS);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, GRASS);
return true;
}
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, GRASS);
return true;
}
// Grass can spread to nearby dirt
if (Math.random() < 0.0005) {
const directions = [
{dx: -1, dy: 0}, {dx: 1, dy: 0},
{dx: 0, dy: -1}, {dx: 0, dy: 1}
];
const dir = directions[Math.floor(Math.random() * directions.length)];
if (getPixel(x + dir.dx, y + dir.dy) === DIRT) {
setPixel(x + dir.dx, y + dir.dy, GRASS);
return true;
}
}
// Grass dies if covered (no air above)
if (getPixel(x, y - 1) !== EMPTY && Math.random() < 0.05) {
setPixel(x, y, DIRT);
return true;
}
return false;
}
function updateSeed(x, y) {
// Seeds fall like sand with stronger gravity
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, 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) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, SEED);
moveMetadata(x, y, x - 1, y + 1);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SEED);
moveMetadata(x, y, x + 1, y + 1);
return true;
}
// Seeds can float on water
else if (getPixel(x, y + 1) === WATER) {
// Just float, don't do anything
return false;
}
// If seed is on dirt or grass, it can germinate
else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) {
const metadata = getMetadata(x, y);
// Check if this is a flower seed
if (metadata && metadata.type === 'flower') {
setPixel(x, y, FLOWER);
// Set a random flower color
setMetadata(x, y, {
type: 'flower',
color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)],
age: 0,
height: 1
});
return true;
} else {
// Regular seed becomes a grass blade
setPixel(x, y, GRASS_BLADE);
setMetadata(x, y, { age: 0, height: 1 });
return true;
}
}
return false;
}
function updateGrassBlade(x, y) {
// Grass blades are static once grown
const metadata = getMetadata(x, y);
if (!metadata) {
setMetadata(x, y, { age: 0, height: 1 });
return true;
}
// Increment age
metadata.age++;
setMetadata(x, y, metadata);
// Grass blades can grow taller up to a limit
if (metadata.age % 200 === 0 && metadata.height < 3 && getPixel(x, y - 1) === EMPTY) {
setPixel(x, y - 1, GRASS_BLADE);
setMetadata(x, y - 1, { age: 0, height: metadata.height + 1 });
metadata.isTop = false;
setMetadata(x, y, metadata);
return true;
}
// Grass blades die if covered by something other than another grass blade
if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== GRASS_BLADE && Math.random() < 0.01) {
setPixel(x, y, EMPTY);
removeMetadata(x, y);
return true;
}
return false;
}
function updateFlower(x, y) {
// Flowers are similar to grass blades but with a colored top
const metadata = getMetadata(x, y);
if (!metadata) {
setMetadata(x, y, {
type: 'flower',
color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)],
age: 0,
height: 1
});
return true;
}
// Increment age
metadata.age++;
setMetadata(x, y, metadata);
// Flowers can grow taller up to a limit (2x bigger)
if (metadata.age % 300 === 0 && metadata.height < 8 && getPixel(x, y - 1) === EMPTY) {
// If this is the top of the flower, make it a stem and put a new flower on top
setPixel(x, y - 1, FLOWER);
setMetadata(x, y - 1, {
type: 'flower',
color: metadata.color,
age: 0,
height: metadata.height + 1,
isTop: true
});
metadata.isTop = false;
setMetadata(x, y, metadata);
return true;
}
// Flowers die if covered
if (getPixel(x, y - 1) !== EMPTY && getPixel(x, y - 1) !== FLOWER && Math.random() < 0.01) {
setPixel(x, y, EMPTY);
removeMetadata(x, y);
return true;
}
// Flowers can drop seeds occasionally
if (metadata.isTop && Math.random() < 0.0001) {
const directions = [-1, 1];
const dir = directions[Math.floor(Math.random() * directions.length)];
if (getPixel(x + dir, y) === EMPTY) {
setPixel(x + dir, y, SEED);
setMetadata(x + dir, y, { type: 'flower' });
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;
}

146
js/elements/trees.js Normal file
View File

@@ -0,0 +1,146 @@
// Tree element behaviors
function updateTreeSeed(x, y) {
// Tree seeds fall like other seeds with stronger gravity
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, 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) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, TREE_SEED);
moveMetadata(x, y, x - 1, y + 1);
return true;
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, TREE_SEED);
moveMetadata(x, y, x + 1, y + 1);
return true;
}
// Seeds can float on water
else if (getPixel(x, y + 1) === WATER) {
// Just float, don't do anything
return false;
}
// If seed is on dirt or grass, it can grow into a tree
else if (getPixel(x, y + 1) === DIRT || getPixel(x, y + 1) === GRASS) {
// Start growing a tree
growTree(x, y);
return true;
}
return false;
}
function growTree(x, y) {
// Replace the seed with the trunk
setPixel(x, y, WOOD);
// Determine tree height (50-80 blocks, 10x bigger)
const treeHeight = 50 + Math.floor(Math.random() * 31);
// Generate consistent wood color for this tree
const woodColorIndex = Math.floor(Math.random() * 10);
setMetadata(x, y, { colorIndex: woodColorIndex });
// Grow the trunk upward
for (let i = 1; i < treeHeight; i++) {
if (getPixel(x, y - i) === EMPTY) {
setPixel(x, y - i, WOOD);
// Use the same wood color for the entire trunk
setMetadata(x, y - i, { colorIndex: woodColorIndex });
} else {
break; // Stop if we hit something
}
}
// Add leaves at the top (10x bigger radius)
addLeaves(x, y - treeHeight + 1, 20 + Math.floor(Math.random() * 10));
// Add some branches
addBranches(x, y, treeHeight);
}
function addBranches(x, y, treeHeight) {
// Add 2-4 branches at different heights
const numBranches = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < numBranches; i++) {
// Position branch at different heights along the trunk
const branchY = y - Math.floor(treeHeight * (0.3 + 0.4 * i / numBranches));
// Choose left or right direction
const direction = Math.random() > 0.5 ? 1 : -1;
// Branch length (10-15 blocks)
const branchLength = 10 + Math.floor(Math.random() * 6);
// Create the branch
for (let j = 1; j <= branchLength; j++) {
// Branch goes out horizontally with some upward angle
const branchX = x + (j * direction);
const upwardAngle = Math.floor(j * 0.3);
if (getPixel(branchX, branchY - upwardAngle) === EMPTY) {
setPixel(branchX, branchY - upwardAngle, WOOD);
} else {
break; // Stop if we hit something
}
// Add small leaf clusters at the end of branches
if (j === branchLength) {
addLeaves(branchX, branchY - upwardAngle, 8 + Math.floor(Math.random() * 4));
}
}
}
}
function addLeaves(x, y, radius) {
// Generate a few leaf color variations for this tree
const baseLeafColorIndex = Math.floor(Math.random() * 10);
const leafColorIndices = [
baseLeafColorIndex,
(baseLeafColorIndex + 1) % 10,
(baseLeafColorIndex + 2) % 10
];
// Add a cluster of leaves around the point
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
// Skip the exact center (trunk position)
if (dx === 0 && dy === 0) continue;
// Make it more circular by checking distance
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance <= radius) {
// Random chance to place a leaf based on distance from center
// More dense leaves for larger trees
const density = radius > 10 ? 0.8 : 0.6;
if (Math.random() < (1 - distance/radius/density)) {
if (getPixel(x + dx, y + dy) === EMPTY) {
setPixel(x + dx, y + dy, LEAF);
// Assign one of the tree's leaf colors
const colorIndex = leafColorIndices[Math.floor(Math.random() * leafColorIndices.length)];
setMetadata(x + dx, y + dy, { colorIndex });
}
}
}
}
}
}

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);
}
}

294
js/input.js Normal file
View File

@@ -0,0 +1,294 @@
// Input handling functions
let isDrawing = false;
let isDragging = false;
let lastMouseX, lastMouseY;
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) {
currentTool = tool;
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
if (tool === SAND) {
document.getElementById('sand-btn').classList.add('active');
} else if (tool === WATER) {
document.getElementById('water-btn').classList.add('active');
} else if (tool === DIRT) {
document.getElementById('dirt-btn').classList.add('active');
} else if (tool === STONE) {
document.getElementById('stone-btn').classList.add('active');
} else if (tool === GRASS) {
document.getElementById('grass-btn').classList.add('active');
} else if (tool === WOOD) {
document.getElementById('wood-btn').classList.add('active');
} else if (tool === SEED) {
document.getElementById('seed-btn').classList.add('active');
} else if (tool === TREE_SEED) {
document.getElementById('tree-seed-btn').classList.add('active');
} else if (tool === FIRE) {
document.getElementById('fire-btn').classList.add('active');
} else if (tool === LAVA) {
document.getElementById('lava-btn').classList.add('active');
} else if (tool === 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) {
document.getElementById('eraser-btn').classList.add('active');
}
}
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Right mouse button for dragging the world
if (e.button === 2 || e.ctrlKey || e.shiftKey) {
isDragging = true;
lastMouseX = x;
lastMouseY = y;
worldOffsetXBeforeDrag = worldOffsetX;
worldOffsetYBeforeDrag = worldOffsetY;
} else {
// Left mouse button for drawing or breaking blocks
if (player) {
// If player exists, start breaking blocks
player.startBreaking();
} else {
// Otherwise use normal drawing
isDrawing = true;
draw(x, y);
}
}
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Always update current mouse position
currentMouseX = x;
currentMouseY = y;
if (isDragging) {
// Calculate how much the mouse has moved
const dx = x - lastMouseX;
const dy = y - lastMouseY;
// Move the world in the opposite direction (divide by pixel size to convert to world coordinates)
moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE);
// Update the last mouse position
lastMouseX = x;
lastMouseY = y;
} else if (isDrawing) {
draw(x, y);
}
}
function handleMouseUp(e) {
isDrawing = false;
if (isDragging) {
// Calculate the total movement during this drag
const totalDragX = worldOffsetX - worldOffsetXBeforeDrag;
const totalDragY = worldOffsetY - worldOffsetYBeforeDrag;
if (debugMode) {
console.log(`Drag completed: ${totalDragX}, ${totalDragY}`);
}
}
isDragging = false;
// Stop breaking blocks if player exists
if (player) {
player.stopBreaking();
}
}
function draw(x, y) {
if (!isDrawing) return;
// Convert screen coordinates to world coordinates
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY;
// 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)
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const pixelX = worldX + dx;
const pixelY = worldY + dy;
// Special handling for fire - only set fire to flammable materials
if (currentTool === FIRE) {
const currentPixel = getPixel(pixelX, pixelY);
if (FLAMMABLE_MATERIALS.includes(currentPixel)) {
setPixel(pixelX, pixelY, FIRE);
setMetadata(pixelX, pixelY, {
lifetime: 100 + Math.floor(Math.random() * 100),
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
});
}
}
// 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 {
setPixel(pixelX, pixelY, currentTool);
// Add metadata for special types
if (currentTool === SEED) {
setMetadata(pixelX, pixelY, { type: 'regular' });
} else if (currentTool === FLOWER) {
setMetadata(pixelX, pixelY, {
type: 'flower',
color: FLOWER_COLORS[Math.floor(Math.random() * FLOWER_COLORS.length)],
age: 0,
height: 1
});
}
}
}
}
}
function handleTouchStart(e) {
e.preventDefault();
// Check if we have multiple touch points (for dragging)
if (e.touches.length > 1) {
isDragging = true;
lastMouseX = e.touches[0].clientX;
lastMouseY = e.touches[0].clientY;
worldOffsetXBeforeDrag = worldOffsetX;
worldOffsetYBeforeDrag = worldOffsetY;
} else {
// Single touch for drawing
isDrawing = true;
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
currentMouseX = x;
currentMouseY = y;
draw(x, y);
}
}
function handleTouchMove(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
if (isDragging && e.touches.length > 1) {
// Calculate how much the touch has moved
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
const dx = x - lastMouseX;
const dy = y - lastMouseY;
// Move the world in the opposite direction
moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE);
// Update the last touch position
lastMouseX = x;
lastMouseY = y;
} else if (isDrawing) {
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
currentMouseX = x;
currentMouseY = y;
draw(x, y);
}
}
function toggleDebug() {
debugMode = !debugMode;
document.getElementById('debug-btn').classList.toggle('active');
// 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);
}
}

193
js/main.js Normal file
View File

@@ -0,0 +1,193 @@
// Main game variables and initialization
let canvas, ctx;
let currentTool = SAND;
let lastFrameTime = 0;
let fps = 0;
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
window.onload = function() {
canvas = document.getElementById('simulation-canvas');
ctx = canvas.getContext('2d');
// Set canvas size to fill the screen
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Tool selection
document.getElementById('sand-btn').addEventListener('click', () => setTool(SAND));
document.getElementById('water-btn').addEventListener('click', () => setTool(WATER));
document.getElementById('dirt-btn').addEventListener('click', () => setTool(DIRT));
document.getElementById('stone-btn').addEventListener('click', () => setTool(STONE));
document.getElementById('grass-btn').addEventListener('click', () => setTool(GRASS));
document.getElementById('wood-btn').addEventListener('click', () => setTool(WOOD));
document.getElementById('seed-btn').addEventListener('click', () => setTool(SEED));
document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED));
document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE));
document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA));
document.getElementById('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));
// Add player spawn button
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
// Navigation controls
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0));
document.getElementById('move-up').addEventListener('click', () => moveWorld(0, -CHUNK_SIZE/2));
document.getElementById('move-down').addEventListener('click', () => moveWorld(0, CHUNK_SIZE/2));
document.getElementById('debug-btn').addEventListener('click', toggleDebug);
// Drawing events
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// Touch events for mobile
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleMouseUp);
// Initialize the first chunk and generate terrain around it
getOrCreateChunk(0, 0);
// Explicitly create and mark the stone layer as dirty
for (let dx = -5; dx <= 5; dx++) {
const chunkX = dx;
const chunkY = 1; // Stone layer
const key = getChunkKey(chunkX, chunkY);
getOrCreateChunk(chunkX, chunkY);
dirtyChunks.add(key);
}
generateChunksAroundPlayer();
// Start the simulation loop
requestAnimationFrame(simulationLoop);
// Initialize physics variables
window.physicsUpdateRate = 16; // ms between physics updates
window.lastPhysicsTime = 0;
window.fireUpdateCounter = 0;
};
function resizeCanvas() {
canvas.width = window.innerWidth;
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) {
// Calculate FPS
const deltaTime = timestamp - lastFrameTime;
lastFrameTime = timestamp;
fps = Math.round(1000 / deltaTime);
document.getElementById('fps').textContent = `FPS: ${fps}`;
// Update player movement if player exists
if (player) {
updatePlayerMovement();
}
// 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();
}
// Memory management: Clean up chunk cache for chunks that are far away
if (timestamp % 5000 < 16) { // Run every ~5 seconds
cleanupChunkCache();
}
// Continue the loop
requestAnimationFrame(simulationLoop);
}
// Clean up chunk cache to prevent memory leaks
function cleanupChunkCache() {
if (!chunkCanvasCache) return;
const visibleChunks = getVisibleChunks();
const visibleKeys = new Set();
// Get all visible chunk keys
for (const { chunkX, chunkY } of visibleChunks) {
visibleKeys.add(getChunkKey(chunkX, chunkY));
}
// Remove cached canvases for chunks that are far from view
for (const key of chunkCanvasCache.keys()) {
if (!visibleKeys.has(key)) {
// Keep stone layer chunks in cache longer
if (key.split(',')[1] === '1') {
// Only remove if it's really far away
const [chunkX, chunkY] = key.split(',').map(Number);
const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
if (Math.abs(chunkX - centerChunkX) > 10) {
chunkCanvasCache.delete(key);
}
} else {
chunkCanvasCache.delete(key);
}
}
}
}

88
js/physics.js Normal file
View File

@@ -0,0 +1,88 @@
// Physics simulation functions
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
const visibleChunks = getVisibleChunks();
// Increment fire update counter
fireUpdateCounter++;
// Process each visible chunk
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
// Skip physics calculations for chunks that are not visible
if (!isVisible) continue;
const chunk = getOrCreateChunk(chunkX, chunkY);
let chunkModified = false;
// Process from bottom to top, right to left for correct gravity simulation
for (let y = CHUNK_SIZE - 1; y >= 0; y--) {
// Alternate direction each row for more natural flow
const startX = y % 2 === 0 ? 0 : CHUNK_SIZE - 1;
const endX = y % 2 === 0 ? CHUNK_SIZE : -1;
const step = y % 2 === 0 ? 1 : -1;
for (let x = startX; x !== endX; x += step) {
const index = y * CHUNK_SIZE + x;
const type = chunk[index];
if (type === EMPTY || type === STONE || type === WOOD || type === LEAF) continue;
const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y;
// Use a lookup table for faster element updates
const updateFunctions = {
[SAND]: updateSand,
[WATER]: updateWater,
[DIRT]: updateDirt,
[GRASS]: updateGrass,
[SEED]: updateSeed,
[GRASS_BLADE]: updateGrassBlade,
[FLOWER]: updateFlower,
[TREE_SEED]: updateTreeSeed,
[FIRE]: updateFire,
[LAVA]: updateLava
};
const updateFunction = updateFunctions[type];
if (updateFunction) {
const wasModified = updateFunction(worldX, worldY);
if (wasModified) {
chunkModified = true;
}
}
}
}
// Mark chunk as dirty if it was modified
if (chunkModified) {
dirtyChunks.add(getChunkKey(chunkX, chunkY));
}
}
// Adaptive physics rate based on FPS
if (fps < 30 && physicsUpdateRate < 32) {
// If FPS is low, update physics less frequently
physicsUpdateRate = Math.min(32, physicsUpdateRate + 2);
} else if (fps > 50 && physicsUpdateRate > 16) {
// If FPS is high, update physics more frequently
physicsUpdateRate = Math.max(16, physicsUpdateRate - 2);
}
}

338
js/render.js Normal file
View File

@@ -0,0 +1,338 @@
// Rendering functions
// Cache for rendered chunks
let chunkCanvasCache = new Map();
function render() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Set pixelated rendering for the entire canvas
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
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
// Skip rendering for chunks that are not visible
if (!isVisible) continue;
const key = getChunkKey(chunkX, chunkY);
if (!chunks.has(key)) continue;
// Calculate screen position of chunk
const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE;
const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE;
// Check if we need to render this chunk
const needsRender = dirtyChunks.has(key) || worldMoved || !chunkCanvasCache.has(key);
// Always render the chunk if it's in the stone layer (for visibility)
// or if it needs rendering
if (needsRender) {
renderChunkToCache(chunkX, chunkY, key);
}
// Draw the cached chunk to the main canvas
const cachedCanvas = chunkCanvasCache.get(key);
if (cachedCanvas) {
ctx.drawImage(cachedCanvas, screenX, screenY);
}
// Draw chunk border in debug mode
if (debugMode) {
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
ctx.strokeRect(
screenX,
screenY,
CHUNK_SIZE * PIXEL_SIZE,
CHUNK_SIZE * PIXEL_SIZE
);
// Draw chunk coordinates
ctx.fillStyle = '#ffffff';
ctx.font = '12px Arial';
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
}
// Remove this chunk from the dirty list after rendering
if (dirtyChunks.has(key)) {
dirtyChunks.delete(key);
}
}
// 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
if (currentMouseX !== undefined && currentMouseY !== undefined) {
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
// Update coordinates display in debug mode
if (debugMode) {
document.getElementById('coords').textContent =
`Chunk: ${Math.floor(worldOffsetX / CHUNK_SIZE)},${Math.floor(worldOffsetY / CHUNK_SIZE)} | ` +
`Mouse: ${worldX},${worldY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`;
// Draw cursor outline
const cursorScreenX = (worldX - worldOffsetX) * PIXEL_SIZE;
const cursorScreenY = (worldY - worldOffsetY) * PIXEL_SIZE;
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
ctx.strokeRect(
cursorScreenX - PIXEL_SIZE,
cursorScreenY - PIXEL_SIZE,
PIXEL_SIZE * 3,
PIXEL_SIZE * 3
);
// Draw a dot at the exact mouse position
ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(currentMouseX, currentMouseY, 3, 0, Math.PI * 2);
ctx.fill();
}
}
}
// 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
);
}
}
}

188
js/terrain.js Normal file
View File

@@ -0,0 +1,188 @@
// Terrain generation functions
function generateChunksAroundPlayer() {
const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
const centerChunkY = Math.floor(worldOffsetY / CHUNK_SIZE);
const radius = 3; // Generate chunks within 3 chunks of the player
// Generate chunks in a square around the player
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const chunkX = centerChunkX + dx;
const chunkY = centerChunkY + dy;
// Only generate terrain for chunks at or above y=0
if (chunkY >= 0) {
getOrCreateChunk(chunkX, chunkY);
}
}
}
}
function generateTerrain(chunkX, chunkY, chunkData) {
// Use a seeded random number generator based on chunk coordinates
const seed = chunkX * 10000 + chunkY;
const random = createSeededRandom(seed);
// Generate base terrain (hills)
generateHills(chunkX, chunkY, chunkData, random);
// Add lakes
if (random() < 0.3) { // 30% chance for a lake in a chunk
generateLake(chunkX, chunkY, chunkData, random);
}
// Add stone formations
if (random() < 0.4) { // 40% chance for stone formations
generateStoneFormation(chunkX, chunkY, chunkData, random);
}
// Add vegetation (seeds and trees)
addVegetation(chunkX, chunkY, chunkData, random);
}
function createSeededRandom(seed) {
// Simple seeded random function
return function() {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
}
function generateHills(chunkX, chunkY, chunkData, random) {
// Generate a height map for this chunk using simplex-like noise
const heightMap = new Array(CHUNK_SIZE).fill(0);
// Base height (higher for chunks further from origin)
const baseHeight = Math.max(0, 50 - Math.sqrt(chunkX*chunkX + chunkY*chunkY) * 5);
// Generate a smooth height map
for (let x = 0; x < CHUNK_SIZE; x++) {
// Use multiple frequencies for more natural looking terrain
const noise1 = Math.sin(x * 0.02 + chunkX * CHUNK_SIZE * 0.02 + random() * 10) * 15;
const noise2 = Math.sin(x * 0.05 + chunkX * CHUNK_SIZE * 0.05 + random() * 10) * 7;
const noise3 = Math.sin(x * 0.1 + chunkX * CHUNK_SIZE * 0.1 + random() * 10) * 3;
// Combine noise at different frequencies
heightMap[x] = Math.floor(baseHeight + noise1 + noise2 + noise3);
// Ensure height is positive
heightMap[x] = Math.max(0, heightMap[x]);
}
// Fill the terrain based on the height map
for (let x = 0; x < CHUNK_SIZE; x++) {
const height = heightMap[x];
for (let y = CHUNK_SIZE - 1; y >= 0; y--) {
const worldY = chunkY * CHUNK_SIZE + y;
const depth = CHUNK_SIZE - 1 - y;
if (depth < height) {
const index = y * CHUNK_SIZE + x;
// Top layer is grass
if (depth === 0) {
chunkData[index] = GRASS;
// Set metadata with color index for grass
const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y;
setMetadata(worldX, worldY, { colorIndex: Math.floor(Math.random() * 10) });
}
// Next few layers are dirt
else if (depth < 5) {
chunkData[index] = DIRT;
// Set metadata with color index for dirt
const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y;
setMetadata(worldX, worldY, { colorIndex: Math.floor(Math.random() * 10) });
}
// Deeper layers are stone
else {
chunkData[index] = STONE;
// Set metadata with color index for stone
const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y;
setMetadata(worldX, worldY, { colorIndex: Math.floor(Math.random() * 10) });
}
}
}
}
}
function generateLake(chunkX, chunkY, chunkData, random) {
// Lake parameters
const lakeX = Math.floor(random() * (CHUNK_SIZE - 60)) + 30;
const lakeY = Math.floor(random() * (CHUNK_SIZE - 60)) + 30;
const lakeWidth = Math.floor(random() * 40) + 20;
const lakeHeight = Math.floor(random() * 20) + 10;
// Create an elliptical lake
for (let y = 0; y < CHUNK_SIZE; y++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
// Calculate distance from lake center (normalized to create an ellipse)
const dx = (x - lakeX) / lakeWidth;
const dy = (y - lakeY) / lakeHeight;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < 1) {
const index = y * CHUNK_SIZE + x;
// Water in the center
if (distance < 0.8) {
chunkData[index] = WATER;
}
// Sand around the edges
else {
chunkData[index] = SAND;
}
}
}
}
}
function generateStoneFormation(chunkX, chunkY, chunkData, random) {
// Stone formation parameters
const formationX = Math.floor(random() * (CHUNK_SIZE - 40)) + 20;
const formationWidth = Math.floor(random() * 30) + 10;
const formationHeight = Math.floor(random() * 40) + 20;
// Create a stone hill/mountain
for (let x = formationX - formationWidth; x < formationX + formationWidth; x++) {
if (x < 0 || x >= CHUNK_SIZE) continue;
// Calculate height at this x position (higher in the middle)
const dx = (x - formationX) / formationWidth;
const height = Math.floor(formationHeight * (1 - dx*dx));
for (let y = CHUNK_SIZE - 1; y >= CHUNK_SIZE - height; y--) {
if (y < 0 || y >= CHUNK_SIZE) continue;
const index = y * CHUNK_SIZE + x;
chunkData[index] = STONE;
}
}
}
function addVegetation(chunkX, chunkY, chunkData, random) {
// Add vegetation on grass
for (let y = 0; y < CHUNK_SIZE; y++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
const index = y * CHUNK_SIZE + x;
// Only add vegetation on grass
if (chunkData[index] === GRASS) {
// Check if there's empty space above
if (y > 0 && chunkData[(y-1) * CHUNK_SIZE + x] === EMPTY) {
// Random chance to add different types of vegetation
const roll = random();
if (roll < 0.01) { // 1% chance for a tree seed
chunkData[(y-1) * CHUNK_SIZE + x] = TREE_SEED;
} else if (roll < 0.05) { // 4% chance for a regular seed
chunkData[(y-1) * CHUNK_SIZE + x] = SEED;
}
}
}
}
}
}

573
js/world.js Normal file
View File

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

617
script.js
View File

@@ -1,616 +1 @@
// Constants // This file has been replaced by modular files in the js/ directory
const CHUNK_SIZE = 200;
const PIXEL_SIZE = 4;
const GRAVITY = 0.5;
const WATER_SPREAD = 3;
const SAND_COLOR = '#e6c588';
const WATER_COLOR = '#4a80f5';
const WALL_COLOR = '#888888';
const DIRT_COLOR = '#8B4513';
const STONE_COLOR = '#A9A9A9';
const GRASS_COLOR = '#7CFC00';
const WOOD_COLOR = '#8B5A2B';
// Element types
const EMPTY = 0;
const SAND = 1;
const WATER = 2;
const WALL = 3;
const DIRT = 4;
const STONE = 5;
const GRASS = 6;
const WOOD = 7;
// Global variables
let canvas, ctx;
let currentTool = SAND;
let isDrawing = false;
let isDragging = false;
let lastMouseX, lastMouseY;
let currentMouseX, currentMouseY;
let lastFrameTime = 0;
let fps = 0;
let worldOffsetX = 0;
let worldOffsetY = 0;
let worldOffsetXBeforeDrag = 0;
let worldOffsetYBeforeDrag = 0;
let chunks = new Map(); // Map to store chunks with key "x,y"
let debugMode = false;
// Initialize the simulation
window.onload = function() {
canvas = document.getElementById('simulation-canvas');
ctx = canvas.getContext('2d');
// Set canvas size to fill the screen
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Tool selection
document.getElementById('sand-btn').addEventListener('click', () => setTool(SAND));
document.getElementById('water-btn').addEventListener('click', () => setTool(WATER));
document.getElementById('dirt-btn').addEventListener('click', () => setTool(DIRT));
document.getElementById('stone-btn').addEventListener('click', () => setTool(STONE));
document.getElementById('grass-btn').addEventListener('click', () => setTool(GRASS));
document.getElementById('wood-btn').addEventListener('click', () => setTool(WOOD));
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
// Navigation controls
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0));
document.getElementById('move-up').addEventListener('click', () => moveWorld(0, -CHUNK_SIZE/2));
document.getElementById('move-down').addEventListener('click', () => moveWorld(0, CHUNK_SIZE/2));
document.getElementById('debug-btn').addEventListener('click', toggleDebug);
// Drawing events
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// Touch events for mobile
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleMouseUp);
// Initialize the first chunk
getOrCreateChunk(0, 0);
// Start the simulation loop
requestAnimationFrame(simulationLoop);
};
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
}
function setTool(tool) {
currentTool = tool;
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
if (tool === SAND) {
document.getElementById('sand-btn').classList.add('active');
} else if (tool === WATER) {
document.getElementById('water-btn').classList.add('active');
} else if (tool === DIRT) {
document.getElementById('dirt-btn').classList.add('active');
} else if (tool === STONE) {
document.getElementById('stone-btn').classList.add('active');
} else if (tool === GRASS) {
document.getElementById('grass-btn').classList.add('active');
} else if (tool === WOOD) {
document.getElementById('wood-btn').classList.add('active');
} else if (tool === EMPTY) {
document.getElementById('eraser-btn').classList.add('active');
}
}
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Right mouse button for dragging the world
if (e.button === 2 || e.ctrlKey || e.shiftKey) {
isDragging = true;
lastMouseX = x;
lastMouseY = y;
worldOffsetXBeforeDrag = worldOffsetX;
worldOffsetYBeforeDrag = worldOffsetY;
} else {
// Left mouse button for drawing
isDrawing = true;
draw(x, y);
}
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Always update current mouse position
currentMouseX = x;
currentMouseY = y;
if (isDragging) {
// Calculate how much the mouse has moved
const dx = x - lastMouseX;
const dy = y - lastMouseY;
// Move the world in the opposite direction (divide by pixel size to convert to world coordinates)
moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE);
// Update the last mouse position
lastMouseX = x;
lastMouseY = y;
} else if (isDrawing) {
draw(x, y);
}
}
function handleMouseUp(e) {
isDrawing = false;
if (isDragging) {
// Calculate the total movement during this drag
const totalDragX = worldOffsetX - worldOffsetXBeforeDrag;
const totalDragY = worldOffsetY - worldOffsetYBeforeDrag;
if (debugMode) {
console.log(`Drag completed: ${totalDragX}, ${totalDragY}`);
}
}
isDragging = false;
}
function draw(x, y) {
if (!isDrawing) return;
// Convert screen coordinates to world coordinates
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY;
// Draw a small brush (3x3)
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
setPixel(worldX + dx, worldY + dy, currentTool);
}
}
}
function handleTouchStart(e) {
e.preventDefault();
// Check if we have multiple touch points (for dragging)
if (e.touches.length > 1) {
isDragging = true;
lastMouseX = e.touches[0].clientX;
lastMouseY = e.touches[0].clientY;
worldOffsetXBeforeDrag = worldOffsetX;
worldOffsetYBeforeDrag = worldOffsetY;
} else {
// Single touch for drawing
isDrawing = true;
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
currentMouseX = x;
currentMouseY = y;
draw(x, y);
}
}
function handleTouchMove(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
if (isDragging && e.touches.length > 1) {
// Calculate how much the touch has moved
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
const dx = x - lastMouseX;
const dy = y - lastMouseY;
// Move the world in the opposite direction
moveWorld(-dx / PIXEL_SIZE, -dy / PIXEL_SIZE);
// Update the last touch position
lastMouseX = x;
lastMouseY = y;
} else if (isDrawing) {
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
currentMouseX = x;
currentMouseY = y;
draw(x, y);
}
}
function moveWorld(dx, dy) {
worldOffsetX += dx;
worldOffsetY += dy;
updateCoordinatesDisplay();
}
function updateCoordinatesDisplay() {
const chunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
const chunkY = Math.floor(worldOffsetY / CHUNK_SIZE);
document.getElementById('coords').textContent = `Chunk: ${chunkX},${chunkY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`;
}
function getChunkKey(chunkX, chunkY) {
return `${chunkX},${chunkY}`;
}
function getOrCreateChunk(chunkX, chunkY) {
const key = getChunkKey(chunkX, chunkY);
if (!chunks.has(key)) {
// Create a new chunk with empty pixels
const chunkData = new Array(CHUNK_SIZE * CHUNK_SIZE).fill(EMPTY);
// Add floor at the bottom of the world (y = 0)
if (chunkY === 0) {
// Fill the bottom row with walls
for (let x = 0; x < CHUNK_SIZE; x++) {
chunkData[0 * CHUNK_SIZE + x] = WALL;
}
}
chunks.set(key, chunkData);
}
return chunks.get(key);
}
function getChunkCoordinates(worldX, worldY) {
const chunkX = Math.floor(worldX / CHUNK_SIZE);
const chunkY = Math.floor(worldY / CHUNK_SIZE);
const localX = ((worldX % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const localY = ((worldY % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
return { chunkX, chunkY, localX, localY };
}
function setPixel(worldX, worldY, type) {
const { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY);
const chunk = getOrCreateChunk(chunkX, chunkY);
const index = localY * CHUNK_SIZE + localX;
chunk[index] = type;
}
function getPixel(worldX, worldY) {
// Special case: floor at the bottom of the world
if (worldY === 0) {
return WALL;
}
const { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY);
const key = getChunkKey(chunkX, chunkY);
if (!chunks.has(key)) {
return EMPTY;
}
const chunk = chunks.get(key);
const index = localY * CHUNK_SIZE + localX;
return chunk[index];
}
function simulationLoop(timestamp) {
// Calculate FPS
const deltaTime = timestamp - lastFrameTime;
lastFrameTime = timestamp;
fps = Math.round(1000 / deltaTime);
document.getElementById('fps').textContent = `FPS: ${fps}`;
// Update physics
updatePhysics();
// Render
render();
// Continue the loop
requestAnimationFrame(simulationLoop);
}
function updatePhysics() {
// Get visible chunks
const visibleChunks = getVisibleChunks();
// Process each visible chunk
for (const { chunkX, chunkY } of visibleChunks) {
const chunk = getOrCreateChunk(chunkX, chunkY);
// Process from bottom to top, right to left for correct gravity simulation
for (let y = CHUNK_SIZE - 1; y >= 0; y--) {
// Alternate direction each row for more natural flow
const startX = y % 2 === 0 ? 0 : CHUNK_SIZE - 1;
const endX = y % 2 === 0 ? CHUNK_SIZE : -1;
const step = y % 2 === 0 ? 1 : -1;
for (let x = startX; x !== endX; x += step) {
const index = y * CHUNK_SIZE + x;
const type = chunk[index];
if (type === EMPTY || type === STONE || type === WOOD) continue;
const worldX = chunkX * CHUNK_SIZE + x;
const worldY = chunkY * CHUNK_SIZE + y;
if (type === SAND) {
updateSand(worldX, worldY);
} else if (type === WATER) {
updateWater(worldX, worldY);
} else if (type === DIRT) {
updateDirt(worldX, worldY);
} else if (type === GRASS) {
updateGrass(worldX, worldY);
}
}
}
}
}
function updateSand(x, y) {
// Try to move down
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, SAND);
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, SAND);
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, SAND);
}
// Sand can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, SAND);
}
}
function updateDirt(x, y) {
// Try to move down
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, DIRT);
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, DIRT);
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, DIRT);
}
// Dirt can displace water
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, DIRT);
}
// Dirt can turn into grass if exposed to air above
if (getPixel(x, y - 1) === EMPTY && Math.random() < 0.001) {
setPixel(x, y, GRASS);
}
}
function updateGrass(x, y) {
// Grass behaves like dirt for physics
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, GRASS);
}
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, GRASS);
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, GRASS);
}
else if (getPixel(x, y + 1) === WATER) {
setPixel(x, y, WATER);
setPixel(x, y + 1, GRASS);
}
// Grass can spread to nearby dirt
if (Math.random() < 0.0005) {
const directions = [
{dx: -1, dy: 0}, {dx: 1, dy: 0},
{dx: 0, dy: -1}, {dx: 0, dy: 1}
];
const dir = directions[Math.floor(Math.random() * directions.length)];
if (getPixel(x + dir.dx, y + dir.dy) === DIRT) {
setPixel(x + dir.dx, y + dir.dy, GRASS);
}
}
// Grass dies if covered (no air above)
if (getPixel(x, y - 1) !== EMPTY && Math.random() < 0.05) {
setPixel(x, y, DIRT);
}
}
function updateWater(x, y) {
// Try to move down
if (getPixel(x, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x, y + 1, WATER);
}
// Try to move down-left or down-right
else if (getPixel(x - 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y + 1, WATER);
}
else if (getPixel(x + 1, y + 1) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y + 1, WATER);
}
// Try to spread horizontally
else {
let moved = false;
// Randomly choose direction first
const goLeft = Math.random() > 0.5;
if (goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER);
moved = true;
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moved = true;
}
// Try the other direction if first failed
else if (!goLeft && getPixel(x - 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x - 1, y, WATER);
moved = true;
} else if (goLeft && getPixel(x + 1, y) === EMPTY) {
setPixel(x, y, EMPTY);
setPixel(x + 1, y, WATER);
moved = true;
}
}
}
function getVisibleChunks() {
const visibleChunks = [];
// Calculate visible chunk range
const startChunkX = Math.floor(worldOffsetX / CHUNK_SIZE) - 1;
const endChunkX = Math.ceil((worldOffsetX + canvas.width / PIXEL_SIZE) / CHUNK_SIZE) + 1;
const startChunkY = Math.floor(worldOffsetY / CHUNK_SIZE) - 1;
const endChunkY = Math.ceil((worldOffsetY + canvas.height / PIXEL_SIZE) / CHUNK_SIZE) + 1;
for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) {
for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) {
visibleChunks.push({ chunkX, chunkY });
}
}
return visibleChunks;
}
function toggleDebug() {
debugMode = !debugMode;
document.getElementById('debug-btn').classList.toggle('active');
}
function render() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Get visible chunks
const visibleChunks = getVisibleChunks();
// Render each visible chunk
for (const { chunkX, chunkY } of visibleChunks) {
const key = getChunkKey(chunkX, chunkY);
if (!chunks.has(key)) continue;
const chunk = chunks.get(key);
// Calculate screen position of chunk
const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE;
const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE;
// Draw chunk border in debug mode
if (debugMode) {
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
ctx.strokeRect(
screenX,
screenY,
CHUNK_SIZE * PIXEL_SIZE,
CHUNK_SIZE * PIXEL_SIZE
);
// Draw chunk coordinates
ctx.fillStyle = '#ffffff';
ctx.font = '12px Arial';
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
}
// Render each pixel in the chunk
for (let y = 0; y < CHUNK_SIZE; y++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
const index = y * CHUNK_SIZE + x;
const type = chunk[index];
if (type === EMPTY) continue;
// Set color based on type
if (type === SAND) {
ctx.fillStyle = SAND_COLOR;
} else if (type === WATER) {
ctx.fillStyle = WATER_COLOR;
} else if (type === WALL) {
ctx.fillStyle = WALL_COLOR;
} else if (type === DIRT) {
ctx.fillStyle = DIRT_COLOR;
} else if (type === STONE) {
ctx.fillStyle = STONE_COLOR;
} else if (type === GRASS) {
ctx.fillStyle = GRASS_COLOR;
} else if (type === WOOD) {
ctx.fillStyle = WOOD_COLOR;
}
// Draw the pixel
ctx.fillRect(
screenX + x * PIXEL_SIZE,
screenY + y * PIXEL_SIZE,
PIXEL_SIZE,
PIXEL_SIZE
);
}
}
}
// Draw cursor position and update debug info
if (currentMouseX !== undefined && currentMouseY !== undefined) {
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
// Update coordinates display in debug mode
if (debugMode) {
document.getElementById('coords').textContent =
`Chunk: ${Math.floor(worldOffsetX / CHUNK_SIZE)},${Math.floor(worldOffsetY / CHUNK_SIZE)} | ` +
`Mouse: ${worldX},${worldY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`;
// Draw cursor outline
const cursorScreenX = (worldX - worldOffsetX) * PIXEL_SIZE;
const cursorScreenY = (worldY - worldOffsetY) * PIXEL_SIZE;
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
ctx.strokeRect(
cursorScreenX - PIXEL_SIZE,
cursorScreenY - PIXEL_SIZE,
PIXEL_SIZE * 3,
PIXEL_SIZE * 3
);
// Draw a dot at the exact mouse position
ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(currentMouseX, currentMouseY, 3, 0, Math.PI * 2);
ctx.fill();
}
}
}

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);
}