diff --git a/index.html b/index.html index 2f756db..a569d0c 100644 --- a/index.html +++ b/index.html @@ -2,78 +2,161 @@ <html lang="en"> <head> <meta charset="UTF-8" /> - <title>Virtual World: More Rabbits, Separate Wolf Spawning</title> + <title>Virtual World</title> <style> + /* Futuristic / fun styling */ + + /* A gentle gradient background with animation */ body { margin: 0; padding: 0; - background: #e0f7fa; - font-family: sans-serif; + font-family: 'Trebuchet MS', sans-serif; + background: linear-gradient(120deg, #0ff, #f0f); + background-size: 400% 400%; + animation: gradientShift 10s ease infinite alternate; } + + @keyframes gradientShift { + 0% { background-position: 0% 50%; } + 100% { background-position: 100% 50%; } + } + + /* Container for everything */ #app { display: flex; flex-direction: column; align-items: center; padding: 10px; + position: relative; + z-index: 1; /* so it stays above the gradient */ } - h1 { - margin: 5px; - color: #333; + + /* Info bar top-left */ + #infoBar { + position: absolute; + top: 0; + left: 0; + margin: 8px; + padding: 5px 10px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid #888; + border-radius: 6px; + font-size: 14px; + z-index: 10; + transition: box-shadow 0.3s; } - #controls { - margin-bottom: 5px; + #infoBar:hover { + box-shadow: 0 0 10px rgba(255,255,255,0.8); } + + #menu { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 8px 0; + justify-content: center; + } + + .menu-button { + padding: 6px 12px; + background: #fff; + border: 2px solid #444; + cursor: pointer; + border-radius: 4px; + transition: transform 0.25s ease, background-color 0.25s ease; + } + .menu-button:hover { + transform: scale(1.08); + background-color: #c8fffa; + } + + #toggleLogsBtn { + margin-left: 10px; + } + canvas { background: #ffffff; border: 2px solid #444; cursor: grab; + transition: box-shadow 0.3s; } canvas:active { cursor: grabbing; + box-shadow: 0 0 10px rgba(0,0,0,0.3); } + #log { width: 800px; height: 120px; margin-top: 10px; - overflow-y: auto; border: 2px solid #444; background: #f9f9f9; padding: 5px; font-size: 0.9rem; + overflow-y: auto; + border-radius: 6px; + transition: box-shadow 0.3s; + } + #log:hover { + box-shadow: 0 0 6px rgba(0,0,0,0.15); } .log-entry { margin: 2px 0; } + + /* Responsive adjustments */ + @media (max-width: 850px) { + #log { + width: 95%; + height: 100px; + } + canvas { + width: 95% !important; + height: 400px !important; + } + } </style> </head> <body> +<!-- Info Bar with money/citizen count --> +<div id="infoBar"> + <p id="moneyDisplay">Money: $0</p> + <p id="citizenCount">Citizens: 0</p> +</div> + <div id="app"> - <h1>Virtual World: Rabbits & Wolves (Separated Spawn)</h1> - <p id="controls">Drag to move the map, scroll to zoom in/out.</p> + <!-- Store Menu --> + <div id="menu"> + <button class="menu-button" id="buyHouseBtn">Buy House ($300)</button> + <button class="menu-button" id="buyRoadBtn">Buy Road ($150)</button> + <button class="menu-button" id="buyBuilderBtn">Buy Builder ($100)</button> + <button class="menu-button" id="buyFarmerBtn">Buy Farmer ($100)</button> + <button class="menu-button" id="buySpawnerBtn">Buy Spawner ($500)</button> + <button class="menu-button" id="buyTreeBtn">Buy Tree ($50)</button> + <button class="menu-button" id="toggleLogsBtn">Hide Logs</button> + </div> + <canvas id="worldCanvas" width="800" height="600"></canvas> <div id="log"></div> </div> <script> /* - --------------------------------------------------------------------------------- - Key Differences: - - STARTING_RABBITS = 30 (spawned randomly across [-1000, 1000]) - - STARTING_WOLVES = 5 (spawned in a separate area [500..1000]) - - Rest of the simulation remains the same: - * Citizens with Professions, Houses, Roads, Fruit Trees - * Rabbits & Wolves Reproduction - * Basic Predator/Prey & Combat - --------------------------------------------------------------------------------- + Virtual World with advanced AI (Farmers/Builders), Wolves & Rabbits, + user can buy House, Road, Citizens, Spawner, Tree, logs are togglable, + user starts with 1 builder, plus a new citizen every 3000 frames. + Fix "spawnBabyAnimal is not defined" by ensuring we define it properly. */ /********************************************************************** - * GLOBALS & BASIC STRUCTURES + * GLOBALS & STRUCTURES **********************************************************************/ const canvas = document.getElementById('worldCanvas'); const ctx = canvas.getContext('2d'); const logContainer = document.getElementById('log'); +const moneyDisplay = document.getElementById('moneyDisplay'); +const citizenCountDisplay = document.getElementById('citizenCount'); // Pan & Zoom let offsetX = 0; @@ -83,260 +166,267 @@ let isDragging = false; let lastMouseX = 0; let lastMouseY = 0; -// Frame counter for timed events +// Basic money system +let money = 5000; // Start with 500 +let purchaseMode = null; // "House" or "Road" or "Builder" or "Farmer", etc. + +// Frame counter let frameCount = 0; -// City storage for wood +// Shared city storage (wood, etc.) const cityStorage = { wood: 0 }; -// Resource nodes (trees & fruit trees) -let resources = []; - -// Buildings array (houses, roads) -let buildings = []; - -// Citizens +// Arrays +let resources = []; // Trees, FruitTrees +let buildings = []; // House, Road, Spawner let citizens = []; - -// Animals (rabbits & wolves) let animals = []; /********************************************************************** - * SIMULATION CONSTANTS + * COSTS & EARNINGS **********************************************************************/ -// Hunger & energy for citizens -const HUNGER_INCREMENT = 0.005; +const COST_HOUSE = 300; +const COST_ROAD = 150; +const COST_BUILDER = 100; +const COST_FARMER = 100; +const COST_SPAWNER = 500; +const COST_TREE = 50; + +// Earn money +const REWARD_DELIVER_WOOD = 5; +const REWARD_BUILD_COMPLETE = 20; + +/********************************************************************** + * STATS & CONSTANTS + **********************************************************************/ +// Hunger & energy +const HUNGER_MAX = 100; +const ENERGY_MAX = 100; +const HUNGER_INCREMENT = 0.005; const ENERGY_DECREMENT_WORK = 0.02; const ENERGY_INCREMENT_REST = 0.05; const HUNGER_THRESHOLD = 50; const ENERGY_THRESHOLD = 30; -const HUNGER_MAX = 100; -const ENERGY_MAX = 100; -// House building -const HOUSE_WOOD_REQUIRED = 50; -const HOUSE_BUILD_RATE = 0.2; -const HOUSE_MAX_FRUIT = 30; - -// Road building -const ROAD_WOOD_REQUIRED = 10; -const ROAD_BUILD_RATE = 0.3; - -// Fruit trees +// Trees const FRUIT_TREE_START_AMOUNT = 20; const FRUIT_GATHER_RATE = 1; const FRUIT_PLANT_COST = 1; +// House & Road +const HOUSE_WOOD_REQUIRED = 50; +const HOUSE_BUILD_RATE = 0.2; +const HOUSE_MAX_FRUIT = 30; + +const ROAD_WOOD_REQUIRED = 10; +const ROAD_BUILD_RATE = 0.3; + // Professions -const PROFESSIONS = ["Farmer", "Builder"]; +const PROFESSIONS = ["Farmer","Builder"]; +const WEAPON_WOOD_COST = 10; // forging cost -// Weapons -const WEAPON_WOOD_COST = 10; // cost for a citizen to craft a weapon - -// Animal logic -const STARTING_RABBITS = 30; -const STARTING_WOLVES = 5; - -// Rabbit hunger -const RABBIT_HUNGER_INCREMENT = 0.003; -// Wolf hunger -const WOLF_HUNGER_INCREMENT = 0.006; - -const ANIMAL_HUNGER_MAX = 100; -const KNOCKBACK_DISTANCE = 20; - -// Reproduction -const RABBIT_REPRO_COOLDOWN = 3000; // frames (~50s if ~60fps) -const WOLF_REPRO_COOLDOWN = 5000; // ~80s -const RABBIT_REPRO_CHANCE = 0.0005; -const WOLF_REPRO_CHANCE = 0.0003; +// Wolves & Rabbits +const STARTING_RABBITS = 5; +const STARTING_WOLVES = 3; +const ANIMAL_HUNGER_MAX = 100; +const RABBIT_HUNGER_INCREMENT = 0.003; +const WOLF_HUNGER_INCREMENT = 0.006; +const RABBIT_REPRO_COOLDOWN = 3000; +const WOLF_REPRO_COOLDOWN = 5000; +const RABBIT_REPRO_CHANCE = 0.0005; +const WOLF_REPRO_CHANCE = 0.0003; +const KNOCKBACK_DISTANCE = 20; /********************************************************************** - * LOGGING + * LOGGING & MONEY **********************************************************************/ function logAction(text) { const entry = document.createElement('div'); - entry.className = 'log-entry'; - entry.textContent = text; + entry.className='log-entry'; + entry.textContent=text; logContainer.appendChild(entry); - logContainer.scrollTop = logContainer.scrollHeight; + logContainer.scrollTop=logContainer.scrollHeight; +} +function addMoney(amount, reason="") { + money += amount; + if(money<0) money=0; + updateMoneyDisplay(); + if(reason){ + logAction(`Money +$${amount} from ${reason}. Total=$${money}`); + } +} +function updateMoneyDisplay(){ + moneyDisplay.textContent=`Money: $${money}`; + citizenCountDisplay.textContent=`Citizens: ${citizens.length}`; } /********************************************************************** * RANDOM UTILS **********************************************************************/ -const firstNames = ["Al", "Bea", "Cal", "Dee", "Eve", "Fay", "Gil", "Hal", "Ian", "Joy", "Kay", "Lee", "Max", "Ned", "Oda", "Pam", "Ray", "Sue", "Tim", "Ula", "Vic", "Wyn", "Xan", "Yel", "Zed"]; -const lastNames = ["Apple", "Berry", "Cherry", "Delta", "Echo", "Flint", "Gran", "Hills", "Iris", "Jones", "Knight", "Lemon", "Myer", "Noble", "Olson", "Prime", "Quartz", "Row", "Smith", "Turn", "Umbra", "Vale", "Wick", "Xeno", "Yolk", "Zoom"]; +const firstNames = ["Al","Bea","Cal","Dee","Eve","Fay","Gil","Hal","Ian","Joy", + "Kay","Lee","Max","Ned","Oda","Pam","Ray","Sue","Tim","Ula", + "Vic","Wyn","Xan","Yel","Zed"]; +const lastNames = ["Apple","Berry","Cherry","Delta","Echo","Flint","Gran","Hills", + "Iris","Jones","Knight","Lemon","Myer","Noble","Olson","Prime", + "Quartz","Row","Smith","Turn","Umbra","Vale","Wick","Xeno","Yolk","Zoom"]; function randomName() { - const f = firstNames[Math.floor(Math.random() * firstNames.length)]; - const l = lastNames[Math.floor(Math.random() * lastNames.length)]; - return f + " " + l; + const f=firstNames[Math.floor(Math.random()*firstNames.length)]; + const l=lastNames[Math.floor(Math.random()*lastNames.length)]; + return `${f} ${l}`; } - -function randInt(min, max) { - return Math.floor(Math.random() * (max - min)) + min; +function randInt(min,max){ + return Math.floor(Math.random()*(max-min))+min; } /********************************************************************** * ENTITY DEFINITIONS **********************************************************************/ -function createCitizen(name, x, y) { - const profession = PROFESSIONS[Math.floor(Math.random() * PROFESSIONS.length)]; +function createCitizen(name,x,y, forcedProfession=null) { + // If forcedProfession is "Builder"/"Farmer", override random + let profession = forcedProfession + ? forcedProfession + : PROFESSIONS[Math.floor(Math.random()*PROFESSIONS.length)]; + return { name, profession, - x, - y, - vx: (Math.random() - 0.5) * 0.3, - vy: (Math.random() - 0.5) * 0.3, - color: `hsl(${Math.random() * 360}, 70%, 50%)`, - task: null, - target: null, - carryingWood: 0, - carryingFruit: 0, - carryingCapacity: 10, - hunger: 0, - energy: ENERGY_MAX, - hasWeapon: false + x,y, + vx:(Math.random()-0.5)*0.3, + vy:(Math.random()-0.5)*0.3, + color:`hsl(${Math.random()*360},70%,50%)`, + task:null, + target:null, + carryingWood:0, + carryingFruit:0, + carryingCapacity:10, + hunger:0, + energy:ENERGY_MAX, + hasWeapon:false }; } -function createResource(type, x, y, amount) { +function createResource(type,x,y,amount){ return { type, x, y, amount }; } -function createHouseSite(x, y) { +function createHouseSite(x,y){ return { - buildingType: "House", - x, - y, - requiredWood: HOUSE_WOOD_REQUIRED, - deliveredWood: 0, - buildProgress: 0, - completed: false, - storedFruit: 0, - maxFruit: HOUSE_MAX_FRUIT + buildingType:"House", + x,y, + requiredWood:HOUSE_WOOD_REQUIRED, + deliveredWood:0, + buildProgress:0, + completed:false, + storedFruit:0, + maxFruit:HOUSE_MAX_FRUIT }; } -function createRoadSite(x1, y1, x2, y2) { - const mx = (x1 + x2) / 2; - const my = (y1 + y2) / 2; +function createRoadSite(x1,y1,x2,y2){ + const mx=(x1+x2)/2; + const my=(y1+y2)/2; return { - buildingType: "Road", - x: mx, - y: my, - x1, y1, - x2, y2, - requiredWood: ROAD_WOOD_REQUIRED, - deliveredWood: 0, - buildProgress: 0, - completed: false + buildingType:"Road", + x:mx, + y:my, + x1,y1,x2,y2, + requiredWood:ROAD_WOOD_REQUIRED, + deliveredWood:0, + buildProgress:0, + completed:false }; } -/** Animals: Rabbit & Wolf */ -function createAnimal(type, x, y) { - const initialCooldown = (type === "Rabbit") - ? randInt(0, RABBIT_REPRO_COOLDOWN) - : randInt(0, WOLF_REPRO_COOLDOWN); +function createAnimal(type,x,y){ + let cd=(type==="Rabbit")?randInt(0,RABBIT_REPRO_COOLDOWN):randInt(0,WOLF_REPRO_COOLDOWN); return { - type, - x, - y, - vx: (Math.random() - 0.5) * 0.4, - vy: (Math.random() - 0.5) * 0.4, - hunger: 0, - dead: false, - reproductionCooldown: initialCooldown + type, + x,y, + vx:(Math.random()-0.5)*0.4, + vy:(Math.random()-0.5)*0.4, + hunger:0, + dead:false, + reproductionCooldown:cd }; } /********************************************************************** - * WORLD INITIALIZATION + * SPAWN BABY ANIMAL (to fix ReferenceError) **********************************************************************/ -function initWorld() { - // Create normal trees - for (let i = 0; i < 15; i++) { - const x = randInt(-1000, 1000); - const y = randInt(-1000, 1000); - resources.push(createResource("Tree", x, y, 100)); +function spawnBabyAnimal(type,x,y){ + const nx=x+randInt(-20,20); + const ny=y+randInt(-20,20); + const baby=createAnimal(type,nx,ny); + animals.push(baby); + logAction(`A new baby ${type} is born!`); +} + +/********************************************************************** + * INIT WORLD + **********************************************************************/ +function initWorld(){ + // Normal trees + for(let i=0;i<15;i++){ + resources.push(createResource("Tree", randInt(-1000,1000), randInt(-1000,1000),100)); + } + // Fruit trees + for(let i=0;i<10;i++){ + resources.push(createResource("FruitTree", randInt(-1000,1000), randInt(-1000,1000), FRUIT_TREE_START_AMOUNT)); } - // Create fruit trees - for (let i = 0; i < 10; i++) { - const x = randInt(-1000, 1000); - const y = randInt(-1000, 1000); - resources.push(createResource("FruitTree", x, y, FRUIT_TREE_START_AMOUNT)); + // Start with 1 citizen => always a "Builder" + let c=createCitizen(randomName(), randInt(-200,200), randInt(-200,200), "Builder"); + citizens.push(c); + logAction(`Initial Citizen joined: ${c.name} [Builder]`); + + // Spawn some rabbits & wolves + for(let i=0;i<STARTING_RABBITS;i++){ + animals.push(createAnimal("Rabbit", randInt(-1000,1000), randInt(-1000,1000))); + } + for(let i=0;i<STARTING_WOLVES;i++){ + animals.push(createAnimal("Wolf", randInt(500,1000), randInt(500,1000))); } - // Create 5 initial citizens - for (let i = 0; i < 5; i++) { - const c = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200)); - citizens.push(c); - logAction(`Citizen joined: ${c.name} [${c.profession}]`); - } - - // Spawn more rabbits all across the map - for (let i = 0; i < STARTING_RABBITS; i++) { - const rx = randInt(-1000, 1000); - const ry = randInt(-1000, 1000); - animals.push(createAnimal("Rabbit", rx, ry)); - } - - // Spawn wolves in a distant region (e.g. x,y in [500..1000]) - for (let i = 0; i < STARTING_WOLVES; i++) { - const wx = randInt(500, 1000); - const wy = randInt(500, 1000); - animals.push(createAnimal("Wolf", wx, wy)); - } - - // Start simulation requestAnimationFrame(update); } /********************************************************************** - * MAIN UPDATE LOOP + * UPDATE LOOP **********************************************************************/ -function update() { +function update(){ frameCount++; - // Update citizens - citizens.forEach((cit) => { - updateCitizen(cit); + // Citizens + citizens.forEach((cit)=>updateCitizen(cit)); + + // Animals + animals.forEach((ani)=>{ + if(!ani.dead) updateAnimal(ani); }); + animals=animals.filter(a=>!a.dead); - // Update animals - animals.forEach((ani) => { - if (!ani.dead) { - updateAnimal(ani); - } - }); - - // Remove any dead animals - animals = animals.filter(a => !a.dead); - - // Periodic child births for citizens - if (frameCount % 600 === 0) { - const baby = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200)); + // Automatic new citizen every 3000 frames + if(frameCount%3000===0){ + let baby=createCitizen(randomName(), randInt(-200,200), randInt(-200,200)); citizens.push(baby); - logAction(`A child is born: ${baby.name} [${baby.profession}]`); + logAction(`A new citizen is born: ${baby.name} [${baby.profession}]`); } - // Update buildings - buildings.forEach((b) => { - if (!b.completed && b.deliveredWood >= b.requiredWood) { - let buildRate = (b.buildingType === "Road") ? ROAD_BUILD_RATE : HOUSE_BUILD_RATE; - b.buildProgress += buildRate; - if (b.buildProgress >= 100) { - b.completed = true; - if (b.buildingType === "House") { - logAction(`A new House is completed at (${b.x}, ${b.y})!`); + // Buildings + buildings.forEach((b)=>{ + if(!b.completed && b.deliveredWood>=b.requiredWood){ + let buildRate=(b.buildingType==="Road")?ROAD_BUILD_RATE:HOUSE_BUILD_RATE; + b.buildProgress+=buildRate; + if(b.buildProgress>=100){ + b.completed=true; + addMoney(REWARD_BUILD_COMPLETE, `Complete ${b.buildingType}`); + if(b.buildingType==="House"){ + logAction(`A new House completed @(${b.x},${b.y})!`); maybeBuildRoad(b); - } else { + } else if(b.buildingType==="Road"){ logAction(`A Road has been completed!`); } } @@ -348,28 +438,25 @@ function update() { } /********************************************************************** - * CITIZEN UPDATE + * CITIZEN AI **********************************************************************/ -function updateCitizen(cit) { - // Basic hunger & energy - cit.hunger += HUNGER_INCREMENT; - if (cit.hunger > HUNGER_MAX) cit.hunger = HUNGER_MAX; +function updateCitizen(cit){ + cit.hunger+=HUNGER_INCREMENT; + if(cit.hunger>HUNGER_MAX) cit.hunger=HUNGER_MAX; - if (["chop","gatherFruit","build"].includes(cit.task)) { - cit.energy -= ENERGY_DECREMENT_WORK; - } else if (cit.task === "restAtHouse") { - cit.energy += ENERGY_INCREMENT_REST; + if(["chop","gatherFruit","build"].includes(cit.task)){ + cit.energy-=ENERGY_DECREMENT_WORK; + } else if(cit.task==="restAtHouse"){ + cit.energy+=ENERGY_INCREMENT_REST; } else { - cit.energy -= 0.0005; + cit.energy-=0.0005; } - if (cit.energy < 0) cit.energy = 0; - if (cit.energy > ENERGY_MAX) cit.energy = ENERGY_MAX; + if(cit.energy<0) cit.energy=0; + if(cit.energy>ENERGY_MAX) cit.energy=ENERGY_MAX; - if (!cit.task) { - assignNewTask(cit); - } + if(!cit.task) assignNewTask(cit); - switch (cit.task) { + switch(cit.task){ case 'chop': chopTask(cit); break; case 'deliverWood': deliverWoodTask(cit); break; case 'build': buildTask(cit); break; @@ -381,438 +468,353 @@ function updateCitizen(cit) { default: randomWander(cit); break; } - cit.x += cit.vx; - cit.y += cit.vy; + cit.x+=cit.vx; + cit.y+=cit.vy; } /********************************************************************** - * ANIMAL UPDATE + * ANIMAL AI **********************************************************************/ -function updateAnimal(ani) { - if (ani.type === "Rabbit") { +function updateAnimal(ani){ + if(ani.type==="Rabbit"){ updateRabbit(ani); - } else if (ani.type === "Wolf") { + } else { updateWolf(ani); } - - ani.x += ani.vx; - ani.y += ani.vy; - - // Decrement reproduction cooldown - if (ani.reproductionCooldown > 0) { - ani.reproductionCooldown -= 1; + ani.x+=ani.vx; + ani.y+=ani.vy; + if(ani.reproductionCooldown>0){ + ani.reproductionCooldown--; } } -function updateRabbit(rabbit) { - rabbit.hunger += RABBIT_HUNGER_INCREMENT; - if (rabbit.hunger >= ANIMAL_HUNGER_MAX) { - rabbit.dead = true; +function updateRabbit(r){ + r.hunger+=RABBIT_HUNGER_INCREMENT; + if(r.hunger>=ANIMAL_HUNGER_MAX){ + r.dead=true; logAction("A rabbit starved to death."); return; } - - // Reproduction - if (rabbit.hunger < 50 && rabbit.reproductionCooldown <= 0) { - if (Math.random() < RABBIT_REPRO_CHANCE) { - spawnBabyAnimal("Rabbit", rabbit.x, rabbit.y); - rabbit.reproductionCooldown = RABBIT_REPRO_COOLDOWN; + // Reproduce + if(r.hunger<50&&r.reproductionCooldown<=0){ + if(Math.random()<RABBIT_REPRO_CHANCE){ + spawnBabyAnimal("Rabbit",r.x,r.y); + r.reproductionCooldown=RABBIT_REPRO_COOLDOWN; } } - - if (rabbit.hunger > 50) { - const tree = findNearestResourceOfType({x: rabbit.x, y: rabbit.y}, "FruitTree"); - if (tree) { - moveToward(rabbit, tree.x, tree.y, 0.4); - if (distance(rabbit.x, rabbit.y, tree.x, tree.y) < 10) { - if (tree.amount > 0) { - const eatAmount = 1; - tree.amount -= eatAmount; - rabbit.hunger -= eatAmount * 2; - if (rabbit.hunger < 0) rabbit.hunger = 0; - if (Math.random() < 0.02) { - logAction("A rabbit is eating fruit..."); - } + if(r.hunger>50){ + let tree=findNearestResourceOfType(r,"FruitTree"); + if(tree){ + moveToward(r,tree.x,tree.y,0.4); + if(distance(r.x,r.y,tree.x,tree.y)<10){ + if(tree.amount>0){ + tree.amount--; + r.hunger-=2; + if(r.hunger<0) r.hunger=0; + if(Math.random()<0.02) logAction("A rabbit is eating fruit..."); } } } else { - randomAnimalWander(rabbit); + randomAnimalWander(r); } } else { - randomAnimalWander(rabbit); + randomAnimalWander(r); } } -function updateWolf(wolf) { - wolf.hunger += WOLF_HUNGER_INCREMENT; - if (wolf.hunger >= ANIMAL_HUNGER_MAX) { - wolf.dead = true; +function updateWolf(w){ + w.hunger+=WOLF_HUNGER_INCREMENT; + if(w.hunger>=ANIMAL_HUNGER_MAX){ + w.dead=true; logAction("A wolf starved to death."); return; } - - // Reproduction - if (wolf.hunger < 50 && wolf.reproductionCooldown <= 0) { - if (Math.random() < WOLF_REPRO_CHANCE) { - spawnBabyAnimal("Wolf", wolf.x, wolf.y); - wolf.reproductionCooldown = WOLF_REPRO_COOLDOWN; + // Reproduce + if(w.hunger<50&&w.reproductionCooldown<=0){ + if(Math.random()<WOLF_REPRO_CHANCE){ + spawnBabyAnimal("Wolf",w.x,w.y); + w.reproductionCooldown=WOLF_REPRO_COOLDOWN; } } - - // 1) hunt rabbits - let targetRabbit = findNearestAnimalOfType(wolf, "Rabbit"); - if (targetRabbit) { - moveToward(wolf, targetRabbit.x, targetRabbit.y, 0.5); - if (distance(wolf.x, wolf.y, targetRabbit.x, targetRabbit.y) < 10) { - targetRabbit.dead = true; - wolf.hunger = Math.max(0, wolf.hunger - 30); + // hunt rabbit + let bunny=findNearestAnimalOfType(w,"Rabbit"); + if(bunny){ + moveToward(w,bunny.x,bunny.y,0.5); + if(distance(w.x,w.y,bunny.x,bunny.y)<10){ + bunny.dead=true; + w.hunger=Math.max(0,w.hunger-30); logAction("A wolf killed a rabbit!"); - knockback(wolf, targetRabbit.x, targetRabbit.y, KNOCKBACK_DISTANCE); + knockback(w,bunny.x,bunny.y,KNOCKBACK_DISTANCE); } return; } - - // 2) no rabbit => maybe attack a citizen if hunger > 60 - if (wolf.hunger > 60) { - let targetHuman = findNearestCitizen(wolf); - if (targetHuman) { - moveToward(wolf, targetHuman.x, targetHuman.y, 0.5); - if (distance(wolf.x, wolf.y, targetHuman.x, targetHuman.y) < 10) { - if (targetHuman.hasWeapon) { - wolf.dead = true; - logAction(`${targetHuman.name} defended against a wolf with a weapon! Wolf died.`); + // if hunger>60 => try attack citizen + if(w.hunger>60){ + let hum=findNearestCitizen(w); + if(hum){ + moveToward(w,hum.x,hum.y,0.5); + if(distance(w.x,w.y,hum.x,hum.y)<10){ + if(hum.hasWeapon){ + w.dead=true; + logAction(`${hum.name} defended vs wolf using a weapon! Wolf died.`); } else { - logAction(`A wolf killed ${targetHuman.name}!`); - citizens.splice(citizens.indexOf(targetHuman), 1); + logAction(`A wolf killed ${hum.name}!`); + citizens.splice(citizens.indexOf(hum),1); } - knockback(wolf, targetHuman.x, targetHuman.y, KNOCKBACK_DISTANCE); + knockback(w,hum.x,hum.y,KNOCKBACK_DISTANCE); } } else { - randomAnimalWander(wolf); + randomAnimalWander(w); } } else { - randomAnimalWander(wolf); - } -} - -function spawnBabyAnimal(type, x, y) { - const nx = x + randInt(-20, 20); - const ny = y + randInt(-20, 20); - const baby = createAnimal(type, nx, ny); - animals.push(baby); - logAction(`A new baby ${type} is born!`); -} - -function knockback(entity, tx, ty, dist) { - const dx = entity.x - tx; - const dy = entity.y - ty; - const length = Math.sqrt(dx*dx + dy*dy); - if (length > 0.1) { - entity.x += (dx / length) * dist; - entity.y += (dy / length) * dist; + randomAnimalWander(w); } } /********************************************************************** - * FINDERS + * TASKS & AI LOGIC **********************************************************************/ -function findNearestResourceOfType(ref, rtype) { - let nearest = null; - let minDist = Infinity; - for (const res of resources) { - if (res.type === rtype && res.amount > 0) { - const d = distance(ref.x, ref.y, res.x, res.y); - if (d < minDist) { - minDist = d; - nearest = res; - } - } - } - return nearest; -} - -function findNearestAnimalOfType(wolf, targetType) { - let nearest = null; - let minDist = Infinity; - for (const a of animals) { - if (a !== wolf && !a.dead && a.type === targetType) { - const d = distance(wolf.x, wolf.y, a.x, a.y); - if (d < minDist) { - minDist = d; - nearest = a; - } - } - } - return nearest; -} - -function findNearestCitizen(wolf) { - let nearest = null; - let minDist = Infinity; - for (const c of citizens) { - const d = distance(wolf.x, wolf.y, c.x, c.y); - if (d < minDist) { - minDist = d; - nearest = c; - } - } - return nearest; -} - -function findHouseWithFruit() { - return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit > 0); -} - -function findAnyCompletedHouse() { - return buildings.find(b => b.buildingType === "House" && b.completed); -} - -function findHouseNeedingFruit() { - return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit < b.maxFruit); -} - -/********************************************************************** - * CITIZEN TASKS - **********************************************************************/ -function assignNewTask(cit) { - if (cit.hunger >= HUNGER_THRESHOLD) { - const houseWithFruit = findHouseWithFruit(); - if (houseWithFruit) { - cit.task = 'eatAtHouse'; - cit.target = houseWithFruit; +function assignNewTask(cit){ + if(cit.hunger>=HUNGER_THRESHOLD){ + let houseWithFruit=findHouseWithFruit(); + if(houseWithFruit){ + cit.task="eatAtHouse"; + cit.target=houseWithFruit; return; } } - if (cit.energy <= ENERGY_THRESHOLD) { - const completedHouse = findAnyCompletedHouse(); - if (completedHouse) { - cit.task = 'restAtHouse'; - cit.target = completedHouse; + if(cit.energy<=ENERGY_THRESHOLD){ + let compHouse=findAnyCompletedHouse(); + if(compHouse){ + cit.task="restAtHouse"; + cit.target=compHouse; return; } } - // If builder & no weapon => craft if enough wood - if (cit.profession === "Builder" && !cit.hasWeapon) { - if (cityStorage.wood >= WEAPON_WOOD_COST) { - cityStorage.wood -= WEAPON_WOOD_COST; - cit.hasWeapon = true; - logAction(`${cit.name} crafted a wooden weapon for defense!`); - cit.task = null; - cit.target = null; + if(cit.profession==="Builder"&&!cit.hasWeapon){ + if(cityStorage.wood>=WEAPON_WOOD_COST){ + cityStorage.wood-=WEAPON_WOOD_COST; + cit.hasWeapon=true; + logAction(`${cit.name} crafted a wooden weapon!`); + cit.task=null; + cit.target=null; return; } } - // Profession tasks - if (cit.profession === "Builder") { + if(cit.profession==="Builder"){ builderTasks(cit); } else { farmerTasks(cit); } } -function builderTasks(cit) { - const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood); - if (buildingNeedingWood) { - if (cit.carryingWood > 0) { - cit.task = 'deliverWood'; - cit.target = buildingNeedingWood; +function builderTasks(cit){ + let buildingNeedingWood=buildings.find(b=>!b.completed&&b.deliveredWood<b.requiredWood); + if(buildingNeedingWood){ + if(cit.carryingWood>0){ + cit.task="deliverWood"; + cit.target=buildingNeedingWood; return; } - const tree = findNearestResourceOfType(cit, "Tree"); - if (tree) { - cit.task = 'chop'; - cit.target = tree; + let tree=findNearestResourceOfType(cit,"Tree"); + if(tree){ + cit.task="chop"; + cit.target=tree; return; } } - const buildingToConstruct = buildings.find(b => !b.completed && b.deliveredWood >= b.requiredWood); - if (buildingToConstruct) { - cit.task = 'build'; - cit.target = buildingToConstruct; + let buildingToConstruct=buildings.find(b=>!b.completed&&b.deliveredWood>=b.requiredWood); + if(buildingToConstruct){ + cit.task="build"; + cit.target=buildingToConstruct; return; } - const anyTree = findNearestResourceOfType(cit, "Tree"); - if (anyTree) { - cit.task = 'chop'; - cit.target = anyTree; + let anyTree=findNearestResourceOfType(cit,"Tree"); + if(anyTree){ + cit.task="chop"; + cit.target=anyTree; return; } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } -function farmerTasks(cit) { - if (cit.carryingFruit > 0) { - const houseNeedingFruit = findHouseNeedingFruit(); - if (houseNeedingFruit) { - cit.task = 'deliverFruit'; - cit.target = houseNeedingFruit; +function farmerTasks(cit){ + if(cit.carryingFruit>0){ + let houseNeedFruit=findHouseNeedingFruit(); + if(houseNeedFruit){ + cit.task="deliverFruit"; + cit.target=houseNeedFruit; return; } } - const fruitTree = findNearestResourceOfType(cit, "FruitTree"); - if (fruitTree && fruitTree.amount > 0) { - cit.task = 'gatherFruit'; - cit.target = fruitTree; + let fruitTree=findNearestResourceOfType(cit,"FruitTree"); + if(fruitTree&&fruitTree.amount>0){ + cit.task="gatherFruit"; + cit.target=fruitTree; return; } - if (cit.carryingFruit >= FRUIT_PLANT_COST && Math.random() < 0.1) { - cit.task = 'plantFruitTree'; - cit.target = null; + if(cit.carryingFruit>=FRUIT_PLANT_COST&&Math.random()<0.1){ + cit.task="plantFruitTree"; + cit.target=null; return; } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } -function chopTask(cit) { - const tree = cit.target; - if (!tree || tree.amount <= 0) { - cit.task = null; - cit.target = null; +/********************************************************************** + * TASK HANDLERS + **********************************************************************/ +function chopTask(cit){ + let tree=cit.target; + if(!tree||tree.amount<=0){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, tree.x, tree.y, 0.4); - if (distance(cit.x, cit.y, tree.x, tree.y) < 10) { - const canGather = cit.carryingCapacity - cit.carryingWood; - const toGather = Math.min(1, tree.amount, canGather); - tree.amount -= toGather; - cit.carryingWood += toGather; - if (Math.random() < 0.01) { - logAction(`${cit.name} [${cit.profession}] is chopping wood...`); + 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; + let toGather=Math.min(1,tree.amount,canGather); + tree.amount-=toGather; + cit.carryingWood+=toGather; + if(Math.random()<0.01){ + logAction(`${cit.name}[${cit.profession}] chopping wood...`); } - if (cit.carryingWood >= cit.carryingCapacity || tree.amount <= 0) { - cit.task = null; - cit.target = null; + if(cit.carryingWood>=cit.carryingCapacity||tree.amount<=0){ + cit.task=null; + cit.target=null; } } } -function deliverWoodTask(cit) { - const b = cit.target; - if (!b || b.completed) { - cit.task = null; - cit.target = null; +function deliverWoodTask(cit){ + let b=cit.target; + if(!b||b.completed){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, b.x, b.y, 0.4); - if (distance(cit.x, cit.y, b.x, b.y) < 20) { - const needed = b.requiredWood - b.deliveredWood; - if (needed > 0 && cit.carryingWood > 0) { - const toDeliver = Math.min(cit.carryingWood, needed); - b.deliveredWood += toDeliver; - cit.carryingWood -= toDeliver; + moveToward(cit,b.x,b.y,0.4); + if(distance(cit.x,cit.y,b.x,b.y)<20){ + let needed=b.requiredWood-b.deliveredWood; + if(needed>0&&cit.carryingWood>0){ + let toDeliver=Math.min(cit.carryingWood,needed); + b.deliveredWood+=toDeliver; + cit.carryingWood-=toDeliver; logAction(`${cit.name} delivered ${toDeliver} wood to ${b.buildingType}.`); + addMoney(REWARD_DELIVER_WOOD, "deliver wood"); } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } } -function buildTask(cit) { - const b = cit.target; - if (!b || b.completed) { - cit.task = null; - cit.target = null; +function buildTask(cit){ + let b=cit.target; + if(!b||b.completed){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, b.x, b.y, 0.3); + moveToward(cit,b.x,b.y,0.3); } -function gatherFruitTask(cit) { - const tree = cit.target; - if (!tree || tree.amount <= 0) { - cit.task = null; - cit.target = null; +function gatherFruitTask(cit){ + let tree=cit.target; + if(!tree||tree.amount<=0){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, tree.x, tree.y, 0.4); - if (distance(cit.x, cit.y, tree.x, tree.y) < 10) { - const canGather = cit.carryingCapacity - cit.carryingFruit; - const toGather = Math.min(FRUIT_GATHER_RATE, tree.amount, canGather); - tree.amount -= toGather; - cit.carryingFruit += toGather; - if (Math.random() < 0.01) { + moveToward(cit,tree.x,tree.y,0.4); + if(distance(cit.x,cit.y,tree.x,tree.y)<10){ + let canGather=cit.carryingCapacity-cit.carryingFruit; + let toGather=Math.min(FRUIT_GATHER_RATE, tree.amount, canGather); + tree.amount-=toGather; + cit.carryingFruit+=toGather; + if(Math.random()<0.01){ logAction(`${cit.name} [Farmer] is gathering fruit...`); } - if (cit.carryingFruit >= cit.carryingCapacity || tree.amount <= 0) { - cit.task = null; - cit.target = null; + if(cit.carryingFruit>=cit.carryingCapacity||tree.amount<=0){ + cit.task=null; + cit.target=null; } } } -function deliverFruitTask(cit) { - const house = cit.target; - if (!house || !house.completed) { - cit.task = null; - cit.target = null; +function deliverFruitTask(cit){ + let house=cit.target; + if(!house||!house.completed){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, house.x, house.y, 0.4); - if (distance(cit.x, cit.y, house.x, house.y) < 20) { - const space = house.maxFruit - house.storedFruit; - if (space > 0 && cit.carryingFruit > 0) { - const toDeliver = Math.min(cit.carryingFruit, space); - house.storedFruit += toDeliver; - cit.carryingFruit -= toDeliver; + moveToward(cit,house.x,house.y,0.4); + if(distance(cit.x,cit.y,house.x,house.y)<20){ + let space=house.maxFruit-house.storedFruit; + if(space>0&&cit.carryingFruit>0){ + let toDeliver=Math.min(cit.carryingFruit,space); + house.storedFruit+=toDeliver; + cit.carryingFruit-=toDeliver; logAction(`${cit.name} delivered ${toDeliver} fruit to house.`); } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } } -function plantFruitTreeTask(cit) { - const px = cit.x + randInt(-50, 50); - const py = cit.y + randInt(-50, 50); - moveToward(cit, px, py, 0.4); - if (distance(cit.x, cit.y, px, py) < 10) { - if (cit.carryingFruit >= FRUIT_PLANT_COST) { - cit.carryingFruit -= FRUIT_PLANT_COST; +function plantFruitTreeTask(cit){ + let px=cit.x+randInt(-50,50); + let py=cit.y+randInt(-50,50); + moveToward(cit,px,py,0.4); + if(distance(cit.x,cit.y,px,py)<10){ + if(cit.carryingFruit>=FRUIT_PLANT_COST){ + cit.carryingFruit-=FRUIT_PLANT_COST; resources.push(createResource("FruitTree", px, py, FRUIT_TREE_START_AMOUNT)); - logAction(`${cit.name} [Farmer] planted a new fruit tree!`); + logAction(`${cit.name}[Farmer] planted a new fruit tree!`); } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } } -function eatAtHouseTask(cit) { - const house = cit.target; - if (!house || !house.completed) { - cit.task = null; - cit.target = null; +function eatAtHouseTask(cit){ + let house=cit.target; + if(!house||!house.completed){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, house.x, house.y, 0.4); - if (distance(cit.x, cit.y, house.x, house.y) < 20) { - if (house.storedFruit > 0 && cit.hunger > 0) { - const amountToEat = 10; - const eaten = Math.min(amountToEat, house.storedFruit); - house.storedFruit -= eaten; - cit.hunger -= eaten; - if (cit.hunger < 0) cit.hunger = 0; - logAction(`${cit.name} ate ${eaten} fruit. Hunger now ${Math.floor(cit.hunger)}.`); + moveToward(cit,house.x,house.y,0.4); + if(distance(cit.x,cit.y,house.x,house.y)<20){ + if(house.storedFruit>0 && cit.hunger>0){ + let amtToEat=10; + let eaten=Math.min(amtToEat, house.storedFruit); + house.storedFruit-=eaten; + cit.hunger-=eaten; + if(cit.hunger<0) cit.hunger=0; + logAction(`${cit.name} ate ${eaten} fruit. Hunger => ${Math.floor(cit.hunger)}.`); } - cit.task = null; - cit.target = null; + cit.task=null; + cit.target=null; } } -function restAtHouseTask(cit) { - const house = cit.target; - if (!house || !house.completed) { - cit.task = null; - cit.target = null; +function restAtHouseTask(cit){ + let house=cit.target; + if(!house||!house.completed){ + cit.task=null; + cit.target=null; return; } - moveToward(cit, house.x, house.y, 0.4); - if (distance(cit.x, cit.y, house.x, house.y) < 20) { - if (cit.energy >= ENERGY_MAX - 1) { - cit.energy = ENERGY_MAX; - cit.task = null; - cit.target = null; + moveToward(cit,house.x,house.y,0.4); + if(distance(cit.x,cit.y,house.x,house.y)<20){ + if(cit.energy>=ENERGY_MAX-1){ + cit.energy=ENERGY_MAX; + cit.task=null; + cit.target=null; } } } @@ -820,95 +822,141 @@ function restAtHouseTask(cit) { /********************************************************************** * RANDOM WANDER **********************************************************************/ -function randomWander(cit) { - if (Math.random() < 0.01) { - cit.vx = (Math.random() - 0.5) * 0.3; - cit.vy = (Math.random() - 0.5) * 0.3; +function randomWander(cit){ + if(Math.random()<0.01){ + cit.vx=(Math.random()-0.5)*0.3; + cit.vy=(Math.random()-0.5)*0.3; } } - -function randomAnimalWander(ani) { - if (Math.random() < 0.01) { - ani.vx = (Math.random() - 0.5) * 0.4; - ani.vy = (Math.random() - 0.5) * 0.4; +function randomAnimalWander(a){ + if(Math.random()<0.01){ + a.vx=(Math.random()-0.5)*0.4; + a.vy=(Math.random()-0.5)*0.4; } } /********************************************************************** - * MOVEMENT UTILS + * MOVE & DISTANCE **********************************************************************/ -function moveToward(obj, tx, ty, speed = 0.4) { - const dx = tx - obj.x; - const dy = ty - obj.y; - const dist = Math.sqrt(dx*dx + dy*dy); - if (dist > 1) { - obj.vx = (dx / dist) * speed; - obj.vy = (dy / dist) * speed; +function moveToward(obj,tx,ty,speed=0.4){ + let dx=tx-obj.x; + let dy=ty-obj.y; + let dist=Math.sqrt(dx*dx+dy*dy); + if(dist>1){ + obj.vx=(dx/dist)*speed; + obj.vy=(dy/dist)*speed; } else { - obj.vx = 0; - obj.vy = 0; + obj.vx=0; + obj.vy=0; + } +} +function distance(x1,y1,x2,y2){ + let dx=x2-x1; + let dy=y2-y1; + return Math.sqrt(dx*dx+dy*dy); +} +function knockback(ent,tx,ty,dist){ + let dx=ent.x-tx; + let dy=ent.y-ty; + let length=Math.sqrt(dx*dx+dy*dy); + if(length>0.1){ + ent.x+=(dx/length)*dist; + ent.y+=(dy/length)*dist; } } -function distance(x1, y1, x2, y2) { - const dx = x2 - x1; - const dy = y2 - y1; - return Math.sqrt(dx*dx + dy*dy); -} - /********************************************************************** - * ROAD-BUILDING WHEN HOUSE IS COMPLETED + * FINDERS **********************************************************************/ -function maybeBuildRoad(newHouse) { - const otherHouses = buildings.filter(b => b.buildingType === "House" && b.completed && b !== newHouse); - if (otherHouses.length === 0) return; - let nearest = null; - let minDist = Infinity; - for (const oh of otherHouses) { - const d = distance(newHouse.x, newHouse.y, oh.x, oh.y); - if (d < minDist) { - minDist = d; - nearest = oh; +function findNearestResourceOfType(ref, rtype){ + let best=null; + let bestD=Infinity; + resources.forEach((res)=>{ + if(res.type===rtype && res.amount>0){ + let d=distance(ref.x,ref.y,res.x,res.y); + if(d<bestD){ + bestD=d; + best=res; + } } - } - if (!nearest) return; - const roadSite = createRoadSite(newHouse.x, newHouse.y, nearest.x, nearest.y); - buildings.push(roadSite); - logAction(`A Road site created between houses at (${newHouse.x}, ${newHouse.y}) and (${nearest.x}, ${nearest.y}).`); + }); + return best; +} +function findNearestAnimalOfType(me,targetType){ + let best=null; + let bestD=Infinity; + animals.forEach((a)=>{ + if(a.type===targetType && !a.dead && a!==me){ + let d=distance(me.x,me.y,a.x,a.y); + if(d<bestD){ + bestD=d; + best=a; + } + } + }); + return best; +} +function findNearestCitizen(wolf){ + let best=null; + let bestD=Infinity; + citizens.forEach((c)=>{ + let d=distance(wolf.x,wolf.y,c.x,c.y); + if(d<bestD){ + bestD=d; + best=c; + } + }); + return best; +} +function findHouseWithFruit(){ + return buildings.find(b=>b.buildingType==="House" && b.completed && b.storedFruit>0); +} +function findAnyCompletedHouse(){ + return buildings.find(b=>b.buildingType==="House" && b.completed); +} +function findHouseNeedingFruit(){ + return buildings.find(b=>b.buildingType==="House" && b.completed && b.storedFruit<b.maxFruit); } /********************************************************************** - * AUTO-CREATION OF NEW HOUSE SITES + * ROAD BUILDING WHEN HOUSE COMPLETES **********************************************************************/ -setInterval(() => { - const underConstruction = buildings.find(b => b.buildingType === "House" && !b.completed); - if (!underConstruction && cityStorage.wood >= HOUSE_WOOD_REQUIRED) { - cityStorage.wood -= HOUSE_WOOD_REQUIRED; - const x = randInt(-500, 500); - const y = randInt(-500, 500); - const site = createHouseSite(x, y); - buildings.push(site); - logAction(`A new House site placed at (${x}, ${y}).`); - } -}, 5000); +function maybeBuildRoad(newHouse){ + let otherHouses=buildings.filter(b=>b.buildingType==="House" && b.completed && b!==newHouse); + if(otherHouses.length===0) return; + let nearest=null; + let minD=Infinity; + otherHouses.forEach((oh)=>{ + let d=distance(newHouse.x,newHouse.y,oh.x,oh.y); + if(d<minD){ + minD=d; + nearest=oh; + } + }); + if(!nearest)return; + let road=createRoadSite(newHouse.x,newHouse.y, nearest.x, nearest.y); + buildings.push(road); + logAction(`A Road site created between House@(${newHouse.x},${newHouse.y}) & House@(${nearest.x},${nearest.y}).`); +} /********************************************************************** * DEPOSIT WOOD LOGIC **********************************************************************/ -const originalAssignNewTask = assignNewTask; -assignNewTask = function(cit) { - if (cit.profession === "Builder") { - const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood); - if (cit.carryingWood > 0 && !buildingNeedingWood) { - const distToCenter = distance(cit.x, cit.y, 0, 0); - if (distToCenter < 20) { - cityStorage.wood += cit.carryingWood; +const originalAssignNewTask=assignNewTask; +assignNewTask=function(cit){ + if(cit.profession==="Builder"){ + let buildingNeedingWood=buildings.find(b=>!b.completed&&b.deliveredWood<b.requiredWood); + if(cit.carryingWood>0 && !buildingNeedingWood){ + // deposit wood to city center (0,0) + let d=distance(cit.x,cit.y,0,0); + if(d<20){ + cityStorage.wood+=cit.carryingWood; logAction(`${cit.name} deposited ${cit.carryingWood} wood into city storage.`); - cit.carryingWood = 0; + cit.carryingWood=0; } else { - moveToward(cit, 0, 0, 0.4); - cit.task = null; - cit.target = null; + moveToward(cit,0,0,0.4); + cit.task=null; + cit.target=null; return; } } @@ -919,189 +967,185 @@ assignNewTask = function(cit) { /********************************************************************** * RENDER **********************************************************************/ -function drawWorld() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Grid +function drawWorld(){ + ctx.clearRect(0,0,canvas.width,canvas.height); drawGrid(); - // Resources - resources.forEach((res) => drawResource(res)); + // resources + resources.forEach((res)=>drawResource(res)); - // Roads - buildings - .filter(b => b.buildingType === "Road") - .forEach(b => drawRoad(b)); + // roads + buildings.filter(b=>b.buildingType==="Road").forEach(b=>drawRoad(b)); + // houses + buildings.filter(b=>b.buildingType==="House").forEach(b=>drawHouse(b)); - // Houses - buildings - .filter(b => b.buildingType === "House") - .forEach(b => drawHouse(b)); - - // City storage + // city storage drawCityStorage(); - // Citizens - citizens.forEach((cit) => drawCitizen(cit)); + // citizens + citizens.forEach((cit)=>drawCitizen(cit)); - // Animals - animals.forEach((ani) => drawAnimal(ani)); + // animals + animals.forEach((ani)=>{ + if(!ani.dead) drawAnimal(ani); + }); } -function drawGrid() { +function drawGrid(){ ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); ctx.scale(scale, scale); - ctx.strokeStyle = "#ccc"; - ctx.lineWidth = 1 / scale; - - const range = 2000; - for (let x = -range; x <= range; x += 100) { + ctx.strokeStyle="#ccc"; + ctx.lineWidth=1/scale; + let range=2000; + for(let x=-range;x<=range;x+=100){ ctx.beginPath(); - ctx.moveTo(x, -range); - ctx.lineTo(x, range); + ctx.moveTo(x,-range); + ctx.lineTo(x,range); ctx.stroke(); } - for (let y = -range; y <= range; y += 100) { + for(let y=-range;y<=range;y+=100){ ctx.beginPath(); - ctx.moveTo(-range, y); - ctx.lineTo(range, y); + ctx.moveTo(-range,y); + ctx.lineTo(range,y); ctx.stroke(); } ctx.restore(); } -function drawResource(res) { - if (res.amount <= 0) return; +function drawResource(res){ + if(res.amount<=0)return; ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); ctx.scale(scale, scale); - if (res.type === "Tree") { - ctx.fillStyle = "#228B22"; + if(res.type==="Tree"){ + ctx.fillStyle="#228B22"; ctx.beginPath(); - ctx.arc(res.x, res.y, 10, 0, Math.PI * 2); + ctx.arc(res.x,res.y,10,0,Math.PI*2); ctx.fill(); - ctx.fillStyle = "#000"; - ctx.font = "12px sans-serif"; - ctx.fillText(`Tree (${res.amount})`, res.x - 20, res.y - 12); - } else if (res.type === "FruitTree") { - ctx.fillStyle = "#FF6347"; - ctx.beginPath(); - ctx.arc(res.x, res.y, 10, 0, Math.PI * 2); - ctx.fill(); - ctx.fillStyle = "#000"; - ctx.font = "12px sans-serif"; - ctx.fillText(`Fruit (${res.amount})`, res.x - 25, res.y - 12); - } - ctx.restore(); -} - -function drawHouse(b) { - ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); - ctx.scale(scale, scale); - - if (!b.completed) { - ctx.strokeStyle = "#FF8C00"; - ctx.lineWidth = 2 / scale; - ctx.strokeRect(b.x - 15, b.y - 15, 30, 30); - - ctx.fillStyle = "#000"; - ctx.font = "12px sans-serif"; - ctx.fillText(`House (Building)`, b.x - 30, b.y - 25); - ctx.fillText(`Wood: ${b.deliveredWood}/${b.requiredWood}`, b.x - 30, b.y + 30); - ctx.fillText(`Progress: ${Math.floor(b.buildProgress)}%`, b.x - 30, b.y + 42); + ctx.fillStyle="#000"; + ctx.font="12px sans-serif"; + ctx.fillText(`Tree(${res.amount})`, res.x-20, res.y-12); } else { - ctx.fillStyle = "#DAA520"; - ctx.fillRect(b.x - 15, b.y - 15, 30, 30); - - ctx.fillStyle = "#000"; - ctx.font = "12px sans-serif"; - ctx.fillText(`House`, b.x - 15, b.y - 20); - ctx.fillText(`Fruit: ${b.storedFruit}/${b.maxFruit}`, b.x - 25, b.y + 32); + ctx.fillStyle="#FF6347"; + ctx.beginPath(); + ctx.arc(res.x,res.y,10,0,Math.PI*2); + ctx.fill(); + ctx.fillStyle="#000"; + ctx.font="12px sans-serif"; + ctx.fillText(`Fruit(${res.amount})`, res.x-25,res.y-12); } ctx.restore(); } -function drawRoad(b) { +function drawHouse(b){ ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); ctx.scale(scale, scale); - if (!b.completed) { - ctx.setLineDash([5, 5]); - ctx.strokeStyle = "#888"; + if(!b.completed){ + ctx.strokeStyle="#FF8C00"; + ctx.lineWidth=2/scale; + ctx.strokeRect(b.x-15,b.y-15,30,30); + + ctx.fillStyle="#000"; + ctx.font="12px sans-serif"; + ctx.fillText(`House(Bldg)`, b.x-30, b.y-25); + ctx.fillText(`Wood:${b.deliveredWood}/${b.requiredWood}`, b.x-30, b.y+30); + ctx.fillText(`Progress:${Math.floor(b.buildProgress)}%`, b.x-30, b.y+42); + } else { + ctx.fillStyle="#DAA520"; + ctx.fillRect(b.x-15,b.y-15,30,30); + + ctx.fillStyle="#000"; + ctx.font="12px sans-serif"; + ctx.fillText("House", b.x-15,b.y-20); + ctx.fillText(`Fruit:${b.storedFruit}/${b.maxFruit}`, b.x-25,b.y+32); + } + + ctx.restore(); +} + +function drawRoad(b){ + ctx.save(); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); + ctx.scale(scale, scale); + + if(!b.completed){ + ctx.setLineDash([5,5]); + ctx.strokeStyle="#888"; } else { ctx.setLineDash([]); - ctx.strokeStyle = "#444"; + ctx.strokeStyle="#444"; } - ctx.lineWidth = 2 / scale; + ctx.lineWidth=2/scale; ctx.beginPath(); - ctx.moveTo(b.x1, b.y1); - ctx.lineTo(b.x2, b.y2); + ctx.moveTo(b.x1,b.y1); + ctx.lineTo(b.x2,b.y2); ctx.stroke(); - ctx.fillStyle = "#000"; - ctx.font = "12px sans-serif"; - if (!b.completed) { - ctx.fillText(`Road (Building) ${Math.floor(b.buildProgress)}%`, b.x, b.y - 15); - ctx.fillText(`Wood: ${b.deliveredWood}/${b.requiredWood}`, b.x - 20, b.y + 15); + ctx.fillStyle="#000"; + ctx.font="12px sans-serif"; + if(!b.completed){ + ctx.fillText(`Road(Bldg) ${Math.floor(b.buildProgress)}%`, b.x,b.y-15); + ctx.fillText(`Wood:${b.deliveredWood}/${b.requiredWood}`, b.x-20,b.y+15); } else { - ctx.fillText(`Road`, b.x - 10, b.y - 5); + ctx.fillText("Road", b.x-10,b.y-5); } + ctx.restore(); } -function drawCityStorage() { +function drawCityStorage(){ ctx.save(); - ctx.fillStyle = "#000"; - ctx.font = "16px sans-serif"; - ctx.fillText(`City Wood Storage: ${cityStorage.wood}`, 10, 20); + ctx.fillStyle="#000"; + ctx.font="16px sans-serif"; + ctx.fillText(`City Wood Storage: ${cityStorage.wood}`,10,20); ctx.restore(); } -function drawCitizen(cit) { +function drawCitizen(c){ ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); ctx.scale(scale, scale); - ctx.fillStyle = cit.color; + ctx.fillStyle=c.color; ctx.beginPath(); - ctx.arc(cit.x, cit.y, 7, 0, Math.PI * 2); + ctx.arc(c.x,c.y,7,0,Math.PI*2); ctx.fill(); - ctx.fillStyle = "#000"; - ctx.font = "10px sans-serif"; - const wpn = cit.hasWeapon ? "ARMED" : ""; - ctx.fillText(`${cit.name} [${cit.profession}] ${wpn}`, cit.x + 10, cit.y - 2); - ctx.fillText(`W:${cit.carryingWood} F:${cit.carryingFruit} H:${Math.floor(cit.hunger)} E:${Math.floor(cit.energy)}`, - cit.x + 10, cit.y + 10); + ctx.fillStyle="#000"; + ctx.font="10px sans-serif"; + let wpn=c.hasWeapon?"ARMED":""; + ctx.fillText(`${c.name}[${c.profession}]${wpn}`, c.x+10,c.y-2); + ctx.fillText(`W:${c.carryingWood} F:${c.carryingFruit} H:${Math.floor(c.hunger)} E:${Math.floor(c.energy)}`, + c.x+10,c.y+10); ctx.restore(); } -function drawAnimal(ani) { - if (ani.dead) return; +function drawAnimal(a){ + if(a.dead)return; ctx.save(); - ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); + ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY); ctx.scale(scale, scale); - if (ani.type === "Rabbit") { - ctx.fillStyle = "#999"; + if(a.type==="Rabbit"){ + ctx.fillStyle="#999"; } else { - ctx.fillStyle = "#555"; + ctx.fillStyle="#555"; } ctx.beginPath(); - ctx.arc(ani.x, ani.y, 6, 0, Math.PI * 2); + ctx.arc(a.x,a.y,6,0,Math.PI*2); ctx.fill(); - ctx.fillStyle = "#000"; - ctx.font = "10px sans-serif"; - ctx.fillText(ani.type, ani.x + 8, ani.y + 3); + ctx.fillStyle="#000"; + ctx.font="10px sans-serif"; + ctx.fillText(a.type, a.x+8,a.y+3); ctx.restore(); } @@ -1109,50 +1153,165 @@ function drawAnimal(ani) { /********************************************************************** * PAN & ZOOM **********************************************************************/ -canvas.addEventListener('mousedown', (e) => { - isDragging = true; - lastMouseX = e.clientX; - lastMouseY = e.clientY; +canvas.addEventListener('mousedown',(e)=>{ + isDragging=true; + lastMouseX=e.clientX; + lastMouseY=e.clientY; }); - -canvas.addEventListener('mouseup', () => { - isDragging = false; -}); - -canvas.addEventListener('mouseleave', () => { - isDragging = false; -}); - -canvas.addEventListener('mousemove', (e) => { - if (isDragging) { - const dx = e.clientX - lastMouseX; - const dy = e.clientY - lastMouseY; - offsetX += dx; - offsetY += dy; - lastMouseX = e.clientX; - lastMouseY = e.clientY; +canvas.addEventListener('mouseup',()=>{isDragging=false;}); +canvas.addEventListener('mouseleave',()=>{isDragging=false;}); +canvas.addEventListener('mousemove',(e)=>{ + if(isDragging){ + let dx=e.clientX-lastMouseX; + let dy=e.clientY-lastMouseY; + offsetX+=dx; + offsetY+=dy; + lastMouseX=e.clientX; + lastMouseY=e.clientY; } }); - -canvas.addEventListener('wheel', (e) => { +canvas.addEventListener('wheel',(e)=>{ e.preventDefault(); - const zoomSpeed = 0.001; - const delta = e.deltaY * zoomSpeed; - const oldScale = scale; - scale -= delta; - if (scale < 0.1) scale = 0.1; - if (scale > 5) scale = 5; + let zoomSpeed=0.001; + let delta=e.deltaY*zoomSpeed; + let oldScale=scale; + scale-=delta; + if(scale<0.1) scale=0.1; + if(scale>5) scale=5; - const mouseX = e.clientX - (canvas.width / 2 + offsetX); - const mouseY = e.clientY - (canvas.height / 2 + offsetY); - offsetX -= mouseX * (scale - oldScale); - offsetY -= mouseY * (scale - oldScale); -}, { passive: false }); + let mouseX=e.clientX-(canvas.width/2+offsetX); + let mouseY=e.clientY-(canvas.height/2+offsetY); + offsetX-=mouseX*(scale-oldScale); + offsetY-=mouseY*(scale-oldScale); +},{passive:false}); + +/********************************************************************** + * BUY MENU LOGIC + **********************************************************************/ +document.getElementById('buyHouseBtn').addEventListener('click',()=>{ + purchaseMode="House"; + logAction("Click on map to place a House site."); +}); +document.getElementById('buyRoadBtn').addEventListener('click',()=>{ + purchaseMode="Road"; + logAction("Click on map to place a Road site."); +}); +document.getElementById('buyBuilderBtn').addEventListener('click',()=>{ + purchaseMode="Builder"; + logAction("Click on map to place a new Builder citizen."); +}); +document.getElementById('buyFarmerBtn').addEventListener('click',()=>{ + purchaseMode="Farmer"; + logAction("Click on map to place a new Farmer citizen."); +}); +document.getElementById('buySpawnerBtn').addEventListener('click',()=>{ + purchaseMode="Spawner"; + logAction("Click on map to place a Spawner building (not implemented fully)."); +}); +document.getElementById('buyTreeBtn').addEventListener('click',()=>{ + purchaseMode="Tree"; + logAction("Click on map to place a new Tree."); +}); + +canvas.addEventListener('click',(e)=>{ + if(!purchaseMode)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; + + if(purchaseMode==="House"){ + if(money>=COST_HOUSE){ + addMoney(-COST_HOUSE, "Buy House"); + let site=createHouseSite(worldX,worldY); + buildings.push(site); + logAction(`Purchased House site @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy House!"); + } + } + else if(purchaseMode==="Road"){ + if(money>=COST_ROAD){ + addMoney(-COST_ROAD, "Buy Road"); + // For simplicity, we create a short horizontal road + let site=createRoadSite(worldX-50,worldY, worldX+50,worldY); + buildings.push(site); + logAction(`Purchased Road site @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Road!"); + } + } + else if(purchaseMode==="Builder"){ + if(money>=COST_BUILDER){ + addMoney(-COST_BUILDER, "Buy Builder"); + let c=createCitizen(randomName(), worldX,worldY, "Builder"); + citizens.push(c); + logAction(`Purchased new Builder @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Builder!"); + } + } + else if(purchaseMode==="Farmer"){ + if(money>=COST_FARMER){ + addMoney(-COST_FARMER, "Buy Farmer"); + let c=createCitizen(randomName(), worldX,worldY, "Farmer"); + citizens.push(c); + logAction(`Purchased new Farmer @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Farmer!"); + } + } + else if(purchaseMode==="Spawner"){ + if(money>=COST_SPAWNER){ + addMoney(-COST_SPAWNER, "Buy Spawner"); + // We'll treat a Spawner as a completed building that spawns new Citizens automatically + // For demonstration, let's keep it simple or expand later + let spawnBuild={ + buildingType:"Spawner", + x:worldX, y:worldY, + completed:true, + lastSpawnTime:Date.now() // can spawn 1 citizen/min + }; + buildings.push(spawnBuild); + logAction(`Purchased Spawner @(${Math.floor(worldX)},${Math.floor(worldY)}) + (not fully implemented, but can expand).`); + } else { + logAction("Not enough money to buy Spawner!"); + } + } + else if(purchaseMode==="Tree"){ + if(money>=COST_TREE){ + addMoney(-COST_TREE, "Buy Tree"); + let t=createResource("Tree", worldX,worldY, 100); + resources.push(t); + logAction(`Purchased a new Tree @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Tree!"); + } + } + + purchaseMode=null; +}); + +/********************************************************************** + * TOGGLE LOGS + **********************************************************************/ +document.getElementById('toggleLogsBtn').addEventListener('click',(e)=>{ + if(logContainer.style.display==="none"){ + logContainer.style.display="block"; + e.target.textContent="Hide Logs"; + } else { + logContainer.style.display="none"; + e.target.textContent="Show Logs"; + } +}); /********************************************************************** * START **********************************************************************/ initWorld(); +updateMoneyDisplay(); </script> </body> </html>