From 18ac996f65bbe66865862625a5e7748b258bed06 Mon Sep 17 00:00:00 2001
From: "Kacper Kostka (aider)" <kacper.kostka08@gmail.com>
Date: Wed, 2 Apr 2025 22:33:18 +0200
Subject: [PATCH] feat: Add Planner citizen type with automated city expansion

---
 ai.js      | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 events.js  |  16 +++++
 game.js    |  19 ++++--
 index.html |   1 +
 render.js  |   1 +
 5 files changed, 208 insertions(+), 4 deletions(-)

diff --git a/ai.js b/ai.js
index 4d383f5..61f358b 100644
--- a/ai.js
+++ b/ai.js
@@ -263,6 +263,9 @@ function assignNewTask(cit) {
     case "Soldier":
       soldierTasks(cit);
       break;
+    case "Planner":
+      plannerTasks(cit);
+      break;
     default:
       cit.task = null;
       cit.target = null;
@@ -385,9 +388,181 @@ function soldierTasks(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
  **********************************************************************/
+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) {
   let wolf = cit.target;
   if(!wolf || wolf.dead) {
diff --git a/events.js b/events.js
index f2baf13..dcb526e 100644
--- a/events.js
+++ b/events.js
@@ -96,6 +96,11 @@ function setupBuyButtons() {
     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', () => {
     purchaseMode = "Market";
     logAction("Click on map to place a Market site.");
@@ -395,6 +400,17 @@ function setupCanvasClick() {
         }
         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":
         if(money >= COST_SPAWNER) {
           addMoney(-COST_SPAWNER, "Buy Spawner");
diff --git a/game.js b/game.js
index 4bdc5f8..5d5579d 100644
--- a/game.js
+++ b/game.js
@@ -36,6 +36,7 @@ const COST_SCHOOL   = 450;
 const COST_SPAWNER  = 500;
 const COST_TREE     = 50;
 const COST_SOLDIER  = 250;
+const COST_PLANNER  = 1000;
 
 // Terrain costs
 const COST_WATER    = 20;
@@ -90,14 +91,14 @@ const SCHOOL_WOOD_REQUIRED = 65;
 const SCHOOL_BUILD_RATE = 0.12;
 
 // Professions
-const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher", "Soldier"];
+const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher", "Soldier", "Planner"];
 
 // Animals
 const STARTING_RABBITS = 10;
-const STARTING_WOLVES = 3;
+const STARTING_WOLVES = 5;
 const RABBIT_HUNGER_INCREMENT = 0.003;
-const RABBIT_REPRO_COOLDOWN = 3000;
-const RABBIT_REPRO_CHANCE = 0.0005;
+const RABBIT_REPRO_COOLDOWN = 5000;
+const RABBIT_REPRO_CHANCE = 0.0002;
 const WOLF_SPEED = 0.7; // Faster than rabbits
 
 /**********************************************************************
@@ -187,6 +188,16 @@ function initWorld() {
   
   // Spawn wolves at the corners of the map
   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);
 }
diff --git a/index.html b/index.html
index 4b798ce..2650bb4 100644
--- a/index.html
+++ b/index.html
@@ -194,6 +194,7 @@
       <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="buySoldierBtn">Buy Soldier ($250)</button>
+      <button class="menu-button" id="buyPlannerBtn">Buy Planner ($1000)</button>
     </div>
     
     <div class="menu-category">
diff --git a/render.js b/render.js
index 2fae340..44b4cec 100644
--- a/render.js
+++ b/render.js
@@ -372,6 +372,7 @@ function drawCitizen(c) {
     case "Doctor": icon = "💉"; break;
     case "Teacher": icon = "📚"; break;
     case "Soldier": icon = "⚔️"; break;
+    case "Planner": icon = "🏗️"; break;
   }
   
   ctx.fillStyle = "#000";