obedabkenobi's picture
build me a clone of build-a-lot
68273ec verified
Raw
History Blame Contribute Delete
20.6 kB
class BuildALotGame {
constructor() {
this.lots = [];
this.money = 50000;
this.workers = 3;
this.maxWorkers = 3;
this.materials = 100;
this.maxMaterials = 100;
this.day = 1;
this.speed = 1;
this.selectedAction = null;
this.level = 1;
this.buildings = {
empty: { name: 'Empty Lot', icon: 'square', color: 'gray', rent: 0 },
cottage: { name: 'Cottage', icon: 'home', color: 'blue', cost: 3000, materials: 50, workers: 1, time: 3, rent: 150, upgradeCost: 1500, sellPrice: 4000 },
house: { name: 'House', icon: 'building-2', color: 'indigo', cost: 6000, materials: 100, workers: 2, time: 5, rent: 400, upgradeCost: 3000, sellPrice: 8000 },
mansion: { name: 'Mansion', icon: 'castle', color: 'purple', cost: 15000, materials: 250, workers: 3, time: 8, rent: 1200, upgradeCost: 7500, sellPrice: 20000 }
};
this.levels = [
{
name: "First Steps",
goals: [
{ type: 'money', target: 75000, current: 0, label: 'Have $75,000' },
{ type: 'cottages', target: 2, current: 0, label: 'Own 2 Cottages' }
],
startingMoney: 50000,
lotCount: 8
},
{
name: "Growing Town",
goals: [
{ type: 'houses', target: 3, current: 0, label: 'Own 3 Houses' },
{ type: 'money', target: 100000, current: 0, label: 'Have $100,000' }
],
startingMoney: 60000,
lotCount: 12
},
{
name: "Mansion District",
goals: [
{ type: 'mansions', target: 2, current: 0, label: 'Own 2 Mansions' },
{ type: 'upgraded', target: 5, current: 0, label: 'Upgrade 5 Properties' }
],
startingMoney: 80000,
lotCount: 16
}
];
this.init();
}
init() {
this.loadLevel(0);
this.startGameLoop();
this.updateUI();
}
loadLevel(levelIndex) {
this.level = levelIndex + 1;
const levelData = this.levels[levelIndex];
this.money = levelData.startingMoney;
this.workers = 3;
this.maxWorkers = 3;
this.materials = 100;
this.maxMaterials = 100;
this.day = 1;
this.lots = [];
// Create lots
for (let i = 0; i < levelData.lotCount; i++) {
this.lots.push({
id: i,
owned: i < 2, // Start with 2 owned lots
building: 'empty',
level: 0,
constructing: false,
constructionProgress: 0,
constructionTime: 0,
upgradeLevel: 0
});
}
// Reset goals
levelData.goals.forEach(goal => goal.current = 0);
this.currentGoals = JSON.parse(JSON.stringify(levelData.goals));
this.currentLevelData = levelData;
this.updateGoals();
this.renderGrid();
this.updateUI();
}
renderGrid() {
const grid = document.getElementById('game-grid');
grid.innerHTML = '';
this.lots.forEach(lot => {
const lotEl = document.createElement('div');
lotEl.className = `lot relative rounded-2xl border-2 cursor-pointer overflow-hidden ${this.getLotClasses(lot)}`;
lotEl.onclick = () => this.handleLotClick(lot);
const building = this.buildings[lot.building];
let content = '';
if (!lot.owned) {
content = `
<div class="absolute inset-0 flex flex-col items-center justify-center bg-gray-100">
<i data-lucide="lock" class="w-8 h-8 text-gray-400 mb-1"></i>
<span class="text-xs font-bold text-gray-500">FOR SALE</span>
<span class="text-sm font-bold text-green-600">$5,000</span>
</div>
`;
} else if (lot.constructing) {
content = `
<div class="absolute inset-0 flex flex-col items-center justify-center constructing bg-${building.color}-100">
<i data-lucide="hammer" class="w-8 h-8 text-${building.color}-600 mb-1 animate-bounce"></i>
<div class="w-16 h-2 bg-gray-300 rounded-full overflow-hidden">
<div class="h-full bg-${building.color}-500 progress-bar" style="width: ${(lot.constructionProgress / lot.constructionTime) * 100}%"></div>
</div>
</div>
`;
} else {
const rentMultiplier = 1 + (lot.upgradeLevel * 0.5);
const currentRent = Math.floor(building.rent * rentMultiplier);
content = `
<div class="absolute inset-0 flex flex-col items-center justify-center bg-${building.color}-50">
<i data-lucide="${building.icon}" class="w-10 h-10 text-${building.color}-600 mb-1"></i>
<span class="text-xs font-bold text-${building.color}-800">${building.name}</span>
${lot.upgradeLevel > 0 ? `<span class="text-xs text-amber-600">★${lot.upgradeLevel}</span>` : ''}
${currentRent > 0 ? `<span class="text-xs text-green-600 font-bold">$${currentRent}/day</span>` : ''}
</div>
`;
}
lotEl.innerHTML = content;
grid.appendChild(lotEl);
});
lucide.createIcons();
}
getLotClasses(lot) {
if (!lot.owned) return 'border-gray-300 bg-gray-50';
if (lot.constructing) return 'border-amber-400';
const building = this.buildings[lot.building];
return `border-${building.color}-200 bg-${building.color}-50`;
}
handleLotClick(lot) {
if (!this.selectedAction) return;
switch (this.selectedAction) {
case 'buy':
this.buyLot(lot);
break;
case 'build-cottage':
case 'build-house':
case 'build-mansion':
const type = this.selectedAction.replace('build-', '');
this.build(lot, type);
break;
case 'upgrade':
this.upgrade(lot);
break;
case 'sell':
this.sell(lot);
break;
}
}
buyLot(lot) {
if (lot.owned) {
this.showToast('Lot already owned!', 'error');
return;
}
if (this.money < 5000) {
this.showToast('Not enough money!', 'error');
return;
}
this.money -= 5000;
lot.owned = true;
this.showToast('Lot purchased!', 'success');
this.cancelAction();
this.updateUI();
this.renderGrid();
this.checkGoals();
}
build(lot, type) {
if (!lot.owned || lot.building !== 'empty' || lot.constructing) {
this.showToast('Cannot build here!', 'error');
return;
}
const building = this.buildings[type];
if (this.money < building.cost) {
this.showToast('Not enough money!', 'error');
return;
}
if (this.materials < building.materials) {
this.showToast('Not enough materials!', 'error');
return;
}
if (this.workers < building.workers) {
this.showToast('Not enough workers!', 'error');
return;
}
this.money -= building.cost;
this.materials -= building.materials;
this.workers -= building.workers;
lot.constructing = true;
lot.constructionProgress = 0;
lot.constructionTime = building.time;
lot.pendingBuilding = type;
this.showToast(`Building ${building.name}...`, 'info');
this.cancelAction();
this.updateUI();
this.renderGrid();
}
upgrade(lot) {
if (!lot.owned || lot.building === 'empty' || lot.constructing) {
this.showToast('Cannot upgrade!', 'error');
return;
}
const building = this.buildings[lot.building];
const upgradeCost = building.upgradeCost * (lot.upgradeLevel + 1);
if (this.money < upgradeCost) {
this.showToast('Not enough money!', 'error');
return;
}
if (this.workers < 1) {
this.showToast('Need 1 worker!', 'error');
return;
}
this.money -= upgradeCost;
this.workers -= 1;
lot.constructing = true;
lot.constructionProgress = 0;
lot.constructionTime = 2;
lot.pendingUpgrade = true;
this.showToast('Upgrading...', 'info');
this.cancelAction();
this.updateUI();
this.renderGrid();
}
sell(lot) {
if (!lot.owned || lot.constructing) {
this.showToast('Cannot sell!', 'error');
return;
}
const building = this.buildings[lot.building];
let sellPrice = lot.building === 'empty' ? 4000 : building.sellPrice;
sellPrice = Math.floor(sellPrice * (1 + lot.upgradeLevel * 0.3));
this.money += sellPrice;
lot.owned = false;
lot.building = 'empty';
lot.upgradeLevel = 0;
this.showToast(`Sold for $${sellPrice}!`, 'success');
this.cancelAction();
this.updateUI();
this.renderGrid();
this.checkGoals();
}
hireWorker() {
if (this.money < 1000) {
this.showToast('Not enough money!', 'error');
return;
}
this.money -= 1000;
this.maxWorkers += 1;
this.workers += 1;
this.updateUI();
this.showToast('Worker hired!', 'success');
}
buyMaterials() {
if (this.money < 100) {
this.showToast('Not enough money!', 'error');
return;
}
if (this.materials >= this.maxMaterials) {
this.showToast('Storage full!', 'error');
return;
}
this.money -= 100;
this.materials = Math.min(this.maxMaterials, this.materials + 50);
this.updateUI();
this.showToast('Materials purchased!', 'success');
}
setAction(action) {
this.selectedAction = action;
// Update UI to show selected action
document.querySelectorAll('#action-buttons button').forEach(btn => {
btn.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50');
});
const btn = document.getElementById(`btn-${action.replace('build-', '')}`);
if (btn) {
btn.classList.add('ring-2', 'ring-indigo-500', 'bg-indigo-50');
}
const info = document.getElementById('action-info');
const text = document.getElementById('action-text');
const texts = {
'buy': 'Select an empty lot to purchase ($5,000)',
'build-cottage': 'Select an owned empty lot to build Cottage',
'build-house': 'Select an owned empty lot to build House',
'build-mansion': 'Select an owned empty lot to build Mansion',
'upgrade': 'Select a building to upgrade',
'sell': 'Select a property to sell'
};
text.textContent = texts[action] || 'Select a target';
info.classList.remove('hidden');
}
cancelAction() {
this.selectedAction = null;
document.querySelectorAll('#action-buttons button').forEach(btn => {
btn.classList.remove('ring-2', 'ring-indigo-500', 'bg-indigo-50');
});
document.getElementById('action-info').classList.add('hidden');
}
updateUI() {
document.getElementById('money-display').textContent = `$${this.money.toLocaleString()}`;
document.getElementById('workers-display').textContent = `${this.workers} / ${this.maxWorkers}`;
document.getElementById('materials-display').textContent = `${this.materials} / ${this.maxMaterials}`;
document.getElementById('day-display').textContent = this.day;
// Update speed buttons
[1, 2, 3].forEach(speed => {
const btn = document.getElementById(`speed-${speed}`);
if (this.speed === speed) {
btn.classList.remove('bg-gray-200', 'text-gray-700');
btn.classList.add('bg-indigo-600', 'text-white');
} else {
btn.classList.add('bg-gray-200', 'text-gray-700');
btn.classList.remove('bg-indigo-600', 'text-white');
}
});
// Enable/disable action buttons based on resources
this.updateActionButtons();
}
updateActionButtons() {
const buttons = {
'btn-cottage': { money: 3000, materials: 50, workers: 1 },
'btn-house': { money: 6000, materials: 100, workers: 2 },
'btn-mansion': { money: 15000, materials: 250, workers: 3 },
'btn-upgrade': { money: 1500, workers: 1 },
'btn-sell': { owned: true },
'btn-buy': { money: 5000 }
};
Object.entries(buttons).forEach(([id, reqs]) => {
const btn = document.getElementById(id);
let enabled = true;
if (reqs.money && this.money < reqs.money) enabled = false;
if (reqs.materials && this.materials < reqs.materials) enabled = false;
if (reqs.workers && this.workers < reqs.workers) enabled = false;
if (enabled) {
btn.disabled = false;
btn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
btn.disabled = true;
btn.classList.add('opacity-50', 'cursor-not-allowed');
}
});
}
updateGoals() {
const container = document.getElementById('goals-container');
container.innerHTML = '';
this.currentGoals.forEach(goal => {
const div = document.createElement('div');
const completed = goal.current >= goal.target;
div.className = `flex items-center gap-2 ${completed ? 'text-green-600' : 'text-gray-600'}`;
div.innerHTML = `
<i data-lucide="${completed ? 'check-circle' : 'circle'}" class="w-4 h-4"></i>
<span class="${completed ? 'line-through' : ''}">${goal.label} (${goal.current}/${goal.target})</span>
`;
container.appendChild(div);
});
lucide.createIcons();
}
checkGoals() {
let allCompleted = true;
this.currentGoals.forEach(goal => {
switch(goal.type) {
case 'money':
goal.current = this.money;
break;
case 'cottages':
goal.current = this.lots.filter(l => l.building === 'cottage').length;
break;
case 'houses':
goal.current = this.lots.filter(l => l.building === 'house').length;
break;
case 'mansions':
goal.current = this.lots.filter(l => l.building === 'mansion').length;
break;
case 'upgraded':
goal.current = this.lots.filter(l => l.upgradeLevel > 0).length;
break;
}
if (goal.current < goal.target) allCompleted = false;
});
this.updateGoals();
if (allCompleted) {
setTimeout(() => {
document.getElementById('level-modal').classList.remove('hidden');
}, 500);
}
// Check game over
if (this.money < 100 && this.workers === this.maxWorkers && !this.lots.some(l => l.owned && this.buildings[l.building].rent > 0)) {
document.getElementById('gameover-modal').classList.remove('hidden');
}
}
startGameLoop() {
setInterval(() => {
// Day progression
this.day += 0.1 * this.speed;
// Construction progress
this.lots.forEach(lot => {
if (lot.constructing) {
lot.constructionProgress += 0.1 * this.speed;
if (lot.constructionProgress >= lot.constructionTime) {
// Construction complete
lot.constructing = false;
if (lot.pendingUpgrade) {
lot.upgradeLevel++;
delete lot.pendingUpgrade;
this.workers += 1;
} else {
lot.building = lot.pendingBuilding;
delete lot.pendingBuilding;
const building = this.buildings[lot.building];
this.workers += building.workers;
}
this.renderGrid();
this.checkGoals();
}
}
});
// Rent collection (daily)
if (Math.floor(this.day) > Math.floor(this.day - 0.1 * this.speed)) {
this.collectRent();
}
this.updateUI();
// Update construction progress bars
this.lots.forEach((lot, idx) => {
if (lot.constructing) {
const progressBars = document.querySelectorAll('.progress-bar');
if (progressBars[idx]) {
progressBars[idx].style.width = `${(lot.constructionProgress / lot.constructionTime) * 100}%`;
}
}
});
}, 100);
}
collectRent() {
let totalRent = 0;
this.lots.forEach(lot => {
if (lot.owned && !lot.constructing && lot.building !== 'empty') {
const building = this.buildings[lot.building];
const rentMultiplier = 1 + (lot.upgradeLevel * 0.5);
const rent = Math.floor(building.rent * rentMultiplier);
totalRent += rent;
// Show floating money
this.showFloatingMoney(lot.id, rent);
}
});
this.money += totalRent;
this.updateUI();
this.checkGoals();
}
showFloatingMoney(lotId, amount) {
const grid = document.getElementById('game-grid');
const lotEl = grid.children[lotId];
if (lotEl) {
const float = document.createElement('div');
float.className = 'absolute top-0 left-1/2 transform -translate-x-1/2 text-green-600 font-bold text-sm money-float z-10 pointer-events-none';
float.textContent = `+$${amount}`;
lotEl.style.position = 'relative';
lotEl.appendChild(float);
setTimeout(() => float.remove(), 1500);
}
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
info: 'bg-blue-500'
};
toast.className = `fixed top-20 left-1/2 transform -translate-x-1/2 ${colors[type]} text-white px-6 py-3 rounded-full shadow-lg z-50 bounce-in`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
nextLevel() {
document.getElementById('level-modal').classList.add('hidden');
if (this.level < this.levels.length) {
this.loadLevel(this.level);
} else {
alert('Congratulations! You beat all levels!');
this.loadLevel(0);
}
}
restartLevel() {
document.getElementById('gameover-modal').classList.add('hidden');
this.loadLevel(this.level - 1);
}
}
// Initialize game
const game = new BuildALotGame();