// 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 { renderSectorTabsHtml } from './ui/production/sectorBar.js'; import { mountUnitDetailView, updateUnitDetailView } from './ui/unitDetailView.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(); } // Dark / Light Theme Toggle this.initTheme(); const themeBtn = document.getElementById('btn-theme-toggle'); if (themeBtn) { themeBtn.onclick = () => this.toggleTheme(); } // Global Keyboard Shortcut: ESC returns to production floor if on unit_detail window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.sm.state.activeTab === 'unit_detail') { this.handleAction({ type: 'CLOSE_UNIT_DETAIL' }); } }); // 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(); } initTheme() { const savedTheme = localStorage.getItem('production_theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); this.applyTheme(savedTheme); } toggleTheme() { const current = document.documentElement.getAttribute('data-theme') || 'light'; const next = current === 'dark' ? 'light' : 'dark'; this.applyTheme(next); this.showToast(`Theme switched to ${next.toUpperCase()} mode`, 'info'); } applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('production_theme', theme); const iconEl = document.getElementById('theme-toggle-icon'); const textEl = document.getElementById('theme-toggle-text'); if (iconEl) { iconEl.setAttribute('icon', theme === 'dark' ? 'lucide:sun' : 'lucide:moon'); } if (textEl) { textEl.textContent = theme === 'dark' ? 'Light' : 'Dark'; } } handleAction(action) { const state = this.sm.state; switch (action.type) { case 'SELECT_SECTOR': state.selectedSector = action.sector; if (state.activeTab !== 'production') { state.activeTab = 'production'; document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === 'production'); }); } this.mountActiveView(); break; case 'OPEN_UNIT_DETAIL': { state.selectedProductId = action.productId; state.activeTab = 'unit_detail'; this.mountActiveView(); window.scrollTo({ top: 0, behavior: 'smooth' }); break; } case 'CLOSE_UNIT_DETAIL': { state.activeTab = 'production'; this.mountActiveView(); requestAnimationFrame(() => { const cardEl = document.getElementById('card-' + state.selectedProductId); if (cardEl) { cardEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); break; } case 'SELECT_PRODUCT': { state.selectedProductId = action.productId; const targetP = getProduct(action.productId); // If user selected a dependency or unit from inspector, ensure we navigate / go to that facility card let needsViewMount = false; if (state.activeTab !== 'production') { state.activeTab = 'production'; document.querySelectorAll('.nav-btn, .nav-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === 'production'); }); needsViewMount = true; } if (targetP && state.selectedSector !== 'all' && state.selectedSector !== targetP.sector) { state.selectedSector = targetP.sector; needsViewMount = true; } if (needsViewMount) { this.mountActiveView(); } this.mountInspectorPane(); // Smooth scroll to the facility card and highlight it requestAnimationFrame(() => { const cardEl = document.getElementById('card-' + action.productId); if (cardEl) { cardEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); cardEl.classList.remove('card-highlight-pulse'); void cardEl.offsetWidth; // Force CSS reflow to re-trigger animation cardEl.classList.add('card-highlight-pulse'); setTimeout(() => { cardEl.classList.remove('card-highlight-pulse'); }, 1600); } }); 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 'TOGGLE_LOGISTICS': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { const nextVal = pState.logisticsEnabled === false ? true : false; pState.logisticsEnabled = nextVal; this.showToast(`${p.name} Wholesale Fleet: ${nextVal ? 'ENABLED' : 'PAUSED'}`, nextVal ? 'success' : 'info'); this.sync.queueAction(action); this.updateActiveView(); this.updateInspectorPane(); } break; } case 'TOGGLE_DOWNSTREAM_SUPPLY': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { const nextVal = pState.supplyDownstreamEnabled === false ? true : false; pState.supplyDownstreamEnabled = nextVal; this.showToast(`${p.name} Downstream Supply: ${nextVal ? 'ALLOWED' : 'BLOCKED'}`, nextVal ? 'success' : 'warning'); this.sync.queueAction(action); this.updateActiveView(); this.updateInspectorPane(); } break; } case 'SET_PRODUCTION_TARGET': { const p = getProduct(action.productId); const pState = state.products[action.productId]; if (p && pState) { const target = Math.max(0, Math.min(1.0, action.target)); pState.productionTarget = target; const pct = Math.round(target * 100); this.showToast(`${p.name} Production Target set to ${pct}%`, 'info'); this.sync.queueAction(action); this.updateActiveView(); this.updateInspectorPane(); } 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; } } } } } mountHeaderSectorBar() { const headerSectorContainer = document.getElementById('header-sector-container'); if (!headerSectorContainer) return; const selectedSector = this.sm.state.selectedSector || 'all'; headerSectorContainer.innerHTML = renderSectorTabsHtml(selectedSector, this.engine); headerSectorContainer.querySelectorAll('.sector-pill').forEach(btn => { btn.onclick = () => this.handleAction({ type: 'SELECT_SECTOR', sector: btn.dataset.sector }); }); } /** * 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; // Keep header sector tabs in sync with selectedSector this.mountHeaderSectorBar(); if (tab === 'production') { mountProductionFloor(container, this.sm.state, (a) => this.handleAction(a), this.engine); } else if (tab === 'unit_detail') { mountUnitDetailView(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 (legacy stub) */ 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 === 'unit_detail') { updateUnitDetailView(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; });