This commit is contained in:
{{QWERTYKBGUI}} 2025-03-13 21:31:23 +01:00
commit 7325d0da44

951
index.html Normal file
View File

@ -0,0 +1,951 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Virtual World with Professions, Houses, Hunger & Fruit Trees</title>
<style>
body {
margin: 0;
padding: 0;
background: #e0f7fa;
font-family: sans-serif;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
}
h1 {
margin: 5px;
color: #333;
}
#controls {
margin-bottom: 5px;
}
canvas {
background: #ffffff;
border: 2px solid #444;
cursor: grab;
}
canvas:active {
cursor: grabbing;
}
#log {
width: 800px;
height: 120px;
margin-top: 10px;
overflow-y: auto;
border: 2px solid #444;
background: #f9f9f9;
padding: 5px;
font-size: 0.9rem;
}
.log-entry {
margin: 2px 0;
}
</style>
</head>
<body>
<div id="app">
<h1>Virtual World: Builders, Farmers, Houses & Hunger</h1>
<p id="controls">Drag to move the map, scroll to zoom in/out.</p>
<canvas id="worldCanvas" width="800" height="600"></canvas>
<div id="log"></div>
</div>
<script>
/*
---------------------------------------------------------------------------------
Virtual World Simulation with:
- Builder & Farmer Professions
- Hunger & Energy
- Fruit Trees (gather fruit, plant new trees)
- House Construction (stores fruit, citizens can eat & rest)
- Chopping Trees (for wood)
- Child Birth Events
- Logging & Pan/Zoom
---------------------------------------------------------------------------------
*/
/**********************************************************************
* GLOBALS & BASIC STRUCTURES
**********************************************************************/
const canvas = document.getElementById('worldCanvas');
const ctx = canvas.getContext('2d');
const logContainer = document.getElementById('log');
// Pan & Zoom
let offsetX = 0;
let offsetY = 0;
let scale = 1.0;
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
// Frame counter for timed events
let frameCount = 0;
// Shared city storage for wood
const cityStorage = {
wood: 0
};
// Array of resource nodes (trees & fruit trees)
let resources = [];
// Buildings (houses under construction or completed)
let buildings = [];
// Citizens
let citizens = [];
/**********************************************************************
* SIMULATION CONSTANTS
**********************************************************************/
// Hunger & Energy
const HUNGER_INCREMENT = 0.005; // how fast hunger increases per frame
const ENERGY_DECREMENT_WORK = 0.02; // how fast energy decreases when working
const ENERGY_INCREMENT_REST = 0.05; // how fast energy recovers when resting
const HUNGER_THRESHOLD = 50; // if hunger > 50, citizen tries to eat
const ENERGY_THRESHOLD = 30; // if energy < 30, citizen tries to rest
const HUNGER_MAX = 100; // max hunger (if you want advanced mechanics, you could do damage if it hits 100)
const ENERGY_MAX = 100; // max energy
// House building requirements
const HOUSE_WOOD_REQUIRED = 50;
const HOUSE_BUILD_RATE = 0.2; // how quickly build progress advances per frame
// House capacity for fruit
const HOUSE_MAX_FRUIT = 30;
// Fruit tree resource
const FRUIT_TREE_START_AMOUNT = 20; // each fruit tree starts with 20 fruit
const FRUIT_GATHER_RATE = 1; // how much fruit is gathered per tick
const FRUIT_PLANT_COST = 1; // how many fruit is needed to plant a new fruit tree
// Professions
const PROFESSIONS = ["Farmer", "Builder"];
/**********************************************************************
* LOGGING
**********************************************************************/
function logAction(text) {
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.textContent = text;
logContainer.appendChild(entry);
logContainer.scrollTop = logContainer.scrollHeight;
}
/**********************************************************************
* 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;
}
/**********************************************************************
* ENTITY DEFINITIONS
**********************************************************************/
function createCitizen(name, x, y) {
const profession = PROFESSIONS[Math.floor(Math.random() * PROFESSIONS.length)];
return {
name,
profession,
x,
y,
vx: (Math.random() - 0.5) * 0.3, // small random movement
vy: (Math.random() - 0.5) * 0.3,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
task: null, // current task (string)
target: null, // resource or building
carryingWood: 0,
carryingFruit: 0,
carryingCapacity: 10,
hunger: 0, // 0 = not hungry, 100 = extremely hungry
energy: ENERGY_MAX // 0 = exhausted, 100 = fully rested
};
}
function createResource(type, x, y, amount) {
return { type, x, y, amount };
}
function createBuildingSite(buildingType, x, y) {
return {
buildingType,
x,
y,
requiredWood: HOUSE_WOOD_REQUIRED,
deliveredWood: 0,
buildProgress: 0,
completed: false,
storedFruit: 0, // once completed, houses can store fruit
maxFruit: HOUSE_MAX_FRUIT
};
}
/**********************************************************************
* WORLD INITIALIZATION
**********************************************************************/
function initWorld() {
// Create some normal "wood trees" (type: "Tree")
for (let i = 0; i < 15; i++) {
const x = randInt(-1000, 1000);
const y = randInt(-1000, 1000);
resources.push(createResource("Tree", x, y, 100)); // 100 wood each
}
// Create some fruit trees (type: "FruitTree")
for (let i = 0; i < 10; i++) {
const x = randInt(-1000, 1000);
const y = randInt(-1000, 1000);
resources.push(createResource("FruitTree", x, y, FRUIT_TREE_START_AMOUNT));
}
// Create 5 initial citizens
for (let i = 0; i < 5; i++) {
const c = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200));
citizens.push(c);
logAction(`Citizen joined: ${c.name} [${c.profession}]`);
}
// Start simulation
requestAnimationFrame(update);
}
/**********************************************************************
* UPDATE LOOP
**********************************************************************/
function update() {
frameCount++;
// Update each citizen (AI + movement)
citizens.forEach((cit) => {
updateCitizen(cit);
});
// Periodically add new citizens (child births)
// e.g., every ~10 seconds
if (frameCount % 600 === 0) {
const baby = createCitizen(randomName(), randInt(-200, 200), randInt(-200, 200));
citizens.push(baby);
logAction(`A child is born: ${baby.name} [${baby.profession}]`);
}
// Update buildings (construct progress)
buildings.forEach((b) => {
if (!b.completed && b.deliveredWood >= b.requiredWood) {
b.buildProgress += HOUSE_BUILD_RATE;
if (b.buildProgress >= 100) {
b.completed = true;
logAction(`A new ${b.buildingType} is completed at (${b.x}, ${b.y})!`);
}
}
});
// Draw
drawWorld();
requestAnimationFrame(update);
}
/**********************************************************************
* CITIZEN UPDATE (AI + Movement)
**********************************************************************/
function updateCitizen(cit) {
// Increase hunger over time
cit.hunger += HUNGER_INCREMENT;
if (cit.hunger > HUNGER_MAX) {
cit.hunger = HUNGER_MAX; // cap
// (Optional) If you want to handle "starvation" logic, do it here
}
// If working (chopping, gathering, building, etc.), reduce energy
if (cit.task === 'chop' || cit.task === 'gatherFruit' || cit.task === 'build') {
cit.energy -= ENERGY_DECREMENT_WORK;
} else if (cit.task === 'restAtHouse') {
cit.energy += ENERGY_INCREMENT_REST;
} else {
// Slight random movement => small passive energy drop
cit.energy -= 0.0005;
}
// Cap energy
if (cit.energy < 0) cit.energy = 0;
if (cit.energy > ENERGY_MAX) cit.energy = ENERGY_MAX;
// If the citizen has no task or just finished something, assign a new one
if (!cit.task) {
assignNewTask(cit);
}
// Execute task logic
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;
default:
// Idle or random wandering
randomWander(cit);
break;
}
// Apply velocity
cit.x += cit.vx;
cit.y += cit.vy;
}
/**********************************************************************
* TASK ASSIGNMENT & HELPER LOGIC
**********************************************************************/
function assignNewTask(cit) {
// 1) If hunger is too high, find a house that has fruit to eat
if (cit.hunger >= HUNGER_THRESHOLD) {
const houseWithFruit = findHouseWithFruit();
if (houseWithFruit) {
cit.task = 'eatAtHouse';
cit.target = houseWithFruit;
return;
}
}
// 2) If energy is too low, find any completed house to rest
if (cit.energy <= ENERGY_THRESHOLD) {
const completedHouse = findAnyCompletedHouse();
if (completedHouse) {
cit.task = 'restAtHouse';
cit.target = completedHouse;
return;
}
}
// Profession-based tasks
if (cit.profession === "Builder") {
builderTasks(cit);
} else {
farmerTasks(cit);
}
}
/**
* Builder logic:
* - If carrying wood and building needs wood, deliver it
* - If there's a building under construction that is short on wood, chop or deliver
* - If building is ready to build (wood delivered), help build
* - Otherwise, just gather wood or idle
*/
function builderTasks(cit) {
// Check if there's a building that still needs wood
const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood);
if (buildingNeedingWood) {
// If carrying wood, deliver
if (cit.carryingWood > 0) {
cit.task = 'deliverWood';
cit.target = buildingNeedingWood;
return;
}
// Else chop wood
const tree = findNearestResourceOfType(cit, "Tree");
if (tree) {
cit.task = 'chop';
cit.target = tree;
return;
}
}
// If there's a building that has enough wood but not completed, help build
const buildingToConstruct = buildings.find(b => !b.completed && b.deliveredWood >= b.requiredWood);
if (buildingToConstruct) {
cit.task = 'build';
cit.target = buildingToConstruct;
return;
}
// If nothing else, chop wood (store in city storage) so we can build new houses
const anyTree = findNearestResourceOfType(cit, "Tree");
if (anyTree) {
cit.task = 'chop';
cit.target = anyTree;
return;
}
// Otherwise, idle
cit.task = null;
cit.target = null;
}
/**
* Farmer logic:
* - If carrying fruit and there's a house that can store more, deliver fruit
* - If not carrying fruit or house is full, gather fruit
* - Maybe also plant a fruit tree if carrying fruit
*/
function farmerTasks(cit) {
// If carrying fruit, try delivering to a house
if (cit.carryingFruit > 0) {
const houseNeedingFruit = findHouseNeedingFruit();
if (houseNeedingFruit) {
cit.task = 'deliverFruit';
cit.target = houseNeedingFruit;
return;
}
}
// If no fruit in hand, gather fruit
const fruitTree = findNearestResourceOfType(cit, "FruitTree");
if (fruitTree && fruitTree.amount > 0) {
cit.task = 'gatherFruit';
cit.target = fruitTree;
return;
}
// Maybe plant a fruit tree if carrying at least 1 fruit (random chance)
if (cit.carryingFruit >= FRUIT_PLANT_COST && Math.random() < 0.1) {
cit.task = 'plantFruitTree';
cit.target = null; // we'll pick a location near the citizen
return;
}
// Otherwise, idle
cit.task = null;
cit.target = null;
}
/**********************************************************************
* SPECIFIC TASK IMPLEMENTATIONS
**********************************************************************/
/** CHOP TASK - gather wood from normal trees */
function chopTask(cit) {
const tree = cit.target;
if (!tree || tree.amount <= 0) {
// Tree depleted
cit.task = null;
cit.target = null;
return;
}
moveToward(cit, tree.x, tree.y, 0.4);
// If close enough, chop
if (distance(cit.x, cit.y, tree.x, tree.y) < 10) {
const canGather = cit.carryingCapacity - cit.carryingWood;
const toGather = Math.min(1, tree.amount, canGather); // gather 1 wood per tick
tree.amount -= toGather;
cit.carryingWood += toGather;
// Log occasionally
if (Math.random() < 0.01) {
logAction(`${cit.name} [${cit.profession}] is chopping wood...`);
}
// If full or tree depleted, were done
if (cit.carryingWood >= cit.carryingCapacity || tree.amount <= 0) {
cit.task = null;
cit.target = null;
}
}
}
/** DELIVER WOOD TASK - deliver wood to building under construction */
function deliverWoodTask(cit) {
const building = cit.target;
if (!building || building.completed) {
cit.task = null;
cit.target = null;
return;
}
moveToward(cit, building.x, building.y, 0.4);
if (distance(cit.x, cit.y, building.x, building.y) < 20) {
// Deliver wood
const needed = building.requiredWood - building.deliveredWood;
if (needed > 0 && cit.carryingWood > 0) {
const toDeliver = Math.min(cit.carryingWood, needed);
building.deliveredWood += toDeliver;
cit.carryingWood -= toDeliver;
logAction(`${cit.name} delivered ${toDeliver} wood to ${building.buildingType}.`);
}
cit.task = null;
cit.target = null;
}
}
/** BUILD TASK - once wood is delivered, help increase build progress (handled in global update) */
function buildTask(cit) {
const building = cit.target;
if (!building || building.completed) {
cit.task = null;
cit.target = null;
return;
}
moveToward(cit, building.x, building.y, 0.3);
// They passively build while on site; actual progress is in global update
}
/** GATHER FRUIT TASK - from fruit trees */
function gatherFruitTask(cit) {
const fruitTree = cit.target;
if (!fruitTree || fruitTree.amount <= 0) {
cit.task = null;
cit.target = null;
return;
}
moveToward(cit, fruitTree.x, fruitTree.y, 0.4);
if (distance(cit.x, cit.y, fruitTree.x, fruitTree.y) < 10) {
const canGather = cit.carryingCapacity - cit.carryingFruit;
const toGather = Math.min(FRUIT_GATHER_RATE, fruitTree.amount, canGather);
fruitTree.amount -= toGather;
cit.carryingFruit += toGather;
if (Math.random() < 0.01) {
logAction(`${cit.name} [Farmer] is gathering fruit...`);
}
if (cit.carryingFruit >= cit.carryingCapacity || fruitTree.amount <= 0) {
cit.task = null;
cit.target = null;
}
}
}
/** DELIVER FRUIT TASK - deposit fruit into a house */
function deliverFruitTask(cit) {
const 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) {
// Deliver fruit
const space = house.maxFruit - house.storedFruit;
if (space > 0 && cit.carryingFruit > 0) {
const toDeliver = Math.min(cit.carryingFruit, space);
house.storedFruit += toDeliver;
cit.carryingFruit -= toDeliver;
logAction(`${cit.name} delivered ${toDeliver} fruit to house.`);
}
cit.task = null;
cit.target = null;
}
}
/** PLANT FRUIT TREE TASK - if carrying fruit, plant a new tree somewhere near */
function plantFruitTreeTask(cit) {
// Choose a spot near the citizen
const px = cit.x + randInt(-50, 50);
const py = cit.y + randInt(-50, 50);
// Move there
moveToward(cit, px, py, 0.4);
// If close enough to that spot, plant
if (distance(cit.x, cit.y, px, py) < 10) {
if (cit.carryingFruit >= FRUIT_PLANT_COST) {
cit.carryingFruit -= FRUIT_PLANT_COST;
resources.push(createResource("FruitTree", px, py, FRUIT_TREE_START_AMOUNT));
logAction(`${cit.name} [Farmer] planted a new fruit tree!`);
}
cit.task = null;
cit.target = null;
}
}
/** EAT AT HOUSE TASK - if a house has fruit, reduce hunger */
function eatAtHouseTask(cit) {
const 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) {
// Eat some fruit to reduce hunger
if (house.storedFruit > 0 && cit.hunger > 0) {
const amountToEat = 10; // each 'meal' reduces 10 hunger
const eaten = Math.min(amountToEat, house.storedFruit);
house.storedFruit -= eaten;
cit.hunger -= eaten;
if (cit.hunger < 0) cit.hunger = 0;
logAction(`${cit.name} ate ${eaten} fruit. Hunger now ${Math.floor(cit.hunger)}.`);
}
// Done eating
cit.task = null;
cit.target = null;
}
}
/** REST AT HOUSE TASK - if house is completed, recover energy */
function restAtHouseTask(cit) {
const house = cit.target;
if (!house || !house.completed) {
cit.task = null;
cit.target = null;
return;
}
moveToward(cit, house.x, house.y, 0.4);
// If close enough, we are "resting" => see updateCitizen (rest recovers energy).
if (distance(cit.x, cit.y, house.x, house.y) < 20) {
// If we reached max energy, done
if (cit.energy >= ENERGY_MAX - 1) {
cit.energy = ENERGY_MAX;
cit.task = null;
cit.target = null;
}
}
}
/**********************************************************************
* 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 the first completed house that has >0 storedFruit
return buildings.find(b => b.completed && b.storedFruit > 0);
}
function findAnyCompletedHouse() {
return buildings.find(b => b.completed);
}
function findHouseNeedingFruit() {
return buildings.find(b => b.completed && b.storedFruit < b.maxFruit);
}
/**********************************************************************
* RANDOM/IDLE MOVEMENT
**********************************************************************/
function randomWander(cit) {
// Occasional random direction change
if (Math.random() < 0.01) {
cit.vx = (Math.random() - 0.5) * 0.3;
cit.vy = (Math.random() - 0.5) * 0.3;
}
}
/**********************************************************************
* MOVEMENT UTILS
**********************************************************************/
function moveToward(cit, tx, ty, speed = 0.4) {
const dx = tx - cit.x;
const dy = ty - cit.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 1) {
cit.vx = (dx / dist) * speed;
cit.vy = (dy / dist) * speed;
} else {
cit.vx = 0;
cit.vy = 0;
}
}
function distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx*dx + dy*dy);
}
/**********************************************************************
* BUILDING LOGIC
**********************************************************************/
/**
* If no building is under construction and cityStorage has enough wood,
* place a new house building site.
* We'll do it once every few seconds to show city growth.
*/
setInterval(() => {
const underConstruction = buildings.find(b => !b.completed);
if (!underConstruction && cityStorage.wood >= HOUSE_WOOD_REQUIRED) {
cityStorage.wood -= HOUSE_WOOD_REQUIRED;
const x = randInt(-500, 500);
const y = randInt(-500, 500);
const site = createBuildingSite("House", x, y);
buildings.push(site);
logAction(`A new House construction site placed at (${x}, ${y}).`);
}
}, 5000); // check every 5 seconds
/**********************************************************************
* DEPOSIT WOOD IN CITY STORAGE IF CITIZENS ARE IDLE
* (Similar to previous examples)
**********************************************************************/
const originalAssignNewTask = assignNewTask;
assignNewTask = function(cit) {
// If a builder is carrying wood but no building needs it,
// deposit to city storage (which we treat as (0,0))
if (cit.profession === "Builder") {
const buildingNeedingWood = buildings.find(b => !b.completed && b.deliveredWood < b.requiredWood);
if (cit.carryingWood > 0 && !buildingNeedingWood) {
const distToCenter = distance(cit.x, cit.y, 0, 0);
if (distToCenter < 20) {
// Deposit wood
cityStorage.wood += cit.carryingWood;
logAction(`${cit.name} deposited ${cit.carryingWood} wood into city storage.`);
cit.carryingWood = 0;
} else {
// Move to center
moveToward(cit, 0, 0, 0.4);
// We'll remain idle if we get close enough
cit.task = null;
cit.target = null;
return;
}
}
}
// Then do the original logic
originalAssignNewTask(cit);
};
/**********************************************************************
* RENDER
**********************************************************************/
function drawWorld() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw grid
drawGrid();
// Draw resources (trees, fruit trees)
resources.forEach((res) => {
drawResource(res);
});
// Draw buildings
buildings.forEach((b) => {
drawBuilding(b);
});
// Draw city storage info
drawCityStorage();
// Draw citizens
citizens.forEach((cit) => {
drawCitizen(cit);
});
}
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;
const range = 2000;
const startX = -range;
const endX = range;
const startY = -range;
const endY = range;
ctx.beginPath();
for (let x = startX; x <= endX; x += 100) {
ctx.moveTo(x, startY);
ctx.lineTo(x, endY);
}
for (let y = startY; y <= endY; y += 100) {
ctx.moveTo(startX, y);
ctx.lineTo(endX, y);
}
ctx.stroke();
ctx.restore();
}
function drawResource(res) {
if (res.amount <= 0) return; // skip depleted
ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale);
if (res.type === "Tree") {
ctx.fillStyle = "#228B22"; // green
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(`${res.type} (${res.amount})`, res.x - 20, res.y - 12);
} else if (res.type === "FruitTree") {
ctx.fillStyle = "#FF6347"; // tomato color
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 drawBuilding(b) {
ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale);
if (!b.completed) {
// Construction site
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(`${b.buildingType} (Building)`, b.x - 30, b.y - 25);
ctx.fillText(`Wood: ${b.deliveredWood}/${b.requiredWood}`, b.x - 30, b.y + 30);
ctx.fillText(`Progress: ${Math.floor(b.buildProgress)}%`, b.x - 30, b.y + 42);
} else {
// Completed house
ctx.fillStyle = "#DAA520";
ctx.fillRect(b.x - 15, b.y - 15, 30, 30);
ctx.fillStyle = "#000";
ctx.font = "12px sans-serif";
ctx.fillText(`${b.buildingType}`, b.x - 15, b.y - 20);
ctx.fillText(`Fruit: ${b.storedFruit}/${b.maxFruit}`, b.x - 25, b.y + 32);
}
ctx.restore();
}
function drawCityStorage() {
ctx.save();
ctx.fillStyle = "#000";
ctx.font = "16px sans-serif";
ctx.fillText(`City Wood Storage: ${cityStorage.wood}`, 10, 20);
ctx.restore();
}
function drawCitizen(cit) {
ctx.save();
ctx.translate(canvas.width / 2 + offsetX, canvas.height / 2 + offsetY);
ctx.scale(scale, scale);
// Citizen circle
ctx.fillStyle = cit.color;
ctx.beginPath();
ctx.arc(cit.x, cit.y, 7, 0, Math.PI * 2);
ctx.fill();
// Show name + profession + carrying
ctx.fillStyle = "#000";
ctx.font = "10px sans-serif";
ctx.fillText(`${cit.name} [${cit.profession}]`, cit.x + 10, cit.y - 2);
// Additional info: wood/fruit/hunger/energy
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();
}
/**********************************************************************
* PAN & ZOOM
**********************************************************************/
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
});
canvas.addEventListener('mousemove', (e) => {
if (isDragging) {
const dx = e.clientX - lastMouseX;
const dy = e.clientY - lastMouseY;
offsetX += dx;
offsetY += dy;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
}
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomSpeed = 0.001;
const delta = e.deltaY * zoomSpeed;
const oldScale = scale;
scale -= delta;
if (scale < 0.1) scale = 0.1;
if (scale > 5) scale = 5;
const mouseX = e.clientX - (canvas.width / 2 + offsetX);
const mouseY = e.clientY - (canvas.height / 2 + offsetY);
offsetX -= mouseX * (scale - oldScale);
offsetY -= mouseY * (scale - oldScale);
}, { passive: false });
/**********************************************************************
* START
**********************************************************************/
initWorld();
</script>
</body>
</html>