feat: Making the game harder to 'beat'
This commit is contained in:
@@ -5,11 +5,27 @@
|
||||
import { modifiersFor } from './modifiers.js';
|
||||
import { createState, META_KEYS } from './state.js';
|
||||
|
||||
/** Points a restart would grant right now (0 until the run is big enough). */
|
||||
/**
|
||||
* How many points a lifetime production deserves in total. Diminishing returns
|
||||
* (a power below 1) on EVERYTHING ever produced, not on a single run: each
|
||||
* further point needs far more production than the last, so restarting more and
|
||||
* more often cannot snowball.
|
||||
*/
|
||||
export function pointsFor(lifetimeProduced, ctx, mods) {
|
||||
const { threshold, exponent } = ctx.rules.legacy;
|
||||
if (!(lifetimeProduced >= threshold)) return 0;
|
||||
return Math.floor((lifetimeProduced / threshold) ** exponent * mods.legacyGain);
|
||||
}
|
||||
|
||||
/** Lifetime production at which the next point is earned (ignoring bonuses to point gains). */
|
||||
export function nextPointAt(state, ctx) {
|
||||
const { threshold, exponent } = ctx.rules.legacy;
|
||||
return threshold * (state.legacy.earned + 1) ** (1 / exponent);
|
||||
}
|
||||
|
||||
/** Points a restart would grant right now (0 until the next point is within reach). */
|
||||
export function legacyGain(state, ctx, mods) {
|
||||
const { threshold } = ctx.rules.legacy;
|
||||
if (state.totalProduced < threshold) return 0;
|
||||
return Math.floor(Math.sqrt(state.totalProduced / threshold) * mods.legacyGain);
|
||||
return Math.max(0, pointsFor(state.lifetimeProduced, ctx, mods) - state.legacy.earned);
|
||||
}
|
||||
|
||||
export function perkLevel(state, perk) {
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import { foldEffects } from './effects.js';
|
||||
import { chipDemandBonus } from './formulas.js';
|
||||
|
||||
/** The multiplier given by all legacy points earned so far: diminishing returns (a power below 1). */
|
||||
export function passiveBonus(state, ctx) {
|
||||
const { passiveScale, passivePower } = ctx.rules.legacy;
|
||||
return 1 + passiveScale * state.legacy.earned ** passivePower;
|
||||
}
|
||||
|
||||
/** All `{ effect, times }` entries that apply to `state`. */
|
||||
export function collectEffects(state, ctx) {
|
||||
const entries = [];
|
||||
@@ -18,10 +24,10 @@ export function collectEffects(state, ctx) {
|
||||
entries.push({ effect: ctx.perksById.get(id)?.effect, times: level });
|
||||
}
|
||||
|
||||
// Every legacy point ever earned gives a small passive bonus.
|
||||
const passive = 1 + ctx.rules.legacy.passivePerPoint;
|
||||
entries.push({ effect: { type: 'global_mult', value: passive }, times: state.legacy.earned });
|
||||
entries.push({ effect: { type: 'demand_mult', value: passive }, times: state.legacy.earned });
|
||||
// Every legacy point ever earned adds a flat bonus. It has diminishing returns and does
|
||||
// not compound: stacking x1.05 per point once reached 1e100 within a few runs.
|
||||
entries.push({ effect: { type: 'global_mult', value: passiveBonus(state, ctx) } });
|
||||
entries.push({ effect: { type: 'demand_mult', value: passiveBonus(state, ctx) } });
|
||||
|
||||
// Chips make the brand more attractive, with diminishing returns.
|
||||
entries.push({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user