fix: Changing prices and dynamics in revenus
This commit is contained in:
@@ -74,4 +74,4 @@ Keys: `res.units|funds|material`, `gen.<id>`, `upg.<id>`. Anything not listed ke
|
|||||||
Everything numeric is in `public/data/`: `rules.json` (market, raw material, marketing, offline, autosave), `generators.json`, `upgrades.json`.
|
Everything numeric is in `public/data/`: `rules.json` (market, raw material, marketing, offline, autosave), `generators.json`, `upgrades.json`.
|
||||||
|
|
||||||
- **Selling:** demand is a smooth curve, `baseDemand × demandMultiplier × (referencePrice / price) ^ elasticity` units/s: lower the price and more people buy, with no ceiling. The best price is where demand just matches your output (selling for less gains no volume, selling for more leaves stock unsold). That point moves with every generator, upgrade and marketing level, and the game does not display it: the market hint only says whether you are selling too cheap, too dear, or about right.
|
- **Selling:** demand is a smooth curve, `baseDemand × demandMultiplier × (referencePrice / price) ^ elasticity` units/s: lower the price and more people buy, with no ceiling. The best price is where demand just matches your output (selling for less gains no volume, selling for more leaves stock unsold). That point moves with every generator, upgrade and marketing level, and the game does not display it: the market hint only says whether you are selling too cheap, too dear, or about right.
|
||||||
- **Raw material** (wire, herbs…): the price per unit starts high (`startUnitPrice`, about a third of the sale price) and falls towards `floorUnitPrice` as total production grows (economies of scale). Lots grow with the operation, in doublings. The market is erratic: the price is re-drawn every 10 s (`updateEverySeconds`) with wide random swings plus frequent shortages and gluts (`spikeChance`, `spikeSize`), and is shown to the cent, with ▲/▼ for its last move. The auto-buy upgrade is patient: it waits for a fair price unless stock is nearly gone.
|
- **Raw material** (wire, herbs…): the price per unit starts high (`startUnitPrice`, about a third of the sale price) and falls towards `floorUnitPrice` as total production grows (economies of scale). A lot covers about 10 s of current output (in doublings), multiplied by the "stack" upgrades (`batch_size_mult`, a chain of five, ×180 in total). A lot costs in proportion to its size, so bigger stacks cost more per purchase while the price per unit still falls with scale. The market is erratic: the price is re-drawn every 10 s (`updateEverySeconds`) with wide random swings plus frequent shortages and gluts (`spikeChance`, `spikeSize`), and is shown to the cent, with ▲/▼ for its last move. The auto-buy upgrade is patient: it waits for a fair price unless stock is nearly gone.
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
},
|
},
|
||||||
"materialMarket": {
|
"materialMarket": {
|
||||||
"minBatch": 100,
|
"minBatch": 100,
|
||||||
"batchShareOfTotal": 0.1,
|
"batchSecondsOfProduction": 10,
|
||||||
"batchSecondsOfProduction": 60,
|
|
||||||
"startUnitPrice": 0.08,
|
"startUnitPrice": 0.08,
|
||||||
"floorUnitPrice": 0.01,
|
"floorUnitPrice": 0.01,
|
||||||
"scale": 3000,
|
"scale": 3000,
|
||||||
|
|||||||
@@ -34,5 +34,10 @@
|
|||||||
{ "id": "mat_eff_2", "cost": 10000, "unlock": { "totalProduced": 5000 }, "requires": "mat_eff_1", "effect": { "type": "material_efficiency", "value": 1.5 } },
|
{ "id": "mat_eff_2", "cost": 10000, "unlock": { "totalProduced": 5000 }, "requires": "mat_eff_1", "effect": { "type": "material_efficiency", "value": 1.5 } },
|
||||||
{ "id": "mat_disc_1", "cost": 500, "unlock": { "totalProduced": 500 }, "effect": { "type": "batch_discount", "value": 0.8 } },
|
{ "id": "mat_disc_1", "cost": 500, "unlock": { "totalProduced": 500 }, "effect": { "type": "batch_discount", "value": 0.8 } },
|
||||||
{ "id": "mat_disc_2", "cost": 20000, "unlock": { "totalProduced": 8000 }, "requires": "mat_disc_1", "effect": { "type": "batch_discount", "value": 0.7 } },
|
{ "id": "mat_disc_2", "cost": 20000, "unlock": { "totalProduced": 8000 }, "requires": "mat_disc_1", "effect": { "type": "batch_discount", "value": 0.7 } },
|
||||||
|
{ "id": "stack_1", "cost": 120, "unlock": { "totalProduced": 400 }, "effect": { "type": "batch_size_mult", "value": 2 } },
|
||||||
|
{ "id": "stack_2", "cost": 2500, "unlock": { "totalProduced": 4000 }, "requires": "stack_1", "effect": { "type": "batch_size_mult", "value": 2 } },
|
||||||
|
{ "id": "stack_3", "cost": 40000, "unlock": { "totalProduced": 40000 }, "requires": "stack_2", "effect": { "type": "batch_size_mult", "value": 3 } },
|
||||||
|
{ "id": "stack_4", "cost": 600000, "unlock": { "totalProduced": 500000 }, "requires": "stack_3", "effect": { "type": "batch_size_mult", "value": 3 } },
|
||||||
|
{ "id": "stack_5", "cost": 10000000, "unlock": { "totalProduced": 5000000 }, "requires": "stack_4", "effect": { "type": "batch_size_mult", "value": 5 } },
|
||||||
{ "id": "auto_buy", "cost": 800, "unlock": { "totalProduced": 800 }, "effect": { "type": "auto_buy_material" } }
|
{ "id": "auto_buy", "cost": 800, "unlock": { "totalProduced": 800 }, "effect": { "type": "auto_buy_material" } }
|
||||||
]
|
]
|
||||||
|
|||||||
Vendored
+2
@@ -10,6 +10,7 @@ export function newModifiers() {
|
|||||||
value: 1,
|
value: 1,
|
||||||
materialEfficiency: 1,
|
materialEfficiency: 1,
|
||||||
batchDiscount: 1,
|
batchDiscount: 1,
|
||||||
|
batchSize: 1,
|
||||||
autoBuyMaterial: false,
|
autoBuyMaterial: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ const APPLY = {
|
|||||||
value_mult: (m, e) => void (m.value *= e.value),
|
value_mult: (m, e) => void (m.value *= e.value),
|
||||||
material_efficiency: (m, e) => void (m.materialEfficiency *= e.value),
|
material_efficiency: (m, e) => void (m.materialEfficiency *= e.value),
|
||||||
batch_discount: (m, e) => void (m.batchDiscount *= e.value),
|
batch_discount: (m, e) => void (m.batchDiscount *= e.value),
|
||||||
|
batch_size_mult: (m, e) => void (m.batchSize *= e.value),
|
||||||
auto_buy_material: (m) => void (m.autoBuyMaterial = true),
|
auto_buy_material: (m) => void (m.autoBuyMaterial = true),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function derive(state, ctx) {
|
|||||||
rate,
|
rate,
|
||||||
state.materialFactor,
|
state.materialFactor,
|
||||||
mods.batchDiscount,
|
mods.batchDiscount,
|
||||||
|
mods.batchSize,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
clickYield: rules.click.yield * mods.click,
|
clickYield: rules.click.yield * mods.click,
|
||||||
|
|||||||
@@ -36,15 +36,19 @@ export function materialUnitPrice(mm, totalProduced) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What one purchase of material looks like. The lot grows with the operation
|
* What one purchase of material looks like.
|
||||||
* (a share of everything produced so far, or `batchSecondsOfProduction` of
|
*
|
||||||
* current output), in doubling steps so the price stays readable.
|
* The lot covers `batchSecondsOfProduction` of current output (in doubling
|
||||||
* `factor` is the market's current noise, `discount` comes from upgrades.
|
* 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) {
|
export function materialBatch(mm, totalProduced, rate, factor, discount, sizeMultiplier = 1) {
|
||||||
const basis = Math.max(totalProduced * mm.batchShareOfTotal, rate * mm.batchSecondsOfProduction);
|
const basis = rate * mm.batchSecondsOfProduction;
|
||||||
const doublings = Math.max(0, Math.floor(Math.log2(Math.max(1, basis / mm.minBatch))));
|
const doublings = Math.max(0, Math.floor(Math.log2(Math.max(1, basis / mm.minBatch))));
|
||||||
const size = mm.minBatch * 2 ** doublings;
|
const size = mm.minBatch * 2 ** doublings * sizeMultiplier;
|
||||||
const cents = size * materialUnitPrice(mm, totalProduced) * factor * discount;
|
const cents = size * materialUnitPrice(mm, totalProduced) * factor * discount;
|
||||||
return { size, cost: Math.max(0.01, Math.round(cents * 100) / 100) };
|
return { size, cost: Math.max(0.01, Math.round(cents * 100) / 100) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"value_mult": "Sale value ×{value}",
|
"value_mult": "Sale value ×{value}",
|
||||||
"material_efficiency": "{@theme.res.material} goes ×{value} further",
|
"material_efficiency": "{@theme.res.material} goes ×{value} further",
|
||||||
"batch_discount": "{@theme.res.material} is {percent}% cheaper",
|
"batch_discount": "{@theme.res.material} is {percent}% cheaper",
|
||||||
"auto_buy_material": "Restocks {@theme.res.material} automatically"
|
"auto_buy_material": "Restocks {@theme.res.material} automatically",
|
||||||
|
"batch_size_mult": "{@theme.res.material}: ×{value} per purchase"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"seconds": "{value} s",
|
"seconds": "{value} s",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"value_mult": "Valeur de vente ×{value}",
|
"value_mult": "Valeur de vente ×{value}",
|
||||||
"material_efficiency": "{@theme.res.material} : rendement ×{value}",
|
"material_efficiency": "{@theme.res.material} : rendement ×{value}",
|
||||||
"batch_discount": "{@theme.res.material} : {percent} % moins cher",
|
"batch_discount": "{@theme.res.material} : {percent} % moins cher",
|
||||||
"auto_buy_material": "Réapprovisionnement automatique : {@theme.res.material}"
|
"auto_buy_material": "Réapprovisionnement automatique : {@theme.res.material}",
|
||||||
|
"batch_size_mult": "{@theme.res.material} : ×{value} par achat"
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"seconds": "{value} s",
|
"seconds": "{value} s",
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Molecular Sieve",
|
"mat_eff_2": "Molecular Sieve",
|
||||||
"mat_disc_1": "Asteroid Contract",
|
"mat_disc_1": "Asteroid Contract",
|
||||||
"mat_disc_2": "Own the Mine",
|
"mat_disc_2": "Own the Mine",
|
||||||
|
"stack_1": "Bigger Cargo Pods",
|
||||||
|
"stack_2": "Container Drones",
|
||||||
|
"stack_3": "Freighter Holds",
|
||||||
|
"stack_4": "Convoy Fleets",
|
||||||
|
"stack_5": "Orbital Stockpile",
|
||||||
"auto_buy": "Autonomous Freighter"
|
"auto_buy": "Autonomous Freighter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Tamis moléculaire",
|
"mat_eff_2": "Tamis moléculaire",
|
||||||
"mat_disc_1": "Contrat d'astéroïde",
|
"mat_disc_1": "Contrat d'astéroïde",
|
||||||
"mat_disc_2": "Posséder la mine",
|
"mat_disc_2": "Posséder la mine",
|
||||||
|
"stack_1": "Nacelles de fret plus grandes",
|
||||||
|
"stack_2": "Drones-conteneurs",
|
||||||
|
"stack_3": "Soutes de cargo",
|
||||||
|
"stack_4": "Flottes de convoi",
|
||||||
|
"stack_5": "Stock orbital",
|
||||||
"auto_buy": "Cargo autonome"
|
"auto_buy": "Cargo autonome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Silk Recycling",
|
"mat_eff_2": "Silk Recycling",
|
||||||
"mat_disc_1": "Spider Contract",
|
"mat_disc_1": "Spider Contract",
|
||||||
"mat_disc_2": "Silkworm Farm",
|
"mat_disc_2": "Silkworm Farm",
|
||||||
|
"stack_1": "Bigger Silk Bundles",
|
||||||
|
"stack_2": "Cocoon Sacks",
|
||||||
|
"stack_3": "Web Warehouses",
|
||||||
|
"stack_4": "Spider Caravans",
|
||||||
|
"stack_5": "Great Silk Hoard",
|
||||||
"auto_buy": "Spider Delivery Service"
|
"auto_buy": "Spider Delivery Service"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Recyclage de la soie",
|
"mat_eff_2": "Recyclage de la soie",
|
||||||
"mat_disc_1": "Contrat avec l'araignée",
|
"mat_disc_1": "Contrat avec l'araignée",
|
||||||
"mat_disc_2": "Ferme de vers à soie",
|
"mat_disc_2": "Ferme de vers à soie",
|
||||||
|
"stack_1": "Écheveaux de soie plus gros",
|
||||||
|
"stack_2": "Sacs de cocons",
|
||||||
|
"stack_3": "Entrepôts de toile",
|
||||||
|
"stack_4": "Caravanes d'araignées",
|
||||||
|
"stack_5": "Grand trésor de soie",
|
||||||
"auto_buy": "Livraison par araignée"
|
"auto_buy": "Livraison par araignée"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Wire Recycling",
|
"mat_eff_2": "Wire Recycling",
|
||||||
"mat_disc_1": "Bulk Wire Contract",
|
"mat_disc_1": "Bulk Wire Contract",
|
||||||
"mat_disc_2": "Direct From the Mill",
|
"mat_disc_2": "Direct From the Mill",
|
||||||
|
"stack_1": "Bigger Spools",
|
||||||
|
"stack_2": "Pallet Orders",
|
||||||
|
"stack_3": "Container Loads",
|
||||||
|
"stack_4": "Freight Trains",
|
||||||
|
"stack_5": "Wire Mills",
|
||||||
"auto_buy": "WireBuyer"
|
"auto_buy": "WireBuyer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Recyclage du fil",
|
"mat_eff_2": "Recyclage du fil",
|
||||||
"mat_disc_1": "Contrat de fil en gros",
|
"mat_disc_1": "Contrat de fil en gros",
|
||||||
"mat_disc_2": "Directement de la fonderie",
|
"mat_disc_2": "Directement de la fonderie",
|
||||||
|
"stack_1": "Bobines plus grosses",
|
||||||
|
"stack_2": "Commandes par palette",
|
||||||
|
"stack_3": "Chargements en conteneur",
|
||||||
|
"stack_4": "Trains de marchandises",
|
||||||
|
"stack_5": "Tréfileries",
|
||||||
"auto_buy": "AcheteurDeFil"
|
"auto_buy": "AcheteurDeFil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Distilled Essence",
|
"mat_eff_2": "Distilled Essence",
|
||||||
"mat_disc_1": "Forager's Pact",
|
"mat_disc_1": "Forager's Pact",
|
||||||
"mat_disc_2": "Enchanted Garden",
|
"mat_disc_2": "Enchanted Garden",
|
||||||
|
"stack_1": "Larger Baskets",
|
||||||
|
"stack_2": "Herb Sacks",
|
||||||
|
"stack_3": "Cartloads",
|
||||||
|
"stack_4": "Greenhouse Harvest",
|
||||||
|
"stack_5": "Whole-Forest Rights",
|
||||||
"auto_buy": "Self-Gathering Basket"
|
"auto_buy": "Self-Gathering Basket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
"mat_eff_2": "Essence distillée",
|
"mat_eff_2": "Essence distillée",
|
||||||
"mat_disc_1": "Pacte des cueilleurs",
|
"mat_disc_1": "Pacte des cueilleurs",
|
||||||
"mat_disc_2": "Jardin enchanté",
|
"mat_disc_2": "Jardin enchanté",
|
||||||
|
"stack_1": "Paniers plus grands",
|
||||||
|
"stack_2": "Sacs d'herbes",
|
||||||
|
"stack_3": "Charrettes pleines",
|
||||||
|
"stack_4": "Récolte en serre",
|
||||||
|
"stack_5": "Droits sur toute la forêt",
|
||||||
"auto_buy": "Panier qui cueille tout seul"
|
"auto_buy": "Panier qui cueille tout seul"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-6
@@ -165,15 +165,56 @@ test('material is costly at first and cheaper relative to the sale price as prod
|
|||||||
assert.ok(costShare(1e9) < 0.1, 'a small share of revenue late in the game');
|
assert.ok(costShare(1e9) < 0.1, 'a small share of revenue late in the game');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('material lots grow with the operation, in doublings', () => {
|
test('material lots follow current output, in doublings', () => {
|
||||||
const sizeAt = (total) => {
|
const sizeWith = (gen1) => {
|
||||||
const s = fresh();
|
const s = fresh();
|
||||||
s.totalProduced = total;
|
s.generators.gen1 = gen1;
|
||||||
return derive(s, ctx).batchSize;
|
return derive(s, ctx).batchSize;
|
||||||
};
|
};
|
||||||
assert.equal(sizeAt(0), ctx.rules.materialMarket.minBatch);
|
const { minBatch } = ctx.rules.materialMarket;
|
||||||
assert.ok(sizeAt(1e6) > sizeAt(1e4));
|
assert.equal(sizeWith(0), minBatch);
|
||||||
assert.equal(Math.log2(sizeAt(1e6) / ctx.rules.materialMarket.minBatch) % 1, 0);
|
assert.ok(sizeWith(500) > sizeWith(5));
|
||||||
|
assert.equal(Math.log2(sizeWith(500) / minBatch) % 1, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stack upgrades multiply the lot, and its price grows in proportion', () => {
|
||||||
|
const lot = (upgrades) => {
|
||||||
|
const s = fresh();
|
||||||
|
s.upgrades = upgrades;
|
||||||
|
s.materialFactor = 1;
|
||||||
|
return derive(s, ctx);
|
||||||
|
};
|
||||||
|
const base = lot([]);
|
||||||
|
const doubled = lot(['stack_1']);
|
||||||
|
const x12 = lot(['stack_1', 'stack_2', 'stack_3']);
|
||||||
|
assert.equal(doubled.batchSize, base.batchSize * 2);
|
||||||
|
assert.equal(x12.batchSize, base.batchSize * 12);
|
||||||
|
assert.ok(Math.abs(doubled.batchCost - base.batchCost * 2) <= 0.01, 'twice the wire costs twice as much');
|
||||||
|
assert.ok(Math.abs(x12.batchCost - base.batchCost * 12) <= 0.06);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the price PER UNIT of material is unchanged by stack size and keeps falling with scale', () => {
|
||||||
|
const unitPrice = (totalProduced, upgrades) => {
|
||||||
|
const s = fresh();
|
||||||
|
s.totalProduced = totalProduced;
|
||||||
|
s.upgrades = upgrades;
|
||||||
|
s.materialFactor = 1;
|
||||||
|
const d = derive(s, ctx);
|
||||||
|
return d.batchCost / d.batchSize;
|
||||||
|
};
|
||||||
|
const early = unitPrice(0, []);
|
||||||
|
assert.ok(Math.abs(unitPrice(0, ['stack_1', 'stack_2']) - early) < early * 0.01);
|
||||||
|
assert.ok(unitPrice(1e6, ['stack_1', 'stack_2']) < unitPrice(1e4, ['stack_1', 'stack_2']));
|
||||||
|
assert.ok(unitPrice(1e9, []) < early / 5, 'much cheaper per unit late in the game');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stack upgrades form a chain', () => {
|
||||||
|
const s = fresh();
|
||||||
|
s.totalProduced = 1e9;
|
||||||
|
assert.ok(upgradeVisible(s, ctx.upgradesById.get('stack_1')));
|
||||||
|
assert.equal(upgradeVisible(s, ctx.upgradesById.get('stack_2')), false);
|
||||||
|
s.upgrades = ['stack_1'];
|
||||||
|
assert.ok(upgradeVisible(s, ctx.upgradesById.get('stack_2')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the material market is erratic but stays within its bounds', () => {
|
test('the material market is erratic but stays within its bounds', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user