// Production - The Idle Game: State Container & Persistence import { PRODUCTS } from '../data/products.js'; import { BOARDROOM_PERKS, calculateSharesEarned } from '../data/boardroom.js'; import { CONTRACT_TEMPLATES } from '../data/contracts.js'; import { ACHIEVEMENTS } from '../data/achievements.js'; const STORAGE_KEY = 'production_the_idle_game_save_v1'; export function getInitialState() { const state = { version: 1, companyName: 'Apex Production Industries', cash: 30.0, lifetimeRevenue: 0.0, valuation: 30.0, ventureShares: 0, totalVentureShares: 0, ipoCount: 0, reputation: 0, repAllocations: { b2b: 0, clients: 0, brand: 0 }, buyMode: '1', activeTab: 'production', floorViewMode: 'standard', // 'standard' | 'compact' selectedSector: 'all', selectedProductId: 'wheat', stats: { totalProduced: 0, contractsCompleted: 0, managersHired: 0 }, products: {}, boardroomPerks: {}, achievements: {}, activeContracts: [], lastTick: Date.now() }; // Initialize all products PRODUCTS.forEach(p => { state.products[p.id] = { unlocked: p.unlockedDefault, stock: 0.0, lines: p.unlockedDefault ? 1 : 0, manager: false, logistics: p.unlockedDefault ? 1 : 0, logisticsEnabled: true, supplyDownstreamEnabled: true, productionTarget: 1.0, cycleProgress: 0.0, isBottlenecked: false, totalProduced: 0.0 }; }); // Initialize boardroom perks BOARDROOM_PERKS.forEach(perk => { state.boardroomPerks[perk.id] = 0; }); // Initialize achievements ACHIEVEMENTS.forEach(ach => { state.achievements[ach.id] = 0; }); // Generate initial starting contracts state.activeContracts = [ { uid: 'c_start_1', templateId: CONTRACT_TEMPLATES[0].id, productId: CONTRACT_TEMPLATES[0].productId, title: CONTRACT_TEMPLATES[0].title, clientId: CONTRACT_TEMPLATES[0].clientId, targetAmount: CONTRACT_TEMPLATES[0].requiredAmount, remainingSeconds: CONTRACT_TEMPLATES[0].timeLimit, rewardCash: CONTRACT_TEMPLATES[0].rewardCash, rewardRep: CONTRACT_TEMPLATES[0].rewardRep } ]; return state; } export class StateManager { constructor() { this.state = this.load(); this.listeners = new Set(); } subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); } notify(changedKeys = []) { for (const fn of this.listeners) { fn(this.state, changedKeys); } } load() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return getInitialState(); const parsed = JSON.parse(raw); const fresh = getInitialState(); // Merge safely to preserve schema additions const merged = { ...fresh, ...parsed }; merged.products = { ...fresh.products, ...(parsed.products || {}) }; merged.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) }; merged.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) }; merged.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) }; merged.stats = { ...fresh.stats, ...(parsed.stats || {}) }; if (!Array.isArray(merged.activeContracts) || merged.activeContracts.length === 0) { merged.activeContracts = fresh.activeContracts; } return merged; } catch (err) { console.error('Failed to load local state, fallback to fresh:', err); return getInitialState(); } } save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state)); } catch (err) { console.error('Failed to save state to localStorage:', err); } } reset() { this.state = getInitialState(); this.save(); this.notify(); } exportState() { return JSON.stringify(this.state, null, 2); } importState(jsonStringOrObject) { try { const parsed = typeof jsonStringOrObject === 'string' ? JSON.parse(jsonStringOrObject) : jsonStringOrObject; if (!parsed || typeof parsed !== 'object') { throw new Error('Invalid savegame format: must be an object.'); } const fresh = getInitialState(); // Validate & sanitize incoming state against schema const validated = { ...fresh, ...parsed }; validated.version = fresh.version; validated.cash = typeof parsed.cash === 'number' && !isNaN(parsed.cash) ? Math.max(0, parsed.cash) : fresh.cash; validated.lifetimeRevenue = typeof parsed.lifetimeRevenue === 'number' && !isNaN(parsed.lifetimeRevenue) ? Math.max(0, parsed.lifetimeRevenue) : fresh.lifetimeRevenue; validated.valuation = typeof parsed.valuation === 'number' && !isNaN(parsed.valuation) ? Math.max(0, parsed.valuation) : fresh.valuation; validated.ventureShares = typeof parsed.ventureShares === 'number' && !isNaN(parsed.ventureShares) ? Math.max(0, parsed.ventureShares) : fresh.ventureShares; validated.totalVentureShares = typeof parsed.totalVentureShares === 'number' && !isNaN(parsed.totalVentureShares) ? Math.max(0, parsed.totalVentureShares) : fresh.totalVentureShares; validated.ipoCount = typeof parsed.ipoCount === 'number' ? Math.max(0, parsed.ipoCount) : 0; validated.reputation = typeof parsed.reputation === 'number' ? Math.max(0, parsed.reputation) : 0; validated.lastTick = Date.now(); validated.products = { ...fresh.products }; if (parsed.products && typeof parsed.products === 'object') { for (const pid of Object.keys(fresh.products)) { if (parsed.products[pid]) { const pp = parsed.products[pid]; validated.products[pid] = { ...fresh.products[pid], unlocked: Boolean(pp.unlocked), stock: typeof pp.stock === 'number' && !isNaN(pp.stock) ? Math.max(0, pp.stock) : 0, lines: typeof pp.lines === 'number' && !isNaN(pp.lines) ? Math.max(0, pp.lines) : 0, manager: Boolean(pp.manager), logistics: typeof pp.logistics === 'number' ? Math.max(0, pp.logistics) : 0, logisticsEnabled: pp.logisticsEnabled !== false, supplyDownstreamEnabled: pp.supplyDownstreamEnabled !== false, productionTarget: typeof pp.productionTarget === 'number' ? Math.max(0, Math.min(1.0, pp.productionTarget)) : 1.0, cycleProgress: 0.0, isBottlenecked: false, totalProduced: typeof pp.totalProduced === 'number' ? Math.max(0, pp.totalProduced) : 0 }; } } } validated.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) }; validated.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) }; validated.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) }; validated.stats = { ...fresh.stats, ...(parsed.stats || {}) }; if (!Array.isArray(validated.activeContracts) || validated.activeContracts.length === 0) { validated.activeContracts = fresh.activeContracts; } this.state = validated; this.save(); this.notify(); return { success: true, companyName: validated.companyName || 'Apex Production Industries' }; } catch (err) { console.error('Save import failed:', err); return { success: false, error: err.message || 'Unknown save format' }; } } performIPO() { const earned = calculateSharesEarned(this.state.lifetimeRevenue); if (earned <= 0) return false; const startingBonusPerk = this.state.boardroomPerks['angel_syndicate'] || 0; const startingCashBonuses = [30.0, 500.0, 5000.0, 50000.0, 500000.0, 5000000.0]; const newStartingCash = startingCashBonuses[Math.min(startingBonusPerk, startingCashBonuses.length - 1)]; // Brand Legacy: Retain 10% - 50% of total accumulated Reputation across IPOs const brandLegacyLvl = this.state.boardroomPerks['brand_legacy'] || 0; const repRetentionRatios = [0.0, 0.10, 0.20, 0.30, 0.40, 0.50]; const ratio = repRetentionRatios[Math.min(brandLegacyLvl, repRetentionRatios.length - 1)]; const allocs = this.state.repAllocations || {}; const totalRep = (this.state.reputation || 0) + (allocs.b2b || 0) + (allocs.clients || 0) + (allocs.brand || 0); const preservedRep = Math.floor(totalRep * ratio); const preservedShares = this.state.ventureShares + earned; const preservedTotalShares = this.state.totalVentureShares + earned; const preservedPerks = { ...this.state.boardroomPerks }; const preservedIpoCount = this.state.ipoCount + 1; const preservedAchievements = { ...this.state.achievements }; // Fresh state this.state = getInitialState(); this.state.cash = newStartingCash; this.state.ventureShares = preservedShares; this.state.totalVentureShares = preservedTotalShares; this.state.ipoCount = preservedIpoCount; this.state.boardroomPerks = preservedPerks; this.state.achievements = preservedAchievements; this.state.reputation = preservedRep; this.state.repAllocations = { b2b: 0, clients: 0, brand: 0 }; this.save(); this.notify(); return true; } } export const GState = new StateManager();