feat: Add Planner citizen type with automated city expansion
This commit is contained in:
parent
3e4622fa0e
commit
18ac996f65
175
ai.js
175
ai.js
@ -263,6 +263,9 @@ function assignNewTask(cit) {
|
|||||||
case "Soldier":
|
case "Soldier":
|
||||||
soldierTasks(cit);
|
soldierTasks(cit);
|
||||||
break;
|
break;
|
||||||
|
case "Planner":
|
||||||
|
plannerTasks(cit);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
cit.task = null;
|
cit.task = null;
|
||||||
cit.target = null;
|
cit.target = null;
|
||||||
@ -385,9 +388,181 @@ function soldierTasks(cit) {
|
|||||||
builderTasks(cit);
|
builderTasks(cit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function plannerTasks(cit) {
|
||||||
|
// Check if it's time to plan a new building
|
||||||
|
if(Math.random() < 0.05) {
|
||||||
|
cit.task = "planBuilding";
|
||||||
|
cit.target = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help with other tasks when not planning
|
||||||
|
if(Math.random() < 0.5) {
|
||||||
|
builderTasks(cit);
|
||||||
|
} else {
|
||||||
|
// Explore the map to find good building locations
|
||||||
|
cit.task = "explore";
|
||||||
|
cit.target = {
|
||||||
|
x: randInt(-500, 500),
|
||||||
|
y: randInt(-500, 500)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**********************************************************************
|
/**********************************************************************
|
||||||
* NEW TASK HANDLERS
|
* NEW TASK HANDLERS
|
||||||
**********************************************************************/
|
**********************************************************************/
|
||||||
|
function planBuildingTask(cit) {
|
||||||
|
// Find a good spot to place a new building
|
||||||
|
let buildingType = null;
|
||||||
|
let buildingCost = 0;
|
||||||
|
|
||||||
|
// Decide what type of building to place based on current needs
|
||||||
|
const houseCount = buildings.filter(b => b.buildingType === "House" && b.completed).length;
|
||||||
|
const marketCount = buildings.filter(b => b.buildingType === "Market" && b.completed).length;
|
||||||
|
const hospitalCount = buildings.filter(b => b.buildingType === "Hospital" && b.completed).length;
|
||||||
|
const schoolCount = buildings.filter(b => b.buildingType === "School" && b.completed).length;
|
||||||
|
|
||||||
|
// Prioritize based on what the city needs
|
||||||
|
if (houseCount < 3 || Math.random() < 0.5) {
|
||||||
|
buildingType = "House";
|
||||||
|
buildingCost = COST_HOUSE;
|
||||||
|
} else if (marketCount < 1 && money >= COST_MARKET) {
|
||||||
|
buildingType = "Market";
|
||||||
|
buildingCost = COST_MARKET;
|
||||||
|
} else if (hospitalCount < 1 && money >= COST_HOSPITAL) {
|
||||||
|
buildingType = "Hospital";
|
||||||
|
buildingCost = COST_HOSPITAL;
|
||||||
|
} else if (schoolCount < 1 && money >= COST_SCHOOL) {
|
||||||
|
buildingType = "School";
|
||||||
|
buildingCost = COST_SCHOOL;
|
||||||
|
} else if (Math.random() < 0.3) {
|
||||||
|
buildingType = "Road";
|
||||||
|
buildingCost = COST_ROAD;
|
||||||
|
} else {
|
||||||
|
buildingType = "House";
|
||||||
|
buildingCost = COST_HOUSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can afford it
|
||||||
|
if (money < buildingCost) {
|
||||||
|
cit.task = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a good location - away from other buildings but not too far from city center
|
||||||
|
let bestX = 0, bestY = 0;
|
||||||
|
let bestScore = -Infinity;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 10; attempt++) {
|
||||||
|
// Try to find locations expanding outward from existing buildings
|
||||||
|
let existingBuilding = buildings[Math.floor(Math.random() * buildings.length)];
|
||||||
|
if (!existingBuilding) {
|
||||||
|
// If no buildings yet, start near center
|
||||||
|
existingBuilding = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = Math.random() * Math.PI * 2;
|
||||||
|
const distance = randInt(100, 300);
|
||||||
|
const testX = existingBuilding.x + Math.cos(direction) * distance;
|
||||||
|
const testY = existingBuilding.y + Math.sin(direction) * distance;
|
||||||
|
|
||||||
|
// Skip if not valid placement
|
||||||
|
if (!isValidPlacement(testX, testY)) continue;
|
||||||
|
|
||||||
|
// Calculate score based on:
|
||||||
|
// - Distance from other buildings (not too close)
|
||||||
|
// - Distance from center (not too far)
|
||||||
|
// - Terrain type
|
||||||
|
|
||||||
|
let minDistToBuilding = Infinity;
|
||||||
|
buildings.forEach(b => {
|
||||||
|
const dist = distance(testX, testY, b.x, b.y);
|
||||||
|
if (dist < minDistToBuilding) minDistToBuilding = dist;
|
||||||
|
});
|
||||||
|
|
||||||
|
const distToCenter = distance(testX, testY, 0, 0);
|
||||||
|
|
||||||
|
// Score calculation - higher is better
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Prefer some distance from other buildings
|
||||||
|
if (minDistToBuilding < 50) score -= 100;
|
||||||
|
else if (minDistToBuilding < 100) score += minDistToBuilding - 50;
|
||||||
|
else score += 50;
|
||||||
|
|
||||||
|
// Prefer not too far from center
|
||||||
|
score -= distToCenter * 0.1;
|
||||||
|
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestX = testX;
|
||||||
|
bestY = testY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a good spot, place the building
|
||||||
|
if (bestScore > -Infinity) {
|
||||||
|
addMoney(-buildingCost, `Planner: ${buildingType}`);
|
||||||
|
|
||||||
|
let newBuilding;
|
||||||
|
switch (buildingType) {
|
||||||
|
case "House":
|
||||||
|
newBuilding = createHouseSite(bestX, bestY);
|
||||||
|
break;
|
||||||
|
case "Road":
|
||||||
|
// Find nearest building to connect to
|
||||||
|
let nearest = null;
|
||||||
|
let minDist = Infinity;
|
||||||
|
buildings.forEach(b => {
|
||||||
|
if (b.buildingType !== "Road") {
|
||||||
|
const dist = distance(bestX, bestY, b.x, b.y);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nearest) {
|
||||||
|
newBuilding = createRoadSite(bestX, bestY, nearest.x, nearest.y);
|
||||||
|
} else {
|
||||||
|
newBuilding = createRoadSite(bestX - 50, bestY, bestX + 50, bestY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Market":
|
||||||
|
newBuilding = createMarketSite(bestX, bestY);
|
||||||
|
break;
|
||||||
|
case "Hospital":
|
||||||
|
newBuilding = createHospitalSite(bestX, bestY);
|
||||||
|
break;
|
||||||
|
case "School":
|
||||||
|
newBuilding = createSchoolSite(bestX, bestY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildings.push(newBuilding);
|
||||||
|
logAction(`${cit.name} [Planner] placed a new ${buildingType} site at (${Math.floor(bestX)}, ${Math.floor(bestY)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cit.task = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exploreTask(cit) {
|
||||||
|
if (!cit.target) {
|
||||||
|
cit.task = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToward(cit, cit.target.x, cit.target.y, 0.5);
|
||||||
|
|
||||||
|
// When reached exploration point, find a new task
|
||||||
|
if (distance(cit.x, cit.y, cit.target.x, cit.target.y) < 10) {
|
||||||
|
cit.task = null;
|
||||||
|
cit.target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function huntWolfTask(cit) {
|
function huntWolfTask(cit) {
|
||||||
let wolf = cit.target;
|
let wolf = cit.target;
|
||||||
if(!wolf || wolf.dead) {
|
if(!wolf || wolf.dead) {
|
||||||
|
16
events.js
16
events.js
@ -96,6 +96,11 @@ function setupBuyButtons() {
|
|||||||
logAction("Click on map to place a new Soldier citizen.");
|
logAction("Click on map to place a new Soldier citizen.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('buyPlannerBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "Planner";
|
||||||
|
logAction("Click on map to place a new Planner citizen.");
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('buyMarketBtn').addEventListener('click', () => {
|
document.getElementById('buyMarketBtn').addEventListener('click', () => {
|
||||||
purchaseMode = "Market";
|
purchaseMode = "Market";
|
||||||
logAction("Click on map to place a Market site.");
|
logAction("Click on map to place a Market site.");
|
||||||
@ -395,6 +400,17 @@ function setupCanvasClick() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "Planner":
|
||||||
|
if(money >= COST_PLANNER) {
|
||||||
|
addMoney(-COST_PLANNER, "Buy Planner");
|
||||||
|
let c = createCitizen(randomName(), worldX, worldY, "Planner");
|
||||||
|
citizens.push(c);
|
||||||
|
logAction(`Purchased new Planner @(${Math.floor(worldX)},${Math.floor(worldY)})`);
|
||||||
|
} else {
|
||||||
|
logAction("Not enough money to buy Planner!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "Spawner":
|
case "Spawner":
|
||||||
if(money >= COST_SPAWNER) {
|
if(money >= COST_SPAWNER) {
|
||||||
addMoney(-COST_SPAWNER, "Buy Spawner");
|
addMoney(-COST_SPAWNER, "Buy Spawner");
|
||||||
|
19
game.js
19
game.js
@ -36,6 +36,7 @@ const COST_SCHOOL = 450;
|
|||||||
const COST_SPAWNER = 500;
|
const COST_SPAWNER = 500;
|
||||||
const COST_TREE = 50;
|
const COST_TREE = 50;
|
||||||
const COST_SOLDIER = 250;
|
const COST_SOLDIER = 250;
|
||||||
|
const COST_PLANNER = 1000;
|
||||||
|
|
||||||
// Terrain costs
|
// Terrain costs
|
||||||
const COST_WATER = 20;
|
const COST_WATER = 20;
|
||||||
@ -90,14 +91,14 @@ const SCHOOL_WOOD_REQUIRED = 65;
|
|||||||
const SCHOOL_BUILD_RATE = 0.12;
|
const SCHOOL_BUILD_RATE = 0.12;
|
||||||
|
|
||||||
// Professions
|
// Professions
|
||||||
const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher", "Soldier"];
|
const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher", "Soldier", "Planner"];
|
||||||
|
|
||||||
// Animals
|
// Animals
|
||||||
const STARTING_RABBITS = 10;
|
const STARTING_RABBITS = 10;
|
||||||
const STARTING_WOLVES = 3;
|
const STARTING_WOLVES = 5;
|
||||||
const RABBIT_HUNGER_INCREMENT = 0.003;
|
const RABBIT_HUNGER_INCREMENT = 0.003;
|
||||||
const RABBIT_REPRO_COOLDOWN = 3000;
|
const RABBIT_REPRO_COOLDOWN = 5000;
|
||||||
const RABBIT_REPRO_CHANCE = 0.0005;
|
const RABBIT_REPRO_CHANCE = 0.0002;
|
||||||
const WOLF_SPEED = 0.7; // Faster than rabbits
|
const WOLF_SPEED = 0.7; // Faster than rabbits
|
||||||
|
|
||||||
/**********************************************************************
|
/**********************************************************************
|
||||||
@ -187,6 +188,16 @@ function initWorld() {
|
|||||||
|
|
||||||
// Spawn wolves at the corners of the map
|
// Spawn wolves at the corners of the map
|
||||||
spawnWolvesAtCorners();
|
spawnWolvesAtCorners();
|
||||||
|
|
||||||
|
// Spawn additional wolves in random locations
|
||||||
|
for(let i=0; i<5; i++) {
|
||||||
|
let x, y;
|
||||||
|
do {
|
||||||
|
x = randInt(-1500,1500);
|
||||||
|
y = randInt(-1500,1500);
|
||||||
|
} while (!isValidPlacement(x, y));
|
||||||
|
animals.push(createAnimal("Wolf", x, y));
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(update);
|
requestAnimationFrame(update);
|
||||||
}
|
}
|
||||||
|
@ -194,6 +194,7 @@
|
|||||||
<button class="menu-button" id="buyDoctorBtn">Buy Doctor ($200)</button>
|
<button class="menu-button" id="buyDoctorBtn">Buy Doctor ($200)</button>
|
||||||
<button class="menu-button" id="buyTeacherBtn">Buy Teacher ($180)</button>
|
<button class="menu-button" id="buyTeacherBtn">Buy Teacher ($180)</button>
|
||||||
<button class="menu-button" id="buySoldierBtn">Buy Soldier ($250)</button>
|
<button class="menu-button" id="buySoldierBtn">Buy Soldier ($250)</button>
|
||||||
|
<button class="menu-button" id="buyPlannerBtn">Buy Planner ($1000)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-category">
|
<div class="menu-category">
|
||||||
|
@ -372,6 +372,7 @@ function drawCitizen(c) {
|
|||||||
case "Doctor": icon = "💉"; break;
|
case "Doctor": icon = "💉"; break;
|
||||||
case "Teacher": icon = "📚"; break;
|
case "Teacher": icon = "📚"; break;
|
||||||
case "Soldier": icon = "⚔️"; break;
|
case "Soldier": icon = "⚔️"; break;
|
||||||
|
case "Planner": icon = "🏗️"; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = "#000";
|
ctx.fillStyle = "#000";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user