feat: Making the game harder to 'beat'

This commit is contained in:
gauvainboiche
2026-10-08 17:17:21 +02:00
parent 2f7c307f0e
commit 8b15c76b14
20 changed files with 637 additions and 91 deletions
+48 -2
View File
@@ -1,6 +1,7 @@
import { clamp } from './formulas.js';
export const STATE_VERSION = 2;
// 3: legacy points come from lifetime production and the passive bonus no longer compounds.
export const STATE_VERSION = 3;
const MAX_NUMBER = 1e300;
const MAX_OWNED = 4000; // keeps cost formulas (growth ** owned) finite
@@ -17,6 +18,7 @@ const TRANSIENT = [
'contractTimer',
'eventTimer',
'gambleCooldown',
'repaired',
];
// What a legacy reset keeps (the "meta" progression); everything else is the run.
@@ -81,6 +83,7 @@ export function createState(ctx, now = Date.now()) {
contractTimer: contracts.offerEverySeconds,
eventTimer: events.minSeconds,
gambleCooldown: 0,
repaired: false, // set when an old save had to be rebalanced, so the game can say so
};
}
@@ -168,7 +171,7 @@ function sanitizeEvents(raw, ctx) {
* Unknown ids are dropped and numbers are clamped; returns null if `raw` is not an object.
* Saves from older versions are upgraded: missing fields take their defaults.
*/
export function sanitizeState(raw, ctx, now = Date.now()) {
function parseState(raw, ctx, now) {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
const { market, materialMarket, gamble } = ctx.rules;
const base = createState(ctx, now);
@@ -228,3 +231,46 @@ export function serializeState(state) {
typeof value === 'number' && Number.isFinite(value) ? Number(value.toPrecision(10)) : value,
);
}
// ------------------------------------------------------- repairing old saves
const MAX_LEGIT_POINTS = 25; // more than this in a save older than v3 can only come from the old runaway bonus
const RUNAWAY_PRODUCTION = 1e40;
const REPAIRED_POINTS = 10; // what a runaway save is brought back to
const REPAIRED_PERK_LEVEL = 3;
/**
* Before version 3 every legacy point multiplied output by 1.05 on top of the
* others, so a few thousand points gave 1e100 and runs were over in seconds.
* Such a save keeps its research and achievements but gets its legacy points
* and perks capped and its (meaningless) run restarted. Any older save also has
* its lifetime production capped, so an inflated total cannot hand out a pile
* of points at the next restart.
*/
function repairOlderSave(state, ctx, now) {
const runaway = state.legacy.earned > MAX_LEGIT_POINTS || state.lifetimeProduced > RUNAWAY_PRODUCTION;
if (runaway) {
state.legacy.earned = Math.min(state.legacy.earned, REPAIRED_POINTS);
state.legacy.points = 0;
for (const id of Object.keys(state.legacy.perks)) {
state.legacy.perks[id] = Math.min(state.legacy.perks[id], REPAIRED_PERK_LEVEL);
}
const fresh = createState(ctx, now);
for (const key of Object.keys(fresh)) {
if (!META_KEYS.includes(key) && key !== 'v' && key !== 'savedAt') state[key] = fresh[key];
}
state.repaired = true;
}
const { threshold, exponent } = ctx.rules.legacy;
state.lifetimeProduced = Math.min(state.lifetimeProduced, threshold * (state.legacy.earned + 3) ** (1 / exponent));
return state;
}
/** Untrusted data (cookie, imported file) in, valid and up-to-date state out; null if unusable. */
export function sanitizeState(raw, ctx, now = Date.now()) {
const state = parseState(raw, ctx, now);
if (!state) return null;
return (Number(raw.v) || 1) < STATE_VERSION ? repairOlderSave(state, ctx, now) : state;
}