248 lines
10 KiB
JavaScript
248 lines
10 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,
|
|
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();
|