// 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(); } // View Mode Toggle (Standard / Compact) const viewModeBtn = document.getElementById('btn-toggle-view-mode'); if (viewModeBtn) { viewModeBtn.onclick = () => this.toggleFloorViewMode(); } this.updateViewModeButtonUi(); 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 & Profile Modal Event Listeners const closeAuth = document.getElementById('btn-close-auth'); if (closeAuth) closeAuth.onclick = () => this.closeAuthModal(); const closeAuthX = document.getElementById('btn-close-auth-x'); if (closeAuthX) closeAuthX.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(); const logoutBtn = document.getElementById('btn-logout-submit'); if (logoutBtn) logoutBtn.onclick = () => this.handleLogout(); const cloudSyncBtn = document.getElementById('btn-cloud-sync-now'); if (cloudSyncBtn) cloudSyncBtn.onclick = () => this.handleCloudSyncNow(); // Savegame Management: Export & Import const exportFileBtn = document.getElementById('btn-export-file'); if (exportFileBtn) exportFileBtn.onclick = () => this.handleExportSaveFile(); const exportCopyBtn = document.getElementById('btn-export-clipboard'); if (exportCopyBtn) exportCopyBtn.onclick = () => this.handleExportSaveClipboard(); const importFileInput = document.getElementById('import-file-input'); if (importFileInput) importFileInput.onchange = (e) => this.handleImportSaveFile(e); const importClipBtn = document.getElementById('btn-import-clipboard'); if (importClipBtn) importClipBtn.onclick = () => this.toggleImportTextBox(); const importApplyBtn = document.getElementById('btn-import-text-apply'); if (importApplyBtn) importApplyBtn.onclick = () => this.handleApplyImportText(); const importCancelBtn = document.getElementById('btn-import-text-cancel'); if (importCancelBtn) importCancelBtn.onclick = () => this.toggleImportTextBox(false); } 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 = ''; } // Toggle guest vs user view const isLoggedIn = this.sync.isLoggedIn(); const guestSec = document.getElementById('auth-guest-section'); const userSec = document.getElementById('auth-user-section'); const nameEl = document.getElementById('profile-display-name'); const badgeEl = document.getElementById('profile-badge-pill'); const descEl = document.getElementById('profile-sync-desc'); const iconEl = document.getElementById('profile-status-icon'); if (isLoggedIn) { if (guestSec) guestSec.style.display = 'none'; if (userSec) userSec.style.display = 'block'; if (nameEl) nameEl.textContent = this.sync.getUsername(); if (badgeEl) { badgeEl.textContent = 'Cloud Synced'; badgeEl.className = 'badge-pill-soft badge-online'; } if (descEl) descEl.textContent = 'Account active. Enterprise data synchronizes automatically with the cloud server.'; if (iconEl) iconEl.setAttribute('icon', 'lucide:shield-check'); } else { if (guestSec) guestSec.style.display = 'flex'; if (userSec) userSec.style.display = 'none'; if (nameEl) nameEl.textContent = 'Executive Guest'; if (badgeEl) { badgeEl.textContent = 'Offline Guest'; badgeEl.className = 'badge-pill-soft'; } if (descEl) descEl.textContent = 'Progress is saved locally in this browser. Register or login to sync across devices.'; if (iconEl) iconEl.setAttribute('icon', 'lucide:shield'); } // Hide raw import box if open const importBox = document.getElementById('import-text-box'); if (importBox) importBox.style.display = 'none'; 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'); // Check for cloud state upon login const cloud = await this.sync.fetchCloudState(); if (cloud && cloud.lastTick > this.sm.state.lastTick) { this.sm.importState(cloud); this.showToast('Restored latest game state from cloud!', 'info'); } else { // If local save is newer or first login, push local state to cloud await this.sync.saveCloudState(this.sm.state); } this.closeAuthModal(); this.updateHud(); this.mountActiveView(); } 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'); // Back up current game state to new account await this.sync.saveCloudState(this.sm.state); this.closeAuthModal(); this.updateHud(); } catch (err) { this.showAuthError(err.message || 'Registration failed'); } } handleLogout() { this.sync.logout(); this.showToast('Logged out. Playing as Guest.', 'info'); this.openAuthModal(); // Refresh modal to show guest state this.updateHud(); } async handleCloudSyncNow() { if (!this.sync.isLoggedIn()) { return this.showToast('You must be logged in to sync to the cloud', 'warning'); } try { const ok = await this.sync.saveCloudState(this.sm.state); if (ok) { this.showToast('Empire successfully synced to cloud server!', 'success'); } else { this.showToast('Cloud sync failed. Please try again.', 'warning'); } } catch (err) { this.showToast('Sync error: ' + err.message, 'warning'); } } // Savegame File Export handleExportSaveFile() { try { const jsonStr = this.sm.exportState(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const fileName = `production_save_${timestamp}.json`; const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showToast(`Savegame downloaded: ${fileName}`, 'success'); } catch (err) { console.error('Export error:', err); this.showToast('Failed to export save: ' + err.message, 'warning'); } } // Savegame Clipboard Copy async handleExportSaveClipboard() { try { const jsonStr = this.sm.exportState(); await navigator.clipboard.writeText(jsonStr); this.showToast('Savegame copied to clipboard!', 'success'); } catch (err) { // Fallback for browsers with restricted clipboard const promptOk = window.prompt('Copy your savegame JSON string below:', this.sm.exportState()); if (promptOk !== null) { this.showToast('Savegame copied!', 'success'); } } } // Toggle Paste Save container toggleImportTextBox(show = null) { const box = document.getElementById('import-text-box'); const area = document.getElementById('import-raw-input'); if (!box) return; const willShow = show !== null ? show : (box.style.display === 'none' || !box.style.display); box.style.display = willShow ? 'flex' : 'none'; if (willShow && area) { area.value = ''; area.focus(); } } // Import from uploaded file handleImportSaveFile(e) { const file = e.target.files && e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (event) => { const text = event.target.result; await this.executeSaveImport(text, `file "${file.name}"`); e.target.value = ''; // Reset input to allow selecting same file again }; reader.onerror = () => { this.showToast('Could not read the selected file.', 'warning'); e.target.value = ''; }; reader.readAsText(file); } // Import from pasted text async handleApplyImportText() { const raw = document.getElementById('import-raw-input')?.value.trim(); if (!raw) { return this.showToast('Please paste a savegame JSON string first.', 'warning'); } await this.executeSaveImport(raw, 'clipboard text'); this.toggleImportTextBox(false); } // Common confirmation & restore executor async executeSaveImport(rawJson, sourceLabel = 'input') { let parsed; try { parsed = JSON.parse(rawJson); } catch (err) { return this.showToast('Invalid JSON savegame: syntax error.', 'warning'); } const compName = parsed.companyName || 'Unknown Enterprise'; const cashAmt = parsed.cash !== undefined ? fmtCurrency(parsed.cash) : '$0.00'; const ipoCount = parsed.ipoCount || 0; const confirmed = await this.showConfirmModal({ title: 'Restore Savegame', subtitle: 'Data Override Confirmation', icon: 'lucide:download', text: `Are you sure you want to load savegame from ${sourceLabel}?

