Compare commits
84 Commits
6ad564eea0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82f415f4f | ||
|
|
80c243ebf8 | ||
|
|
d765defa9d | ||
|
|
755be2f5a4 | ||
|
|
c5b7f2f224 | ||
|
|
bb1e25e753 | ||
|
|
d2ab5094ab | ||
|
|
583544840b | ||
|
|
cb62097150 | ||
|
|
8e2575f6fc | ||
|
|
04295f9f9f | ||
|
|
0c8e13d630 | ||
|
|
f592c74412 | ||
|
|
84e08b397d | ||
|
|
c853738bbf | ||
|
|
0a13dfc0a3 | ||
|
|
afba547fce | ||
|
|
8562c86986 | ||
|
|
ad90b9320f | ||
|
|
85a96f153f | ||
|
|
db5b49ee7f | ||
|
|
d86baa8f99 | ||
|
|
f0b00c3ccb | ||
|
|
724c5907a1 | ||
|
|
288c4a8772 | ||
|
|
03b192ae0a | ||
|
|
da895b11df | ||
|
|
853da1a61d | ||
|
|
d2a4927577 | ||
|
|
90650fefdd | ||
|
|
61ee259f6b | ||
|
|
5ac6d205ad | ||
|
|
f1a18f9168 | ||
|
|
f9cb363b37 | ||
|
|
cf64b6db48 | ||
|
|
20f5036848 | ||
|
|
34dd7e2d62 | ||
|
|
2067dad1d3 | ||
|
|
7cabd79d5f | ||
|
|
349d38c04c | ||
|
|
b5d1a643bd | ||
|
|
b7d12114d3 | ||
|
|
a948fab619 | ||
|
|
bcd61b7433 | ||
|
|
bdab2974c6 | ||
|
|
032793292f | ||
|
|
ba4fa3eb37 | ||
|
|
ebb96846ed | ||
|
|
60c2db558f | ||
|
|
bb45c9fba8 | ||
|
|
d87105baad | ||
|
|
883c3d9a08 | ||
|
|
15fb106246 | ||
|
|
60a1757ab1 | ||
|
|
1369822dc9 | ||
|
|
4c96226d05 | ||
|
|
c7735b8578 | ||
|
|
d34a695bb8 | ||
|
|
9c49af57cb | ||
|
|
206e9eb5cc | ||
|
|
908c819441 | ||
|
|
7aa0c1785b | ||
|
|
b85ae017b8 | ||
|
|
eab3a9b790 | ||
|
|
d5f46d94c3 | ||
|
|
ec7f8f9522 | ||
|
|
0b96238226 | ||
|
|
170b8d0a85 | ||
|
|
4716cbe692 | ||
|
|
1584600d0b | ||
|
|
7806b39a51 | ||
|
|
076f21f9ca | ||
|
|
a31f401378 | ||
|
|
c35f6cd448 | ||
|
|
8ef18f52ab | ||
|
|
a86acfff3a | ||
|
|
a5702a210f | ||
|
|
3f4f8bc09c | ||
|
|
9cdf7eba78 | ||
|
|
5ef436498f | ||
|
|
c858da4c3e | ||
|
|
d6d874ab99 | ||
|
|
0788b3067d | ||
|
|
d8e868aad8 |
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.aider*
|
||||||
26
index.html
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
265
js/elements/physics_objects.js
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 526 B |
BIN
sprites/farnel.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
sprites/pingwin.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/player.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
sprites/postac.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
sprites/purplin.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
sprites/rabbit.png
Normal file
|
After Width: | Height: | Size: 335 B |
66
styles.css
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||