Update index.html

This commit is contained in:
{{QWERTYKBGUI}} 2025-03-13 22:23:14 +01:00
parent 11165b76b6
commit cababc87bc

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Virtual World</title> <title>Virtual World: More Rabbits, Separate Wolf Spawning</title>
<style> <style>
body { body {
margin: 0; margin: 0;
@ -10,33 +10,27 @@
background: #e0f7fa; background: #e0f7fa;
font-family: sans-serif; font-family: sans-serif;
} }
#app { #app {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
} }
h1 { h1 {
margin: 5px; margin: 5px;
color: #333; color: #333;
} }
#controls { #controls {
margin-bottom: 5px; margin-bottom: 5px;
} }
canvas { canvas {
background: #ffffff; background: #ffffff;
border: 2px solid #444; border: 2px solid #444;
cursor: grab; cursor: grab;
} }
canvas:active { canvas:active {
cursor: grabbing; cursor: grabbing;
} }
#log { #log {
width: 800px; width: 800px;
height: 120px; height: 120px;
@ -47,7 +41,6 @@
padding: 5px; padding: 5px;
font-size: 0.9rem; font-size: 0.9rem;
} }
.log-entry { .log-entry {
margin: 2px 0; margin: 2px 0;
} }
@ -56,23 +49,22 @@
<body> <body>
<div id="app"> <div id="app">
<h1>Virtual World</h1> <h1>Virtual World: Rabbits & Wolves (Separated Spawn)</h1>
<p id="controls">Drag to move the map, scroll to zoom in/out.</p> <p id="controls">Drag to move the map, scroll to zoom in/out.</p>
<canvas id="worldCanvas" width="800" height="600"></canvas> <canvas id="worldCanvas" width="800" height="600"></canvas>
<div id="log"></div> <div id="log"></div>
</div> </div>
<script> <script>
/* /*
--------------------------------------------------------------------------------- ---------------------------------------------------------------------------------
Virtual World Simulation with: Key Differences:
- Builder & Farmer Professions - STARTING_RABBITS = 30 (spawned randomly across [-1000, 1000])
- Hunger & Energy - STARTING_WOLVES = 5 (spawned in a separate area [500..1000])
- Fruit Trees (gather fruit, plant new trees) - Rest of the simulation remains the same:
- Houses (store fruit, citizens can eat & rest) * Citizens with Professions, Houses, Roads, Fruit Trees
- Roads automatically built between houses once they finish construction! * Rabbits & Wolves Reproduction
- Logging & Pan/Zoom * Basic Predator/Prey & Combat
- Child Birth Events
--------------------------------------------------------------------------------- ---------------------------------------------------------------------------------
*/ */
@ -94,24 +86,27 @@ let lastMouseY = 0;
// Frame counter for timed events // Frame counter for timed events
let frameCount = 0; let frameCount = 0;
// Shared city storage for wood // City storage for wood
const cityStorage = { const cityStorage = {
wood: 0 wood: 0
}; };
// Array of resource nodes (trees & fruit trees) // Resource nodes (trees & fruit trees)
let resources = []; let resources = [];
// Buildings array (includes both House sites and Road sites) // Buildings array (houses, roads)
let buildings = []; let buildings = [];
// Citizens // Citizens
let citizens = []; let citizens = [];
// Animals (rabbits & wolves)
let animals = [];
/********************************************************************** /**********************************************************************
* SIMULATION CONSTANTS * SIMULATION CONSTANTS
**********************************************************************/ **********************************************************************/
// Hunger & Energy // Hunger & energy for citizens
const HUNGER_INCREMENT = 0.005; const HUNGER_INCREMENT = 0.005;
const ENERGY_DECREMENT_WORK = 0.02; const ENERGY_DECREMENT_WORK = 0.02;
const ENERGY_INCREMENT_REST = 0.05; const ENERGY_INCREMENT_REST = 0.05;
@ -120,12 +115,16 @@ const ENERGY_THRESHOLD = 30;
const HUNGER_MAX = 100; const HUNGER_MAX = 100;
const ENERGY_MAX = 100; const ENERGY_MAX = 100;
// House building requirements // House building
const HOUSE_WOOD_REQUIRED = 50; const HOUSE_WOOD_REQUIRED = 50;
const HOUSE_BUILD_RATE = 0.2; const HOUSE_BUILD_RATE = 0.2;
const HOUSE_MAX_FRUIT = 30; const HOUSE_MAX_FRUIT = 30;
// Fruit tree resource // Road building
const ROAD_WOOD_REQUIRED = 10;
const ROAD_BUILD_RATE = 0.3;
// Fruit trees
const FRUIT_TREE_START_AMOUNT = 20; const FRUIT_TREE_START_AMOUNT = 20;
const FRUIT_GATHER_RATE = 1; const FRUIT_GATHER_RATE = 1;
const FRUIT_PLANT_COST = 1; const FRUIT_PLANT_COST = 1;
@ -133,10 +132,26 @@ const FRUIT_PLANT_COST = 1;
// Professions // Professions
const PROFESSIONS = ["Farmer", "Builder"]; const PROFESSIONS = ["Farmer", "Builder"];
// Road building requirements // Weapons
// We'll make roads also be "buildings" with buildingType = "Road" const WEAPON_WOOD_COST = 10; // cost for a citizen to craft a weapon
const ROAD_WOOD_REQUIRED = 10;
const ROAD_BUILD_RATE = 0.3; // Animal logic
const STARTING_RABBITS = 30;
const STARTING_WOLVES = 5;
// Rabbit hunger
const RABBIT_HUNGER_INCREMENT = 0.003;
// Wolf hunger
const WOLF_HUNGER_INCREMENT = 0.006;
const ANIMAL_HUNGER_MAX = 100;
const KNOCKBACK_DISTANCE = 20;
// Reproduction
const RABBIT_REPRO_COOLDOWN = 3000; // frames (~50s if ~60fps)
const WOLF_REPRO_COOLDOWN = 5000; // ~80s
const RABBIT_REPRO_CHANCE = 0.0005;
const WOLF_REPRO_CHANCE = 0.0003;
/********************************************************************** /**********************************************************************
* LOGGING * LOGGING
@ -184,7 +199,8 @@ function createCitizen(name, x, y) {
carryingFruit: 0, carryingFruit: 0,
carryingCapacity: 10, carryingCapacity: 10,
hunger: 0, hunger: 0,
energy: ENERGY_MAX energy: ENERGY_MAX,
hasWeapon: false
}; };
} }
@ -192,18 +208,6 @@ function createResource(type, x, y, amount) {
return { type, x, y, amount }; return { type, x, y, amount };
} }
/**
* Buildings can be:
* - House:
* x, y, buildingType="House"
* requiredWood, deliveredWood, buildProgress, completed
* storedFruit, maxFruit
* - Road:
* buildingType="Road"
* (x, y) as midpoint for drawing info
* x1, y1, x2, y2 for endpoints
* requiredWood, deliveredWood, buildProgress, completed
*/
function createHouseSite(x, y) { function createHouseSite(x, y) {
return { return {
buildingType: "House", buildingType: "House",
@ -219,15 +223,13 @@ function createHouseSite(x, y) {
} }
function createRoadSite(x1, y1, x2, y2) { function createRoadSite(x1, y1, x2, y2) {
// midpoint for label
const mx = (x1 + x2) / 2; const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2; const my = (y1 + y2) / 2;
return { return {
buildingType: "Road", buildingType: "Road",
x: mx, x: mx,
y: my, y: my,
x1, y1, x1, y1,
x2, y2, x2, y2,
requiredWood: ROAD_WOOD_REQUIRED, requiredWood: ROAD_WOOD_REQUIRED,
deliveredWood: 0, deliveredWood: 0,
@ -236,18 +238,35 @@ function createRoadSite(x1, y1, x2, y2) {
}; };
} }
/** Animals: Rabbit & Wolf */
function createAnimal(type, x, y) {
const initialCooldown = (type === "Rabbit")
? randInt(0, RABBIT_REPRO_COOLDOWN)
: randInt(0, WOLF_REPRO_COOLDOWN);
return {
type,
x,
y,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
hunger: 0,
dead: false,
reproductionCooldown: initialCooldown
};
}
/********************************************************************** /**********************************************************************
* WORLD INITIALIZATION * WORLD INITIALIZATION
**********************************************************************/ **********************************************************************/
function initWorld() { function initWorld() {
// Create some normal wood trees // Create normal trees
for (let i = 0; i < 15; i++) { for (let i = 0; i < 15; i++) {
const x = randInt(-1000, 1000); const x = randInt(-1000, 1000);
const y = randInt(-1000, 1000); const y = randInt(-1000, 1000);
resources.push(createResource("Tree", x, y, 100)); resources.push(createResource("Tree", x, y, 100));
} }
// Create some fruit trees // Create fruit trees
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const x = randInt(-1000, 1000); const x = randInt(-1000, 1000);
const y = randInt(-1000, 1000); const y = randInt(-1000, 1000);
@ -261,22 +280,46 @@ function initWorld() {
logAction(`Citizen joined: ${c.name} [${c.profession}]`); logAction(`Citizen joined: ${c.name} [${c.profession}]`);
} }
// Spawn more rabbits all across the map
for (let i = 0; i < STARTING_RABBITS; i++) {
const rx = randInt(-1000, 1000);
const ry = randInt(-1000, 1000);
animals.push(createAnimal("Rabbit", rx, ry));
}
// Spawn wolves in a distant region (e.g. x,y in [500..1000])
for (let i = 0; i < STARTING_WOLVES; i++) {
const wx = randInt(500, 1000);
const wy = randInt(500, 1000);
animals.push(createAnimal("Wolf", wx, wy));
}
// Start simulation // Start simulation
requestAnimationFrame(update); requestAnimationFrame(update);
} }
/********************************************************************** /**********************************************************************
* UPDATE LOOP * MAIN UPDATE LOOP
**********************************************************************/ **********************************************************************/
function update() { function update() {
frameCount++; frameCount++;
// Update each citizen // Update citizens
citizens.forEach((cit) => { citizens.forEach((cit) => {
updateCitizen(cit); updateCitizen(cit);
}); });
// Periodically add new citizens (child births) // Update animals
animals.forEach((ani) => {
if (!ani.dead) {
updateAnimal(ani);
}
});
// Remove any dead animals
animals = animals.filter(a => !a.dead);
// Periodic child births for citizens
if (frameCount % 600 === 0) { if (frameCount % 600 === 0) {
const baby = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200)); const baby = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200));
citizens.push(baby); citizens.push(baby);
@ -286,17 +329,12 @@ function update() {
// Update buildings // Update buildings
buildings.forEach((b) => { buildings.forEach((b) => {
if (!b.completed && b.deliveredWood >= b.requiredWood) { if (!b.completed && b.deliveredWood >= b.requiredWood) {
// House or Road? Different build rates let buildRate = (b.buildingType === "Road") ? ROAD_BUILD_RATE : HOUSE_BUILD_RATE;
let buildRate = HOUSE_BUILD_RATE;
if (b.buildingType === "Road") {
buildRate = ROAD_BUILD_RATE;
}
b.buildProgress += buildRate; b.buildProgress += buildRate;
if (b.buildProgress >= 100) { if (b.buildProgress >= 100) {
b.completed = true; b.completed = true;
if (b.buildingType === "House") { if (b.buildingType === "House") {
logAction(`A new House is completed at (${b.x}, ${b.y})!`); logAction(`A new House is completed at (${b.x}, ${b.y})!`);
// Once the house is completed, build a road from it to the nearest house
maybeBuildRoad(b); maybeBuildRoad(b);
} else { } else {
logAction(`A Road has been completed!`); logAction(`A Road has been completed!`);
@ -310,30 +348,27 @@ function update() {
} }
/********************************************************************** /**********************************************************************
* CITIZEN UPDATE (AI + Movement) * CITIZEN UPDATE
**********************************************************************/ **********************************************************************/
function updateCitizen(cit) { function updateCitizen(cit) {
// Hunger & energy // Basic hunger & energy
cit.hunger += HUNGER_INCREMENT; cit.hunger += HUNGER_INCREMENT;
if (cit.hunger > HUNGER_MAX) cit.hunger = HUNGER_MAX; if (cit.hunger > HUNGER_MAX) cit.hunger = HUNGER_MAX;
if (cit.task === 'chop' || cit.task === 'gatherFruit' || cit.task === 'build') { if (["chop","gatherFruit","build"].includes(cit.task)) {
cit.energy -= ENERGY_DECREMENT_WORK; cit.energy -= ENERGY_DECREMENT_WORK;
} else if (cit.task === 'restAtHouse') { } else if (cit.task === "restAtHouse") {
cit.energy += ENERGY_INCREMENT_REST; cit.energy += ENERGY_INCREMENT_REST;
} else { } else {
cit.energy -= 0.0005; // slight passive drain cit.energy -= 0.0005;
} }
if (cit.energy < 0) cit.energy = 0; if (cit.energy < 0) cit.energy = 0;
if (cit.energy > ENERGY_MAX) cit.energy = ENERGY_MAX; if (cit.energy > ENERGY_MAX) cit.energy = ENERGY_MAX;
// Assign a new task if none
if (!cit.task) { if (!cit.task) {
assignNewTask(cit); assignNewTask(cit);
} }
// Execute current task logic
switch (cit.task) { switch (cit.task) {
case 'chop': chopTask(cit); break; case 'chop': chopTask(cit); break;
case 'deliverWood': deliverWoodTask(cit); break; case 'deliverWood': deliverWoodTask(cit); break;
@ -346,16 +381,200 @@ function updateCitizen(cit) {
default: randomWander(cit); break; default: randomWander(cit); break;
} }
// Apply velocity
cit.x += cit.vx; cit.x += cit.vx;
cit.y += cit.vy; cit.y += cit.vy;
} }
/********************************************************************** /**********************************************************************
* TASK ASSIGNMENT * ANIMAL UPDATE
**********************************************************************/
function updateAnimal(ani) {
if (ani.type === "Rabbit") {
updateRabbit(ani);
} else if (ani.type === "Wolf") {
updateWolf(ani);
}
ani.x += ani.vx;
ani.y += ani.vy;
// Decrement reproduction cooldown
if (ani.reproductionCooldown > 0) {
ani.reproductionCooldown -= 1;
}
}
function updateRabbit(rabbit) {
rabbit.hunger += RABBIT_HUNGER_INCREMENT;
if (rabbit.hunger >= ANIMAL_HUNGER_MAX) {
rabbit.dead = true;
logAction("A rabbit starved to death.");
return;
}
// Reproduction
if (rabbit.hunger < 50 && rabbit.reproductionCooldown <= 0) {
if (Math.random() < RABBIT_REPRO_CHANCE) {
spawnBabyAnimal("Rabbit", rabbit.x, rabbit.y);
rabbit.reproductionCooldown = RABBIT_REPRO_COOLDOWN;
}
}
if (rabbit.hunger > 50) {
const tree = findNearestResourceOfType({x: rabbit.x, y: rabbit.y}, "FruitTree");
if (tree) {
moveToward(rabbit, tree.x, tree.y, 0.4);
if (distance(rabbit.x, rabbit.y, tree.x, tree.y) < 10) {
if (tree.amount > 0) {
const eatAmount = 1;
tree.amount -= eatAmount;
rabbit.hunger -= eatAmount * 2;
if (rabbit.hunger < 0) rabbit.hunger = 0;
if (Math.random() < 0.02) {
logAction("A rabbit is eating fruit...");
}
}
}
} else {
randomAnimalWander(rabbit);
}
} else {
randomAnimalWander(rabbit);
}
}
function updateWolf(wolf) {
wolf.hunger += WOLF_HUNGER_INCREMENT;
if (wolf.hunger >= ANIMAL_HUNGER_MAX) {
wolf.dead = true;
logAction("A wolf starved to death.");
return;
}
// Reproduction
if (wolf.hunger < 50 && wolf.reproductionCooldown <= 0) {
if (Math.random() < WOLF_REPRO_CHANCE) {
spawnBabyAnimal("Wolf", wolf.x, wolf.y);
wolf.reproductionCooldown = WOLF_REPRO_COOLDOWN;
}
}
// 1) hunt rabbits
let targetRabbit = findNearestAnimalOfType(wolf, "Rabbit");
if (targetRabbit) {
moveToward(wolf, targetRabbit.x, targetRabbit.y, 0.5);
if (distance(wolf.x, wolf.y, targetRabbit.x, targetRabbit.y) < 10) {
targetRabbit.dead = true;
wolf.hunger = Math.max(0, wolf.hunger - 30);
logAction("A wolf killed a rabbit!");
knockback(wolf, targetRabbit.x, targetRabbit.y, KNOCKBACK_DISTANCE);
}
return;
}
// 2) no rabbit => maybe attack a citizen if hunger > 60
if (wolf.hunger > 60) {
let targetHuman = findNearestCitizen(wolf);
if (targetHuman) {
moveToward(wolf, targetHuman.x, targetHuman.y, 0.5);
if (distance(wolf.x, wolf.y, targetHuman.x, targetHuman.y) < 10) {
if (targetHuman.hasWeapon) {
wolf.dead = true;
logAction(`${targetHuman.name} defended against a wolf with a weapon! Wolf died.`);
} else {
logAction(`A wolf killed ${targetHuman.name}!`);
citizens.splice(citizens.indexOf(targetHuman), 1);
}
knockback(wolf, targetHuman.x, targetHuman.y, KNOCKBACK_DISTANCE);
}
} else {
randomAnimalWander(wolf);
}
} else {
randomAnimalWander(wolf);
}
}
function spawnBabyAnimal(type, x, y) {
const nx = x + randInt(-20, 20);
const ny = y + randInt(-20, 20);
const baby = createAnimal(type, nx, ny);
animals.push(baby);
logAction(`A new baby ${type} is born!`);
}
function knockback(entity, tx, ty, dist) {
const dx = entity.x - tx;
const dy = entity.y - ty;
const length = Math.sqrt(dx*dx + dy*dy);
if (length > 0.1) {
entity.x += (dx / length) * dist;
entity.y += (dy / length) * dist;
}
}
/**********************************************************************
* FINDERS
**********************************************************************/
function findNearestResourceOfType(ref, rtype) {
let nearest = null;
let minDist = Infinity;
for (const res of resources) {
if (res.type === rtype && res.amount > 0) {
const d = distance(ref.x, ref.y, res.x, res.y);
if (d < minDist) {
minDist = d;
nearest = res;
}
}
}
return nearest;
}
function findNearestAnimalOfType(wolf, targetType) {
let nearest = null;
let minDist = Infinity;
for (const a of animals) {
if (a !== wolf && !a.dead && a.type === targetType) {
const d = distance(wolf.x, wolf.y, a.x, a.y);
if (d < minDist) {
minDist = d;
nearest = a;
}
}
}
return nearest;
}
function findNearestCitizen(wolf) {
let nearest = null;
let minDist = Infinity;
for (const c of citizens) {
const d = distance(wolf.x, wolf.y, c.x, c.y);
if (d < minDist) {
minDist = d;
nearest = c;
}
}
return nearest;
}
function findHouseWithFruit() {
return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit > 0);
}
function findAnyCompletedHouse() {
return buildings.find(b => b.buildingType === "House" && b.completed);
}
function findHouseNeedingFruit() {
return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit < b.maxFruit);
}
/**********************************************************************
* CITIZEN TASKS
**********************************************************************/ **********************************************************************/
function assignNewTask(cit) { function assignNewTask(cit) {
// If hunger too high, eat if possible
if (cit.hunger >= HUNGER_THRESHOLD) { if (cit.hunger >= HUNGER_THRESHOLD) {
const houseWithFruit = findHouseWithFruit(); const houseWithFruit = findHouseWithFruit();
if (houseWithFruit) { if (houseWithFruit) {
@ -364,8 +583,6 @@ function assignNewTask(cit) {
return; return;
} }
} }
// If energy too low, rest at any completed house
if (cit.energy <= ENERGY_THRESHOLD) { if (cit.energy <= ENERGY_THRESHOLD) {
const completedHouse = findAnyCompletedHouse(); const completedHouse = findAnyCompletedHouse();
if (completedHouse) { if (completedHouse) {
@ -374,8 +591,18 @@ function assignNewTask(cit) {
return; return;
} }
} }
// If builder & no weapon => craft if enough wood
// Profession-based if (cit.profession === "Builder" && !cit.hasWeapon) {
if (cityStorage.wood >= WEAPON_WOOD_COST) {
cityStorage.wood -= WEAPON_WOOD_COST;
cit.hasWeapon = true;
logAction(`${cit.name} crafted a wooden weapon for defense!`);
cit.task = null;
cit.target = null;
return;
}
}
// Profession tasks
if (cit.profession === "Builder") { if (cit.profession === "Builder") {
builderTasks(cit); builderTasks(cit);
} else { } else {
@ -384,16 +611,13 @@ function assignNewTask(cit) {
} }
function builderTasks(cit) { function builderTasks(cit) {
// Find building needing wood
const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood); const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood);
if (buildingNeedingWood) { if (buildingNeedingWood) {
// If carrying wood, deliver
if (cit.carryingWood > 0) { if (cit.carryingWood > 0) {
cit.task = 'deliverWood'; cit.task = 'deliverWood';
cit.target = buildingNeedingWood; cit.target = buildingNeedingWood;
return; return;
} }
// Otherwise chop wood
const tree = findNearestResourceOfType(cit, "Tree"); const tree = findNearestResourceOfType(cit, "Tree");
if (tree) { if (tree) {
cit.task = 'chop'; cit.task = 'chop';
@ -401,30 +625,23 @@ function builderTasks(cit) {
return; return;
} }
} }
// If there's a building with enough wood but not finished, build
const buildingToConstruct = buildings.find(b => !b.completed && b.deliveredWood >= b.requiredWood); const buildingToConstruct = buildings.find(b => !b.completed && b.deliveredWood >= b.requiredWood);
if (buildingToConstruct) { if (buildingToConstruct) {
cit.task = 'build'; cit.task = 'build';
cit.target = buildingToConstruct; cit.target = buildingToConstruct;
return; return;
} }
// Otherwise chop wood (to store)
const anyTree = findNearestResourceOfType(cit, "Tree"); const anyTree = findNearestResourceOfType(cit, "Tree");
if (anyTree) { if (anyTree) {
cit.task = 'chop'; cit.task = 'chop';
cit.target = anyTree; cit.target = anyTree;
return; return;
} }
// Idle
cit.task = null; cit.task = null;
cit.target = null; cit.target = null;
} }
function farmerTasks(cit) { function farmerTasks(cit) {
// If carrying fruit, try delivering to a house
if (cit.carryingFruit > 0) { if (cit.carryingFruit > 0) {
const houseNeedingFruit = findHouseNeedingFruit(); const houseNeedingFruit = findHouseNeedingFruit();
if (houseNeedingFruit) { if (houseNeedingFruit) {
@ -433,30 +650,21 @@ function farmerTasks(cit) {
return; return;
} }
} }
// If no fruit in hand, gather fruit
const fruitTree = findNearestResourceOfType(cit, "FruitTree"); const fruitTree = findNearestResourceOfType(cit, "FruitTree");
if (fruitTree && fruitTree.amount > 0) { if (fruitTree && fruitTree.amount > 0) {
cit.task = 'gatherFruit'; cit.task = 'gatherFruit';
cit.target = fruitTree; cit.target = fruitTree;
return; return;
} }
// Occasionally plant a new fruit tree if carrying fruit
if (cit.carryingFruit >= FRUIT_PLANT_COST && Math.random() < 0.1) { if (cit.carryingFruit >= FRUIT_PLANT_COST && Math.random() < 0.1) {
cit.task = 'plantFruitTree'; cit.task = 'plantFruitTree';
cit.target = null; cit.target = null;
return; return;
} }
// Idle
cit.task = null; cit.task = null;
cit.target = null; cit.target = null;
} }
/**********************************************************************
* TASK HANDLERS
**********************************************************************/
function chopTask(cit) { function chopTask(cit) {
const tree = cit.target; const tree = cit.target;
if (!tree || tree.amount <= 0) { if (!tree || tree.amount <= 0) {
@ -509,7 +717,6 @@ function buildTask(cit) {
return; return;
} }
moveToward(cit, b.x, b.y, 0.3); moveToward(cit, b.x, b.y, 0.3);
// Building progress handled globally
} }
function gatherFruitTask(cit) { function gatherFruitTask(cit) {
@ -544,7 +751,6 @@ function deliverFruitTask(cit) {
} }
moveToward(cit, house.x, house.y, 0.4); moveToward(cit, house.x, house.y, 0.4);
if (distance(cit.x, cit.y, house.x, house.y) < 20) { if (distance(cit.x, cit.y, house.x, house.y) < 20) {
// Deliver fruit
const space = house.maxFruit - house.storedFruit; const space = house.maxFruit - house.storedFruit;
if (space > 0 && cit.carryingFruit > 0) { if (space > 0 && cit.carryingFruit > 0) {
const toDeliver = Math.min(cit.carryingFruit, space); const toDeliver = Math.min(cit.carryingFruit, space);
@ -558,10 +764,8 @@ function deliverFruitTask(cit) {
} }
function plantFruitTreeTask(cit) { function plantFruitTreeTask(cit) {
// Plant near the citizen
const px = cit.x + randInt(-50, 50); const px = cit.x + randInt(-50, 50);
const py = cit.y + randInt(-50, 50); const py = cit.y + randInt(-50, 50);
moveToward(cit, px, py, 0.4); moveToward(cit, px, py, 0.4);
if (distance(cit.x, cit.y, px, py) < 10) { if (distance(cit.x, cit.y, px, py) < 10) {
if (cit.carryingFruit >= FRUIT_PLANT_COST) { if (cit.carryingFruit >= FRUIT_PLANT_COST) {
@ -583,7 +787,6 @@ function eatAtHouseTask(cit) {
} }
moveToward(cit, house.x, house.y, 0.4); moveToward(cit, house.x, house.y, 0.4);
if (distance(cit.x, cit.y, house.x, house.y) < 20) { if (distance(cit.x, cit.y, house.x, house.y) < 20) {
// Eat some fruit
if (house.storedFruit > 0 && cit.hunger > 0) { if (house.storedFruit > 0 && cit.hunger > 0) {
const amountToEat = 10; const amountToEat = 10;
const eaten = Math.min(amountToEat, house.storedFruit); const eaten = Math.min(amountToEat, house.storedFruit);
@ -606,7 +809,6 @@ function restAtHouseTask(cit) {
} }
moveToward(cit, house.x, house.y, 0.4); moveToward(cit, house.x, house.y, 0.4);
if (distance(cit.x, cit.y, house.x, house.y) < 20) { if (distance(cit.x, cit.y, house.x, house.y) < 20) {
// Gains energy passively
if (cit.energy >= ENERGY_MAX - 1) { if (cit.energy >= ENERGY_MAX - 1) {
cit.energy = ENERGY_MAX; cit.energy = ENERGY_MAX;
cit.task = null; cit.task = null;
@ -615,88 +817,36 @@ function restAtHouseTask(cit) {
} }
} }
/**********************************************************************
* ROAD-BUILDING LOGIC
**********************************************************************/
/**
* When a new house is completed, we build a road from it
* to the nearest existing house (if one exists).
*/
function maybeBuildRoad(newHouse) {
// Find the nearest completed house (other than the new one)
const otherHouses = buildings.filter(b => b.buildingType === "House" && b.completed && b !== newHouse);
if (otherHouses.length === 0) return; // no other house to connect
let nearest = null;
let minDist = Infinity;
otherHouses.forEach((oh) => {
const d = distance(newHouse.x, newHouse.y, oh.x, oh.y);
if (d < minDist) {
minDist = d;
nearest = oh;
}
});
if (!nearest) return;
// Place a road site
const roadSite = createRoadSite(newHouse.x, newHouse.y, nearest.x, nearest.y);
buildings.push(roadSite);
logAction(`A Road site created between houses at (${newHouse.x}, ${newHouse.y}) and (${nearest.x}, ${nearest.y}).`);
}
/**********************************************************************
* FIND BUILDINGS OR RESOURCES
**********************************************************************/
function findNearestResourceOfType(cit, rtype) {
let nearest = null;
let nearestDist = Infinity;
for (const res of resources) {
if (res.type === rtype && res.amount > 0) {
const d = distance(cit.x, cit.y, res.x, res.y);
if (d < nearestDist) {
nearestDist = d;
nearest = res;
}
}
}
return nearest;
}
function findHouseWithFruit() {
return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit > 0);
}
function findAnyCompletedHouse() {
return buildings.find(b => b.buildingType === "House" && b.completed);
}
function findHouseNeedingFruit() {
return buildings.find(b => b.buildingType === "House" && b.completed && b.storedFruit < b.maxFruit);
}
/********************************************************************** /**********************************************************************
* RANDOM WANDER * RANDOM WANDER
**********************************************************************/ **********************************************************************/
function randomWander(cit) { function randomWander(cit) {
if (Math.random() < 0.01) { if (Math.random() < 0.01) {
cit.vx = (Math.random() - 0.5) * 0.5; cit.vx = (Math.random() - 0.5) * 0.3;
cit.vy = (Math.random() - 0.5) * 0.5; cit.vy = (Math.random() - 0.5) * 0.3;
}
}
function randomAnimalWander(ani) {
if (Math.random() < 0.01) {
ani.vx = (Math.random() - 0.5) * 0.4;
ani.vy = (Math.random() - 0.5) * 0.4;
} }
} }
/********************************************************************** /**********************************************************************
* MOVEMENT UTILS * MOVEMENT UTILS
**********************************************************************/ **********************************************************************/
function moveToward(cit, tx, ty, speed = 0.4) { function moveToward(obj, tx, ty, speed = 0.4) {
const dx = tx - cit.x; const dx = tx - obj.x;
const dy = ty - cit.y; const dy = ty - obj.y;
const dist = Math.sqrt(dx*dx + dy*dy); const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 1) { if (dist > 1) {
cit.vx = (dx / dist) * speed; obj.vx = (dx / dist) * speed;
cit.vy = (dy / dist) * speed; obj.vy = (dy / dist) * speed;
} else { } else {
cit.vx = 0; obj.vx = 0;
cit.vy = 0; obj.vy = 0;
} }
} }
@ -706,10 +856,29 @@ function distance(x1, y1, x2, y2) {
return Math.sqrt(dx*dx + dy*dy); return Math.sqrt(dx*dx + dy*dy);
} }
/**********************************************************************
* ROAD-BUILDING WHEN HOUSE IS COMPLETED
**********************************************************************/
function maybeBuildRoad(newHouse) {
const otherHouses = buildings.filter(b => b.buildingType === "House" && b.completed && b !== newHouse);
if (otherHouses.length === 0) return;
let nearest = null;
let minDist = Infinity;
for (const oh of otherHouses) {
const d = distance(newHouse.x, newHouse.y, oh.x, oh.y);
if (d < minDist) {
minDist = d;
nearest = oh;
}
}
if (!nearest) return;
const roadSite = createRoadSite(newHouse.x, newHouse.y, nearest.x, nearest.y);
buildings.push(roadSite);
logAction(`A Road site created between houses at (${newHouse.x}, ${newHouse.y}) and (${nearest.x}, ${nearest.y}).`);
}
/********************************************************************** /**********************************************************************
* AUTO-CREATION OF NEW HOUSE SITES * AUTO-CREATION OF NEW HOUSE SITES
* Similar logic to previous examples: if cityStorage has enough wood,
* we place a new House site occasionally.
**********************************************************************/ **********************************************************************/
setInterval(() => { setInterval(() => {
const underConstruction = buildings.find(b => b.buildingType === "House" && !b.completed); const underConstruction = buildings.find(b => b.buildingType === "House" && !b.completed);
@ -724,11 +893,10 @@ setInterval(() => {
}, 5000); }, 5000);
/********************************************************************** /**********************************************************************
* DEPOSIT WOOD IN CITY STORAGE IF IDLE BUILDER * DEPOSIT WOOD LOGIC
**********************************************************************/ **********************************************************************/
const originalAssignNewTask = assignNewTask; const originalAssignNewTask = assignNewTask;
assignNewTask = function(cit) { assignNewTask = function(cit) {
// If builder carrying wood, but no building needs it, deposit to city center
if (cit.profession === "Builder") { if (cit.profession === "Builder") {
const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood); const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood);
if (cit.carryingWood > 0 && !buildingNeedingWood) { if (cit.carryingWood > 0 && !buildingNeedingWood) {
@ -738,7 +906,6 @@ assignNewTask = function(cit) {
logAction(`${cit.name} deposited ${cit.carryingWood} wood into city storage.`); logAction(`${cit.name} deposited ${cit.carryingWood} wood into city storage.`);
cit.carryingWood = 0; cit.carryingWood = 0;
} else { } else {
// Move to city center
moveToward(cit, 0, 0, 0.4); moveToward(cit, 0, 0, 0.4);
cit.task = null; cit.task = null;
cit.target = null; cit.target = null;
@ -755,29 +922,30 @@ assignNewTask = function(cit) {
function drawWorld() { function drawWorld() {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw grid // Grid
drawGrid(); drawGrid();
// Draw resources // Resources
resources.forEach((res) => drawResource(res)); resources.forEach((res) => drawResource(res));
// Draw roads first (both completed or under construction) // Roads
buildings buildings
.filter(b => b.buildingType === "Road") .filter(b => b.buildingType === "Road")
.forEach(b => drawRoad(b)); .forEach(b => drawRoad(b));
// Draw houses // Houses
buildings buildings
.filter(b => b.buildingType === "House") .filter(b => b.buildingType === "House")
.forEach(b => drawHouse(b)); .forEach(b => drawHouse(b));
// Draw city storage info // City storage
drawCityStorage(); drawCityStorage();
// Draw citizens // Citizens
citizens.forEach((cit) => { citizens.forEach((cit) => drawCitizen(cit));
drawCitizen(cit);
}); // Animals
animals.forEach((ani) => drawAnimal(ani));
} }
function drawGrid() { function drawGrid() {
@ -801,7 +969,6 @@ function drawGrid() {
ctx.lineTo(range, y); ctx.lineTo(range, y);
ctx.stroke(); ctx.stroke();
} }
ctx.restore(); ctx.restore();
} }
@ -828,20 +995,15 @@ function drawResource(res) {
ctx.font = "12px sans-serif"; ctx.font = "12px sans-serif";
ctx.fillText(`Fruit (${res.amount})`, res.x - 25, res.y - 12); ctx.fillText(`Fruit (${res.amount})`, res.x - 25, res.y - 12);
} }
ctx.restore(); ctx.restore();
} }
/**
* Draw a House building site or a completed House
*/
function drawHouse(b) { function drawHouse(b) {
ctx.save(); ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale); ctx.scale(scale, scale);
if (!b.completed) { if (!b.completed) {
// Construction site
ctx.strokeStyle = "#FF8C00"; ctx.strokeStyle = "#FF8C00";
ctx.lineWidth = 2 / scale; ctx.lineWidth = 2 / scale;
ctx.strokeRect(b.x - 15, b.y - 15, 30, 30); ctx.strokeRect(b.x - 15, b.y - 15, 30, 30);
@ -852,7 +1014,6 @@ function drawHouse(b) {
ctx.fillText(`Wood: ${b.deliveredWood}/${b.requiredWood}`, b.x - 30, b.y + 30); ctx.fillText(`Wood: ${b.deliveredWood}/${b.requiredWood}`, b.x - 30, b.y + 30);
ctx.fillText(`Progress: ${Math.floor(b.buildProgress)}%`, b.x - 30, b.y + 42); ctx.fillText(`Progress: ${Math.floor(b.buildProgress)}%`, b.x - 30, b.y + 42);
} else { } else {
// Completed
ctx.fillStyle = "#DAA520"; ctx.fillStyle = "#DAA520";
ctx.fillRect(b.x - 15, b.y - 15, 30, 30); ctx.fillRect(b.x - 15, b.y - 15, 30, 30);
@ -865,17 +1026,13 @@ function drawHouse(b) {
ctx.restore(); ctx.restore();
} }
/**
* Draw a Road building site or a completed Road
*/
function drawRoad(b) { function drawRoad(b) {
ctx.save(); ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale); ctx.scale(scale, scale);
// Draw line between (b.x1, b.y1) and (b.x2, b.y2)
if (!b.completed) { if (!b.completed) {
ctx.setLineDash([5, 5]); // dashed if under construction ctx.setLineDash([5, 5]);
ctx.strokeStyle = "#888"; ctx.strokeStyle = "#888";
} else { } else {
ctx.setLineDash([]); ctx.setLineDash([]);
@ -888,7 +1045,6 @@ function drawRoad(b) {
ctx.lineTo(b.x2, b.y2); ctx.lineTo(b.x2, b.y2);
ctx.stroke(); ctx.stroke();
// Draw small label near midpoint
ctx.fillStyle = "#000"; ctx.fillStyle = "#000";
ctx.font = "12px sans-serif"; ctx.font = "12px sans-serif";
if (!b.completed) { if (!b.completed) {
@ -897,13 +1053,9 @@ function drawRoad(b) {
} else { } else {
ctx.fillText(`Road`, b.x - 10, b.y - 5); ctx.fillText(`Road`, b.x - 10, b.y - 5);
} }
ctx.restore(); ctx.restore();
} }
/**
* Show city storage
*/
function drawCityStorage() { function drawCityStorage() {
ctx.save(); ctx.save();
ctx.fillStyle = "#000"; ctx.fillStyle = "#000";
@ -917,17 +1069,39 @@ function drawCitizen(cit) {
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY); ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale); ctx.scale(scale, scale);
// Citizen circle
ctx.fillStyle = cit.color; ctx.fillStyle = cit.color;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cit.x, cit.y, 7, 0, Math.PI * 2); ctx.arc(cit.x, cit.y, 7, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Show name, profession, carrying, hunger, energy
ctx.fillStyle = "#000"; ctx.fillStyle = "#000";
ctx.font = "10px sans-serif"; ctx.font = "10px sans-serif";
ctx.fillText(`${cit.name} [${cit.profession}]`, cit.x + 10, cit.y - 2); const wpn = cit.hasWeapon ? "ARMED" : "";
ctx.fillText(`W:${cit.carryingWood} F:${cit.carryingFruit} H:${Math.floor(cit.hunger)} E:${Math.floor(cit.energy)}`, cit.x + 10, cit.y + 10); ctx.fillText(`${cit.name} [${cit.profession}] ${wpn}`, cit.x + 10, cit.y - 2);
ctx.fillText(`W:${cit.carryingWood} F:${cit.carryingFruit} H:${Math.floor(cit.hunger)} E:${Math.floor(cit.energy)}`,
cit.x + 10, cit.y + 10);
ctx.restore();
}
function drawAnimal(ani) {
if (ani.dead) return;
ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale);
if (ani.type === "Rabbit") {
ctx.fillStyle = "#999";
} else {
ctx.fillStyle = "#555";
}
ctx.beginPath();
ctx.arc(ani.x, ani.y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#000";
ctx.font = "10px sans-serif";
ctx.fillText(ani.type, ani.x + 8, ani.y + 3);
ctx.restore(); ctx.restore();
} }