` + `Company: ${compName}
` + `Capital: ${cashAmt}
` + `IPO Prestige: ${ipoCount} iterations

` + `Warning: This will replace your current game state immediately!`, okText: 'Restore & Load', cancelText: 'Cancel' }); if (confirmed) { const res = this.sm.importState(parsed); if (res.success) { // If logged in, also sync the newly restored state to the cloud if (this.sync.isLoggedIn()) { this.sync.saveCloudState(this.sm.state); } this.closeAuthModal(); this.mountActiveView(); this.mountInspectorPane(); this.updateHud(); this.showToast(`Savegame successfully restored: ${res.companyName}!`, 'success'); } else { this.showToast(`Failed to restore save: ${res.error}`, 'warning'); } } } toggleFloorViewMode() { const current = this.sm.state.floorViewMode || 'standard'; const next = current === 'standard' ? 'compact' : 'standard'; this.sm.state.floorViewMode = next; this.sm.save(); this.updateViewModeButtonUi(); this.showToast(`Overview Layout: ${next === 'compact' ? 'COMPACT' : 'STANDARD'}`, 'info'); if (this.sm.state.activeTab === 'production') { this.mountActiveView(); } } updateViewModeButtonUi() { const isCompact = this.sm.state.floorViewMode === 'compact'; const iconEl = document.getElementById('view-mode-icon'); const textEl = document.getElementById('view-mode-text'); if (iconEl) { iconEl.setAttribute('icon', isCompact ? 'lucide:layout-list' : 'lucide:layout-grid'); } if (textEl) { textEl.textContent = isCompact ? 'Compact' : 'Standard'; } } 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; });