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