feat: Prevent placing entities on water and improve land generation

This commit is contained in:
Kacper Kostka (aider) 2025-04-02 12:14:34 +02:00
parent cde5301c2b
commit 3fa8a695b3
6 changed files with 113 additions and 13 deletions

17
ai.js
View File

@ -648,8 +648,21 @@ function deliverFruitTask(cit) {
} }
function plantFruitTreeTask(cit) { function plantFruitTreeTask(cit) {
let px = cit.x + randInt(-50, 50); // Try to find a valid land position nearby
let py = cit.y + randInt(-50, 50); let px, py;
let attempts = 0;
do {
px = cit.x + randInt(-50, 50);
py = cit.y + randInt(-50, 50);
attempts++;
// Give up after too many attempts and find a new task
if (attempts > 20) {
cit.task = null;
cit.target = null;
return;
}
} while (!isValidPlacement(px, py));
moveToward(cit, px, py, 0.4); moveToward(cit, px, py, 0.4);
if(distance(cit.x, cit.y, px, py) < 10) { if(distance(cit.x, cit.y, px, py) < 10) {
if(cit.carryingFruit >= FRUIT_PLANT_COST) { if(cit.carryingFruit >= FRUIT_PLANT_COST) {

View File

@ -1,3 +1,7 @@
// Track mouse position for placement indicator
let lastMouseWorldX = null;
let lastMouseWorldY = null;
/********************************************************************** /**********************************************************************
* PAN & ZOOM * PAN & ZOOM
**********************************************************************/ **********************************************************************/
@ -20,6 +24,13 @@ function setupPanZoom() {
lastMouseX = e.clientX; lastMouseX = e.clientX;
lastMouseY = e.clientY; lastMouseY = e.clientY;
} }
// Update world coordinates for placement indicator
const rect = canvas.getBoundingClientRect();
let cx = e.clientX - rect.left;
let cy = e.clientY - rect.top;
lastMouseWorldX = (cx - (canvas.width/2) - offsetX) / scale;
lastMouseWorldY = (cy - (canvas.height/2) - offsetY) / scale;
}); });
canvas.addEventListener('wheel', (e) => { canvas.addEventListener('wheel', (e) => {
@ -127,6 +138,12 @@ function setupCanvasClick() {
let worldX = (cx - (canvas.width/2) - offsetX) / scale; let worldX = (cx - (canvas.width/2) - offsetX) / scale;
let worldY = (cy - (canvas.height/2) - offsetY) / scale; let worldY = (cy - (canvas.height/2) - offsetY) / scale;
// Check if the placement is valid (not in water)
if (!isValidPlacement(worldX, worldY)) {
logAction("Cannot build on water! Choose a land location.");
return;
}
switch(purchaseMode) { switch(purchaseMode) {
case "House": case "House":
if(money >= COST_HOUSE) { if(money >= COST_HOUSE) {

44
game.js
View File

@ -97,27 +97,53 @@ const WOLF_SPEED = 0.7; // Faster than rabbits
* INIT WORLD * INIT WORLD
**********************************************************************/ **********************************************************************/
function initWorld() { function initWorld() {
// Normal trees // Normal trees - only on land
for(let i=0; i<15; i++) { for(let i=0; i<15; i++) {
resources.push(createResource("Tree", randInt(-1000,1000), randInt(-1000,1000), 100)); let x, y;
do {
x = randInt(-1000,1000);
y = randInt(-1000,1000);
} while (!isValidPlacement(x, y));
resources.push(createResource("Tree", x, y, 100));
} }
// Fruit trees
// Fruit trees - only on land
for(let i=0; i<10; i++) { for(let i=0; i<10; i++) {
resources.push(createResource("FruitTree", randInt(-1000,1000), randInt(-1000,1000), FRUIT_TREE_START_AMOUNT)); let x, y;
do {
x = randInt(-1000,1000);
y = randInt(-1000,1000);
} while (!isValidPlacement(x, y));
resources.push(createResource("FruitTree", x, y, FRUIT_TREE_START_AMOUNT));
} }
// Start with 1 citizen => always a "Builder" // Start with 1 citizen => always a "Builder" - only on land
let c = createCitizen(randomName(), randInt(-200,200), randInt(-200,200), "Builder"); let cx, cy;
do {
cx = randInt(-200,200);
cy = randInt(-200,200);
} while (!isValidPlacement(cx, cy));
let c = createCitizen(randomName(), cx, cy, "Builder");
citizens.push(c); citizens.push(c);
logAction(`Initial Citizen joined: ${c.name} [Builder]`); logAction(`Initial Citizen joined: ${c.name} [Builder]`);
// Spawn some animals // Spawn some animals - only on land
for(let i=0; i<STARTING_RABBITS; i++) { for(let i=0; i<STARTING_RABBITS; i++) {
animals.push(createAnimal("Rabbit", randInt(-1000,1000), randInt(-1000,1000))); let x, y;
do {
x = randInt(-1000,1000);
y = randInt(-1000,1000);
} while (!isValidPlacement(x, y));
animals.push(createAnimal("Rabbit", x, y));
} }
for(let i=0; i<STARTING_WOLVES; i++) { for(let i=0; i<STARTING_WOLVES; i++) {
animals.push(createAnimal("Wolf", randInt(-1000,1000), randInt(-1000,1000))); let x, y;
do {
x = randInt(-1000,1000);
y = randInt(-1000,1000);
} while (!isValidPlacement(x, y));
animals.push(createAnimal("Wolf", x, y));
} }
requestAnimationFrame(update); requestAnimationFrame(update);

View File

@ -31,6 +31,42 @@ function drawWorld() {
animals.forEach((ani) => { animals.forEach((ani) => {
if(!ani.dead) drawAnimal(ani); if(!ani.dead) drawAnimal(ani);
}); });
// Draw placement indicator when in purchase mode
if (purchaseMode) {
drawPlacementIndicator();
}
}
// Draw a placement indicator at mouse position
function drawPlacementIndicator() {
if (!lastMouseWorldX || !lastMouseWorldY) return;
ctx.save();
ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY);
ctx.scale(scale, scale);
const isValid = isValidPlacement(lastMouseWorldX, lastMouseWorldY);
// Draw indicator circle
ctx.beginPath();
ctx.arc(lastMouseWorldX, lastMouseWorldY, 15, 0, Math.PI*2);
ctx.strokeStyle = isValid ? "rgba(0, 255, 0, 0.7)" : "rgba(255, 0, 0, 0.7)";
ctx.lineWidth = 2/scale;
ctx.stroke();
// Draw X if invalid
if (!isValid) {
ctx.beginPath();
ctx.moveTo(lastMouseWorldX - 10, lastMouseWorldY - 10);
ctx.lineTo(lastMouseWorldX + 10, lastMouseWorldY + 10);
ctx.moveTo(lastMouseWorldX + 10, lastMouseWorldY - 10);
ctx.lineTo(lastMouseWorldX - 10, lastMouseWorldY + 10);
ctx.strokeStyle = "rgba(255, 0, 0, 0.7)";
ctx.stroke();
}
ctx.restore();
} }
function drawTerrain() { function drawTerrain() {

View File

@ -109,8 +109,8 @@ function generateTerrain(width, height, scale) {
value /= 1.75; // Normalize value /= 1.75; // Normalize
// Determine terrain type based on noise value // Determine terrain type based on noise value
// Increase water threshold to create more water areas // Lower water threshold to create more land areas
if (value < 0.0) { if (value < -0.3) { // Changed from 0.0 to -0.3 to reduce water
terrain[x][y] = TERRAIN_WATER; terrain[x][y] = TERRAIN_WATER;
} else { } else {
terrain[x][y] = TERRAIN_GRASS; terrain[x][y] = TERRAIN_GRASS;

View File

@ -90,6 +90,14 @@ function knockback(ent, tx, ty, dist) {
} }
} }
/**********************************************************************
* PLACEMENT VALIDATION
**********************************************************************/
// Check if a position is valid for placement (not in water)
function isValidPlacement(x, y) {
return !isWater(x, y);
}
/********************************************************************** /**********************************************************************
* FINDERS * FINDERS
**********************************************************************/ **********************************************************************/