refactor: Spread game code into modular files for better maintainability
This commit reorganizes the game's source code into multiple files within a `js/` directory, creating a more modular and maintainable structure. The changes include: - Created separate files for different game components: - `constants.js`: Game constants and element types - `world.js`: World management functions - `terrain.js`: Terrain generation logic - `physics.js`: Physics simulation - `render.js`: Rendering functions - `input.js`: Input handling - `main.js`: Main game initialization and loop - Element-specific files in `js/elements/`: - `basic.js`: Sand, water, dirt behaviors - `plants.js`: Grass, seeds, flowers - `trees.js`: Tree growth and leaf generation - `fire.js`: Fire and lava behaviors - Updated `index.html` to load modules in the correct order - Removed the monolithic `script.js` The modular approach improves code readability, makes future extensions easier, and separates concerns more effectively.
This commit is contained in:
parent
a5702a210f
commit
a86acfff3a
13
index.html
13
index.html
@ -37,6 +37,17 @@
|
||||
</div>
|
||||
<canvas id="simulation-canvas"></canvas>
|
||||
</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/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>
|
||||
</html>
|
||||
|
39
js/constants.js
Normal file
39
js/constants.js
Normal file
@ -0,0 +1,39 @@
|
||||
// Game constants
|
||||
const CHUNK_SIZE = 200;
|
||||
const PIXEL_SIZE = 4;
|
||||
const GRAVITY = 0.5;
|
||||
const WATER_SPREAD = 3;
|
||||
|
||||
// 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'];
|
||||
|
||||
// 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;
|
||||
|
||||
// Flammable materials
|
||||
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];
|
130
js/elements/basic.js
Normal file
130
js/elements/basic.js
Normal file
@ -0,0 +1,130 @@
|
||||
// Basic element behaviors (sand, water, dirt)
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
} 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' });
|
||||
} else if (seedRoll < 0.00026) { // Tree seed (rare)
|
||||
setPixel(x, y - 1, TREE_SEED);
|
||||
}
|
||||
}
|
||||
}
|
166
js/elements/fire.js
Normal file
166
js/elements/fire.js
Normal file
@ -0,0 +1,166 @@
|
||||
// Fire and lava element behaviors
|
||||
let fireUpdateCounter = 0;
|
||||
|
||||
function updateFire(x, y) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Decrease lifetime
|
||||
metadata.lifetime--;
|
||||
|
||||
// Randomly change color for flickering effect
|
||||
if (Math.random() < 0.2) {
|
||||
metadata.colorIndex = Math.floor(Math.random() * FIRE_COLORS.length);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fire burns out after its lifetime
|
||||
if (metadata.lifetime <= 0) {
|
||||
setPixel(x, y, EMPTY);
|
||||
removeMetadata(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLava(x, y) {
|
||||
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)
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
} else if (!goLeft && getPixel(x + 1, y) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x + 1, y, LAVA);
|
||||
moveMetadata(x, y, x + 1, y);
|
||||
}
|
||||
// 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);
|
||||
} else if (goLeft && getPixel(x + 1, y) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x + 1, y, LAVA);
|
||||
moveMetadata(x, y, x + 1, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
});
|
||||
}
|
||||
|
||||
// Lava can melt sand into glass (stone)
|
||||
else if (nearbyType === SAND && Math.random() < 0.05) {
|
||||
setPixel(x + dir.dx, y + dir.dy, STONE);
|
||||
}
|
||||
|
||||
// Lava can burn dirt
|
||||
else if (nearbyType === DIRT && Math.random() < 0.02) {
|
||||
setPixel(x + dir.dx, y + dir.dy, EMPTY);
|
||||
}
|
||||
}
|
||||
}
|
159
js/elements/plants.js
Normal file
159
js/elements/plants.js
Normal file
@ -0,0 +1,159 @@
|
||||
// Plant element behaviors (grass, seeds, trees)
|
||||
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 updateSeed(x, y) {
|
||||
// Seeds fall like sand
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, SEED);
|
||||
moveMetadata(x, y, x, y + 1);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
// Seeds can float on water
|
||||
else if (getPixel(x, y + 1) === WATER) {
|
||||
// Just float, don't do anything
|
||||
}
|
||||
// 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
|
||||
});
|
||||
} else {
|
||||
// Regular seed becomes a grass blade
|
||||
setPixel(x, y, GRASS_BLADE);
|
||||
setMetadata(x, y, { age: 0, height: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
}
|
||||
}
|
108
js/elements/trees.js
Normal file
108
js/elements/trees.js
Normal file
@ -0,0 +1,108 @@
|
||||
// Tree element behaviors
|
||||
function updateTreeSeed(x, y) {
|
||||
// Tree seeds fall like other seeds
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, TREE_SEED);
|
||||
moveMetadata(x, y, x, y + 1);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
// Seeds can float on water
|
||||
else if (getPixel(x, y + 1) === WATER) {
|
||||
// Just float, don't do anything
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Grow the trunk upward
|
||||
for (let i = 1; i < treeHeight; i++) {
|
||||
if (getPixel(x, y - i) === EMPTY) {
|
||||
setPixel(x, y - i, WOOD);
|
||||
} 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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
188
js/input.js
Normal file
188
js/input.js
Normal file
@ -0,0 +1,188 @@
|
||||
// Input handling functions
|
||||
let isDrawing = false;
|
||||
let isDragging = false;
|
||||
let lastMouseX, lastMouseY;
|
||||
let currentMouseX, currentMouseY;
|
||||
|
||||
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 === 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++) {
|
||||
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)
|
||||
});
|
||||
}
|
||||
} 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');
|
||||
}
|
76
js/main.js
Normal file
76
js/main.js
Normal file
@ -0,0 +1,76 @@
|
||||
// Main game variables and initialization
|
||||
let canvas, ctx;
|
||||
let currentTool = SAND;
|
||||
let lastFrameTime = 0;
|
||||
let fps = 0;
|
||||
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('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('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 and generate terrain around it
|
||||
getOrCreateChunk(0, 0);
|
||||
generateChunksAroundPlayer();
|
||||
|
||||
// Start the simulation loop
|
||||
requestAnimationFrame(simulationLoop);
|
||||
};
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
53
js/physics.js
Normal file
53
js/physics.js
Normal file
@ -0,0 +1,53 @@
|
||||
// Physics simulation functions
|
||||
function updatePhysics() {
|
||||
// Get visible chunks
|
||||
const visibleChunks = getVisibleChunks();
|
||||
|
||||
// Increment fire update counter
|
||||
fireUpdateCounter++;
|
||||
|
||||
// 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 || type === LEAF) 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);
|
||||
} else if (type === SEED) {
|
||||
updateSeed(worldX, worldY);
|
||||
} else if (type === GRASS_BLADE) {
|
||||
updateGrassBlade(worldX, worldY);
|
||||
} else if (type === FLOWER) {
|
||||
updateFlower(worldX, worldY);
|
||||
} else if (type === TREE_SEED) {
|
||||
updateTreeSeed(worldX, worldY);
|
||||
} else if (type === FIRE) {
|
||||
updateFire(worldX, worldY);
|
||||
} else if (type === LAVA) {
|
||||
updateLava(worldX, worldY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
js/render.js
Normal file
127
js/render.js
Normal file
@ -0,0 +1,127 @@
|
||||
// Rendering functions
|
||||
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;
|
||||
} else if (type === SEED) {
|
||||
ctx.fillStyle = SEED_COLOR;
|
||||
} else if (type === GRASS_BLADE) {
|
||||
ctx.fillStyle = GRASS_COLOR;
|
||||
} else if (type === FLOWER) {
|
||||
// Get flower color from metadata or use a default
|
||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||
ctx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0];
|
||||
} else if (type === TREE_SEED) {
|
||||
ctx.fillStyle = SEED_COLOR;
|
||||
} else if (type === LEAF) {
|
||||
ctx.fillStyle = LEAF_COLOR;
|
||||
} else if (type === FIRE) {
|
||||
// Get fire color from metadata
|
||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||
const colorIndex = metadata ? metadata.colorIndex : 0;
|
||||
ctx.fillStyle = FIRE_COLORS[colorIndex];
|
||||
} else if (type === LAVA) {
|
||||
// Get lava color from metadata
|
||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||
const colorIndex = metadata ? metadata.colorIndex : 0;
|
||||
ctx.fillStyle = LAVA_COLORS[colorIndex];
|
||||
}
|
||||
|
||||
// Draw the pixel
|
||||
ctx.fillRect(
|
||||
screenX + x * PIXEL_SIZE,
|
||||
screenY + y * PIXEL_SIZE,
|
||||
PIXEL_SIZE,
|
||||
PIXEL_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
176
js/terrain.js
Normal file
176
js/terrain.js
Normal file
@ -0,0 +1,176 @@
|
||||
// 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;
|
||||
}
|
||||
// Next few layers are dirt
|
||||
else if (depth < 5) {
|
||||
chunkData[index] = DIRT;
|
||||
}
|
||||
// Deeper layers are stone
|
||||
else {
|
||||
chunkData[index] = STONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
js/world.js
Normal file
128
js/world.js
Normal file
@ -0,0 +1,128 @@
|
||||
// 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
|
||||
|
||||
function moveWorld(dx, dy) {
|
||||
worldOffsetX += dx;
|
||||
worldOffsetY += dy;
|
||||
updateCoordinatesDisplay();
|
||||
|
||||
// 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);
|
||||
|
||||
// Add floor at the bottom of the world (y = 0 and y = 1)
|
||||
if (chunkY === 0 || chunkY === 1) {
|
||||
// Fill the bottom row with walls
|
||||
for (let x = 0; x < CHUNK_SIZE; x++) {
|
||||
chunkData[(CHUNK_SIZE - 1) * 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 (first two chunks)
|
||||
const floorChunkY = Math.floor(worldY / CHUNK_SIZE);
|
||||
if (worldY % CHUNK_SIZE === CHUNK_SIZE - 1 && (floorChunkY === 0 || floorChunkY === 1)) {
|
||||
return WALL;
|
||||
}
|
||||
|
||||
const { chunkX, chunkY, localX, localY } = getChunkCoordinates(worldX, worldY);
|
||||
const key = getChunkKey(chunkX, chunkY);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user