init
This commit is contained in:
commit
7325d0da44
951
index.html
Normal file
951
index.html
Normal 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, we’re 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>
|
Loading…
x
Reference in New Issue
Block a user