// Production - The Idle Game: Main Application Orchestrator import { GState } from './core/state.js'; import { SimulationEngine } from './core/engine.js'; import { SyncManager } from './core/sync.js'; import { fmtCurrency, fmtNum, fmtRate, getNotation, setNotation } from './core/formatters.js'; import { PRODUCTS, getProduct } from './data/products.js'; import { renderProductionFloor, calculateMultiCost } from './ui/productionView.js'; import { renderContractsBoard } from './ui/contractsView.js'; import { renderBoardroom } from './ui/boardroomView.js'; import { renderInspector } from './ui/inspectorView.js'; import { renderAchievements } from './ui/achievementsView.js'; class ProductionApp { constructor() { this.sm = GState; this.engine = new SimulationEngine(this.sm); this.sync = new SyncManager(this.sm); this.lastFrameTime = performance.now(); this.lastSaveTime = Date.now(); } async init() { // Check for offline progress const now = Date.now(); const elapsedSeconds = (now - this.sm.state.lastTick) / 1000; if (elapsedSeconds > 5) { const result = this.engine.processOffline(elapsedSeconds); if (result.earned > 0) { this.showToast(`Executive Briefing: Your automated lines generated ${fmtCurrency(result.earned)} during ${Math.round(result.seconds / 60)}m away!`, 'success'); } } this.setupDomEvents(); this.renderAll(); // Check if logged in and sync cloud state if (this.sync.isLoggedIn()) { this.sync.fetchCloudState().then(cloud => { if (cloud && cloud.lastTick > this.sm.state.lastTick) { // Cloud save is newer, merge and save this.sm.state = { ...this.sm.state, ...cloud }; this.sm.save(); this.renderAll(); } }); } // Main game loop (tick at 30-60fps) const loop = (currentTime) => { const dt = Math.min((currentTime - this.lastFrameTime) / 1000, 0.5); // cap at 0.5s per frame this.lastFrameTime = currentTime; this.engine.tick(dt); this.updateHud(); // Auto-save every 5 seconds if (Date.now() - this.lastSaveTime > 5000) { this.sm.save(); this.lastSaveTime = Date.now(); } requestAnimationFrame(loop); }; requestAnimationFrame(loop); // Periodic view refresh for timers and progress bars (every 250ms) setInterval(() => { this.renderActiveView(); this.renderInspectorPane(); }, 250); } setupDomEvents() { // Navigation Tabs (Desktop sidebar + Mobile bottom nav) document.querySelectorAll('.nav-btn, .nav-tab').forEach(btn => { btn.onclick = () => { const tab = btn.dataset.tab; if (!tab) return; this.sm.state.activeTab = tab; document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === tab); }); this.renderActiveView(); }; }); // Buy Multiplier Bar document.querySelectorAll('.btn-multi-pill').forEach(btn => { btn.onclick = () => { const mode = btn.dataset.mode; this.sm.state.buyMode = mode; document.querySelectorAll('.btn-multi-pill').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.renderActiveView(); }; }); // User Profile & Settings const userBadge = document.getElementById('user-badge'); if (userBadge) { userBadge.onclick = () => this.openAuthModal(); } const settingsBtn = document.getElementById('btn-open-settings'); if (settingsBtn) { settingsBtn.onclick = () => this.toggleSettings(); } // Auth Modal Close & Submit const closeAuth = document.getElementById('btn-close-auth'); if (closeAuth) closeAuth.onclick = () => this.closeAuthModal(); const loginBtn = document.getElementById('btn-login-submit'); if (loginBtn) loginBtn.onclick = () => this.handleLogin(); const registerBtn = document.getElementById('btn-register-submit'); if (registerBtn) registerBtn.onclick = () => this.handleRegister(); } handleAction(action) { const state = this.sm.state; switch (action.type) { case 'SELECT_SECTOR': state.selectedSector = action.sector; this.renderActiveView(); break; case 'SELECT_PRODUCT': state.selectedProductId = action.productId; this.renderActiveView(); this.renderInspectorPane(); break; case 'UNLOCK_PRODUCT': { const p = getProduct(action.productId); if (p && state.cash >= p.unlockCost && !state.products[p.id]?.unlocked) { state.cash -= p.unlockCost; if (!state.products[p.id]) state.products[p.id] = {}; state.products[p.id].unlocked = true; state.products[p.id].lines = 1; state.selectedProductId = p.id; this.showToast(`Industrial Permit Acquired: ${p.name}!`, 'success'); this.sync.queueAction(action); this.renderAll(); } break; } case 'BUY_LINES': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, state.buyMode); if (state.cash >= calc.cost) { state.cash -= calc.cost; pState.lines += calc.count; this.showToast(`Expanded ${p.facilityName}: +${calc.count} lines!`, 'info'); this.sync.queueAction(action); this.renderAll(); } } break; } case 'HIRE_MANAGER': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState && !pState.manager && state.cash >= p.baseManagerCost) { state.cash -= p.baseManagerCost; pState.manager = true; state.stats.managersHired = (state.stats.managersHired || 0) + 1; this.showToast(`Operations Lead Hired: ${p.managerName}!`, 'success'); this.sync.queueAction(action); this.renderAll(); } break; } case 'BUY_LOGISTICS': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { const cost = p.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0); if (state.cash >= cost) { state.cash -= cost; pState.logistics = (pState.logistics || 0) + 1; this.showToast(`Logistics Fleet Expanded for ${p.name}!`, 'info'); this.sync.queueAction(action); this.renderAll(); } } break; } case 'MANUAL_CYCLE': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { // Check inputs const isRaw = !p.inputs || p.inputs.length === 0; let canRun = true; if (!isRaw) { canRun = p.inputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount); } if (canRun) { if (!isRaw) { p.inputs.forEach(inp => { state.products[inp.id].stock -= inp.amount; }); } const yieldAmt = p.outputAmount * Math.max(1, pState.lines); pState.stock += yieldAmt; pState.totalProduced = (pState.totalProduced || 0) + yieldAmt; state.stats.totalProduced = (state.stats.totalProduced || 0) + yieldAmt; this.showToast(`Batch Produced: +${yieldAmt} ${p.name}!`, 'info'); } else { this.showToast(`Insufficient inputs to cycle batch!`, 'warning'); } this.renderAll(); } break; } case 'FULFILL_CONTRACT': { const idx = (state.activeContracts || []).findIndex(c => c.uid === action.uid); if (idx !== -1) { const c = state.activeContracts[idx]; const pState = state.products[c.productId]; if (pState && pState.stock >= c.targetAmount) { pState.stock -= c.targetAmount; // Add subsidy bonus if researched const subsidyBonus = 1.0 + 0.35 * (state.boardroomPerks['government_subsidies'] || 0); const totalPayout = c.rewardCash * subsidyBonus; state.cash += totalPayout; state.lifetimeRevenue += totalPayout; state.reputation = (state.reputation || 0) + c.rewardRep; state.stats.contractsCompleted = (state.stats.contractsCompleted || 0) + 1; state.activeContracts.splice(idx, 1); this.showToast(`Contract Fulfilled! Earned ${fmtCurrency(totalPayout)} + ${c.rewardRep} Rep!`, 'success'); this.sync.queueAction(action); this.renderAll(); } } break; } case 'TRIGGER_IPO': { const confirmed = confirm('Confirm Corporate Initial Public Offering (IPO)?\n\nYour factories and cash will reset in exchange for permanent Venture Shares ($V-Shares) and corporate equity!'); if (confirmed) { const success = this.sm.performIPO(); if (success) { this.showToast(`IPO Successful! Welcome to Wall Street, CEO!`, 'success'); this.renderAll(); } } break; } case 'BUY_PERK': { const perk = (state.boardroomPerks || {})[action.perkId] || 0; // Find perk definition import('./data/boardroom.js').then(({ BOARDROOM_PERKS }) => { const def = BOARDROOM_PERKS.find(p => p.id === action.perkId); if (def && perk < def.maxLevel) { const cost = def.costShares[perk]; if (state.ventureShares >= cost) { state.ventureShares -= cost; state.boardroomPerks[action.perkId] = perk + 1; this.showToast(`Boardroom R&D Unlocked: ${def.name}!`, 'success'); this.renderAll(); } } }); break; } case 'LIQUIDATE_STOCK': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState && pState.stock > 0) { const ratio = action.ratio || 1.0; const sold = Math.floor(pState.stock * ratio); if (sold > 0) { pState.stock -= sold; const earned = sold * p.basePrice; state.cash += earned; state.lifetimeRevenue += earned; this.showToast(`Liquidated ${fmtNum(sold, 0)} ${p.name} for ${fmtCurrency(earned)}!`, 'info'); this.renderAll(); } } break; } } } updateHud() { const state = this.sm.state; const elCash = document.getElementById('hud-cash'); if (elCash) elCash.textContent = fmtNum(state.cash); const elRate = document.getElementById('hud-rate'); if (elRate) elRate.textContent = fmtRate(this.engine.cashRate).replace('+$', ''); const elValuation = document.getElementById('hud-valuation'); if (elValuation) elValuation.textContent = fmtCurrency(state.valuation); const elShares = document.getElementById('hud-shares'); if (elShares) elShares.textContent = fmtNum(state.ventureShares, 0); const elContractsBadge = document.getElementById('contracts-badge'); if (elContractsBadge) { const count = (state.activeContracts || []).length; elContractsBadge.textContent = count; elContractsBadge.style.display = count > 0 ? 'inline-flex' : 'none'; } const userBadge = document.getElementById('user-badge'); if (userBadge) { userBadge.textContent = this.sync.isLoggedIn() ? `[${this.sync.getUsername()}]` : `[GUEST]`; } } renderActiveView() { const container = document.getElementById('main-viewport'); if (!container) return; const tab = this.sm.state.activeTab || 'production'; if (tab === 'production') { renderProductionFloor(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'contracts') { renderContractsBoard(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'boardroom') { renderBoardroom(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'milestones') { renderAchievements(container, this.sm.state, (a) => this.handleAction(a)); } } renderInspectorPane() { const inspectorContainer = document.getElementById('desktop-inspector-panel'); if (inspectorContainer) { renderInspector(inspectorContainer, this.sm.state, (a) => this.handleAction(a)); } } renderAll() { this.updateHud(); this.renderActiveView(); this.renderInspectorPane(); } showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast-msg toast-${type}`; toast.innerHTML = ` ${message} `; container.appendChild(toast); setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => toast.remove(), 400); }, 3200); } openAuthModal() { const modal = document.getElementById('auth-modal'); if (modal) modal.classList.add('open'); } closeAuthModal() { const modal = document.getElementById('auth-modal'); if (modal) modal.classList.remove('open'); } async handleLogin() { const u = document.getElementById('auth-username')?.value.trim(); const p = document.getElementById('auth-password')?.value; if (!u || !p) return alert('Please enter username and password'); try { await this.sync.login(u, p); this.showToast(`Logged in as ${u}`, 'success'); this.closeAuthModal(); this.updateHud(); } catch (err) { alert(err.message || 'Login failed'); } } async handleRegister() { const u = document.getElementById('auth-username')?.value.trim(); const p = document.getElementById('auth-password')?.value; const hp = document.getElementById('auth-website-hp')?.value; if (!u || !p) return alert('Please enter username and password'); try { await this.sync.register(u, p, hp); this.showToast(`Account created! Welcome ${u}`, 'success'); this.closeAuthModal(); this.updateHud(); } catch (err) { alert(err.message || 'Registration failed'); } } toggleSettings() { const current = getNotation(); const next = current === 'standard' ? 'scientific' : 'standard'; setNotation(next); this.showToast(`Number Notation: ${next.toUpperCase()}`, 'info'); this.renderAll(); } } // Bootstrap window.addEventListener('DOMContentLoaded', () => { const app = new ProductionApp(); app.init(); window.App = app; });