feat: New features (research, invest, gambling, new game+, trophies)
This commit is contained in:
+59
-18
@@ -1,9 +1,23 @@
|
||||
// Game rules. Every function takes the mutable `state` and the static `ctx`
|
||||
// (see context.js) and never touches the DOM, so it runs the same in the
|
||||
// browser and in Node (tests, balance simulations).
|
||||
//
|
||||
// This file holds the production/market loop and ties the other systems
|
||||
// (investments, research, contracts, events, achievements...) into time.
|
||||
|
||||
import { computeModifiers } from './effects.js';
|
||||
import { bulkCost, clamp, demandPerSecond, materialBatch, maxAffordable } from './formulas.js';
|
||||
import { checkAchievements } from './achievements.js';
|
||||
import { stepContracts, stepEvents } from './contracts.js';
|
||||
import {
|
||||
balancedPrice,
|
||||
bulkCost,
|
||||
clamp,
|
||||
demandPerSecond,
|
||||
materialBatch,
|
||||
maxAffordable,
|
||||
} from './formulas.js';
|
||||
import { stepInvestments } from './investments.js';
|
||||
import { modifiersFor } from './modifiers.js';
|
||||
import { rpPerSecond, stepResearch } from './research.js';
|
||||
|
||||
const MAX_STEP_SECONDS = 1;
|
||||
|
||||
@@ -11,6 +25,7 @@ const MAX_STEP_SECONDS = 1;
|
||||
|
||||
export function isUnlocked(state, condition = {}) {
|
||||
if (condition.totalProduced != null && state.totalProduced < condition.totalProduced) return false;
|
||||
if (condition.lifetimeProduced != null && state.lifetimeProduced < condition.lifetimeProduced) return false;
|
||||
if (condition.generator != null && (state.generators[condition.generator] ?? 0) < (condition.count ?? 1)) {
|
||||
return false;
|
||||
}
|
||||
@@ -26,9 +41,8 @@ export function upgradeVisible(state, upgrade) {
|
||||
// ------------------------------------------------------------------- derived
|
||||
|
||||
/** Everything computed from the state (rates, multipliers, current prices). */
|
||||
export function derive(state, ctx) {
|
||||
export function derive(state, ctx, mods = modifiersFor(state, ctx)) {
|
||||
const { rules } = ctx;
|
||||
const mods = computeModifiers(state.upgrades, ctx.upgradesById);
|
||||
|
||||
const perGenerator = {};
|
||||
let rate = 0;
|
||||
@@ -43,10 +57,11 @@ export function derive(state, ctx) {
|
||||
state.totalProduced,
|
||||
rate,
|
||||
state.materialFactor,
|
||||
mods.batchDiscount,
|
||||
mods.batchDiscount * mods.materialPrice,
|
||||
mods.batchSize,
|
||||
);
|
||||
return {
|
||||
mods,
|
||||
clickYield: rules.click.yield * mods.click,
|
||||
perGenerator,
|
||||
rate,
|
||||
@@ -58,6 +73,15 @@ export function derive(state, ctx) {
|
||||
batchCost: batch.cost,
|
||||
autoBuyMaterial: mods.autoBuyMaterial,
|
||||
marketingCost: Math.ceil(rules.marketing.baseCost * rules.marketing.costGrowth ** state.marketing),
|
||||
rpRate: rpPerSecond(state, ctx, mods),
|
||||
// Only researched: the price where demand matches output (see formulas.balancedPrice).
|
||||
advisedPrice: mods.priceAdvisor
|
||||
? clamp(
|
||||
balancedPrice(rules.market, Math.max(rate, 1e-9), demandMultiplier),
|
||||
rules.market.minPrice,
|
||||
rules.market.maxPrice,
|
||||
)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,6 +95,7 @@ export function click(state, ctx) {
|
||||
state.material -= made * d.materialPerUnit;
|
||||
state.units += made;
|
||||
state.totalProduced += made;
|
||||
state.lifetimeProduced += made;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,12 +115,13 @@ export function buyMarketing(state, ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** How many generators the player gets for `amount` ("max" or a number). */
|
||||
export function generatorQuote(state, ctx, id, amount) {
|
||||
/** How many generators the player gets for `amount` ("max" or a number), and what it costs. */
|
||||
export function generatorQuote(state, ctx, id, amount, mods = modifiersFor(state, ctx)) {
|
||||
const g = ctx.generatorsById.get(id);
|
||||
const owned = state.generators[id] ?? 0;
|
||||
const count = amount === 'max' ? maxAffordable(g.baseCost, g.costGrowth, owned, state.funds) : amount;
|
||||
return { count, cost: bulkCost(g.baseCost, g.costGrowth, owned, count) };
|
||||
const base = g.baseCost * mods.generatorCost;
|
||||
const count = amount === 'max' ? maxAffordable(base, g.costGrowth, owned, state.funds) : amount;
|
||||
return { count, cost: bulkCost(base, g.costGrowth, owned, count) };
|
||||
}
|
||||
|
||||
export function buyGenerator(state, ctx, id, amount = 1) {
|
||||
@@ -138,7 +164,7 @@ function stepMaterialMarket(state, ctx, dt, rng) {
|
||||
}
|
||||
}
|
||||
|
||||
function step(state, ctx, dt, rng, totals) {
|
||||
function step(state, ctx, dt, rng, totals, offline) {
|
||||
stepMaterialMarket(state, ctx, dt, rng);
|
||||
const d = derive(state, ctx);
|
||||
|
||||
@@ -147,6 +173,7 @@ function step(state, ctx, dt, rng, totals) {
|
||||
state.material -= made * d.materialPerUnit;
|
||||
state.units += made;
|
||||
state.totalProduced += made;
|
||||
state.lifetimeProduced += made;
|
||||
|
||||
// Automatic restock: patient while the price is high, urgent when nearly out.
|
||||
if (d.autoBuyMaterial && state.funds >= d.batchCost) {
|
||||
@@ -165,28 +192,42 @@ function step(state, ctx, dt, rng, totals) {
|
||||
totals.produced += made;
|
||||
totals.sold += sold;
|
||||
totals.revenue += revenue;
|
||||
|
||||
// The other systems.
|
||||
stepInvestments(state, ctx, dt, rng, d.mods);
|
||||
stepResearch(state, ctx, dt, d.mods);
|
||||
stepContracts(state, ctx, dt, sold, d, d.mods, rng, offline, totals);
|
||||
stepEvents(state, ctx, dt, d, rng, offline, totals);
|
||||
state.gambleCooldown = Math.max(0, state.gambleCooldown - dt);
|
||||
for (const id of checkAchievements(state, ctx)) totals.notices.push({ kind: 'achievement', id });
|
||||
}
|
||||
|
||||
/** Advance the simulation by `seconds`. Returns what happened during that time. */
|
||||
export function advance(state, ctx, seconds, rng = Math.random) {
|
||||
const totals = { produced: 0, sold: 0, revenue: 0 };
|
||||
/**
|
||||
* Advance the simulation by `seconds`. Returns what happened during that time,
|
||||
* including `notices` (things worth telling the player). `offline` skips the
|
||||
* parts that need the player present (contracts, events).
|
||||
*/
|
||||
export function advance(state, ctx, seconds, rng = Math.random, { offline = false } = {}) {
|
||||
const totals = { produced: 0, sold: 0, revenue: 0, notices: [] };
|
||||
let left = Math.max(0, seconds);
|
||||
while (left > 1e-9) {
|
||||
const dt = Math.min(left, MAX_STEP_SECONDS);
|
||||
step(state, ctx, dt, rng, totals);
|
||||
step(state, ctx, dt, rng, totals, offline);
|
||||
left -= dt;
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch up on the time the game was closed. Deterministic (no price noise)
|
||||
* and discounted by `offline.efficiency`. Returns null if too short to matter.
|
||||
* Catch up on the time the game was closed. Deterministic (no price noise),
|
||||
* discounted by the offline efficiency (research and legacy raise it).
|
||||
* Returns null if too short to matter.
|
||||
*/
|
||||
export function simulateOffline(state, ctx, elapsedSeconds) {
|
||||
const { maxSeconds, efficiency, minSeconds } = ctx.rules.offline;
|
||||
if (elapsedSeconds < minSeconds) return null;
|
||||
const seconds = Math.min(elapsedSeconds, maxSeconds) * efficiency;
|
||||
const totals = advance(state, ctx, seconds, () => 0.5);
|
||||
const effective = Math.min(1, efficiency + modifiersFor(state, ctx).offline);
|
||||
const seconds = Math.min(elapsedSeconds, maxSeconds) * effective;
|
||||
const totals = advance(state, ctx, seconds, () => 0.5, { offline: true });
|
||||
return { ...totals, seconds: Math.min(elapsedSeconds, maxSeconds) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user