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
+66 -20
View File
@@ -13,8 +13,8 @@ import {
reliabilityCost,
} from '../public/js/engine/gamble.js';
import { buyShares, holdingValue, portfolio, sellShares } from '../public/js/engine/investments.js';
import { buyPerk, legacyGain, perkCost, prestige } from '../public/js/engine/legacy.js';
import { modifiersFor } from '../public/js/engine/modifiers.js';
import { buyPerk, legacyGain, nextPointAt, perkCost, pointsFor, prestige } from '../public/js/engine/legacy.js';
import { modifiersFor, passiveBonus } from '../public/js/engine/modifiers.js';
import { buyLab, buyResearch, labCost, researchStatus } from '../public/js/engine/research.js';
import { createState, sanitizeState, serializeState } from '../public/js/engine/state.js';
import { loadContext } from './helpers.js';
@@ -241,22 +241,45 @@ test('the price advisor research reveals the balanced price', () => {
// ---------------------------------------------------------------------- legacy
test('a legacy restart needs a big enough run, and rewards the square root of it', () => {
test('legacy points come from LIFETIME production, with diminishing returns', () => {
const { threshold, exponent } = ctx.rules.legacy;
const points = (lifetime) => pointsFor(lifetime, ctx, mods(fresh()));
assert.equal(points(threshold - 1), 0, 'nothing below the threshold');
assert.equal(points(threshold), 1);
// To double the points, production must be multiplied by 2 ** (1 / exponent): far more than 2.
const factor = 2 ** (1 / exponent);
assert.ok(factor > 4);
assert.equal(points(threshold * 4 ** (1 / exponent) * 1.001), 4);
assert.ok(points(threshold * 100) < 100, 'a 100x bigger lifetime does not give 100x the points');
const s = fresh();
s.totalProduced = ctx.rules.legacy.threshold - 1;
s.legacy.earned = 3;
assert.ok(Math.abs(nextPointAt(s, ctx) - threshold * 4 ** (1 / exponent)) < 1, 'the next point is at the 4th step');
});
test('a restart gives only what was NOT already rewarded: restarting often cannot snowball', () => {
const s = fresh();
s.lifetimeProduced = s.totalProduced = 6e13;
assert.equal(legacyGain(s, ctx, mods(s)), 2);
assert.equal(prestige(s, ctx), 2);
// Straight away, with the same lifetime, there is nothing left to claim.
assert.equal(legacyGain(s, ctx, mods(s)), 0);
assert.equal(prestige(s, ctx), 0);
// A short new run changes little: points are about the whole lifetime, not about each run.
s.lifetimeProduced += 1e13;
assert.equal(legacyGain(s, ctx, mods(s)), 0);
s.totalProduced = ctx.rules.legacy.threshold;
assert.equal(legacyGain(s, ctx, mods(s)), 1);
s.totalProduced = ctx.rules.legacy.threshold * 100;
assert.equal(legacyGain(s, ctx, mods(s)), 10);
});
test('restarting resets the run but keeps the meta progression', () => {
const s = fresh();
s.totalProduced = s.lifetimeProduced = 4e9;
s.totalProduced = s.lifetimeProduced = 6e13;
s.funds = 1e9;
s.generators.gen3 = 40;
s.upgrades = ['click_1'];
s.endless = { e_global: 4 };
s.chips = 12;
s.invest.holdings.inv_low_1 = 5;
s.research = ['r_prod_1'];
@@ -270,9 +293,10 @@ test('restarting resets the run but keeps the meta progression', () => {
assert.equal(s.totalProduced, 0);
assert.equal(s.generators.gen3, 0);
assert.deepEqual(s.upgrades, []);
assert.deepEqual(s.endless, {});
assert.equal(s.chips, 0);
assert.equal(s.invest.holdings.inv_low_1, 0);
assert.equal(s.lifetimeProduced, 4e9);
assert.equal(s.lifetimeProduced, 6e13);
assert.deepEqual(s.research, ['r_prod_1']);
assert.equal(s.labs, 2);
assert.equal(s.rp, 33);
@@ -283,41 +307,63 @@ test('restarting resets the run but keeps the meta progression', () => {
test('restarting is refused below the threshold', () => {
const s = fresh();
s.totalProduced = 1e6;
s.totalProduced = s.lifetimeProduced = 1e6;
assert.equal(prestige(s, ctx), 0);
assert.equal(s.totalProduced, 1e6);
});
test('the passive bonus grows slowly and can never run away (it once reached 1e100)', () => {
const s = fresh();
const at = (earned) => passiveBonus({ ...s, legacy: { ...s.legacy, earned } }, ctx);
assert.equal(at(0), 1);
assert.ok(at(1) > 1 && at(1) < 1.5);
assert.ok(at(100) < 3);
assert.ok(at(400) < 2 * at(100), 'four times the points, less than twice the bonus');
assert.ok(at(1e6) < 1e3, 'a million points is nowhere near astronomical');
assert.ok(at(5000) < 100, 'the number of points that used to give 1e100');
});
test('legacy points give a passive bonus and buy perks that stay after a restart', () => {
const s = fresh();
s.legacy = { points: 10, earned: 10, prestiges: 1, perks: {} };
s.generators.gen1 = 10;
const base = derive({ ...s, legacy: { ...s.legacy, earned: 0 } }, ctx).rate;
assert.ok(Math.abs(derive(s, ctx).rate / base - 1.05 ** 10) < 1e-9, 'passive +5 % per point earned');
const bonus = passiveBonus(s, ctx);
assert.ok(Math.abs(derive(s, ctx).rate / base - bonus) < 1e-9);
assert.ok(bonus > 1 && bonus < 2, 'a modest bonus');
const perk = ctx.perksById.get('lg_prod');
const cost = perkCost(perk, 0);
assert.ok(buyPerk(s, ctx, 'lg_prod'));
assert.equal(s.legacy.points, 10 - cost);
assert.ok(Math.abs(derive(s, ctx).rate / base - 1.05 ** 10 * 1.3) < 1e-9);
assert.ok(Math.abs(derive(s, ctx).rate / base - bonus * perk.effect.value) < 1e-9);
s.totalProduced = 1e9;
prestige(s, ctx);
s.lifetimeProduced = s.totalProduced = 1e16;
assert.ok(prestige(s, ctx) > 0);
assert.equal(s.legacy.perks.lg_prod, 1, 'perk survives the restart');
});
test('perks have levels, growing costs and a cap; head start grants funds', () => {
test('perks have levels, steeply growing costs and a cap; head start grants funds', () => {
const s = fresh();
s.legacy.points = 1e6;
s.legacy.points = 1e9;
const perk = ctx.perksById.get('lg_start');
assert.ok(perkCost(perk, 3) > perkCost(perk, 0));
assert.ok(perkCost(perk, 3) >= perkCost(perk, 0) * 6, 'each level costs about twice the last');
for (let i = 0; i < 20; i++) buyPerk(s, ctx, 'lg_start');
assert.equal(s.legacy.perks.lg_start, perk.maxLevel);
s.totalProduced = 1e9;
prestige(s, ctx);
s.lifetimeProduced = s.totalProduced = 1e13;
assert.equal(prestige(s, ctx), 1);
assert.equal(s.funds, perk.effect.value * perk.maxLevel);
});
test('the legacy perks together are a bounded boost, not a runaway', () => {
const s = fresh();
s.generators.gen1 = 10;
const base = derive(s, ctx).rate;
s.legacy.perks = Object.fromEntries(ctx.legacy.map((p) => [p.id, p.maxLevel]));
const boosted = derive(s, ctx).rate / base;
assert.ok(boosted > 3 && boosted < 20, `all perks maxed multiply production by ${boosted.toFixed(1)}`);
});
test('generator cost perks make generators cheaper', () => {
const s = fresh();
s.funds = 1e6;