// Pure math helpers. No state, no DOM. export function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } /** 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 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 * extra production is only worth selling at a higher price while supply is * short; to sell more at the fair price, the player needs more demand. */ export function demandPerSecond(market, price, multiplier) { const overpriced = Math.max(1, price / market.fairPrice); return (market.baseDemand * multiplier) / overpriced ** market.elasticity; }