feat: New theme + wire price adjustement
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user