worldbox-minigame/index.html

1318 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Virtual World</title>
<style>
/* Futuristic / fun styling */
/* A gentle gradient background with animation */
body {
margin: 0;
padding: 0;
font-family: 'Trebuchet MS', sans-serif;
background: linear-gradient(120deg, #0ff, #f0f);
background-size: 400% 400%;
animation: gradientShift 10s ease infinite alternate;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
/* Container for everything */
#app {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
position: relative;
z-index: 1; /* so it stays above the gradient */
}
/* Info bar top-left */
#infoBar {
position: absolute;
top: 0;
left: 0;
margin: 8px;
padding: 5px 10px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid #888;
border-radius: 6px;
font-size: 14px;
z-index: 10;
transition: box-shadow 0.3s;
}
#infoBar:hover {
box-shadow: 0 0 10px rgba(255,255,255,0.8);
}
#menu {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 8px 0;
justify-content: center;
}
.menu-button {
padding: 6px 12px;
background: #fff;
border: 2px solid #444;
cursor: pointer;
border-radius: 4px;
transition: transform 0.25s ease, background-color 0.25s ease;
}
.menu-button:hover {
transform: scale(1.08);
background-color: #c8fffa;
}
#toggleLogsBtn {
margin-left: 10px;
}
canvas {
background: #ffffff;
border: 2px solid #444;
cursor: grab;
transition: box-shadow 0.3s;
}
canvas:active {
cursor: grabbing;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
}
#log {
width: 800px;
height: 120px;
margin-top: 10px;
border: 2px solid #444;
background: #f9f9f9;
padding: 5px;
font-size: 0.9rem;
overflow-y: auto;
border-radius: 6px;
transition: box-shadow 0.3s;
}
#log:hover {
box-shadow: 0 0 6px rgba(0,0,0,0.15);
}
.log-entry {
margin: 2px 0;
}
/* Responsive adjustments */
@media (max-width: 850px) {
#log {
width: 95%;
height: 100px;
}
canvas {
width: 95% !important;
height: 400px !important;
}
}
</style>
</head>
<body>
<!-- Info Bar with money/citizen count -->
<div id="infoBar">
<p id="moneyDisplay">Money: $0</p>
<p id="citizenCount">Citizens: 0</p>
</div>
<div id="app">
<!-- Store Menu -->
<div id="menu">
<button class="menu-button" id="buyHouseBtn">Buy House ($300)</button>
<button class="menu-button" id="buyRoadBtn">Buy Road ($150)</button>
<button class="menu-button" id="buyBuilderBtn">Buy Builder ($100)</button>
<button class="menu-button" id="buyFarmerBtn">Buy Farmer ($100)</button>
<button class="menu-button" id="buySpawnerBtn">Buy Spawner ($500)</button>
<button class="menu-button" id="buyTreeBtn">Buy Tree ($50)</button>
<button class="menu-button" id="toggleLogsBtn">Hide Logs</button>
</div>
<canvas id="worldCanvas" width="800" height="600"></canvas>
<div id="log"></div>
</div>
<script>
/*
Virtual World with advanced AI (Farmers/Builders), Wolves & Rabbits,
user can buy House, Road, Citizens, Spawner, Tree, logs are togglable,
user starts with 1 builder, plus a new citizen every 3000 frames.
Fix "spawnBabyAnimal is not defined" by ensuring we define it properly.
*/
/**********************************************************************
* GLOBALS & STRUCTURES
**********************************************************************/
const canvas = document.getElementById('worldCanvas');
const ctx = canvas.getContext('2d');
const logContainer = document.getElementById('log');
const moneyDisplay = document.getElementById('moneyDisplay');
const citizenCountDisplay = document.getElementById('citizenCount');
// Pan & Zoom
let offsetX = 0;
let offsetY = 0;
let scale = 1.0;
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
// Basic money system
let money = 5000; // Start with 500
let purchaseMode = null; // "House" or "Road" or "Builder" or "Farmer", etc.
// Frame counter
let frameCount = 0;
// Shared city storage (wood, etc.)
const cityStorage = {
wood: 0
};
// Arrays
let resources = []; // Trees, FruitTrees
let buildings = []; // House, Road, Spawner
let citizens = [];
let animals = [];
/**********************************************************************
* COSTS & EARNINGS
**********************************************************************/
const COST_HOUSE = 300;
const COST_ROAD = 150;
const COST_BUILDER = 100;
const COST_FARMER = 100;
const COST_SPAWNER = 500;
const COST_TREE = 50;
// Earn money
const REWARD_DELIVER_WOOD = 5;
const REWARD_BUILD_COMPLETE = 20;
/**********************************************************************
* STATS & CONSTANTS
**********************************************************************/
// Hunger & energy
const HUNGER_MAX = 100;
const ENERGY_MAX = 100;
const HUNGER_INCREMENT = 0.005;
const ENERGY_DECREMENT_WORK = 0.02;
const ENERGY_INCREMENT_REST = 0.05;
const HUNGER_THRESHOLD = 50;
const ENERGY_THRESHOLD = 30;
// Trees
const FRUIT_TREE_START_AMOUNT = 20;
const FRUIT_GATHER_RATE = 1;
const FRUIT_PLANT_COST = 1;
// House & Road
const HOUSE_WOOD_REQUIRED = 50;
const HOUSE_BUILD_RATE = 0.2;
const HOUSE_MAX_FRUIT = 30;
const ROAD_WOOD_REQUIRED = 10;
const ROAD_BUILD_RATE = 0.3;
// Professions
const PROFESSIONS = ["Farmer","Builder"];
const WEAPON_WOOD_COST = 10; // forging cost
// Wolves & Rabbits
const STARTING_RABBITS = 5;
const STARTING_WOLVES = 3;
const ANIMAL_HUNGER_MAX = 100;
const RABBIT_HUNGER_INCREMENT = 0.003;
const WOLF_HUNGER_INCREMENT = 0.006;
const RABBIT_REPRO_COOLDOWN = 3000;
const WOLF_REPRO_COOLDOWN = 5000;
const RABBIT_REPRO_CHANCE = 0.0005;
const WOLF_REPRO_CHANCE = 0.0003;
const KNOCKBACK_DISTANCE = 20;
/**********************************************************************
* LOGGING & MONEY
**********************************************************************/
function logAction(text) {
const entry = document.createElement('div');
entry.className='log-entry';
entry.textContent=text;
logContainer.appendChild(entry);
logContainer.scrollTop=logContainer.scrollHeight;
}
function addMoney(amount, reason="") {
money += amount;
if(money<0) money=0;
updateMoneyDisplay();
if(reason){
logAction(`Money +$${amount} from ${reason}. Total=$${money}`);
}
}
function updateMoneyDisplay(){
moneyDisplay.textContent=`Money: $${money}`;
citizenCountDisplay.textContent=`Citizens: ${citizens.length}`;
}
/**********************************************************************
* RANDOM UTILS
**********************************************************************/
const firstNames = ["Al","Bea","Cal","Dee","Eve","Fay","Gil","Hal","Ian","Joy",
"Kay","Lee","Max","Ned","Oda","Pam","Ray","Sue","Tim","Ula",
"Vic","Wyn","Xan","Yel","Zed"];
const lastNames = ["Apple","Berry","Cherry","Delta","Echo","Flint","Gran","Hills",
"Iris","Jones","Knight","Lemon","Myer","Noble","Olson","Prime",
"Quartz","Row","Smith","Turn","Umbra","Vale","Wick","Xeno","Yolk","Zoom"];
function randomName() {
const f=firstNames[Math.floor(Math.random()*firstNames.length)];
const l=lastNames[Math.floor(Math.random()*lastNames.length)];
return `${f} ${l}`;
}
function randInt(min,max){
return Math.floor(Math.random()*(max-min))+min;
}
/**********************************************************************
* ENTITY DEFINITIONS
**********************************************************************/
function createCitizen(name,x,y, forcedProfession=null) {
// If forcedProfession is "Builder"/"Farmer", override random
let profession = forcedProfession
? forcedProfession
: PROFESSIONS[Math.floor(Math.random()*PROFESSIONS.length)];
return {
name,
profession,
x,y,
vx:(Math.random()-0.5)*0.3,
vy:(Math.random()-0.5)*0.3,
color:`hsl(${Math.random()*360},70%,50%)`,
task:null,
target:null,
carryingWood:0,
carryingFruit:0,
carryingCapacity:10,
hunger:0,
energy:ENERGY_MAX,
hasWeapon:false
};
}
function createResource(type,x,y,amount){
return { type, x, y, amount };
}
function createHouseSite(x,y){
return {
buildingType:"House",
x,y,
requiredWood:HOUSE_WOOD_REQUIRED,
deliveredWood:0,
buildProgress:0,
completed:false,
storedFruit:0,
maxFruit:HOUSE_MAX_FRUIT
};
}
function createRoadSite(x1,y1,x2,y2){
const mx=(x1+x2)/2;
const my=(y1+y2)/2;
return {
buildingType:"Road",
x:mx,
y:my,
x1,y1,x2,y2,
requiredWood:ROAD_WOOD_REQUIRED,
deliveredWood:0,
buildProgress:0,
completed:false
};
}
function createAnimal(type,x,y){
let cd=(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:cd
};
}
/**********************************************************************
* SPAWN BABY ANIMAL (to fix ReferenceError)
**********************************************************************/
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!`);
}
/**********************************************************************
* INIT WORLD
**********************************************************************/
function initWorld(){
// Normal trees
for(let i=0;i<15;i++){
resources.push(createResource("Tree", randInt(-1000,1000), randInt(-1000,1000),100));
}
// Fruit trees
for(let i=0;i<10;i++){
resources.push(createResource("FruitTree", randInt(-1000,1000), randInt(-1000,1000), FRUIT_TREE_START_AMOUNT));
}
// Start with 1 citizen => always a "Builder"
let c=createCitizen(randomName(), randInt(-200,200), randInt(-200,200), "Builder");
citizens.push(c);
logAction(`Initial Citizen joined: ${c.name} [Builder]`);
// Spawn some rabbits & wolves
for(let i=0;i<STARTING_RABBITS;i++){
animals.push(createAnimal("Rabbit", randInt(-1000,1000), randInt(-1000,1000)));
}
for(let i=0;i<STARTING_WOLVES;i++){
animals.push(createAnimal("Wolf", randInt(500,1000), randInt(500,1000)));
}
requestAnimationFrame(update);
}
/**********************************************************************
* UPDATE LOOP
**********************************************************************/
function update(){
frameCount++;
// Citizens
citizens.forEach((cit)=>updateCitizen(cit));
// Animals
animals.forEach((ani)=>{
if(!ani.dead) updateAnimal(ani);
});
animals=animals.filter(a=>!a.dead);
// Automatic new citizen every 3000 frames
if(frameCount%3000===0){
let baby=createCitizen(randomName(), randInt(-200,200), randInt(-200,200));
citizens.push(baby);
logAction(`A new citizen is born: ${baby.name} [${baby.profession}]`);
}
// Buildings
buildings.forEach((b)=>{
if(!b.completed && b.deliveredWood>=b.requiredWood){
let buildRate=(b.buildingType==="Road")?ROAD_BUILD_RATE:HOUSE_BUILD_RATE;
b.buildProgress+=buildRate;
if(b.buildProgress>=100){
b.completed=true;
addMoney(REWARD_BUILD_COMPLETE, `Complete ${b.buildingType}`);
if(b.buildingType==="House"){
logAction(`A new House completed @(${b.x},${b.y})!`);
maybeBuildRoad(b);
} else if(b.buildingType==="Road"){
logAction(`A Road has been completed!`);
}
}
}
});
drawWorld();
requestAnimationFrame(update);
}
/**********************************************************************
* CITIZEN AI
**********************************************************************/
function updateCitizen(cit){
cit.hunger+=HUNGER_INCREMENT;
if(cit.hunger>HUNGER_MAX) cit.hunger=HUNGER_MAX;
if(["chop","gatherFruit","build"].includes(cit.task)){
cit.energy-=ENERGY_DECREMENT_WORK;
} else if(cit.task==="restAtHouse"){
cit.energy+=ENERGY_INCREMENT_REST;
} else {
cit.energy-=0.0005;
}
if(cit.energy<0) cit.energy=0;
if(cit.energy>ENERGY_MAX) cit.energy=ENERGY_MAX;
if(!cit.task) assignNewTask(cit);
switch(cit.task){
case 'chop': chopTask(cit); break;
case 'deliverWood': deliverWoodTask(cit); break;
case 'build': buildTask(cit); break;
case 'gatherFruit': gatherFruitTask(cit); break;
case 'deliverFruit': deliverFruitTask(cit); break;
case 'plantFruitTree': plantFruitTreeTask(cit); break;
case 'eatAtHouse': eatAtHouseTask(cit); break;
case 'restAtHouse': restAtHouseTask(cit); break;
default: randomWander(cit); break;
}
cit.x+=cit.vx;
cit.y+=cit.vy;
}
/**********************************************************************
* ANIMAL AI
**********************************************************************/
function updateAnimal(ani){
if(ani.type==="Rabbit"){
updateRabbit(ani);
} else {
updateWolf(ani);
}
ani.x+=ani.vx;
ani.y+=ani.vy;
if(ani.reproductionCooldown>0){
ani.reproductionCooldown--;
}
}
function updateRabbit(r){
r.hunger+=RABBIT_HUNGER_INCREMENT;
if(r.hunger>=ANIMAL_HUNGER_MAX){
r.dead=true;
logAction("A rabbit starved to death.");
return;
}
// Reproduce
if(r.hunger<50&&r.reproductionCooldown<=0){
if(Math.random()<RABBIT_REPRO_CHANCE){
spawnBabyAnimal("Rabbit",r.x,r.y);
r.reproductionCooldown=RABBIT_REPRO_COOLDOWN;
}
}
if(r.hunger>50){
let tree=findNearestResourceOfType(r,"FruitTree");
if(tree){
moveToward(r,tree.x,tree.y,0.4);
if(distance(r.x,r.y,tree.x,tree.y)<10){
if(tree.amount>0){
tree.amount--;
r.hunger-=2;
if(r.hunger<0) r.hunger=0;
if(Math.random()<0.02) logAction("A rabbit is eating fruit...");
}
}
} else {
randomAnimalWander(r);
}
} else {
randomAnimalWander(r);
}
}
function updateWolf(w){
w.hunger+=WOLF_HUNGER_INCREMENT;
if(w.hunger>=ANIMAL_HUNGER_MAX){
w.dead=true;
logAction("A wolf starved to death.");
return;
}
// Reproduce
if(w.hunger<50&&w.reproductionCooldown<=0){
if(Math.random()<WOLF_REPRO_CHANCE){
spawnBabyAnimal("Wolf",w.x,w.y);
w.reproductionCooldown=WOLF_REPRO_COOLDOWN;
}
}
// hunt rabbit
let bunny=findNearestAnimalOfType(w,"Rabbit");
if(bunny){
moveToward(w,bunny.x,bunny.y,0.5);
if(distance(w.x,w.y,bunny.x,bunny.y)<10){
bunny.dead=true;
w.hunger=Math.max(0,w.hunger-30);
logAction("A wolf killed a rabbit!");
knockback(w,bunny.x,bunny.y,KNOCKBACK_DISTANCE);
}
return;
}
// if hunger>60 => try attack citizen
if(w.hunger>60){
let hum=findNearestCitizen(w);
if(hum){
moveToward(w,hum.x,hum.y,0.5);
if(distance(w.x,w.y,hum.x,hum.y)<10){
if(hum.hasWeapon){
w.dead=true;
logAction(`${hum.name} defended vs wolf using a weapon! Wolf died.`);
} else {
logAction(`A wolf killed ${hum.name}!`);
citizens.splice(citizens.indexOf(hum),1);
}
knockback(w,hum.x,hum.y,KNOCKBACK_DISTANCE);
}
} else {
randomAnimalWander(w);
}
} else {
randomAnimalWander(w);
}
}
/**********************************************************************
* TASKS & AI LOGIC
**********************************************************************/
function assignNewTask(cit){
if(cit.hunger>=HUNGER_THRESHOLD){
let houseWithFruit=findHouseWithFruit();
if(houseWithFruit){
cit.task="eatAtHouse";
cit.target=houseWithFruit;
return;
}
}
if(cit.energy<=ENERGY_THRESHOLD){
let compHouse=findAnyCompletedHouse();
if(compHouse){
cit.task="restAtHouse";
cit.target=compHouse;
return;
}
}
if(cit.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!`);
cit.task=null;
cit.target=null;
return;
}
}
if(cit.profession==="Builder"){
builderTasks(cit);
} else {
farmerTasks(cit);
}
}
function builderTasks(cit){
let buildingNeedingWood=buildings.find(b=>!b.completed&&b.deliveredWood<b.requiredWood);
if(buildingNeedingWood){
if(cit.carryingWood>0){
cit.task="deliverWood";
cit.target=buildingNeedingWood;
return;
}
let tree=findNearestResourceOfType(cit,"Tree");
if(tree){
cit.task="chop";
cit.target=tree;
return;
}
}
let buildingToConstruct=buildings.find(b=>!b.completed&&b.deliveredWood>=b.requiredWood);
if(buildingToConstruct){
cit.task="build";
cit.target=buildingToConstruct;
return;
}
let anyTree=findNearestResourceOfType(cit,"Tree");
if(anyTree){
cit.task="chop";
cit.target=anyTree;
return;
}
cit.task=null;
cit.target=null;
}
function farmerTasks(cit){
if(cit.carryingFruit>0){
let houseNeedFruit=findHouseNeedingFruit();
if(houseNeedFruit){
cit.task="deliverFruit";
cit.target=houseNeedFruit;
return;
}
}
let fruitTree=findNearestResourceOfType(cit,"FruitTree");
if(fruitTree&&fruitTree.amount>0){
cit.task="gatherFruit";
cit.target=fruitTree;
return;
}
if(cit.carryingFruit>=FRUIT_PLANT_COST&&Math.random()<0.1){
cit.task="plantFruitTree";
cit.target=null;
return;
}
cit.task=null;
cit.target=null;
}
/**********************************************************************
* TASK HANDLERS
**********************************************************************/
function chopTask(cit){
let tree=cit.target;
if(!tree||tree.amount<=0){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,tree.x,tree.y,0.4);
if(distance(cit.x,cit.y,tree.x,tree.y)<10){
let canGather=cit.carryingCapacity-cit.carryingWood;
let toGather=Math.min(1,tree.amount,canGather);
tree.amount-=toGather;
cit.carryingWood+=toGather;
if(Math.random()<0.01){
logAction(`${cit.name}[${cit.profession}] chopping wood...`);
}
if(cit.carryingWood>=cit.carryingCapacity||tree.amount<=0){
cit.task=null;
cit.target=null;
}
}
}
function deliverWoodTask(cit){
let b=cit.target;
if(!b||b.completed){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,b.x,b.y,0.4);
if(distance(cit.x,cit.y,b.x,b.y)<20){
let needed=b.requiredWood-b.deliveredWood;
if(needed>0&&cit.carryingWood>0){
let toDeliver=Math.min(cit.carryingWood,needed);
b.deliveredWood+=toDeliver;
cit.carryingWood-=toDeliver;
logAction(`${cit.name} delivered ${toDeliver} wood to ${b.buildingType}.`);
addMoney(REWARD_DELIVER_WOOD, "deliver wood");
}
cit.task=null;
cit.target=null;
}
}
function buildTask(cit){
let b=cit.target;
if(!b||b.completed){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,b.x,b.y,0.3);
}
function gatherFruitTask(cit){
let tree=cit.target;
if(!tree||tree.amount<=0){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,tree.x,tree.y,0.4);
if(distance(cit.x,cit.y,tree.x,tree.y)<10){
let canGather=cit.carryingCapacity-cit.carryingFruit;
let toGather=Math.min(FRUIT_GATHER_RATE, tree.amount, canGather);
tree.amount-=toGather;
cit.carryingFruit+=toGather;
if(Math.random()<0.01){
logAction(`${cit.name} [Farmer] is gathering fruit...`);
}
if(cit.carryingFruit>=cit.carryingCapacity||tree.amount<=0){
cit.task=null;
cit.target=null;
}
}
}
function deliverFruitTask(cit){
let house=cit.target;
if(!house||!house.completed){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,house.x,house.y,0.4);
if(distance(cit.x,cit.y,house.x,house.y)<20){
let space=house.maxFruit-house.storedFruit;
if(space>0&&cit.carryingFruit>0){
let toDeliver=Math.min(cit.carryingFruit,space);
house.storedFruit+=toDeliver;
cit.carryingFruit-=toDeliver;
logAction(`${cit.name} delivered ${toDeliver} fruit to house.`);
}
cit.task=null;
cit.target=null;
}
}
function plantFruitTreeTask(cit){
let px=cit.x+randInt(-50,50);
let py=cit.y+randInt(-50,50);
moveToward(cit,px,py,0.4);
if(distance(cit.x,cit.y,px,py)<10){
if(cit.carryingFruit>=FRUIT_PLANT_COST){
cit.carryingFruit-=FRUIT_PLANT_COST;
resources.push(createResource("FruitTree", px, py, FRUIT_TREE_START_AMOUNT));
logAction(`${cit.name}[Farmer] planted a new fruit tree!`);
}
cit.task=null;
cit.target=null;
}
}
function eatAtHouseTask(cit){
let house=cit.target;
if(!house||!house.completed){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,house.x,house.y,0.4);
if(distance(cit.x,cit.y,house.x,house.y)<20){
if(house.storedFruit>0 && cit.hunger>0){
let amtToEat=10;
let eaten=Math.min(amtToEat, house.storedFruit);
house.storedFruit-=eaten;
cit.hunger-=eaten;
if(cit.hunger<0) cit.hunger=0;
logAction(`${cit.name} ate ${eaten} fruit. Hunger => ${Math.floor(cit.hunger)}.`);
}
cit.task=null;
cit.target=null;
}
}
function restAtHouseTask(cit){
let house=cit.target;
if(!house||!house.completed){
cit.task=null;
cit.target=null;
return;
}
moveToward(cit,house.x,house.y,0.4);
if(distance(cit.x,cit.y,house.x,house.y)<20){
if(cit.energy>=ENERGY_MAX-1){
cit.energy=ENERGY_MAX;
cit.task=null;
cit.target=null;
}
}
}
/**********************************************************************
* RANDOM WANDER
**********************************************************************/
function randomWander(cit){
if(Math.random()<0.01){
cit.vx=(Math.random()-0.5)*0.3;
cit.vy=(Math.random()-0.5)*0.3;
}
}
function randomAnimalWander(a){
if(Math.random()<0.01){
a.vx=(Math.random()-0.5)*0.4;
a.vy=(Math.random()-0.5)*0.4;
}
}
/**********************************************************************
* MOVE & DISTANCE
**********************************************************************/
function moveToward(obj,tx,ty,speed=0.4){
let dx=tx-obj.x;
let dy=ty-obj.y;
let dist=Math.sqrt(dx*dx+dy*dy);
if(dist>1){
obj.vx=(dx/dist)*speed;
obj.vy=(dy/dist)*speed;
} else {
obj.vx=0;
obj.vy=0;
}
}
function distance(x1,y1,x2,y2){
let dx=x2-x1;
let dy=y2-y1;
return Math.sqrt(dx*dx+dy*dy);
}
function knockback(ent,tx,ty,dist){
let dx=ent.x-tx;
let dy=ent.y-ty;
let length=Math.sqrt(dx*dx+dy*dy);
if(length>0.1){
ent.x+=(dx/length)*dist;
ent.y+=(dy/length)*dist;
}
}
/**********************************************************************
* FINDERS
**********************************************************************/
function findNearestResourceOfType(ref, rtype){
let best=null;
let bestD=Infinity;
resources.forEach((res)=>{
if(res.type===rtype && res.amount>0){
let d=distance(ref.x,ref.y,res.x,res.y);
if(d<bestD){
bestD=d;
best=res;
}
}
});
return best;
}
function findNearestAnimalOfType(me,targetType){
let best=null;
let bestD=Infinity;
animals.forEach((a)=>{
if(a.type===targetType && !a.dead && a!==me){
let d=distance(me.x,me.y,a.x,a.y);
if(d<bestD){
bestD=d;
best=a;
}
}
});
return best;
}
function findNearestCitizen(wolf){
let best=null;
let bestD=Infinity;
citizens.forEach((c)=>{
let d=distance(wolf.x,wolf.y,c.x,c.y);
if(d<bestD){
bestD=d;
best=c;
}
});
return best;
}
function findHouseWithFruit(){
return buildings.find(b=>b.buildingType==="House" && b.completed && b.storedFruit>0);
}
function findAnyCompletedHouse(){
return buildings.find(b=>b.buildingType==="House" && b.completed);
}
function findHouseNeedingFruit(){
return buildings.find(b=>b.buildingType==="House" && b.completed && b.storedFruit<b.maxFruit);
}
/**********************************************************************
* ROAD BUILDING WHEN HOUSE COMPLETES
**********************************************************************/
function maybeBuildRoad(newHouse){
let otherHouses=buildings.filter(b=>b.buildingType==="House" && b.completed && b!==newHouse);
if(otherHouses.length===0) return;
let nearest=null;
let minD=Infinity;
otherHouses.forEach((oh)=>{
let d=distance(newHouse.x,newHouse.y,oh.x,oh.y);
if(d<minD){
minD=d;
nearest=oh;
}
});
if(!nearest)return;
let road=createRoadSite(newHouse.x,newHouse.y, nearest.x, nearest.y);
buildings.push(road);
logAction(`A Road site created between House@(${newHouse.x},${newHouse.y}) & House@(${nearest.x},${nearest.y}).`);
}
/**********************************************************************
* DEPOSIT WOOD LOGIC
**********************************************************************/
const originalAssignNewTask=assignNewTask;
assignNewTask=function(cit){
if(cit.profession==="Builder"){
let buildingNeedingWood=buildings.find(b=>!b.completed&&b.deliveredWood<b.requiredWood);
if(cit.carryingWood>0 && !buildingNeedingWood){
// deposit wood to city center (0,0)
let d=distance(cit.x,cit.y,0,0);
if(d<20){
cityStorage.wood+=cit.carryingWood;
logAction(`${cit.name} deposited ${cit.carryingWood} wood into city storage.`);
cit.carryingWood=0;
} else {
moveToward(cit,0,0,0.4);
cit.task=null;
cit.target=null;
return;
}
}
}
originalAssignNewTask(cit);
};
/**********************************************************************
* RENDER
**********************************************************************/
function drawWorld(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawGrid();
// resources
resources.forEach((res)=>drawResource(res));
// roads
buildings.filter(b=>b.buildingType==="Road").forEach(b=>drawRoad(b));
// houses
buildings.filter(b=>b.buildingType==="House").forEach(b=>drawHouse(b));
// city storage
drawCityStorage();
// citizens
citizens.forEach((cit)=>drawCitizen(cit));
// animals
animals.forEach((ani)=>{
if(!ani.dead) drawAnimal(ani);
});
}
function drawGrid(){
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
ctx.strokeStyle="#ccc";
ctx.lineWidth=1/scale;
let range=2000;
for(let x=-range;x<=range;x+=100){
ctx.beginPath();
ctx.moveTo(x,-range);
ctx.lineTo(x,range);
ctx.stroke();
}
for(let y=-range;y<=range;y+=100){
ctx.beginPath();
ctx.moveTo(-range,y);
ctx.lineTo(range,y);
ctx.stroke();
}
ctx.restore();
}
function drawResource(res){
if(res.amount<=0)return;
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
if(res.type==="Tree"){
ctx.fillStyle="#228B22";
ctx.beginPath();
ctx.arc(res.x,res.y,10,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="#000";
ctx.font="12px sans-serif";
ctx.fillText(`Tree(${res.amount})`, res.x-20, res.y-12);
} else {
ctx.fillStyle="#FF6347";
ctx.beginPath();
ctx.arc(res.x,res.y,10,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="#000";
ctx.font="12px sans-serif";
ctx.fillText(`Fruit(${res.amount})`, res.x-25,res.y-12);
}
ctx.restore();
}
function drawHouse(b){
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
if(!b.completed){
ctx.strokeStyle="#FF8C00";
ctx.lineWidth=2/scale;
ctx.strokeRect(b.x-15,b.y-15,30,30);
ctx.fillStyle="#000";
ctx.font="12px sans-serif";
ctx.fillText(`House(Bldg)`, b.x-30, b.y-25);
ctx.fillText(`Wood:${b.deliveredWood}/${b.requiredWood}`, b.x-30, b.y+30);
ctx.fillText(`Progress:${Math.floor(b.buildProgress)}%`, b.x-30, b.y+42);
} else {
ctx.fillStyle="#DAA520";
ctx.fillRect(b.x-15,b.y-15,30,30);
ctx.fillStyle="#000";
ctx.font="12px sans-serif";
ctx.fillText("House", b.x-15,b.y-20);
ctx.fillText(`Fruit:${b.storedFruit}/${b.maxFruit}`, b.x-25,b.y+32);
}
ctx.restore();
}
function drawRoad(b){
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
if(!b.completed){
ctx.setLineDash([5,5]);
ctx.strokeStyle="#888";
} else {
ctx.setLineDash([]);
ctx.strokeStyle="#444";
}
ctx.lineWidth=2/scale;
ctx.beginPath();
ctx.moveTo(b.x1,b.y1);
ctx.lineTo(b.x2,b.y2);
ctx.stroke();
ctx.fillStyle="#000";
ctx.font="12px sans-serif";
if(!b.completed){
ctx.fillText(`Road(Bldg) ${Math.floor(b.buildProgress)}%`, b.x,b.y-15);
ctx.fillText(`Wood:${b.deliveredWood}/${b.requiredWood}`, b.x-20,b.y+15);
} else {
ctx.fillText("Road", b.x-10,b.y-5);
}
ctx.restore();
}
function drawCityStorage(){
ctx.save();
ctx.fillStyle="#000";
ctx.font="16px sans-serif";
ctx.fillText(`City Wood Storage: ${cityStorage.wood}`,10,20);
ctx.restore();
}
function drawCitizen(c){
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
ctx.fillStyle=c.color;
ctx.beginPath();
ctx.arc(c.x,c.y,7,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="#000";
ctx.font="10px sans-serif";
let wpn=c.hasWeapon?"ARMED":"";
ctx.fillText(`${c.name}[${c.profession}]${wpn}`, c.x+10,c.y-2);
ctx.fillText(`W:${c.carryingWood} F:${c.carryingFruit} H:${Math.floor(c.hunger)} E:${Math.floor(c.energy)}`,
c.x+10,c.y+10);
ctx.restore();
}
function drawAnimal(a){
if(a.dead)return;
ctx.save();
ctx.translate(canvas.width/2+offsetX, canvas.height/2+offsetY);
ctx.scale(scale, scale);
if(a.type==="Rabbit"){
ctx.fillStyle="#999";
} else {
ctx.fillStyle="#555";
}
ctx.beginPath();
ctx.arc(a.x,a.y,6,0,Math.PI*2);
ctx.fill();
ctx.fillStyle="#000";
ctx.font="10px sans-serif";
ctx.fillText(a.type, a.x+8,a.y+3);
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){
let dx=e.clientX-lastMouseX;
let dy=e.clientY-lastMouseY;
offsetX+=dx;
offsetY+=dy;
lastMouseX=e.clientX;
lastMouseY=e.clientY;
}
});
canvas.addEventListener('wheel',(e)=>{
e.preventDefault();
let zoomSpeed=0.001;
let delta=e.deltaY*zoomSpeed;
let oldScale=scale;
scale-=delta;
if(scale<0.1) scale=0.1;
if(scale>5) scale=5;
let mouseX=e.clientX-(canvas.width/2+offsetX);
let mouseY=e.clientY-(canvas.height/2+offsetY);
offsetX-=mouseX*(scale-oldScale);
offsetY-=mouseY*(scale-oldScale);
},{passive:false});
/**********************************************************************
* BUY MENU LOGIC
**********************************************************************/
document.getElementById('buyHouseBtn').addEventListener('click',()=>{
purchaseMode="House";
logAction("Click on map to place a House site.");
});
document.getElementById('buyRoadBtn').addEventListener('click',()=>{
purchaseMode="Road";
logAction("Click on map to place a Road site.");
});
document.getElementById('buyBuilderBtn').addEventListener('click',()=>{
purchaseMode="Builder";
logAction("Click on map to place a new Builder citizen.");
});
document.getElementById('buyFarmerBtn').addEventListener('click',()=>{
purchaseMode="Farmer";
logAction("Click on map to place a new Farmer citizen.");
});
document.getElementById('buySpawnerBtn').addEventListener('click',()=>{
purchaseMode="Spawner";
logAction("Click on map to place a Spawner building (not implemented fully).");
});
document.getElementById('buyTreeBtn').addEventListener('click',()=>{
purchaseMode="Tree";
logAction("Click on map to place a new Tree.");
});
canvas.addEventListener('click',(e)=>{
if(!purchaseMode)return;
const rect=canvas.getBoundingClientRect();
let cx=e.clientX-rect.left;
let cy=e.clientY-rect.top;
let worldX=(cx-(canvas.width/2)-offsetX)/scale;
let worldY=(cy-(canvas.height/2)-offsetY)/scale;
if(purchaseMode==="House"){
if(money>=COST_HOUSE){
addMoney(-COST_HOUSE, "Buy House");
let site=createHouseSite(worldX,worldY);
buildings.push(site);
logAction(`Purchased House site @(${Math.floor(worldX)},${Math.floor(worldY)})`);
} else {
logAction("Not enough money to buy House!");
}
}
else if(purchaseMode==="Road"){
if(money>=COST_ROAD){
addMoney(-COST_ROAD, "Buy Road");
// For simplicity, we create a short horizontal road
let site=createRoadSite(worldX-50,worldY, worldX+50,worldY);
buildings.push(site);
logAction(`Purchased Road site @(${Math.floor(worldX)},${Math.floor(worldY)})`);
} else {
logAction("Not enough money to buy Road!");
}
}
else if(purchaseMode==="Builder"){
if(money>=COST_BUILDER){
addMoney(-COST_BUILDER, "Buy Builder");
let c=createCitizen(randomName(), worldX,worldY, "Builder");
citizens.push(c);
logAction(`Purchased new Builder @(${Math.floor(worldX)},${Math.floor(worldY)})`);
} else {
logAction("Not enough money to buy Builder!");
}
}
else if(purchaseMode==="Farmer"){
if(money>=COST_FARMER){
addMoney(-COST_FARMER, "Buy Farmer");
let c=createCitizen(randomName(), worldX,worldY, "Farmer");
citizens.push(c);
logAction(`Purchased new Farmer @(${Math.floor(worldX)},${Math.floor(worldY)})`);
} else {
logAction("Not enough money to buy Farmer!");
}
}
else if(purchaseMode==="Spawner"){
if(money>=COST_SPAWNER){
addMoney(-COST_SPAWNER, "Buy Spawner");
// We'll treat a Spawner as a completed building that spawns new Citizens automatically
// For demonstration, let's keep it simple or expand later
let spawnBuild={
buildingType:"Spawner",
x:worldX, y:worldY,
completed:true,
lastSpawnTime:Date.now() // can spawn 1 citizen/min
};
buildings.push(spawnBuild);
logAction(`Purchased Spawner @(${Math.floor(worldX)},${Math.floor(worldY)})
(not fully implemented, but can expand).`);
} else {
logAction("Not enough money to buy Spawner!");
}
}
else if(purchaseMode==="Tree"){
if(money>=COST_TREE){
addMoney(-COST_TREE, "Buy Tree");
let t=createResource("Tree", worldX,worldY, 100);
resources.push(t);
logAction(`Purchased a new Tree @(${Math.floor(worldX)},${Math.floor(worldY)})`);
} else {
logAction("Not enough money to buy Tree!");
}
}
purchaseMode=null;
});
/**********************************************************************
* TOGGLE LOGS
**********************************************************************/
document.getElementById('toggleLogsBtn').addEventListener('click',(e)=>{
if(logContainer.style.display==="none"){
logContainer.style.display="block";
e.target.textContent="Hide Logs";
} else {
logContainer.style.display="none";
e.target.textContent="Show Logs";
}
});
/**********************************************************************
* START
**********************************************************************/
initWorld();
updateMoneyDisplay();
</script>
</body>
</html>