feat: More bonus (exponential) and method to list icons needed

This commit is contained in:
gauvainboiche
2026-10-08 14:20:58 +02:00
parent fc0d5e3ba4
commit a88b8f5f14
25 changed files with 977 additions and 40 deletions
+10
View File
@@ -29,6 +29,7 @@ export function buildContext({
achievements,
investments,
tabs,
endless,
}) {
const generatorsById = byId(generators, 'generator');
const upgradesById = byId(upgrades, 'upgrade');
@@ -36,6 +37,7 @@ export function buildContext({
const perksById = byId(legacy, 'legacy perk');
const achievementsById = byId(achievements, 'achievement');
const societiesById = byId(investments.societies, 'society');
const endlessById = byId(endless, 'endless line');
byId(tabs, 'tab');
for (const u of upgrades) {
@@ -50,6 +52,12 @@ export function buildContext({
if (!researchById.has(req)) throw new Error(`Research "${node.id}": unknown requirement "${req}"`);
}
}
for (const line of endless) {
checkEffect(`Endless "${line.id}"`, line.effect, generatorsById);
if (line.requires && !upgradesById.has(line.requires)) {
throw new Error(`Endless "${line.id}": unknown requirement "${line.requires}"`);
}
}
for (const perk of legacy) checkEffect(`Perk "${perk.id}"`, perk.effect, generatorsById);
for (const a of achievements) checkEffect(`Achievement "${a.id}"`, a.effect, generatorsById);
for (const s of investments.societies) {
@@ -65,11 +73,13 @@ export function buildContext({
achievements,
investments,
tabs,
endless,
generatorsById,
upgradesById,
researchById,
perksById,
achievementsById,
societiesById,
endlessById,
};
}
+13 -1
View File
@@ -72,11 +72,23 @@ const APPLY = {
export const EFFECT_TYPES = Object.keys(APPLY);
// Endless upgrades stack without limit, so keep every result finite and sensible.
const CAP = 1e100; // low enough that products of several multipliers stay finite; real play never gets close
const FLOORS = { generatorCost: 0.05, batchDiscount: 0.001 };
function finalize(m) {
const keep = (value) => (Number.isFinite(value) ? Math.min(CAP, value) : CAP);
for (const key of Object.keys(m)) if (typeof m[key] === 'number') m[key] = keep(m[key]);
for (const id of Object.keys(m.generators)) m.generators[id] = keep(m.generators[id]);
for (const [key, floor] of Object.entries(FLOORS)) m[key] = Math.max(floor, m[key]);
return m;
}
/** Fold a list of `{ effect, times }` entries into one set of multipliers. */
export function foldEffects(entries) {
const m = newModifiers();
for (const { effect, times = 1 } of entries) {
if (effect && times > 0) APPLY[effect.type]?.(m, effect, times);
}
return m;
return finalize(m);
}
+32
View File
@@ -0,0 +1,32 @@
// Endless upgrades: once a family of upgrades is complete (its `requires`
// milestone is owned), it continues forever as a repeatable line. Every level
// multiplies the effect once more and costs exponentially more, up to
// `maxLevel` levels, which nobody will ever reach.
import { bulkCost, maxAffordable } from './formulas.js';
export const lineLevel = (state, id) => state.endless[id] ?? 0;
export function lineAvailable(state, line) {
return !line.requires || state.upgrades.includes(line.requires);
}
/** How many levels `amount` ("max" or a number) buys right now, and what they cost. */
export function endlessQuote(state, ctx, id, amount) {
const line = ctx.endlessById.get(id);
const level = lineLevel(state, id);
const wanted =
amount === 'max' ? maxAffordable(line.baseCost, line.costGrowth, level, state.funds) : amount;
const count = Math.max(0, Math.min(wanted, line.maxLevel - level));
return { count, cost: bulkCost(line.baseCost, line.costGrowth, level, count) };
}
export function buyEndless(state, ctx, id, amount = 1) {
const line = ctx.endlessById.get(id);
if (!line || !lineAvailable(state, line)) return false;
const { count, cost } = endlessQuote(state, ctx, id, amount);
if (count <= 0 || !(state.funds >= cost)) return false; // an infinite cost is never affordable
state.funds -= cost;
state.endless[id] = lineLevel(state, id) + count;
return true;
}
+21 -8
View File
@@ -99,11 +99,22 @@ export function click(state, ctx) {
return true;
}
/**
* What a material purchase gives right now: the whole lot when it is affordable,
* otherwise as much of it as the funds allow (same unit price). Partial lots
* make sure a player can never be locked out of buying material.
*/
export function materialPurchase(state, d) {
if (state.funds >= d.batchCost) return { amount: d.batchSize, cost: d.batchCost };
if (!(state.funds >= 0.01)) return { amount: 0, cost: 0 };
return { amount: (d.batchSize * state.funds) / d.batchCost, cost: state.funds };
}
export function buyMaterial(state, ctx) {
const d = derive(state, ctx);
if (state.funds < d.batchCost) return false;
state.funds -= d.batchCost;
state.material += d.batchSize;
const purchase = materialPurchase(state, derive(state, ctx));
if (purchase.amount <= 0) return false;
state.funds -= purchase.cost;
state.material += purchase.amount;
return true;
}
@@ -175,11 +186,13 @@ function step(state, ctx, dt, rng, totals, offline) {
state.totalProduced += made;
state.lifetimeProduced += made;
// Automatic restock: patient while the price is high, urgent when nearly out.
if (d.autoBuyMaterial && state.funds >= d.batchCost) {
// Automatic restock: patient (full lots, at a fair price) while stock is fine,
// urgent (whatever the funds allow) when nearly out.
if (d.autoBuyMaterial) {
const { autoBuyBelowFactor } = ctx.rules.materialMarket;
const low = state.material < d.batchSize / 2 && state.materialFactor <= autoBuyBelowFactor;
if (low || state.material < d.batchSize / 10) buyMaterial(state, ctx);
const patient =
state.material < d.batchSize / 2 && state.materialFactor <= autoBuyBelowFactor && state.funds >= d.batchCost;
if (patient || state.material < d.batchSize / 10) buyMaterial(state, ctx);
}
// Customers buy what they want, up to what is in stock.
+3
View File
@@ -8,6 +8,9 @@ export function collectEffects(state, ctx) {
const entries = [];
for (const id of state.upgrades) entries.push({ effect: ctx.upgradesById.get(id)?.effect });
for (const [id, level] of Object.entries(state.endless)) {
entries.push({ effect: ctx.endlessById.get(id)?.effect, times: level });
}
for (const id of state.research) entries.push({ effect: ctx.researchById.get(id)?.effect });
for (const id of state.achievements) entries.push({ effect: ctx.achievementsById.get(id)?.effect });
+11
View File
@@ -56,6 +56,7 @@ export function createState(ctx, now = Date.now()) {
materialFactor: 1,
generators: Object.fromEntries(ctx.generators.map((g) => [g.id, 0])),
upgrades: [],
endless: {},
invest: newInvest(ctx),
chips: 0,
reliabilityLevel: 0,
@@ -107,6 +108,15 @@ function sanitizeInvest(raw, ctx, base) {
return out;
}
function sanitizeEndless(raw, ctx) {
const levels = {};
for (const line of ctx.endless) {
const level = int(raw?.[line.id], line.maxLevel);
if (level > 0) levels[line.id] = level;
}
return levels;
}
function sanitizeContracts(raw, ctx) {
const templates = new Set(ctx.rules.contracts.templates.map((t) => t.id));
const rewardTypes = new Set(ctx.rules.contracts.rewards);
@@ -186,6 +196,7 @@ export function sanitizeState(raw, ctx, now = Date.now()) {
materialFactor: num(raw.materialFactor, 1, materialMarket.minFactor, materialMarket.maxFactor),
generators,
upgrades: knownIds(raw.upgrades, ctx.upgradesById),
endless: sanitizeEndless(raw.endless, ctx),
invest: sanitizeInvest(raw.invest, ctx, base.invest),
chips: int(raw.chips, gamble.maxChips),
reliabilityLevel: int(raw.reliabilityLevel, gamble.reliability.maxLevel),