worldbox-minigame/index.html
2025-03-13 21:31:23 +01:00

952 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>