import { clamp } from './formulas.js'; export const STATE_VERSION = 1; const MAX_NUMBER = 1e300; const MAX_OWNED = 4000; // keeps cost formulas (growth ** owned) finite const MAX_MARKETING = 300; export function createState(ctx, now = Date.now()) { const { start, market } = ctx.rules; return { v: STATE_VERSION, units: start.units, funds: start.funds, material: start.material, totalProduced: 0, totalSold: 0, price: start.price ?? market.basePrice, marketing: 0, materialFactor: 1, generators: Object.fromEntries(ctx.generators.map((g) => [g.id, 0])), upgrades: [], savedAt: now, materialTimer: 0, // transient, never saved }; } function num(value, fallback, min = 0, max = MAX_NUMBER) { return Number.isFinite(value) ? clamp(value, min, max) : fallback; } function int(value, max) { return Math.floor(num(value, 0, 0, max)); } /** * Turn untrusted data (cookie, imported file) into a valid state. * Unknown ids are dropped and numbers are clamped; returns null if `raw` is not an object. */ export function sanitizeState(raw, ctx, now = Date.now()) { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null; const { market, materialMarket } = ctx.rules; const base = createState(ctx, now); const owned = Array.isArray(raw.upgrades) ? raw.upgrades : []; return { ...base, units: num(raw.units, base.units), funds: num(raw.funds, base.funds), material: num(raw.material, base.material), totalProduced: num(raw.totalProduced, 0), totalSold: num(raw.totalSold, 0), price: Math.round(num(raw.price, base.price, market.minPrice, market.maxPrice) * 100) / 100, marketing: int(raw.marketing, MAX_MARKETING), materialFactor: num(raw.materialFactor, 1, materialMarket.minFactor, materialMarket.maxFactor), generators: Object.fromEntries( ctx.generators.map((g) => [g.id, int(raw.generators?.[g.id], MAX_OWNED)]), ), upgrades: [...new Set(owned)].filter((id) => ctx.upgradesById.has(id)), savedAt: num(raw.savedAt, now, 0, now), }; } /** Compact JSON (numbers trimmed to 10 significant digits) for cookies and export. */ export function serializeState(state) { const { materialTimer, ...persisted } = state; return JSON.stringify(persisted, (_key, value) => typeof value === 'number' && Number.isFinite(value) ? Number(value.toPrecision(10)) : value, ); }