ProductionGame/frontend/js/core/state.js

230 lines
9.2 KiB
JavaScript

// 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,
buyMode: '1',
activeTab: 'production',
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.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.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)];
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.save();
this.notify();
return true;
}
}
export const GState = new StateManager();