| 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 = []; |
| |
| |
| for (let i = 0; i < levelData.lotCount; i++) { |
| this.lots.push({ |
| id: i, |
| owned: i < 2, |
| building: 'empty', |
| level: 0, |
| constructing: false, |
| constructionProgress: 0, |
| constructionTime: 0, |
| upgradeLevel: 0 |
| }); |
| } |
|
|
| |
| 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; |
| |
| |
| 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; |
|
|
| |
| [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'); |
| } |
| }); |
|
|
| |
| 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); |
| } |
|
|
| |
| 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(() => { |
| |
| this.day += 0.1 * this.speed; |
| |
| |
| this.lots.forEach(lot => { |
| if (lot.constructing) { |
| lot.constructionProgress += 0.1 * this.speed; |
| |
| if (lot.constructionProgress >= lot.constructionTime) { |
| |
| 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(); |
| } |
| } |
| }); |
|
|
| |
| if (Math.floor(this.day) > Math.floor(this.day - 0.1 * this.speed)) { |
| this.collectRent(); |
| } |
|
|
| this.updateUI(); |
| |
| |
| 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; |
| |
| |
| 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); |
| } |
| } |
|
|
| |
| const game = new BuildALotGame(); |