diff --git a/ai.js b/ai.js index 761fe95..7c93de4 100644 --- a/ai.js +++ b/ai.js @@ -2,6 +2,12 @@ * ANIMAL CONSTANTS **********************************************************************/ const ANIMAL_HUNGER_MAX = 100; +const WOLF_HUNGER_INCREMENT = 0.01; +const WOLF_DAMAGE = 10; +const WOLF_ATTACK_RANGE = 15; +const WOLF_ATTACK_COOLDOWN = 100; +const WOLF_REPRO_CHANCE = 0.0002; +const WOLF_REPRO_COOLDOWN = 5000; /********************************************************************** * CITIZEN AI @@ -10,7 +16,7 @@ function updateCitizen(cit) { cit.hunger += HUNGER_INCREMENT; if(cit.hunger > HUNGER_MAX) cit.hunger = HUNGER_MAX; - if(["chop", "gatherFruit", "build", "treatPatients", "teachStudents", "sellGoods"].includes(cit.task)) { + if(["chop", "gatherFruit", "build", "treatPatients", "teachStudents", "sellGoods", "huntWolf", "patrol"].includes(cit.task)) { cit.energy -= ENERGY_DECREMENT_WORK; } else if(cit.task === "restAtHouse") { cit.energy += ENERGY_INCREMENT_REST; @@ -43,6 +49,8 @@ function updateCitizen(cit) { case 'sellGoods': sellGoodsTask(cit); break; case 'visitHospital': visitHospitalTask(cit); break; case 'visitSchool': visitSchoolTask(cit); break; + case 'huntWolf': huntWolfTask(cit); break; + case 'patrol': patrolTask(cit); break; default: randomWander(cit); break; } @@ -50,18 +58,100 @@ function updateCitizen(cit) { cit.y += cit.vy; } +function updateWolf(w) { + w.hunger += WOLF_HUNGER_INCREMENT; + if(w.hunger >= ANIMAL_HUNGER_MAX) { + w.dead = true; + logAction("A wolf starved to death."); + return; + } + + // Look for prey + if(w.hunger > 30) { + // First try to find rabbits + let rabbit = findNearestAnimalOfType(w, "Rabbit"); + if(rabbit && distance(w.x, w.y, rabbit.x, rabbit.y) < 200) { + moveToward(w, rabbit.x, rabbit.y, 0.5); + + // Attack if close enough and cooldown is ready + if(distance(w.x, w.y, rabbit.x, rabbit.y) < WOLF_ATTACK_RANGE && w.attackCooldown <= 0) { + rabbit.dead = true; + w.hunger -= 30; + if(w.hunger < 0) w.hunger = 0; + w.attackCooldown = WOLF_ATTACK_COOLDOWN; + logAction("A wolf caught and ate a rabbit!"); + } + return; + } + + // If no rabbits nearby, look for citizens (but not if soldiers are nearby) + let nearbySoldier = citizens.find(c => + c.profession === "Soldier" && + distance(w.x, w.y, c.x, c.y) < 100 + ); + + if(!nearbySoldier) { + let citizen = citizens.find(c => distance(w.x, w.y, c.x, c.y) < 150); + if(citizen) { + moveToward(w, citizen.x, citizen.y, 0.45); + + // Attack if close enough and cooldown is ready + if(distance(w.x, w.y, citizen.x, citizen.y) < WOLF_ATTACK_RANGE && w.attackCooldown <= 0) { + citizen.health -= WOLF_DAMAGE; + w.attackCooldown = WOLF_ATTACK_COOLDOWN; + + // Knockback effect + knockback(citizen, w.x, w.y, 20); + + logAction(`A wolf attacked ${citizen.name}! Health: ${Math.floor(citizen.health)}`); + + if(citizen.health <= 0) { + // Remove citizen + citizens = citizens.filter(c => c !== citizen); + logAction(`${citizen.name} was killed by a wolf!`); + w.hunger -= 50; + if(w.hunger < 0) w.hunger = 0; + } + } + return; + } + } else { + // Run away from soldier + moveAway(w, nearbySoldier.x, nearbySoldier.y, 0.5); + return; + } + } + + // Reproduce + if(w.hunger < 30 && w.reproductionCooldown <= 0) { + if(Math.random() < WOLF_REPRO_CHANCE) { + spawnBabyAnimal("Wolf", w.x, w.y); + w.reproductionCooldown = WOLF_REPRO_COOLDOWN; + } + } + + // Random movement + randomAnimalWander(w); +} + /********************************************************************** * ANIMAL AI **********************************************************************/ function updateAnimal(ani) { if(ani.type === "Rabbit") { updateRabbit(ani); + } else if(ani.type === "Wolf") { + updateWolf(ani); } + ani.x += ani.vx; ani.y += ani.vy; if(ani.reproductionCooldown > 0) { ani.reproductionCooldown--; } + if(ani.attackCooldown > 0) { + ani.attackCooldown--; + } } function updateRabbit(r) { @@ -71,6 +161,14 @@ function updateRabbit(r) { logAction("A rabbit starved to death."); return; } + + // Check for nearby wolves and run away + let nearestWolf = findNearestAnimalOfType(r, "Wolf"); + if(nearestWolf && distance(r.x, r.y, nearestWolf.x, nearestWolf.y) < 100) { + // Run away from wolf + moveAway(r, nearestWolf.x, nearestWolf.y, 0.6); + return; + } // Reproduce if(r.hunger < 50 && r.reproductionCooldown <= 0) { if(Math.random() < RABBIT_REPRO_CHANCE) { @@ -130,6 +228,16 @@ function assignNewTask(cit) { } } + // For soldiers, check for wolves to hunt + if(cit.profession === "Soldier") { + let nearbyWolf = findNearestAnimalOfType(cit, "Wolf"); + if(nearbyWolf && distance(cit.x, cit.y, nearbyWolf.x, nearbyWolf.y) < 300) { + cit.task = "huntWolf"; + cit.target = nearbyWolf; + return; + } + } + // Profession-specific tasks switch(cit.profession) { case "Builder": @@ -147,6 +255,9 @@ function assignNewTask(cit) { case "Teacher": teacherTasks(cit); break; + case "Soldier": + soldierTasks(cit); + break; default: cit.task = null; cit.target = null; @@ -254,9 +365,58 @@ function teacherTasks(cit) { builderTasks(cit); } +function soldierTasks(cit) { + // Patrol around the city center + if(Math.random() < 0.1) { + cit.task = "patrol"; + cit.target = { + x: randInt(-200, 200), + y: randInt(-200, 200) + }; + return; + } + + // Help with building when not patrolling + builderTasks(cit); +} + /********************************************************************** * NEW TASK HANDLERS **********************************************************************/ +function huntWolfTask(cit) { + let wolf = cit.target; + if(!wolf || wolf.dead) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, wolf.x, wolf.y, 0.5); + + // Attack if close enough + if(distance(cit.x, cit.y, wolf.x, wolf.y) < 20) { + wolf.dead = true; + logAction(`${cit.name} [Soldier] killed a wolf!`); + cit.task = null; + cit.target = null; + } +} + +function patrolTask(cit) { + let target = cit.target; + if(!target) { + cit.task = null; + return; + } + + moveToward(cit, target.x, target.y, 0.3); + + // When reached patrol point, find a new task + if(distance(cit.x, cit.y, target.x, target.y) < 10) { + cit.task = null; + cit.target = null; + } +} function treatPatientsTask(cit) { let hospital = cit.target; if(!hospital || !hospital.completed) { @@ -376,6 +536,19 @@ function chopTask(cit) { cit.target = null; return; } + + // Check for nearby wolves and run if not a soldier + if(cit.profession !== "Soldier") { + let nearbyWolf = findNearestAnimalOfType(cit, "Wolf"); + if(nearbyWolf && distance(cit.x, cit.y, nearbyWolf.x, nearbyWolf.y) < 100) { + // Run away from wolf + moveAway(cit, nearbyWolf.x, nearbyWolf.y, 0.6); + if(Math.random() < 0.1) { + logAction(`${cit.name} is running away from a wolf!`); + } + return; + } + } moveToward(cit, tree.x, tree.y, 0.4); if(distance(cit.x, cit.y, tree.x, tree.y) < 10) { let canGather = cit.carryingCapacity - cit.carryingWood; @@ -539,3 +712,16 @@ function randomAnimalWander(a) { a.vy = (Math.random() - 0.5) * 0.4; } } + +function moveAway(obj, tx, ty, speed=0.4) { + let dx = obj.x - tx; + let dy = obj.y - ty; + let dist = Math.sqrt(dx*dx + dy*dy); + if(dist > 1) { + obj.vx = (dx/dist) * speed; + obj.vy = (dy/dist) * speed; + } else { + obj.vx = (Math.random() - 0.5) * speed; + obj.vy = (Math.random() - 0.5) * speed; + } +} diff --git a/game.js b/game.js index b9f72de..a803d0f 100644 --- a/game.js +++ b/game.js @@ -35,6 +35,7 @@ const COST_HOSPITAL = 500; const COST_SCHOOL = 450; const COST_SPAWNER = 500; const COST_TREE = 50; +const COST_SOLDIER = 250; // Earn money const REWARD_DELIVER_WOOD = 5; @@ -82,10 +83,11 @@ const SCHOOL_WOOD_REQUIRED = 65; const SCHOOL_BUILD_RATE = 0.12; // Professions -const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher"]; +const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher", "Soldier"]; -// Rabbits +// Animals const STARTING_RABBITS = 10; +const STARTING_WOLVES = 3; const RABBIT_HUNGER_INCREMENT = 0.003; const RABBIT_REPRO_COOLDOWN = 3000; const RABBIT_REPRO_CHANCE = 0.0005; @@ -108,10 +110,14 @@ function initWorld() { citizens.push(c); logAction(`Initial Citizen joined: ${c.name} [Builder]`); - // Spawn some rabbits + // Spawn some animals for(let i=0; i c.profession === "Merchant").length; const doctorCount = citizens.filter(c => c.profession === "Doctor").length; const teacherCount = citizens.filter(c => c.profession === "Teacher").length; + const soldierCount = citizens.filter(c => c.profession === "Soldier").length; - professionCountsDisplay.textContent = `Citizens: 👷${builderCount} 🌾${farmerCount} 💰${merchantCount} 💉${doctorCount} 📚${teacherCount}`; + professionCountsDisplay.textContent = `Citizens: 👷${builderCount} 🌾${farmerCount} 💰${merchantCount} 💉${doctorCount} 📚${teacherCount} ⚔️${soldierCount}`; } diff --git a/index.html b/index.html index a0e852f..15e375b 100644 --- a/index.html +++ b/index.html @@ -193,6 +193,7 @@ +