From 7285fc534b7bf87926f27849c2c6b9885c018ee9 Mon Sep 17 00:00:00 2001 From: "Kacper Kostka (aider)" Date: Tue, 1 Apr 2025 11:47:32 +0200 Subject: [PATCH] feat: Remove wolves, add markets, hospitals, schools, and new citizen types --- ai.js | 536 ++++++++++++++++++ entities.js | 120 ++++ events.js | 267 +++++++++ game.js | 215 +++++++ index.html | 1573 +++++++++------------------------------------------ render.js | 310 ++++++++++ utils.js | 177 ++++++ 7 files changed, 1881 insertions(+), 1317 deletions(-) create mode 100644 ai.js create mode 100644 entities.js create mode 100644 events.js create mode 100644 game.js create mode 100644 render.js create mode 100644 utils.js diff --git a/ai.js b/ai.js new file mode 100644 index 0000000..5938613 --- /dev/null +++ b/ai.js @@ -0,0 +1,536 @@ +/********************************************************************** + * CITIZEN AI + **********************************************************************/ +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)) { + cit.energy -= ENERGY_DECREMENT_WORK; + } else if(cit.task === "restAtHouse") { + cit.energy += ENERGY_INCREMENT_REST; + } else { + cit.energy -= 0.0005; + } + + if(cit.energy < 0) cit.energy = 0; + if(cit.energy > ENERGY_MAX) cit.energy = ENERGY_MAX; + + // Health decreases if hunger is high + if(cit.hunger > 80) { + cit.health -= 0.01; + if(cit.health < 0) cit.health = 0; + } + + if(!cit.task) assignNewTask(cit); + + switch(cit.task) { + case 'chop': chopTask(cit); break; + case 'deliverWood': deliverWoodTask(cit); break; + case 'build': buildTask(cit); break; + case 'gatherFruit': gatherFruitTask(cit); break; + case 'deliverFruit': deliverFruitTask(cit); break; + case 'plantFruitTree': plantFruitTreeTask(cit); break; + case 'eatAtHouse': eatAtHouseTask(cit); break; + case 'restAtHouse': restAtHouseTask(cit); break; + case 'treatPatients': treatPatientsTask(cit); break; + case 'teachStudents': teachStudentsTask(cit); break; + case 'sellGoods': sellGoodsTask(cit); break; + case 'visitHospital': visitHospitalTask(cit); break; + case 'visitSchool': visitSchoolTask(cit); break; + default: randomWander(cit); break; + } + + cit.x += cit.vx; + cit.y += cit.vy; +} + +/********************************************************************** + * ANIMAL AI + **********************************************************************/ +function updateAnimal(ani) { + if(ani.type === "Rabbit") { + updateRabbit(ani); + } + ani.x += ani.vx; + ani.y += ani.vy; + if(ani.reproductionCooldown > 0) { + ani.reproductionCooldown--; + } +} + +function updateRabbit(r) { + r.hunger += RABBIT_HUNGER_INCREMENT; + if(r.hunger >= ANIMAL_HUNGER_MAX) { + r.dead = true; + logAction("A rabbit starved to death."); + return; + } + // 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(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(r); + } + } else { + randomAnimalWander(r); + } +} + +/********************************************************************** + * TASKS & AI LOGIC + **********************************************************************/ +function assignNewTask(cit) { + // Check for critical needs first + if(cit.hunger >= HUNGER_THRESHOLD) { + let houseWithFruit = findHouseWithFruit(); + if(houseWithFruit) { + cit.task = "eatAtHouse"; + cit.target = houseWithFruit; + return; + } + } + + if(cit.energy <= ENERGY_THRESHOLD) { + let compHouse = findAnyCompletedHouse(); + if(compHouse) { + cit.task = "restAtHouse"; + cit.target = compHouse; + return; + } + } + + if(cit.health <= HEALTH_THRESHOLD) { + let hospital = findCompletedHospital(); + if(hospital) { + cit.task = "visitHospital"; + cit.target = hospital; + return; + } + } + + // Profession-specific tasks + switch(cit.profession) { + case "Builder": + builderTasks(cit); + break; + case "Farmer": + farmerTasks(cit); + break; + case "Merchant": + merchantTasks(cit); + break; + case "Doctor": + doctorTasks(cit); + break; + case "Teacher": + teacherTasks(cit); + break; + default: + cit.task = null; + cit.target = null; + } +} + +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; + } + let tree = findNearestResourceOfType(cit, "Tree"); + if(tree) { + cit.task = "chop"; + cit.target = tree; + return; + } + } + + let buildingToConstruct = buildings.find(b => !b.completed && b.deliveredWood >= b.requiredWood); + if(buildingToConstruct) { + cit.task = "build"; + cit.target = buildingToConstruct; + return; + } + + let anyTree = findNearestResourceOfType(cit, "Tree"); + if(anyTree) { + cit.task = "chop"; + cit.target = anyTree; + return; + } + + cit.task = null; + cit.target = null; +} + +function farmerTasks(cit) { + if(cit.carryingFruit > 0) { + let houseNeedFruit = findHouseNeedingFruit(); + if(houseNeedFruit) { + cit.task = "deliverFruit"; + cit.target = houseNeedFruit; + return; + } + } + + 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; + return; + } + + cit.task = null; + cit.target = null; +} + +function merchantTasks(cit) { + let market = findCompletedMarket(); + if(market) { + cit.task = "sellGoods"; + cit.target = market; + return; + } + + // If no market, help with gathering resources + if(Math.random() < 0.5) { + farmerTasks(cit); + } else { + builderTasks(cit); + } +} + +function doctorTasks(cit) { + let hospital = findCompletedHospital(); + if(hospital) { + cit.task = "treatPatients"; + cit.target = hospital; + return; + } + + // If no hospital, help with gathering resources + builderTasks(cit); +} + +function teacherTasks(cit) { + let school = findCompletedSchool(); + if(school) { + cit.task = "teachStudents"; + cit.target = school; + return; + } + + // If no school, help with gathering resources + builderTasks(cit); +} + +/********************************************************************** + * NEW TASK HANDLERS + **********************************************************************/ +function treatPatientsTask(cit) { + let hospital = cit.target; + if(!hospital || !hospital.completed) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, hospital.x, hospital.y, 0.3); + if(distance(cit.x, cit.y, hospital.x, hospital.y) < 20) { + // Generate medicine + if(frameCount % 100 === 0) { + if(hospital.medicine < hospital.maxMedicine) { + hospital.medicine++; + cityStorage.medicine++; + if(Math.random() < 0.1) { + logAction(`${cit.name} [Doctor] created medicine at the hospital.`); + } + } + } + } +} + +function teachStudentsTask(cit) { + let school = cit.target; + if(!school || !school.completed) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, school.x, school.y, 0.3); + if(distance(cit.x, cit.y, school.x, school.y) < 20) { + // Generate knowledge + if(frameCount % 100 === 0) { + if(school.knowledge < school.maxKnowledge) { + school.knowledge++; + cityStorage.knowledge++; + if(Math.random() < 0.1) { + logAction(`${cit.name} [Teacher] is teaching at the school.`); + } + } + } + } +} + +function sellGoodsTask(cit) { + let market = cit.target; + if(!market || !market.completed) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, market.x, market.y, 0.3); + if(distance(cit.x, cit.y, market.x, market.y) < 20) { + // Generate income occasionally + if(frameCount % 200 === 0 && Math.random() < 0.3) { + let income = randInt(5, 15); + addMoney(income, "Market sales"); + if(Math.random() < 0.1) { + logAction(`${cit.name} [Merchant] made $${income} at the market.`); + } + } + } +} + +function visitHospitalTask(cit) { + let hospital = cit.target; + if(!hospital || !hospital.completed) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, hospital.x, hospital.y, 0.4); + if(distance(cit.x, cit.y, hospital.x, hospital.y) < 20) { + if(hospital.medicine > 0 && cit.health < HEALTH_MAX) { + hospital.medicine--; + cit.health += 20; + if(cit.health > HEALTH_MAX) cit.health = HEALTH_MAX; + logAction(`${cit.name} received medical treatment. Health: ${Math.floor(cit.health)}`); + } + cit.task = null; + cit.target = null; + } +} + +function visitSchoolTask(cit) { + let school = cit.target; + if(!school || !school.completed) { + cit.task = null; + cit.target = null; + return; + } + + moveToward(cit, school.x, school.y, 0.4); + if(distance(cit.x, cit.y, school.x, school.y) < 20) { + if(school.knowledge > 0 && cit.education < EDUCATION_MAX) { + school.knowledge--; + cit.education += 10; + if(cit.education > EDUCATION_MAX) cit.education = EDUCATION_MAX; + logAction(`${cit.name} learned at school. Education: ${Math.floor(cit.education)}`); + } + cit.task = null; + cit.target = null; + } +} + +/********************************************************************** + * EXISTING 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) { + 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; + } + } +} + +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) { + 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; + } +} + +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); +} + +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) { + let canGather = cit.carryingCapacity - cit.carryingFruit; + let toGather = Math.min(FRUIT_GATHER_RATE, tree.amount, canGather); + tree.amount -= toGather; + cit.carryingFruit += toGather; + cityStorage.food += toGather / 2; // Half goes to city storage + if(Math.random() < 0.01) { + logAction(`${cit.name} [${cit.profession}] is gathering fruit...`); + } + if(cit.carryingFruit >= cit.carryingCapacity || tree.amount <= 0) { + 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) { + 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; + } +} + +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}[${cit.profession}] planted a new fruit tree!`); + } + 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) { + 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; + } +} + +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; + } + } +} + +/********************************************************************** + * 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 randomAnimalWander(a) { + if(Math.random() < 0.01) { + a.vx = (Math.random() - 0.5) * 0.4; + a.vy = (Math.random() - 0.5) * 0.4; + } +} diff --git a/entities.js b/entities.js new file mode 100644 index 0000000..44811b7 --- /dev/null +++ b/entities.js @@ -0,0 +1,120 @@ +/********************************************************************** + * ENTITY DEFINITIONS + **********************************************************************/ +function createCitizen(name, x, y, forcedProfession=null) { + // If forcedProfession is provided, use it, otherwise random from PROFESSIONS + 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, + carryingMedicine: 0, + carryingCapacity: 10, + hunger: 0, + energy: ENERGY_MAX, + health: HEALTH_MAX, + education: 0, + hasWeapon: false + }; +} + +function createResource(type, x, y, amount) { + return { type, x, y, amount }; +} + +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 + }; +} + +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 + }; +} + +function createMarketSite(x, y) { + return { + buildingType: "Market", + x, y, + requiredWood: MARKET_WOOD_REQUIRED, + deliveredWood: 0, + buildProgress: 0, + completed: false, + lastIncomeTime: 0 + }; +} + +function createHospitalSite(x, y) { + return { + buildingType: "Hospital", + x, y, + requiredWood: HOSPITAL_WOOD_REQUIRED, + deliveredWood: 0, + buildProgress: 0, + completed: false, + medicine: 0, + maxMedicine: 50 + }; +} + +function createSchoolSite(x, y) { + return { + buildingType: "School", + x, y, + requiredWood: SCHOOL_WOOD_REQUIRED, + deliveredWood: 0, + buildProgress: 0, + completed: false, + knowledge: 0, + maxKnowledge: 100 + }; +} + +function createAnimal(type, x, y) { + let cd = (type === "Rabbit") ? randInt(0, RABBIT_REPRO_COOLDOWN) : 0; + return { + type, + x, y, + vx: (Math.random() - 0.5) * 0.4, + vy: (Math.random() - 0.5) * 0.4, + hunger: 0, + dead: false, + reproductionCooldown: cd + }; +} + +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!`); +} diff --git a/events.js b/events.js new file mode 100644 index 0000000..48401e7 --- /dev/null +++ b/events.js @@ -0,0 +1,267 @@ +/********************************************************************** + * PAN & ZOOM + **********************************************************************/ +function setupPanZoom() { + 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) { + let dx = e.clientX - lastMouseX; + let dy = e.clientY - lastMouseY; + offsetX += dx; + offsetY += dy; + lastMouseX = e.clientX; + lastMouseY = e.clientY; + } + }); + + canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + 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; + + 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 + **********************************************************************/ +function setupBuyButtons() { + 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('buyMerchantBtn').addEventListener('click', () => { + purchaseMode = "Merchant"; + logAction("Click on map to place a new Merchant citizen."); + }); + + document.getElementById('buyDoctorBtn').addEventListener('click', () => { + purchaseMode = "Doctor"; + logAction("Click on map to place a new Doctor citizen."); + }); + + document.getElementById('buyTeacherBtn').addEventListener('click', () => { + purchaseMode = "Teacher"; + logAction("Click on map to place a new Teacher citizen."); + }); + + document.getElementById('buyMarketBtn').addEventListener('click', () => { + purchaseMode = "Market"; + logAction("Click on map to place a Market site."); + }); + + document.getElementById('buyHospitalBtn').addEventListener('click', () => { + purchaseMode = "Hospital"; + logAction("Click on map to place a Hospital site."); + }); + + document.getElementById('buySchoolBtn').addEventListener('click', () => { + purchaseMode = "School"; + logAction("Click on map to place a School site."); + }); + + document.getElementById('buySpawnerBtn').addEventListener('click', () => { + purchaseMode = "Spawner"; + logAction("Click on map to place a Spawner building."); + }); + + document.getElementById('buyTreeBtn').addEventListener('click', () => { + purchaseMode = "Tree"; + logAction("Click on map to place a new Tree."); + }); + + 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"; + } + }); +} + +function setupCanvasClick() { + 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; + + switch(purchaseMode) { + case "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!"); + } + break; + + case "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!"); + } + break; + + case "Market": + if(money >= COST_MARKET) { + addMoney(-COST_MARKET, "Buy Market"); + let site = createMarketSite(worldX, worldY); + buildings.push(site); + logAction(`Purchased Market site @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Market!"); + } + break; + + case "Hospital": + if(money >= COST_HOSPITAL) { + addMoney(-COST_HOSPITAL, "Buy Hospital"); + let site = createHospitalSite(worldX, worldY); + buildings.push(site); + logAction(`Purchased Hospital site @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Hospital!"); + } + break; + + case "School": + if(money >= COST_SCHOOL) { + addMoney(-COST_SCHOOL, "Buy School"); + let site = createSchoolSite(worldX, worldY); + buildings.push(site); + logAction(`Purchased School site @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy School!"); + } + break; + + case "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!"); + } + break; + + case "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!"); + } + break; + + case "Merchant": + if(money >= COST_MERCHANT) { + addMoney(-COST_MERCHANT, "Buy Merchant"); + let c = createCitizen(randomName(), worldX, worldY, "Merchant"); + citizens.push(c); + logAction(`Purchased new Merchant @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Merchant!"); + } + break; + + case "Doctor": + if(money >= COST_DOCTOR) { + addMoney(-COST_DOCTOR, "Buy Doctor"); + let c = createCitizen(randomName(), worldX, worldY, "Doctor"); + citizens.push(c); + logAction(`Purchased new Doctor @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Doctor!"); + } + break; + + case "Teacher": + if(money >= COST_TEACHER) { + addMoney(-COST_TEACHER, "Buy Teacher"); + let c = createCitizen(randomName(), worldX, worldY, "Teacher"); + citizens.push(c); + logAction(`Purchased new Teacher @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Teacher!"); + } + break; + + case "Spawner": + if(money >= COST_SPAWNER) { + addMoney(-COST_SPAWNER, "Buy Spawner"); + let spawnBuild = { + buildingType: "Spawner", + x: worldX, y: worldY, + completed: true, + lastSpawnTime: frameCount + }; + buildings.push(spawnBuild); + logAction(`Purchased Spawner @(${Math.floor(worldX)},${Math.floor(worldY)})`); + } else { + logAction("Not enough money to buy Spawner!"); + } + break; + + case "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!"); + } + break; + } + + purchaseMode = null; + }); +} diff --git a/game.js b/game.js new file mode 100644 index 0000000..842d3f2 --- /dev/null +++ b/game.js @@ -0,0 +1,215 @@ +/********************************************************************** + * GAME CORE + **********************************************************************/ +let frameCount = 0; +let money = 5000; // Start with 5000 +let purchaseMode = null; + +// Shared city storage (wood, etc.) +const cityStorage = { + wood: 0, + food: 0, + medicine: 0, + knowledge: 0 +}; + +// Arrays +let resources = []; // Trees, FruitTrees +let buildings = []; // House, Road, Market, Hospital, School +let citizens = []; +let animals = []; + +/********************************************************************** + * COSTS & EARNINGS + **********************************************************************/ +const COST_HOUSE = 300; +const COST_ROAD = 150; +const COST_BUILDER = 100; +const COST_FARMER = 100; +const COST_MERCHANT = 150; +const COST_DOCTOR = 200; +const COST_TEACHER = 180; +const COST_MARKET = 400; +const COST_HOSPITAL = 500; +const COST_SCHOOL = 450; +const COST_SPAWNER = 500; +const COST_TREE = 50; + +// Earn money +const REWARD_DELIVER_WOOD = 5; +const REWARD_BUILD_COMPLETE = 20; +const REWARD_MARKET_INCOME = 15; +const REWARD_EDUCATION = 10; + +/********************************************************************** + * STATS & CONSTANTS + **********************************************************************/ +// Hunger & energy +const HUNGER_MAX = 100; +const ENERGY_MAX = 100; +const HEALTH_MAX = 100; +const EDUCATION_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 HEALTH_THRESHOLD = 40; + +// 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; + +// Market, Hospital, School +const MARKET_WOOD_REQUIRED = 60; +const MARKET_BUILD_RATE = 0.15; + +const HOSPITAL_WOOD_REQUIRED = 70; +const HOSPITAL_BUILD_RATE = 0.1; + +const SCHOOL_WOOD_REQUIRED = 65; +const SCHOOL_BUILD_RATE = 0.12; + +// Professions +const PROFESSIONS = ["Farmer", "Builder", "Merchant", "Doctor", "Teacher"]; + +// Rabbits +const STARTING_RABBITS = 10; +const RABBIT_HUNGER_INCREMENT = 0.003; +const RABBIT_REPRO_COOLDOWN = 3000; +const RABBIT_REPRO_CHANCE = 0.0005; + +/********************************************************************** + * 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)); + } + + // 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 + for(let i=0; i updateCitizen(cit)); + + // Animals + animals.forEach((ani) => { + if(!ani.dead) updateAnimal(ani); + }); + animals = animals.filter(a => !a.dead); + + // 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 new citizen is born: ${baby.name} [${baby.profession}]`); + } + + // Buildings + buildings.forEach((b) => { + if(!b.completed && b.deliveredWood >= b.requiredWood) { + let buildRate; + switch(b.buildingType) { + case "Road": buildRate = ROAD_BUILD_RATE; break; + case "House": buildRate = HOUSE_BUILD_RATE; break; + case "Market": buildRate = MARKET_BUILD_RATE; break; + case "Hospital": buildRate = HOSPITAL_BUILD_RATE; break; + case "School": buildRate = SCHOOL_BUILD_RATE; break; + default: buildRate = 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 @(${Math.floor(b.x)},${Math.floor(b.y)})!`); + maybeBuildRoad(b); + } else if(b.buildingType === "Road") { + logAction(`A Road has been completed!`); + } else if(b.buildingType === "Market") { + logAction(`A Market has been completed! Will generate income.`); + } else if(b.buildingType === "Hospital") { + logAction(`A Hospital has been completed! Citizens can heal here.`); + } else if(b.buildingType === "School") { + logAction(`A School has been completed! Citizens can learn here.`); + } + } + } + + // Special building functions + if(b.completed) { + if(b.buildingType === "Market" && frameCount % 500 === 0) { + addMoney(REWARD_MARKET_INCOME, "Market income"); + } + if(b.buildingType === "School" && frameCount % 800 === 0) { + addMoney(REWARD_EDUCATION, "Education benefits"); + } + } + }); + + drawWorld(); + updateHUD(); + requestAnimationFrame(update); +} + +function updateHUD() { + // Update money and citizen count + moneyDisplay.textContent = `Money: $${money}`; + citizenCountDisplay.textContent = `Citizens: ${citizens.length}`; + + // Update resource counts + woodDisplay.textContent = `Wood: ${cityStorage.wood}`; + foodDisplay.textContent = `Food: ${cityStorage.food}`; + medicineDisplay.textContent = `Medicine: ${cityStorage.medicine}`; + knowledgeDisplay.textContent = `Knowledge: ${cityStorage.knowledge}`; + + // Update building counts + 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; + + buildingCountsDisplay.textContent = `Buildings: 🏠${houseCount} 🏪${marketCount} 🏥${hospitalCount} 🏫${schoolCount}`; + + // Update profession counts + const builderCount = citizens.filter(c => c.profession === "Builder").length; + const farmerCount = citizens.filter(c => c.profession === "Farmer").length; + const merchantCount = citizens.filter(c => c.profession === "Merchant").length; + const doctorCount = citizens.filter(c => c.profession === "Doctor").length; + const teacherCount = citizens.filter(c => c.profession === "Teacher").length; + + professionCountsDisplay.textContent = `Citizens: 👷${builderCount} 🌾${farmerCount} 💰${merchantCount} 💉${doctorCount} 📚${teacherCount}`; +} diff --git a/index.html b/index.html index a569d0c..592205e 100644 --- a/index.html +++ b/index.html @@ -1,1317 +1,256 @@ - - - - - Virtual World - - - - - -
-

