Compare commits
17 Commits
eba5a0503a
...
854ac99f52
Author | SHA1 | Date | |
---|---|---|---|
|
854ac99f52 | ||
|
f9669c9871 | ||
|
ae26d93432 | ||
|
a4ef5e0f40 | ||
|
3fa8a695b3 | ||
|
cde5301c2b | ||
|
1ce5abc901 | ||
|
d506ee1714 | ||
|
8bc13472aa | ||
|
74f1c1eac1 | ||
|
413aebf839 | ||
|
5d2a98b494 | ||
|
a002d4b017 | ||
|
8057ba881b | ||
|
be20e824f3 | ||
|
472a210cd7 | ||
|
7285fc534b |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.aider*
|
752
ai.js
Normal file
752
ai.js
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* ANIMAL CONSTANTS
|
||||||
|
**********************************************************************/
|
||||||
|
const ANIMAL_HUNGER_MAX = 100;
|
||||||
|
const WOLF_HUNGER_INCREMENT = 0.01;
|
||||||
|
const WOLF_DAMAGE = 10;
|
||||||
|
const WOLF_ATTACK_RANGE = 15;
|
||||||
|
const WOLF_ATTACK_COOLDOWN = 100;
|
||||||
|
const WOLF_REPRO_CHANCE = 0.00008; // Much lower than rabbits
|
||||||
|
const WOLF_REPRO_COOLDOWN = 8000; // Longer cooldown than rabbits
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* 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", "huntWolf", "patrol"].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;
|
||||||
|
case 'huntWolf': huntWolfTask(cit); break;
|
||||||
|
case 'patrol': patrolTask(cit); break;
|
||||||
|
default: randomWander(cit); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cit.x += cit.vx;
|
||||||
|
cit.y += cit.vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWolf(w) {
|
||||||
|
w.hunger += WOLF_HUNGER_INCREMENT;
|
||||||
|
if(w.hunger >= ANIMAL_HUNGER_MAX) {
|
||||||
|
w.dead = true;
|
||||||
|
logAction("A wolf starved to death.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for prey
|
||||||
|
if(w.hunger > 30) {
|
||||||
|
// First try to find rabbits
|
||||||
|
let rabbit = findNearestAnimalOfType(w, "Rabbit");
|
||||||
|
if(rabbit && distance(w.x, w.y, rabbit.x, rabbit.y) < 200) {
|
||||||
|
moveToward(w, rabbit.x, rabbit.y, WOLF_SPEED);
|
||||||
|
|
||||||
|
// Attack if close enough and cooldown is ready
|
||||||
|
if(distance(w.x, w.y, rabbit.x, rabbit.y) < WOLF_ATTACK_RANGE && w.attackCooldown <= 0) {
|
||||||
|
rabbit.dead = true;
|
||||||
|
w.hunger -= 30;
|
||||||
|
if(w.hunger < 0) w.hunger = 0;
|
||||||
|
w.attackCooldown = WOLF_ATTACK_COOLDOWN;
|
||||||
|
logAction("A wolf caught and ate a rabbit!");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no rabbits nearby, look for citizens (but not if soldiers are nearby)
|
||||||
|
let nearbySoldier = citizens.find(c =>
|
||||||
|
c.profession === "Soldier" &&
|
||||||
|
distance(w.x, w.y, c.x, c.y) < 100
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!nearbySoldier) {
|
||||||
|
let citizen = citizens.find(c => distance(w.x, w.y, c.x, c.y) < 150);
|
||||||
|
if(citizen) {
|
||||||
|
moveToward(w, citizen.x, citizen.y, WOLF_SPEED);
|
||||||
|
|
||||||
|
// Attack if close enough and cooldown is ready
|
||||||
|
if(distance(w.x, w.y, citizen.x, citizen.y) < WOLF_ATTACK_RANGE && w.attackCooldown <= 0) {
|
||||||
|
citizen.health -= WOLF_DAMAGE;
|
||||||
|
w.attackCooldown = WOLF_ATTACK_COOLDOWN;
|
||||||
|
|
||||||
|
// Knockback effect
|
||||||
|
knockback(citizen, w.x, w.y, 20);
|
||||||
|
|
||||||
|
logAction(`A wolf attacked ${citizen.name}! Health: ${Math.floor(citizen.health)}`);
|
||||||
|
|
||||||
|
if(citizen.health <= 0) {
|
||||||
|
// Remove citizen
|
||||||
|
citizens = citizens.filter(c => c !== citizen);
|
||||||
|
logAction(`${citizen.name} was killed by a wolf!`);
|
||||||
|
w.hunger -= 50;
|
||||||
|
if(w.hunger < 0) w.hunger = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Run away from soldier
|
||||||
|
moveAway(w, nearbySoldier.x, nearbySoldier.y, WOLF_SPEED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce - wolves need to be well-fed and have low hunger to reproduce
|
||||||
|
if(w.hunger < 20 && w.reproductionCooldown <= 0) {
|
||||||
|
// Find another wolf nearby for mating
|
||||||
|
let nearbyWolf = findNearestAnimalOfType(w, "Wolf");
|
||||||
|
if(nearbyWolf && nearbyWolf !== w && distance(w.x, w.y, nearbyWolf.x, nearbyWolf.y) < 50) {
|
||||||
|
if(Math.random() < WOLF_REPRO_CHANCE) {
|
||||||
|
spawnBabyAnimal("Wolf", w.x, w.y);
|
||||||
|
w.reproductionCooldown = WOLF_REPRO_COOLDOWN;
|
||||||
|
logAction("A wolf pack has grown - a new wolf pup was born!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random movement
|
||||||
|
randomAnimalWander(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* ANIMAL AI
|
||||||
|
**********************************************************************/
|
||||||
|
function updateAnimal(ani) {
|
||||||
|
if(ani.type === "Rabbit") {
|
||||||
|
updateRabbit(ani);
|
||||||
|
} else if(ani.type === "Wolf") {
|
||||||
|
updateWolf(ani);
|
||||||
|
}
|
||||||
|
|
||||||
|
ani.x += ani.vx;
|
||||||
|
ani.y += ani.vy;
|
||||||
|
if(ani.reproductionCooldown > 0) {
|
||||||
|
ani.reproductionCooldown--;
|
||||||
|
}
|
||||||
|
if(ani.attackCooldown > 0) {
|
||||||
|
ani.attackCooldown--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRabbit(r) {
|
||||||
|
r.hunger += RABBIT_HUNGER_INCREMENT;
|
||||||
|
if(r.hunger >= ANIMAL_HUNGER_MAX) {
|
||||||
|
r.dead = true;
|
||||||
|
logAction("A rabbit starved to death.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nearby wolves and run away
|
||||||
|
let nearestWolf = findNearestAnimalOfType(r, "Wolf");
|
||||||
|
if(nearestWolf && distance(r.x, r.y, nearestWolf.x, nearestWolf.y) < 100) {
|
||||||
|
// Run away from wolf
|
||||||
|
moveAway(r, nearestWolf.x, nearestWolf.y, 0.6);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Reproduce
|
||||||
|
if(r.hunger < 50 && r.reproductionCooldown <= 0) {
|
||||||
|
if(Math.random() < RABBIT_REPRO_CHANCE) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For soldiers, check for wolves to hunt
|
||||||
|
if(cit.profession === "Soldier") {
|
||||||
|
let nearbyWolf = findNearestAnimalOfType(cit, "Wolf");
|
||||||
|
if(nearbyWolf && distance(cit.x, cit.y, nearbyWolf.x, nearbyWolf.y) < 300) {
|
||||||
|
cit.task = "huntWolf";
|
||||||
|
cit.target = nearbyWolf;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profession-specific tasks
|
||||||
|
switch(cit.profession) {
|
||||||
|
case "Builder":
|
||||||
|
builderTasks(cit);
|
||||||
|
break;
|
||||||
|
case "Farmer":
|
||||||
|
farmerTasks(cit);
|
||||||
|
break;
|
||||||
|
case "Merchant":
|
||||||
|
merchantTasks(cit);
|
||||||
|
break;
|
||||||
|
case "Doctor":
|
||||||
|
doctorTasks(cit);
|
||||||
|
break;
|
||||||
|
case "Teacher":
|
||||||
|
teacherTasks(cit);
|
||||||
|
break;
|
||||||
|
case "Soldier":
|
||||||
|
soldierTasks(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function soldierTasks(cit) {
|
||||||
|
// Patrol around the city center
|
||||||
|
if(Math.random() < 0.1) {
|
||||||
|
cit.task = "patrol";
|
||||||
|
cit.target = {
|
||||||
|
x: randInt(-200, 200),
|
||||||
|
y: randInt(-200, 200)
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help with building when not patrolling
|
||||||
|
builderTasks(cit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* NEW TASK HANDLERS
|
||||||
|
**********************************************************************/
|
||||||
|
function huntWolfTask(cit) {
|
||||||
|
let wolf = cit.target;
|
||||||
|
if(!wolf || wolf.dead) {
|
||||||
|
cit.task = null;
|
||||||
|
cit.target = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToward(cit, wolf.x, wolf.y, 0.5);
|
||||||
|
|
||||||
|
// Attack if close enough
|
||||||
|
if(distance(cit.x, cit.y, wolf.x, wolf.y) < 20) {
|
||||||
|
wolf.dead = true;
|
||||||
|
logAction(`${cit.name} [Soldier] killed a wolf!`);
|
||||||
|
cit.task = null;
|
||||||
|
cit.target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function patrolTask(cit) {
|
||||||
|
let target = cit.target;
|
||||||
|
if(!target) {
|
||||||
|
cit.task = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToward(cit, target.x, target.y, 0.3);
|
||||||
|
|
||||||
|
// When reached patrol point, find a new task
|
||||||
|
if(distance(cit.x, cit.y, target.x, target.y) < 10) {
|
||||||
|
cit.task = null;
|
||||||
|
cit.target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function treatPatientsTask(cit) {
|
||||||
|
let hospital = cit.target;
|
||||||
|
if(!hospital || !hospital.completed) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nearby wolves and run if not a soldier
|
||||||
|
if(cit.profession !== "Soldier") {
|
||||||
|
let nearbyWolf = findNearestAnimalOfType(cit, "Wolf");
|
||||||
|
if(nearbyWolf && distance(cit.x, cit.y, nearbyWolf.x, nearbyWolf.y) < 100) {
|
||||||
|
// Run away from wolf
|
||||||
|
moveAway(cit, nearbyWolf.x, nearbyWolf.y, 0.6);
|
||||||
|
if(Math.random() < 0.1) {
|
||||||
|
logAction(`${cit.name} is running away from a wolf!`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moveToward(cit, tree.x, tree.y, 0.4);
|
||||||
|
if(distance(cit.x, cit.y, tree.x, tree.y) < 10) {
|
||||||
|
let canGather = cit.carryingCapacity - cit.carryingWood;
|
||||||
|
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) {
|
||||||
|
// Try to find a valid land position nearby
|
||||||
|
let px, py;
|
||||||
|
let attempts = 0;
|
||||||
|
do {
|
||||||
|
px = cit.x + randInt(-50, 50);
|
||||||
|
py = cit.y + randInt(-50, 50);
|
||||||
|
attempts++;
|
||||||
|
// Give up after too many attempts and find a new task
|
||||||
|
if (attempts > 20) {
|
||||||
|
cit.task = null;
|
||||||
|
cit.target = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} while (!isValidPlacement(px, py));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveAway(obj, tx, ty, speed=0.4) {
|
||||||
|
let dx = obj.x - tx;
|
||||||
|
let dy = obj.y - ty;
|
||||||
|
let dist = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
|
||||||
|
// Apply water movement penalty if in water
|
||||||
|
let actualSpeed = speed;
|
||||||
|
if (isWater(obj.x, obj.y)) {
|
||||||
|
actualSpeed *= WATER_MOVEMENT_PENALTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dist > 1) {
|
||||||
|
obj.vx = (dx/dist) * actualSpeed;
|
||||||
|
obj.vy = (dy/dist) * actualSpeed;
|
||||||
|
} else {
|
||||||
|
obj.vx = (Math.random() - 0.5) * actualSpeed;
|
||||||
|
obj.vy = (Math.random() - 0.5) * actualSpeed;
|
||||||
|
}
|
||||||
|
}
|
121
entities.js
Normal file
121
entities.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* 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: profession === "Soldier" // Soldiers start with a weapon
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
attackCooldown: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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!`);
|
||||||
|
}
|
428
events.js
Normal file
428
events.js
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
// Track mouse position for placement indicator
|
||||||
|
let lastMouseWorldX = null;
|
||||||
|
let lastMouseWorldY = null;
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* PAN & ZOOM
|
||||||
|
**********************************************************************/
|
||||||
|
function setupPanZoom() {
|
||||||
|
canvas.addEventListener('mousedown', (e) => {
|
||||||
|
// Only start dragging if not in terrain drawing mode
|
||||||
|
if(!purchaseMode || !purchaseMode.startsWith('Draw')) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update world coordinates for placement indicator
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
let cx = e.clientX - rect.left;
|
||||||
|
let cy = e.clientY - rect.top;
|
||||||
|
lastMouseWorldX = (cx - (canvas.width/2) - offsetX) / scale;
|
||||||
|
lastMouseWorldY = (cy - (canvas.height/2) - offsetY) / scale;
|
||||||
|
});
|
||||||
|
|
||||||
|
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('buySoldierBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "Soldier";
|
||||||
|
logAction("Click on map to place a new Soldier 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.");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Terrain drawing buttons
|
||||||
|
document.getElementById('drawWaterBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "DrawWater";
|
||||||
|
logAction("Click and drag on map to draw Water ($20).");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('drawGrassBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "DrawGrass";
|
||||||
|
logAction("Click and drag on map to draw Grass ($10).");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('drawSandBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "DrawSand";
|
||||||
|
logAction("Click and drag on map to draw Sand ($15).");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('drawDirtBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "DrawDirt";
|
||||||
|
logAction("Click and drag on map to draw Dirt ($5).");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('drawStoneBtn').addEventListener('click', () => {
|
||||||
|
purchaseMode = "DrawStone";
|
||||||
|
logAction("Click and drag on map to draw Stone ($25).");
|
||||||
|
});
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables for terrain drawing
|
||||||
|
let isDrawingTerrain = false;
|
||||||
|
let lastDrawX = null;
|
||||||
|
let lastDrawY = null;
|
||||||
|
let terrainDrawCost = 0;
|
||||||
|
let terrainDrawType = null;
|
||||||
|
let terrainDrawCount = 0;
|
||||||
|
|
||||||
|
function setupCanvasClick() {
|
||||||
|
// Mouse down for terrain drawing
|
||||||
|
canvas.addEventListener('mousedown', (e) => {
|
||||||
|
if(!purchaseMode || !purchaseMode.startsWith('Draw')) 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;
|
||||||
|
|
||||||
|
isDrawingTerrain = true;
|
||||||
|
lastDrawX = worldX;
|
||||||
|
lastDrawY = worldY;
|
||||||
|
terrainDrawCount = 0;
|
||||||
|
|
||||||
|
// Set terrain type and cost based on mode
|
||||||
|
switch(purchaseMode) {
|
||||||
|
case "DrawWater":
|
||||||
|
terrainDrawType = TERRAIN_WATER;
|
||||||
|
terrainDrawCost = COST_WATER;
|
||||||
|
break;
|
||||||
|
case "DrawGrass":
|
||||||
|
terrainDrawType = TERRAIN_GRASS;
|
||||||
|
terrainDrawCost = COST_GRASS;
|
||||||
|
break;
|
||||||
|
case "DrawSand":
|
||||||
|
terrainDrawType = TERRAIN_SAND;
|
||||||
|
terrainDrawCost = COST_SAND;
|
||||||
|
break;
|
||||||
|
case "DrawDirt":
|
||||||
|
terrainDrawType = TERRAIN_DIRT;
|
||||||
|
terrainDrawCost = COST_DIRT;
|
||||||
|
break;
|
||||||
|
case "DrawStone":
|
||||||
|
terrainDrawType = TERRAIN_STONE;
|
||||||
|
terrainDrawCost = COST_STONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse move for terrain drawing
|
||||||
|
canvas.addEventListener('mousemove', (e) => {
|
||||||
|
if(isDrawingTerrain && terrainDrawType !== null) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Only draw if moved enough distance
|
||||||
|
if(Math.abs(worldX - lastDrawX) > 5 || Math.abs(worldY - lastDrawY) > 5) {
|
||||||
|
// Check if we can afford it
|
||||||
|
if(money >= terrainDrawCost) {
|
||||||
|
// Draw a 3x3 area of terrain
|
||||||
|
for(let dx = -1; dx <= 1; dx++) {
|
||||||
|
for(let dy = -1; dy <= 1; dy++) {
|
||||||
|
setTerrainType(worldX + dx * 10, worldY + dy * 10, terrainDrawType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMoney(-terrainDrawCost, `Draw ${getTerrainName(terrainDrawType)}`);
|
||||||
|
terrainDrawCount++;
|
||||||
|
|
||||||
|
lastDrawX = worldX;
|
||||||
|
lastDrawY = worldY;
|
||||||
|
} else {
|
||||||
|
// Stop drawing if out of money
|
||||||
|
isDrawingTerrain = false;
|
||||||
|
logAction("Not enough money to continue drawing terrain!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse up to stop terrain drawing
|
||||||
|
canvas.addEventListener('mouseup', () => {
|
||||||
|
if(isDrawingTerrain) {
|
||||||
|
isDrawingTerrain = false;
|
||||||
|
if(terrainDrawCount > 0) {
|
||||||
|
logAction(`Drew ${terrainDrawCount} patches of ${getTerrainName(terrainDrawType)}.`);
|
||||||
|
}
|
||||||
|
terrainDrawType = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouse leave to stop terrain drawing
|
||||||
|
canvas.addEventListener('mouseleave', () => {
|
||||||
|
isDrawingTerrain = false;
|
||||||
|
terrainDrawType = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('click', (e) => {
|
||||||
|
if(!purchaseMode || purchaseMode.startsWith('Draw')) 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;
|
||||||
|
|
||||||
|
// Check if the placement is valid (not in water)
|
||||||
|
if (!isValidPlacement(worldX, worldY)) {
|
||||||
|
logAction("Cannot build on water! Choose a land location.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "Soldier":
|
||||||
|
if(money >= COST_SOLDIER) {
|
||||||
|
addMoney(-COST_SOLDIER, "Buy Soldier");
|
||||||
|
let c = createCitizen(randomName(), worldX, worldY, "Soldier");
|
||||||
|
citizens.push(c);
|
||||||
|
logAction(`Purchased new Soldier @(${Math.floor(worldX)},${Math.floor(worldY)})`);
|
||||||
|
} else {
|
||||||
|
logAction("Not enough money to buy Soldier!");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
263
game.js
Normal file
263
game.js
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* GAME CORE
|
||||||
|
**********************************************************************/
|
||||||
|
// AI functions are defined in ai.js and loaded via script tag
|
||||||
|
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;
|
||||||
|
const COST_SOLDIER = 250;
|
||||||
|
|
||||||
|
// Terrain costs
|
||||||
|
const COST_WATER = 20;
|
||||||
|
const COST_GRASS = 10;
|
||||||
|
const COST_SAND = 15;
|
||||||
|
const COST_DIRT = 5;
|
||||||
|
const COST_STONE = 25;
|
||||||
|
|
||||||
|
// 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", "Soldier"];
|
||||||
|
|
||||||
|
// Animals
|
||||||
|
const STARTING_RABBITS = 10;
|
||||||
|
const STARTING_WOLVES = 3;
|
||||||
|
const RABBIT_HUNGER_INCREMENT = 0.003;
|
||||||
|
const RABBIT_REPRO_COOLDOWN = 3000;
|
||||||
|
const RABBIT_REPRO_CHANCE = 0.0005;
|
||||||
|
const WOLF_SPEED = 0.7; // Faster than rabbits
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* INIT WORLD
|
||||||
|
**********************************************************************/
|
||||||
|
function initWorld() {
|
||||||
|
// Normal trees - only on land
|
||||||
|
for(let i=0; i<15; i++) {
|
||||||
|
let x, y;
|
||||||
|
do {
|
||||||
|
x = randInt(-1000,1000);
|
||||||
|
y = randInt(-1000,1000);
|
||||||
|
} while (!isValidPlacement(x, y));
|
||||||
|
resources.push(createResource("Tree", x, y, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fruit trees - only on land
|
||||||
|
for(let i=0; i<10; i++) {
|
||||||
|
let x, y;
|
||||||
|
do {
|
||||||
|
x = randInt(-1000,1000);
|
||||||
|
y = randInt(-1000,1000);
|
||||||
|
} while (!isValidPlacement(x, y));
|
||||||
|
resources.push(createResource("FruitTree", x, y, FRUIT_TREE_START_AMOUNT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with 1 citizen => always a "Builder" - only on land
|
||||||
|
let cx, cy;
|
||||||
|
do {
|
||||||
|
cx = randInt(-200,200);
|
||||||
|
cy = randInt(-200,200);
|
||||||
|
} while (!isValidPlacement(cx, cy));
|
||||||
|
let c = createCitizen(randomName(), cx, cy, "Builder");
|
||||||
|
citizens.push(c);
|
||||||
|
logAction(`Initial Citizen joined: ${c.name} [Builder]`);
|
||||||
|
|
||||||
|
// Spawn some animals - only on land
|
||||||
|
for(let i=0; i<STARTING_RABBITS; i++) {
|
||||||
|
let x, y;
|
||||||
|
do {
|
||||||
|
x = randInt(-1000,1000);
|
||||||
|
y = randInt(-1000,1000);
|
||||||
|
} while (!isValidPlacement(x, y));
|
||||||
|
animals.push(createAnimal("Rabbit", x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i=0; i<STARTING_WOLVES; i++) {
|
||||||
|
let x, y;
|
||||||
|
do {
|
||||||
|
x = randInt(-1000,1000);
|
||||||
|
y = randInt(-1000,1000);
|
||||||
|
} while (!isValidPlacement(x, y));
|
||||||
|
animals.push(createAnimal("Wolf", x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* UPDATE LOOP
|
||||||
|
**********************************************************************/
|
||||||
|
function update() {
|
||||||
|
frameCount++;
|
||||||
|
|
||||||
|
// Citizens
|
||||||
|
citizens.forEach((cit) => {
|
||||||
|
if(typeof updateCitizen === 'function') {
|
||||||
|
updateCitizen(cit);
|
||||||
|
} else {
|
||||||
|
console.error("updateCitizen function is not defined!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const soldierCount = citizens.filter(c => c.profession === "Soldier").length;
|
||||||
|
|
||||||
|
professionCountsDisplay.textContent = `Citizens: 👷${builderCount} 🌾${farmerCount} 💰${merchantCount} 💉${doctorCount} 📚${teacherCount} ⚔️${soldierCount}`;
|
||||||
|
}
|
1262
index.html
1262
index.html
File diff suppressed because it is too large
Load Diff
429
render.js
Normal file
429
render.js
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* RENDER
|
||||||
|
**********************************************************************/
|
||||||
|
function drawWorld() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw terrain first (water and grass)
|
||||||
|
drawTerrain();
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw placement indicator when in purchase mode
|
||||||
|
if (purchaseMode) {
|
||||||
|
drawPlacementIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a placement indicator at mouse position
|
||||||
|
function drawPlacementIndicator() {
|
||||||
|
if (!lastMouseWorldX || !lastMouseWorldY) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY);
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
const isValid = isValidPlacement(lastMouseWorldX, lastMouseWorldY);
|
||||||
|
|
||||||
|
// Draw indicator circle
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(lastMouseWorldX, lastMouseWorldY, 15, 0, Math.PI*2);
|
||||||
|
ctx.strokeStyle = isValid ? "rgba(0, 255, 0, 0.7)" : "rgba(255, 0, 0, 0.7)";
|
||||||
|
ctx.lineWidth = 2/scale;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw X if invalid
|
||||||
|
if (!isValid) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(lastMouseWorldX - 10, lastMouseWorldY - 10);
|
||||||
|
ctx.lineTo(lastMouseWorldX + 10, lastMouseWorldY + 10);
|
||||||
|
ctx.moveTo(lastMouseWorldX + 10, lastMouseWorldY - 10);
|
||||||
|
ctx.lineTo(lastMouseWorldX - 10, lastMouseWorldY + 10);
|
||||||
|
ctx.strokeStyle = "rgba(255, 0, 0, 0.7)";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTerrain() {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(canvas.width/2 + offsetX, canvas.height/2 + offsetY);
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
// Calculate visible area based on canvas size and scale
|
||||||
|
const visibleWidth = canvas.width / scale;
|
||||||
|
const visibleHeight = canvas.height / scale;
|
||||||
|
const startX = Math.floor((-offsetX / scale) - (visibleWidth / 2));
|
||||||
|
const startY = Math.floor((-offsetY / scale) - (visibleHeight / 2));
|
||||||
|
const endX = Math.ceil(startX + visibleWidth);
|
||||||
|
const endY = Math.ceil(startY + visibleHeight);
|
||||||
|
|
||||||
|
// Draw terrain cells
|
||||||
|
const cellSize = 10; // Size of each terrain cell
|
||||||
|
|
||||||
|
for (let x = startX; x <= endX; x += cellSize) {
|
||||||
|
for (let y = startY; y <= endY; y += cellSize) {
|
||||||
|
// Get terrain type at this position
|
||||||
|
const terrainType = getTerrainType(x, y);
|
||||||
|
|
||||||
|
// Set color based on terrain type
|
||||||
|
switch(terrainType) {
|
||||||
|
case TERRAIN_WATER:
|
||||||
|
ctx.fillStyle = "rgba(0, 100, 255, 0.5)"; // Blue for water
|
||||||
|
break;
|
||||||
|
case TERRAIN_GRASS:
|
||||||
|
ctx.fillStyle = "rgba(100, 200, 100, 0.3)"; // Green for grass
|
||||||
|
break;
|
||||||
|
case TERRAIN_SAND:
|
||||||
|
ctx.fillStyle = "rgba(240, 230, 140, 0.5)"; // Khaki for sand
|
||||||
|
break;
|
||||||
|
case TERRAIN_DIRT:
|
||||||
|
ctx.fillStyle = "rgba(139, 69, 19, 0.3)"; // Brown for dirt
|
||||||
|
break;
|
||||||
|
case TERRAIN_STONE:
|
||||||
|
ctx.fillStyle = "rgba(128, 128, 128, 0.4)"; // Gray for stone
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ctx.fillStyle = "rgba(100, 200, 100, 0.3)"; // Default to grass
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillRect(x, y, cellSize, cellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Different colors for different professions
|
||||||
|
if(c.profession === "Soldier") {
|
||||||
|
ctx.fillStyle = "#8B0000"; // Dark red for soldiers
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = c.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(c.x, c.y, 7, 0, Math.PI*2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw a sword for soldiers
|
||||||
|
if(c.profession === "Soldier") {
|
||||||
|
ctx.strokeStyle = "#DDD";
|
||||||
|
ctx.lineWidth = 1/scale;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(c.x + 5, c.y - 5);
|
||||||
|
ctx.lineTo(c.x + 12, c.y - 12);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
case "Soldier": 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);
|
||||||
|
} else if(a.type === "Wolf") {
|
||||||
|
ctx.fillStyle = "#444";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(a.x, a.y, 10, 0, Math.PI*2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = "#000";
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
ctx.fillText("🐺", a.x+8, a.y+3);
|
||||||
|
|
||||||
|
// Show hunger level for wolves
|
||||||
|
ctx.fillStyle = "#f00";
|
||||||
|
ctx.fillRect(a.x-10, a.y-15, 20 * (a.hunger/ANIMAL_HUNGER_MAX), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
232
terrain.js
Normal file
232
terrain.js
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* TERRAIN GENERATION
|
||||||
|
**********************************************************************/
|
||||||
|
|
||||||
|
// Perlin noise implementation
|
||||||
|
// Based on https://github.com/josephg/noisejs
|
||||||
|
class Perlin {
|
||||||
|
constructor() {
|
||||||
|
this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
|
||||||
|
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
|
||||||
|
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
|
||||||
|
this.p = [];
|
||||||
|
for (let i=0; i<256; i++) {
|
||||||
|
this.p[i] = Math.floor(Math.random()*256);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To remove the need for index wrapping, double the permutation table length
|
||||||
|
this.perm = new Array(512);
|
||||||
|
this.gradP = new Array(512);
|
||||||
|
|
||||||
|
// Skipping the seed function for simplicity
|
||||||
|
this.seed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
seed(seed) {
|
||||||
|
if(seed > 0 && seed < 1) {
|
||||||
|
// Scale the seed out
|
||||||
|
seed *= 65536;
|
||||||
|
}
|
||||||
|
|
||||||
|
seed = Math.floor(seed);
|
||||||
|
if(seed < 256) {
|
||||||
|
seed |= seed << 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i = 0; i < 256; i++) {
|
||||||
|
let v;
|
||||||
|
if (i & 1) {
|
||||||
|
v = this.p[i] ^ (seed & 255);
|
||||||
|
} else {
|
||||||
|
v = this.p[i] ^ ((seed>>8) & 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.perm[i] = this.perm[i + 256] = v;
|
||||||
|
this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2D Perlin Noise
|
||||||
|
noise(x, y) {
|
||||||
|
// Find unit grid cell containing point
|
||||||
|
let X = Math.floor(x), Y = Math.floor(y);
|
||||||
|
// Get relative xy coordinates of point within that cell
|
||||||
|
x = x - X; y = y - Y;
|
||||||
|
// Wrap the integer cells at 255 (smaller integer period can be introduced here)
|
||||||
|
X = X & 255; Y = Y & 255;
|
||||||
|
|
||||||
|
// Calculate noise contributions from each of the four corners
|
||||||
|
let n00 = this.dot2(this.gradP[X+this.perm[Y]], x, y);
|
||||||
|
let n01 = this.dot2(this.gradP[X+this.perm[Y+1]], x, y-1);
|
||||||
|
let n10 = this.dot2(this.gradP[X+1+this.perm[Y]], x-1, y);
|
||||||
|
let n11 = this.dot2(this.gradP[X+1+this.perm[Y+1]], x-1, y-1);
|
||||||
|
|
||||||
|
// Compute the fade curve value for x
|
||||||
|
let u = this.fade(x);
|
||||||
|
|
||||||
|
// Interpolate the four results
|
||||||
|
return this.lerp(
|
||||||
|
this.lerp(n00, n10, u),
|
||||||
|
this.lerp(n01, n11, u),
|
||||||
|
this.fade(y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fading function
|
||||||
|
fade(t) {
|
||||||
|
return t*t*t*(t*(t*6-15)+10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear interpolation
|
||||||
|
lerp(a, b, t) {
|
||||||
|
return (1-t)*a + t*b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot product
|
||||||
|
dot2(g, x, y) {
|
||||||
|
return g[0]*x + g[1]*y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terrain types
|
||||||
|
const TERRAIN_WATER = 0;
|
||||||
|
const TERRAIN_GRASS = 1;
|
||||||
|
const TERRAIN_SAND = 2;
|
||||||
|
const TERRAIN_DIRT = 3;
|
||||||
|
const TERRAIN_STONE = 4;
|
||||||
|
|
||||||
|
// Generate terrain map
|
||||||
|
function generateTerrain(width, height, scale) {
|
||||||
|
const terrain = new Array(width);
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
terrain[x] = new Array(height);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
// Generate noise value
|
||||||
|
let nx = x / scale;
|
||||||
|
let ny = y / scale;
|
||||||
|
let value = perlin.noise(nx, ny);
|
||||||
|
|
||||||
|
// Adjust the value to get more interesting terrain
|
||||||
|
// Add multiple octaves of noise for more natural look
|
||||||
|
value += 0.5 * perlin.noise(nx * 2, ny * 2);
|
||||||
|
value += 0.25 * perlin.noise(nx * 4, ny * 4);
|
||||||
|
value /= 1.75; // Normalize
|
||||||
|
|
||||||
|
// Generate a second noise value for stone distribution
|
||||||
|
let stoneNoise = perlin.noise(nx * 3, ny * 3);
|
||||||
|
|
||||||
|
// Determine terrain type based on noise value
|
||||||
|
// Adjusted to get more water
|
||||||
|
if (value < -0.2) { // More water
|
||||||
|
terrain[x][y] = TERRAIN_WATER;
|
||||||
|
|
||||||
|
// Check for sand near water (beach)
|
||||||
|
let sandCheck = perlin.noise((nx + 0.1) * 8, (ny + 0.1) * 8);
|
||||||
|
if (value > -0.25 && sandCheck > 0) {
|
||||||
|
terrain[x][y] = TERRAIN_SAND;
|
||||||
|
}
|
||||||
|
} else if (value < 0.0) {
|
||||||
|
// Sand appears near water
|
||||||
|
terrain[x][y] = TERRAIN_SAND;
|
||||||
|
} else if (value < 0.3) {
|
||||||
|
// Grass in middle elevations
|
||||||
|
terrain[x][y] = TERRAIN_GRASS;
|
||||||
|
} else if (stoneNoise > 0.3) {
|
||||||
|
// Stone in higher elevations with specific noise pattern
|
||||||
|
terrain[x][y] = TERRAIN_STONE;
|
||||||
|
} else {
|
||||||
|
// Dirt in higher elevations
|
||||||
|
terrain[x][y] = TERRAIN_DIRT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass to smooth terrain and create better beaches
|
||||||
|
smoothTerrain(terrain, width, height);
|
||||||
|
|
||||||
|
return terrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function smoothTerrain(terrain, width, height) {
|
||||||
|
// Create sand around water
|
||||||
|
for (let x = 1; x < width - 1; x++) {
|
||||||
|
for (let y = 1; y < height - 1; y++) {
|
||||||
|
if (terrain[x][y] !== TERRAIN_WATER) {
|
||||||
|
// Check if adjacent to water
|
||||||
|
let adjacentToWater = false;
|
||||||
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
if (x + dx >= 0 && x + dx < width && y + dy >= 0 && y + dy < height) {
|
||||||
|
if (terrain[x + dx][y + dy] === TERRAIN_WATER) {
|
||||||
|
adjacentToWater = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adjacentToWater) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adjacent to water and not already sand, make it sand
|
||||||
|
if (adjacentToWater && terrain[x][y] !== TERRAIN_SAND && Math.random() > 0.3) {
|
||||||
|
terrain[x][y] = TERRAIN_SAND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a position is in water
|
||||||
|
function isWater(x, y) {
|
||||||
|
return getTerrainType(x, y) === TERRAIN_WATER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get terrain type at a position
|
||||||
|
function getTerrainType(x, y) {
|
||||||
|
// Convert world coordinates to terrain grid coordinates
|
||||||
|
const gridX = Math.floor((x + 2000) / 10);
|
||||||
|
const gridY = Math.floor((y + 2000) / 10);
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) {
|
||||||
|
return TERRAIN_GRASS; // Default to grass if out of bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
return terrainMap[gridX][gridY];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set terrain type at a position
|
||||||
|
function setTerrainType(x, y, type) {
|
||||||
|
// Convert world coordinates to terrain grid coordinates
|
||||||
|
const gridX = Math.floor((x + 2000) / 10);
|
||||||
|
const gridY = Math.floor((y + 2000) / 10);
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) {
|
||||||
|
return false; // Can't set if out of bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
terrainMap[gridX][gridY] = type;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get terrain name from type
|
||||||
|
function getTerrainName(type) {
|
||||||
|
switch(type) {
|
||||||
|
case TERRAIN_WATER: return "Water";
|
||||||
|
case TERRAIN_GRASS: return "Grass";
|
||||||
|
case TERRAIN_SAND: return "Sand";
|
||||||
|
case TERRAIN_DIRT: return "Dirt";
|
||||||
|
case TERRAIN_STONE: return "Stone";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terrain dimensions
|
||||||
|
const terrainWidth = 400;
|
||||||
|
const terrainHeight = 400;
|
||||||
|
const terrainScale = 50; // Smaller scale = more detailed terrain
|
||||||
|
|
||||||
|
// Initialize Perlin noise
|
||||||
|
const perlin = new Perlin();
|
||||||
|
|
||||||
|
// Generate the terrain map
|
||||||
|
let terrainMap = generateTerrain(terrainWidth, terrainHeight, terrainScale);
|
200
utils.js
Normal file
200
utils.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
* 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
|
||||||
|
**********************************************************************/
|
||||||
|
// Default implementation of isWater - will be overridden by terrain.js
|
||||||
|
function isWater(x, y) {
|
||||||
|
return false; // Default to no water
|
||||||
|
}
|
||||||
|
|
||||||
|
// Water movement penalty constant
|
||||||
|
const WATER_MOVEMENT_PENALTY = 0.5;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Apply water movement penalty if in water
|
||||||
|
let actualSpeed = speed;
|
||||||
|
if (isWater(obj.x, obj.y)) {
|
||||||
|
actualSpeed *= WATER_MOVEMENT_PENALTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dist > 1) {
|
||||||
|
obj.vx = (dx/dist) * actualSpeed;
|
||||||
|
obj.vy = (dy/dist) * actualSpeed;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* PLACEMENT VALIDATION
|
||||||
|
**********************************************************************/
|
||||||
|
// Check if a position is valid for placement (not in water)
|
||||||
|
function isValidPlacement(x, y) {
|
||||||
|
return !isWater(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************************************************
|
||||||
|
* 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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user