// Production - The Idle Game: Main Application Orchestrator // Fine-grained DOM updates without full innerHTML teardowns 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, SECTORS, getProduct } from './data/products.js'; import { mountProductionFloor, updateProductionFloor, calculateMultiCost } from './ui/productionView.js'; import { mountContractsBoard, updateContractsBoard } from './ui/contractsView.js'; import { mountBoardroom, updateBoardroom } from './ui/boardroomView.js'; import { mountInspector, updateInspector } from './ui/inspectorView.js'; import { mountAchievements, updateAchievements } 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(); this.currentMountedTab = null; this.currentMountedSector = null; this.currentMountedProduct = null; } 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(); // Initial Mount (builds DOM nodes ONCE) this.mountActiveView(); this.mountInspectorPane(); this.updateHud(); // 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) { this.sm.state = { ...this.sm.state, ...cloud }; this.sm.save(); this.mountActiveView(); this.mountInspectorPane(); } }); } // Main game loop (ticks at 60fps, fine-grained updates without innerHTML) const loop = (currentTime) => { const dt = Math.min((currentTime - this.lastFrameTime) / 1000, 0.5); this.lastFrameTime = currentTime; this.engine.tick(dt); this.updateHud(); this.updateActiveView(); this.updateInspectorPane(); // Auto-save every 5 seconds if (Date.now() - this.lastSaveTime > 5000) { this.sm.save(); this.lastSaveTime = Date.now(); } requestAnimationFrame(loop); }; requestAnimationFrame(loop); } 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.mountActiveView(); }; }); // 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.mountActiveView(); // Remount button labels for 1x/10x/100x }; }); // Sidebar Sector Buttons document.querySelectorAll('.sidebar-sector-btn').forEach(btn => { btn.onclick = () => { const sector = btn.dataset.sector; this.sm.state.activeTab = 'production'; this.sm.state.selectedSector = sector; document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === 'production'); }); this.mountActiveView(); }; }); // 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.mountActiveView(); break; case 'SELECT_PRODUCT': state.selectedProductId = action.productId; this.mountInspectorPane(); 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.mountActiveView(); this.mountInspectorPane(); } 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.mountActiveView(); this.mountInspectorPane(); } } 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.mountActiveView(); } 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.mountActiveView(); this.mountInspectorPane(); } } break; } case 'MANUAL_CYCLE': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { 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'); } } 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; 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); state.contractCooldown = 10.0; // 10 second cooldown before new tender RFP appears // Recompute readyContractsCount let readyCount = 0; for (const actC of state.activeContracts) { const ps = state.products[actC.productId]; if (ps && (ps.stock || 0) >= actC.targetAmount) readyCount++; } state.readyContractsCount = readyCount; this.showToast(`Contract Fulfilled! Earned ${fmtCurrency(totalPayout)} + ${c.rewardRep} Rep!`, 'success'); this.sync.queueAction(action); this.mountActiveView(); } } break; } case 'TRIGGER_IPO': { this.showConfirmModal({ title: 'Authorize Initial Public Offering (IPO)', subtitle: 'Corporate Equity Transition', icon: 'lucide:trending-up', text: 'Confirm Corporate Initial Public Offering (IPO)?

