feat: New theme + wire price adjustement

This commit is contained in:
gauvainboiche
2026-10-08 12:25:45 +02:00
parent aa9721ac2a
commit 1734487f1e
19 changed files with 348 additions and 88 deletions
+20 -8
View File
@@ -3,7 +3,7 @@
// browser and in Node (tests, balance simulations).
import { computeModifiers } from './effects.js';
import { bulkCost, clamp, demandPerSecond, maxAffordable } from './formulas.js';
import { bulkCost, clamp, demandPerSecond, materialBatch, maxAffordable } from './formulas.js';
const MAX_STEP_SECONDS = 1;
@@ -38,6 +38,13 @@ export function derive(state, ctx) {
}
const demandMultiplier = mods.demand * rules.marketing.demandPerLevel ** state.marketing;
const batch = materialBatch(
rules.materialMarket,
state.totalProduced,
rate,
state.materialFactor,
mods.batchDiscount,
);
return {
clickYield: rules.click.yield * mods.click,
perGenerator,
@@ -46,8 +53,8 @@ export function derive(state, ctx) {
demand: demandPerSecond(rules.market, state.price, demandMultiplier),
valueMultiplier: mods.value,
materialPerUnit: rules.unit.materialPerUnit / mods.materialEfficiency,
batchSize: rules.materialMarket.batchSize,
batchCost: Math.ceil(rules.materialMarket.basePrice * state.materialFactor * mods.batchDiscount),
batchSize: batch.size,
batchCost: batch.cost,
autoBuyMaterial: mods.autoBuyMaterial,
marketingCost: Math.ceil(rules.marketing.baseCost * rules.marketing.costGrowth ** state.marketing),
};
@@ -121,9 +128,12 @@ function stepMaterialMarket(state, ctx, dt, rng) {
state.materialTimer += dt;
while (state.materialTimer >= m.updateEverySeconds) {
state.materialTimer -= m.updateEverySeconds;
const shock = 1 + (rng() - 0.5) * 2 * m.volatility;
let shock = 1 + (rng() - 0.5) * 2 * m.volatility;
if (rng() < m.spikeChance) shock *= rng() < 0.5 ? m.spikeSize : 1 / m.spikeSize; // shortage or glut
const pulled = state.materialFactor + (1 - state.materialFactor) * m.reversion;
state.materialFactor = clamp(pulled * shock, m.minFactor, m.maxFactor);
const next = clamp(pulled * shock, m.minFactor, m.maxFactor);
state.materialTrend = Math.sign(next - state.materialFactor);
state.materialFactor = next;
}
}
@@ -137,9 +147,11 @@ function step(state, ctx, dt, rng, totals) {
state.units += made;
state.totalProduced += made;
// Automatic restock when the stock runs low.
if (d.autoBuyMaterial && state.material < d.batchSize / 2 && state.funds >= d.batchCost) {
buyMaterial(state, ctx);
// Automatic restock: patient while the price is high, urgent when nearly out.
if (d.autoBuyMaterial && state.funds >= d.batchCost) {
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);
}
// Customers buy what they want, up to what is in stock.
+24
View File
@@ -25,6 +25,30 @@ export function maxAffordable(base, growth, owned, budget) {
return Math.max(n, 0);
}
/**
* Raw-material price per unit of material, before market noise. Expensive at
* first, then cheaper relative to revenue as the operation scales
* (economies of scale): it falls from `startUnitPrice` towards `floorUnitPrice`.
*/
export function materialUnitPrice(mm, totalProduced) {
const scale = 1 / Math.sqrt(1 + totalProduced / mm.scale);
return mm.floorUnitPrice + (mm.startUnitPrice - mm.floorUnitPrice) * scale;
}
/**
* What one purchase of material looks like. The lot grows with the operation
* (a share of everything produced so far, or `batchSecondsOfProduction` of
* current output), in doubling steps so the price stays readable.
* `factor` is the market's current noise, `discount` comes from upgrades.
*/
export function materialBatch(mm, totalProduced, rate, factor, discount) {
const basis = Math.max(totalProduced * mm.batchShareOfTotal, rate * mm.batchSecondsOfProduction);
const doublings = Math.max(0, Math.floor(Math.log2(Math.max(1, basis / mm.minBatch))));
const size = mm.minBatch * 2 ** doublings;
const cost = Math.max(1, Math.ceil(size * materialUnitPrice(mm, totalProduced) * factor * discount));
return { size, cost };
}
/**
* Units customers want to buy per second at `price`.
* Demand is maxed at or below the fair price and drops steeply above it, so
+2 -1
View File
@@ -22,6 +22,7 @@ export function createState(ctx, now = Date.now()) {
upgrades: [],
savedAt: now,
materialTimer: 0, // transient, never saved
materialTrend: 0, // transient: -1 falling, 0 steady, 1 rising
};
}
@@ -63,7 +64,7 @@ export function sanitizeState(raw, ctx, now = Date.now()) {
/** Compact JSON (numbers trimmed to 10 significant digits) for cookies and export. */
export function serializeState(state) {
const { materialTimer, ...persisted } = state;
const { materialTimer, materialTrend, ...persisted } = state;
return JSON.stringify(persisted, (_key, value) =>
typeof value === 'number' && Number.isFinite(value) ? Number(value.toPrecision(10)) : value,
);