feat: Remove wolves, add markets, hospitals, schools, and new citizen types

This commit is contained in:
Kacper Kostka (aider) 2025-04-01 11:47:32 +02:00
parent eba5a0503a
commit 7285fc534b
7 changed files with 1881 additions and 1317 deletions

536
ai.js Normal file
View File

@ -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;
}
}

120
entities.js Normal file
View File

@ -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!`);
}

267
events.js Normal file
View File

@ -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;
});
}

215
game.js Normal file
View File

@ -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<STARTING_RABBITS; i++) {
animals.push(createAnimal("Rabbit", randInt(-1000,1000), randInt(-1000,1000)));
}
requestAnimationFrame(update);
}
/**********************************************************************
* UPDATE LOOP
**********************************************************************/
function update() {
frameCount++;
// Citizens
citizens.forEach((cit) => 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}`;
}

1573
index.html

File diff suppressed because it is too large Load Diff

310
render.js Normal file
View File

@ -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();
}

177
utils.js Normal file
View File

@ -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;
}