Money: $0

-

Citizens: 0

-
- -
- - - - -
-
- - - - + + + + + Virtual World + + + + + +
+

Money: $0

+

Citizens: 0

+

Wood: 0

+

Food: 0

+

Medicine: 0

+

Knowledge: 0

+
+ + +
+

Buildings: 🏠0 🏪0 🏥0 🏫0

+

Citizens: 👷0 🌾0 💰0 💉0 📚0

+
+ +
+ + + + +
+
+ + + + + + + + + + + diff --git a/render.js b/render.js new file mode 100644 index 0000000..9f6fca0 --- /dev/null +++ b/render.js @@ -0,0 +1,310 @@ +/********************************************************************** + * RENDER + **********************************************************************/ +function drawWorld() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawGrid(); + + // resources + resources.forEach((res) => drawResource(res)); + + // roads + buildings.filter(b => b.buildingType === "Road").forEach(b => drawRoad(b)); + + // buildings + buildings.filter(b => b.buildingType === "House").forEach(b => drawHouse(b)); + buildings.filter(b => b.buildingType === "Market").forEach(b => drawMarket(b)); + buildings.filter(b => b.buildingType === "Hospital").forEach(b => drawHospital(b)); + buildings.filter(b => b.buildingType === "School").forEach(b => drawSchool(b)); + + // city storage + drawCityStorage(); + + // citizens + citizens.forEach((cit) => drawCitizen(cit)); + + // animals + animals.forEach((ani) => { + if(!ani.dead) drawAnimal(ani); + }); +} + +function drawGrid() { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + 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.stroke(); + } + for(let y = -range; y <= range; y += 100) { + ctx.beginPath(); + ctx.moveTo(-range, y); + ctx.lineTo(range, y); + ctx.stroke(); + } + ctx.restore(); +} + +function drawResource(res) { + if(res.amount <= 0) return; + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + if(res.type === "Tree") { + ctx.fillStyle = "#228B22"; + 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(`Tree(${res.amount})`, res.x-20, res.y-12); + } else { + 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(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 drawMarket(b) { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + if(!b.completed) { + ctx.strokeStyle = "#4682B4"; + ctx.lineWidth = 2/scale; + ctx.strokeRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText(`Market(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 = "#4682B4"; + ctx.fillRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText("Market", b.x-15, b.y-25); + ctx.fillText("💰", b.x-5, b.y+5); + } + + ctx.restore(); +} + +function drawHospital(b) { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + if(!b.completed) { + ctx.strokeStyle = "#FF6347"; + ctx.lineWidth = 2/scale; + ctx.strokeRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText(`Hospital(Bldg)`, b.x-35, 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 = "#FF6347"; + ctx.fillRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#fff"; + ctx.font = "20px sans-serif"; + ctx.fillText("🏥", b.x-10, b.y+5); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText("Hospital", b.x-20, b.y-25); + ctx.fillText(`Med:${b.medicine}/${b.maxMedicine}`, b.x-25, b.y+32); + } + + ctx.restore(); +} + +function drawSchool(b) { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + if(!b.completed) { + ctx.strokeStyle = "#9370DB"; + ctx.lineWidth = 2/scale; + ctx.strokeRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText(`School(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 = "#9370DB"; + ctx.fillRect(b.x-20, b.y-20, 40, 40); + + ctx.fillStyle = "#fff"; + ctx.font = "20px sans-serif"; + ctx.fillText("📚", b.x-10, b.y+5); + + ctx.fillStyle = "#000"; + ctx.font = "12px sans-serif"; + ctx.fillText("School", b.x-15, b.y-25); + ctx.fillText(`Know:${b.knowledge}/${b.maxKnowledge}`, b.x-30, 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.lineWidth = 2/scale; + + ctx.beginPath(); + 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(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.restore(); +} + +function drawCityStorage() { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + // Draw city center + ctx.fillStyle = "#333"; + ctx.beginPath(); + ctx.arc(0, 0, 15, 0, Math.PI*2); + ctx.fill(); + + ctx.fillStyle = "#fff"; + ctx.font = "16px sans-serif"; + ctx.fillText("🏛️", -8, 5); + + ctx.restore(); +} + +function drawCitizen(c) { + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + ctx.fillStyle = c.color; + ctx.beginPath(); + ctx.arc(c.x, c.y, 7, 0, Math.PI*2); + ctx.fill(); + + // Profession icon + let icon = "👤"; + switch(c.profession) { + case "Builder": icon = "👷"; break; + case "Farmer": icon = "🌾"; break; + case "Merchant": icon = "💰"; break; + case "Doctor": icon = "💉"; break; + case "Teacher": icon = "📚"; break; + } + + ctx.fillStyle = "#000"; + ctx.font = "10px sans-serif"; + ctx.fillText(`${c.name} ${icon}`, c.x+10, c.y-2); + + // Show stats + let stats = `W:${c.carryingWood} F:${c.carryingFruit} H:${Math.floor(c.hunger)} E:${Math.floor(c.energy)}`; + if (c.profession === "Doctor") { + stats += ` M:${c.carryingMedicine}`; + } + if (c.health < HEALTH_MAX) { + stats += ` ❤️:${Math.floor(c.health)}`; + } + if (c.education > 0) { + stats += ` 📖:${Math.floor(c.education)}`; + } + + ctx.fillText(stats, c.x+10, c.y+10); + + ctx.restore(); +} + +function drawAnimal(a) { + if(a.dead) return; + ctx.save(); + ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY); + ctx.scale(scale, scale); + + if(a.type === "Rabbit") { + ctx.fillStyle = "#999"; + ctx.beginPath(); + ctx.arc(a.x, a.y, 6, 0, Math.PI*2); + ctx.fill(); + + ctx.fillStyle = "#000"; + ctx.font = "10px sans-serif"; + ctx.fillText("🐰", a.x+8, a.y+3); + } + + ctx.restore(); +} diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..9f3c33b --- /dev/null +++ b/utils.js @@ -0,0 +1,177 @@ +/********************************************************************** + * LOGGING & MONEY + **********************************************************************/ +function logAction(text) { + const entry = document.createElement('div'); + entry.className = 'log-entry'; + entry.textContent = text; + logContainer.appendChild(entry); + logContainer.scrollTop = logContainer.scrollHeight; +} + +function addMoney(amount, reason="") { + money += amount; + if(money < 0) money = 0; + updateMoneyDisplay(); + if(reason) { + logAction(`Money ${amount >= 0 ? '+' : ''}$${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"]; + +function randomName() { + 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; +} + +/********************************************************************** + * MOVE & DISTANCE + **********************************************************************/ +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; + } +} + +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; + } +} + +/********************************************************************** + * FINDERS + **********************************************************************/ +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; + } + } + }); + 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 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); +} + +function findCompletedMarket() { + return buildings.find(b => b.buildingType === "Market" && b.completed); +} + +function findCompletedHospital() { + return buildings.find(b => b.buildingType === "Hospital" && b.completed); +} + +function findCompletedSchool() { + return buildings.find(b => b.buildingType === "School" && b.completed); +} + +/********************************************************************** + * ROAD BUILDING WHEN HOUSE COMPLETES + **********************************************************************/ +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@(${Math.floor(newHouse.x)},${Math.floor(newHouse.y)}) & House@(${Math.floor(nearest.x)},${Math.floor(nearest.y)}).`); +} + +/********************************************************************** + * DEPOSIT WOOD LOGIC + **********************************************************************/ +function depositWoodToStorage(cit) { + 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; + return true; + } else { + moveToward(cit, 0, 0, 0.4); + return true; + } + } + return false; +}