// 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 b2bRep = state.repAllocations?.b2b || 0;
const b2bBonus = 1.0 + 0.005 * b2bRep; // +0.5% per point
const totalPayout = c.rewardCash * subsidyBonus * b2bBonus;
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 'ALLOCATE_REP': {
if (!state.repAllocations) {
state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
}
const allocs = state.repAllocations;
const dept = action.department;
const delta = action.delta;
if (delta === 'reset') {
const freed = (allocs.b2b || 0) + (allocs.clients || 0) + (allocs.brand || 0);
state.reputation = (state.reputation || 0) + freed;
state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
this.showToast(`All Corporate Rep Points Recalled to Reserve Pool!`, 'info');
this.sync.queueAction(action);
this.mountActiveView();
} else if (dept && allocs[dept] !== undefined) {
const deltaInt = parseInt(delta, 10);
if (deltaInt > 0) {
const toAlloc = Math.min(deltaInt, state.reputation || 0);
if (toAlloc > 0) {
state.reputation -= toAlloc;
allocs[dept] += toAlloc;
this.showToast(`Allocated +${toAlloc} Rep to ${dept.toUpperCase()}!`, 'success');
this.sync.queueAction(action);
this.mountActiveView();
}
} else if (deltaInt < 0) {
const toFree = Math.min(Math.abs(deltaInt), allocs[dept] || 0);
if (toFree > 0) {
allocs[dept] -= toFree;
state.reputation = (state.reputation || 0) + toFree;
this.showToast(`Recalled -${toFree} Rep from ${dept.toUpperCase()}!`, 'info');
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;
});