All factory floors and cash will be liquidated in exchange for permanent Venture Shares ($V-Shares) and corporate prestige bonuses!', okText: 'Execute IPO', cancelText: 'Abort' }).then(confirmed => { if (confirmed) { const success = this.sm.performIPO(); if (success) { this.showToast(`IPO Successful! Welcome to Wall Street, CEO!`, 'success'); this.mountActiveView(); this.mountInspectorPane(); } } }); break; } case 'BUY_PERK': { const perk = (state.boardroomPerks || {})[action.perkId] || 0; 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.mountActiveView(); } } }); 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'); } } break; } } } updateHud() { const state = this.sm.state; const elCash = document.getElementById('hud-cash'); if (elCash) { const val = fmtNum(state.cash); if (elCash.textContent !== val) elCash.textContent = val; } const elRate = document.getElementById('hud-rate'); if (elRate) { const val = fmtRate(this.engine.cashRate).replace('+$', ''); if (elRate.textContent !== val) elRate.textContent = val; } const elValuation = document.getElementById('hud-valuation'); if (elValuation) { const val = fmtCurrency(state.valuation); if (elValuation.textContent !== val) elValuation.textContent = val; } const elShares = document.getElementById('hud-shares'); if (elShares) { const val = fmtNum(state.ventureShares, 0); if (elShares.textContent !== val) elShares.textContent = val; } const elContractsBadge = document.getElementById('contracts-badge'); if (elContractsBadge) { const readyCount = state.readyContractsCount || 0; const textVal = String(readyCount); if (elContractsBadge.textContent !== textVal) { elContractsBadge.textContent = textVal; } elContractsBadge.style.display = readyCount > 0 ? 'inline-flex' : 'none'; } const userBadge = document.getElementById('user-badge'); if (userBadge) { userBadge.textContent = this.sync.isLoggedIn() ? `[${this.sync.getUsername()}]` : `[GUEST]`; } // Update sidebar sector rate badges if (this.engine.sectorStats) { for (const s of SECTORS) { const stats = this.engine.sectorStats[s.id]; if (stats) { const elRate = document.getElementById(`sb-rate-${s.id}`); if (elRate) { const val = `+$${fmtNum(stats.revenueRate, 2)}/s`; if (elRate.textContent !== val) elRate.textContent = val; } } } } } /** * Mounts the main viewport view ONCE when the tab or sector changes */ mountActiveView() { const container = document.getElementById('main-viewport'); if (!container) return; const tab = this.sm.state.activeTab || 'production'; const sector = this.sm.state.selectedSector || 'all'; this.currentMountedTab = tab; this.currentMountedSector = sector; if (tab === 'production') { mountProductionFloor(container, this.sm.state, (a) => this.handleAction(a), this.engine); } else if (tab === 'contracts') { mountContractsBoard(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'boardroom') { mountBoardroom(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'milestones') { mountAchievements(container, this.sm.state, (a) => this.handleAction(a)); } } /** * Mounts the inspector panel ONCE when selected product changes */ mountInspectorPane() { const inspectorContainer = document.getElementById('desktop-inspector-panel'); if (inspectorContainer) { this.currentMountedProduct = this.sm.state.selectedProductId; mountInspector(inspectorContainer, this.sm.state, (a) => this.handleAction(a)); } } /** * Executes fine-grained in-place element updates without innerHTML teardowns */ updateActiveView() { const container = document.getElementById('main-viewport'); if (!container) return; const tab = this.sm.state.activeTab || 'production'; if (tab !== this.currentMountedTab) { this.mountActiveView(); return; } if (tab === 'production') { updateProductionFloor(container, this.sm.state, this.engine); } else if (tab === 'contracts') { updateContractsBoard(container, this.sm.state, (a) => this.handleAction(a)); } else if (tab === 'boardroom') { updateBoardroom(container, this.sm.state); } else if (tab === 'milestones') { updateAchievements(container, this.sm.state); } } updateInspectorPane() { const inspectorContainer = document.getElementById('desktop-inspector-panel'); if (!inspectorContainer) return; if (this.sm.state.selectedProductId !== this.currentMountedProduct) { this.mountInspectorPane(); return; } updateInspector(inspectorContainer, this.sm.state); } 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); } showConfirmModal({ title = 'Executive Authorization', subtitle = 'Board Confirmation', icon = 'lucide:alert-triangle', text = '', okText = 'Confirm', cancelText = 'Cancel' } = {}) { return new Promise((resolve) => { const modal = document.getElementById('confirm-modal'); const elTitle = document.getElementById('confirm-modal-title'); const elSub = document.getElementById('confirm-modal-subtitle'); const elIcon = document.getElementById('confirm-modal-icon'); const elText = document.getElementById('confirm-modal-text'); const btnOk = document.getElementById('btn-confirm-ok'); const btnCancel = document.getElementById('btn-confirm-cancel'); if (!modal) return resolve(false); if (elTitle) elTitle.textContent = title; if (elSub) elSub.textContent = subtitle; if (elIcon) elIcon.setAttribute('icon', icon); if (elText) elText.innerHTML = text; if (btnOk) btnOk.textContent = okText; if (btnCancel) btnCancel.textContent = cancelText; const cleanup = () => { modal.classList.remove('open'); btnOk.onclick = null; btnCancel.onclick = null; }; btnOk.onclick = () => { cleanup(); resolve(true); }; btnCancel.onclick = () => { cleanup(); resolve(false); }; modal.classList.add('open'); }); } openAuthModal() { const modal = document.getElementById('auth-modal'); const errBox = document.getElementById('auth-error-msg'); if (errBox) { errBox.style.display = 'none'; errBox.textContent = ''; } if (modal) modal.classList.add('open'); } closeAuthModal() { const modal = document.getElementById('auth-modal'); if (modal) modal.classList.remove('open'); } showAuthError(msg) { const errBox = document.getElementById('auth-error-msg'); if (errBox) { errBox.textContent = msg; errBox.style.display = 'block'; } else { this.showToast(msg, 'warning'); } } async handleLogin() { const u = document.getElementById('auth-username')?.value.trim(); const p = document.getElementById('auth-password')?.value; if (!u || !p) return this.showAuthError('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) { this.showAuthError(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 this.showAuthError('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) { this.showAuthError(err.message || 'Registration failed'); } } toggleSettings() { const current = getNotation(); const next = current === 'standard' ? 'scientific' : 'standard'; setNotation(next); this.showToast(`Number Notation: ${next.toUpperCase()}`, 'info'); this.mountActiveView(); this.mountInspectorPane(); } } // Bootstrap window.addEventListener('DOMContentLoaded', () => { const app = new ProductionApp(); app.init(); window.App = app; });