// Pure math helpers. No state, no DOM. export function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } /** * Demand bonus from owning chips: diminishing returns (logarithmic), so a big * lucky streak can never run away with the game. */ export function chipDemandBonus(chips, gamble) { return 1 + gamble.chipBonusScale * Math.log2(1 + Math.max(0, chips)); } /** Price of the next item when `owned` items are already owned. */ export function unitCost(base, growth, owned) { return base * growth ** owned; } /** Total price of buying `count` more items (geometric series, rounded up). */ export function bulkCost(base, growth, owned, count) { if (count <= 0) return 0; return Math.ceil((unitCost(base, growth, owned) * (growth ** count - 1)) / (growth - 1)); } /** How many items `budget` can pay for, robust against float error. */ export function maxAffordable(base, growth, owned, budget) { const first = unitCost(base, growth, owned); if (Number.isNaN(budget) || budget < first) return 0; let n = Math.floor(Math.log((budget * (growth - 1)) / first + 1) / Math.log(growth)); while (n > 0 && bulkCost(base, growth, owned, n) > budget) n--; while (bulkCost(base, growth, owned, n + 1) <= budget) n++; 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 covers `batchSecondsOfProduction` of current output (in doubling * steps, never below `minBatch`), times `sizeMultiplier` from the "bigger * stacks" upgrades. The price of a lot is proportional to its size: a bigger * stack costs more in total, while the price PER UNIT keeps falling with scale * (see materialUnitPrice). `factor` is the market's current noise and * `discount` comes from upgrades. Cost is exact to the cent. */ export function materialBatch(mm, totalProduced, rate, factor, discount, sizeMultiplier = 1) { const basis = rate * mm.batchSecondsOfProduction; const doublings = Math.max(0, Math.floor(Math.log2(Math.max(1, basis / mm.minBatch)))); const size = mm.minBatch * 2 ** doublings * sizeMultiplier; const cents = size * materialUnitPrice(mm, totalProduced) * factor * discount; return { size, cost: Math.max(0.01, Math.round(cents * 100) / 100) }; } /** * Units customers want to buy per second at `price`: a smooth curve, with more * demand the lower the price and less the higher it is (`referencePrice` is * just where demand equals baseDemand x multiplier). */ export function demandPerSecond(market, price, multiplier) { return market.baseDemand * multiplier * (market.referencePrice / price) ** market.elasticity; } /** * The price at which customers buy exactly what is produced. Selling for less * gains no volume (everything is already sold); selling for more leaves stock * unsold. It moves with output and every demand bonus, and the UI deliberately * does not reveal it: the player finds it from the market feedback. */ export function balancedPrice(market, supplyPerSecond, multiplier) { const demandAtReference = market.baseDemand * multiplier; return market.referencePrice * (demandAtReference / supplyPerSecond) ** (1 / market.elasticity); }