feat: Enhance terrain generation with new types and drawing mechanics

This commit is contained in:
Kacper Kostka (aider) 2025-04-02 12:18:52 +02:00
parent 3fa8a695b3
commit a4ef5e0f40
5 changed files with 264 additions and 17 deletions

136
events.js
View File

@ -7,9 +7,12 @@ let lastMouseWorldY = null;
**********************************************************************/
function setupPanZoom() {
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
// Only start dragging if not in terrain drawing mode
if(!purchaseMode || !purchaseMode.startsWith('Draw')) {
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
}
});
canvas.addEventListener('mouseup', () => { isDragging = false; });
@ -118,6 +121,32 @@ function setupBuyButtons() {
logAction("Click on map to place a new Tree.");
});
// Terrain drawing buttons
document.getElementById('drawWaterBtn').addEventListener('click', () => {
purchaseMode = "DrawWater";
logAction("Click and drag on map to draw Water ($20).");
});
document.getElementById('drawGrassBtn').addEventListener('click', () => {
purchaseMode = "DrawGrass";
logAction("Click and drag on map to draw Grass ($10).");
});
document.getElementById('drawSandBtn').addEventListener('click', () => {
purchaseMode = "DrawSand";
logAction("Click and drag on map to draw Sand ($15).");
});
document.getElementById('drawDirtBtn').addEventListener('click', () => {
purchaseMode = "DrawDirt";
logAction("Click and drag on map to draw Dirt ($5).");
});
document.getElementById('drawStoneBtn').addEventListener('click', () => {
purchaseMode = "DrawStone";
logAction("Click and drag on map to draw Stone ($25).");
});
document.getElementById('toggleLogsBtn').addEventListener('click', (e) => {
if(logContainer.style.display === "none") {
logContainer.style.display = "block";
@ -129,9 +158,108 @@ function setupBuyButtons() {
});
}
// Variables for terrain drawing
let isDrawingTerrain = false;
let lastDrawX = null;
let lastDrawY = null;
let terrainDrawCost = 0;
let terrainDrawType = null;
let terrainDrawCount = 0;
function setupCanvasClick() {
// Mouse down for terrain drawing
canvas.addEventListener('mousedown', (e) => {
if(!purchaseMode || !purchaseMode.startsWith('Draw')) return;
const rect = canvas.getBoundingClientRect();
let cx = e.clientX - rect.left;
let cy = e.clientY - rect.top;
let worldX = (cx - (canvas.width/2) - offsetX) / scale;
let worldY = (cy - (canvas.height/2) - offsetY) / scale;
isDrawingTerrain = true;
lastDrawX = worldX;
lastDrawY = worldY;
terrainDrawCount = 0;
// Set terrain type and cost based on mode
switch(purchaseMode) {
case "DrawWater":
terrainDrawType = TERRAIN_WATER;
terrainDrawCost = COST_WATER;
break;
case "DrawGrass":
terrainDrawType = TERRAIN_GRASS;
terrainDrawCost = COST_GRASS;
break;
case "DrawSand":
terrainDrawType = TERRAIN_SAND;
terrainDrawCost = COST_SAND;
break;
case "DrawDirt":
terrainDrawType = TERRAIN_DIRT;
terrainDrawCost = COST_DIRT;
break;
case "DrawStone":
terrainDrawType = TERRAIN_STONE;
terrainDrawCost = COST_STONE;
break;
}
});
// Mouse move for terrain drawing
canvas.addEventListener('mousemove', (e) => {
if(isDrawingTerrain && terrainDrawType !== null) {
const rect = canvas.getBoundingClientRect();
let cx = e.clientX - rect.left;
let cy = e.clientY - rect.top;
let worldX = (cx - (canvas.width/2) - offsetX) / scale;
let worldY = (cy - (canvas.height/2) - offsetY) / scale;
// Only draw if moved enough distance
if(Math.abs(worldX - lastDrawX) > 5 || Math.abs(worldY - lastDrawY) > 5) {
// Check if we can afford it
if(money >= terrainDrawCost) {
// Draw a 3x3 area of terrain
for(let dx = -1; dx <= 1; dx++) {
for(let dy = -1; dy <= 1; dy++) {
setTerrainType(worldX + dx * 10, worldY + dy * 10, terrainDrawType);
}
}
addMoney(-terrainDrawCost, `Draw ${getTerrainName(terrainDrawType)}`);
terrainDrawCount++;
lastDrawX = worldX;
lastDrawY = worldY;
} else {
// Stop drawing if out of money
isDrawingTerrain = false;
logAction("Not enough money to continue drawing terrain!");
}
}
}
});
// Mouse up to stop terrain drawing
canvas.addEventListener('mouseup', () => {
if(isDrawingTerrain) {
isDrawingTerrain = false;
if(terrainDrawCount > 0) {
logAction(`Drew ${terrainDrawCount} patches of ${getTerrainName(terrainDrawType)}.`);
}
terrainDrawType = null;
}
});
// Mouse leave to stop terrain drawing
canvas.addEventListener('mouseleave', () => {
isDrawingTerrain = false;
terrainDrawType = null;
});
canvas.addEventListener('click', (e) => {
if(!purchaseMode) return;
if(!purchaseMode || purchaseMode.startsWith('Draw')) return;
const rect = canvas.getBoundingClientRect();
let cx = e.clientX - rect.left;
let cy = e.clientY - rect.top;

View File

@ -37,6 +37,13 @@ const COST_SPAWNER = 500;
const COST_TREE = 50;
const COST_SOLDIER = 250;
// Terrain costs
const COST_WATER = 20;
const COST_GRASS = 10;
const COST_SAND = 15;
const COST_DIRT = 5;
const COST_STONE = 25;
// Earn money
const REWARD_DELIVER_WOOD = 5;
const REWARD_BUILD_COMPLETE = 20;

View File

@ -201,10 +201,19 @@
<button class="menu-button" id="buyTreeBtn">Buy Tree ($50)</button>
</div>
<div class="menu-category">
<div class="category-title">Terrain</div>
<button class="menu-button" id="drawWaterBtn">Draw Water ($20)</button>
<button class="menu-button" id="drawGrassBtn">Draw Grass ($10)</button>
<button class="menu-button" id="drawSandBtn">Draw Sand ($15)</button>
<button class="menu-button" id="drawDirtBtn">Draw Dirt ($5)</button>
<button class="menu-button" id="drawStoneBtn">Draw Stone ($25)</button>
</div>
<button class="menu-button" id="toggleLogsBtn">Hide Logs</button>
</div>
<canvas id="worldCanvas" width="800" height="600"></canvas>
<canvas id="worldCanvas" width="1200" height="800"></canvas>
<div id="log"></div>
</div>

View File

@ -87,14 +87,31 @@ function drawTerrain() {
for (let x = startX; x <= endX; x += cellSize) {
for (let y = startY; y <= endY; y += cellSize) {
// Check if this position is water
if (isWater(x, y)) {
ctx.fillStyle = "rgba(0, 100, 255, 0.5)"; // Blue for water
ctx.fillRect(x, y, cellSize, cellSize);
} else {
ctx.fillStyle = "rgba(100, 200, 100, 0.3)"; // Green for grass
ctx.fillRect(x, y, cellSize, cellSize);
// Get terrain type at this position
const terrainType = getTerrainType(x, y);
// Set color based on terrain type
switch(terrainType) {
case TERRAIN_WATER:
ctx.fillStyle = "rgba(0, 100, 255, 0.5)"; // Blue for water
break;
case TERRAIN_GRASS:
ctx.fillStyle = "rgba(100, 200, 100, 0.3)"; // Green for grass
break;
case TERRAIN_SAND:
ctx.fillStyle = "rgba(240, 230, 140, 0.5)"; // Khaki for sand
break;
case TERRAIN_DIRT:
ctx.fillStyle = "rgba(139, 69, 19, 0.3)"; // Brown for dirt
break;
case TERRAIN_STONE:
ctx.fillStyle = "rgba(128, 128, 128, 0.4)"; // Gray for stone
break;
default:
ctx.fillStyle = "rgba(100, 200, 100, 0.3)"; // Default to grass
}
ctx.fillRect(x, y, cellSize, cellSize);
}
}

View File

@ -90,6 +90,9 @@ class Perlin {
// Terrain types
const TERRAIN_WATER = 0;
const TERRAIN_GRASS = 1;
const TERRAIN_SAND = 2;
const TERRAIN_DIRT = 3;
const TERRAIN_STONE = 4;
// Generate terrain map
function generateTerrain(width, height, scale) {
@ -108,30 +111,113 @@ function generateTerrain(width, height, scale) {
value += 0.25 * perlin.noise(nx * 4, ny * 4);
value /= 1.75; // Normalize
// Generate a second noise value for stone distribution
let stoneNoise = perlin.noise(nx * 3, ny * 3);
// Determine terrain type based on noise value
// Lower water threshold to create more land areas
if (value < -0.3) { // Changed from 0.0 to -0.3 to reduce water
// Adjusted to get more water
if (value < -0.2) { // More water
terrain[x][y] = TERRAIN_WATER;
} else {
// Check for sand near water (beach)
let sandCheck = perlin.noise((nx + 0.1) * 8, (ny + 0.1) * 8);
if (value > -0.25 && sandCheck > 0) {
terrain[x][y] = TERRAIN_SAND;
}
} else if (value < 0.0) {
// Sand appears near water
terrain[x][y] = TERRAIN_SAND;
} else if (value < 0.3) {
// Grass in middle elevations
terrain[x][y] = TERRAIN_GRASS;
} else if (stoneNoise > 0.3) {
// Stone in higher elevations with specific noise pattern
terrain[x][y] = TERRAIN_STONE;
} else {
// Dirt in higher elevations
terrain[x][y] = TERRAIN_DIRT;
}
}
}
// Second pass to smooth terrain and create better beaches
smoothTerrain(terrain, width, height);
return terrain;
}
function smoothTerrain(terrain, width, height) {
// Create sand around water
for (let x = 1; x < width - 1; x++) {
for (let y = 1; y < height - 1; y++) {
if (terrain[x][y] !== TERRAIN_WATER) {
// Check if adjacent to water
let adjacentToWater = false;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (x + dx >= 0 && x + dx < width && y + dy >= 0 && y + dy < height) {
if (terrain[x + dx][y + dy] === TERRAIN_WATER) {
adjacentToWater = true;
break;
}
}
}
if (adjacentToWater) break;
}
// If adjacent to water and not already sand, make it sand
if (adjacentToWater && terrain[x][y] !== TERRAIN_SAND && Math.random() > 0.3) {
terrain[x][y] = TERRAIN_SAND;
}
}
}
}
}
// Check if a position is in water
function isWater(x, y) {
return getTerrainType(x, y) === TERRAIN_WATER;
}
// Get terrain type at a position
function getTerrainType(x, y) {
// Convert world coordinates to terrain grid coordinates
const gridX = Math.floor((x + 2000) / 10);
const gridY = Math.floor((y + 2000) / 10);
// Check bounds
if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) {
return false; // Default to land if out of bounds
return TERRAIN_GRASS; // Default to grass if out of bounds
}
return terrainMap[gridX][gridY] === TERRAIN_WATER;
return terrainMap[gridX][gridY];
}
// Set terrain type at a position
function setTerrainType(x, y, type) {
// Convert world coordinates to terrain grid coordinates
const gridX = Math.floor((x + 2000) / 10);
const gridY = Math.floor((y + 2000) / 10);
// Check bounds
if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) {
return false; // Can't set if out of bounds
}
terrainMap[gridX][gridY] = type;
return true;
}
// Get terrain name from type
function getTerrainName(type) {
switch(type) {
case TERRAIN_WATER: return "Water";
case TERRAIN_GRASS: return "Grass";
case TERRAIN_SAND: return "Sand";
case TERRAIN_DIRT: return "Dirt";
case TERRAIN_STONE: return "Stone";
default: return "Unknown";
}
}
// Terrain dimensions