Files

144 lines
5.0 KiB
JavaScript

// Contracts and random events: the "active play" layer.
//
// Contracts: up to `maxOffers` offers are on the board; accept one and sell
// `target` units before the time runs out (a bit more than your current sales
// pace, so it takes some effort) for a reward. Events: now and then an
// opportunity appears for a short window and must be claimed; rarer incidents
// (like a price crash for the raw material) just happen.
export function salesEstimate(state, d) {
const rate = Math.max(Math.min(d.rate, d.demand), 1);
return { rate, income: rate * state.price * d.valueMultiplier };
}
/** A random element, safe even if rng() ever returns exactly 1. */
function pick(list, rng) {
return list[Math.min(list.length - 1, Math.floor(rng() * list.length))];
}
// ------------------------------------------------------------------ contracts
function makeOffer(state, ctx, d, mods, rng) {
const cfg = ctx.rules.contracts;
const template = pick(cfg.templates, rng);
const type = pick(cfg.rewards, rng);
const { rate, income } = salesEstimate(state, d);
const factor = template.rewardFactor * mods.contract;
let amount;
if (type === 'funds') amount = income * template.seconds * factor;
else if (type === 'research') amount = Math.max(1, Math.round(cfg.researchPerFactor * factor));
else if (type === 'chips') amount = Math.max(1, Math.round(cfg.chipsPerFactor * factor));
else amount = 1 + (cfg.boostMultiplier - 1) * Math.min(2, template.rewardFactor); // boost: a multiplier
return {
template: template.id,
seconds: template.seconds,
target: Math.ceil(rate * template.seconds * template.targetFactor),
reward: { type, amount },
};
}
function grantReward(state, ctx, offer) {
const { type, amount } = offer.reward;
if (type === 'funds') state.funds += amount;
else if (type === 'research') state.rp += amount;
else if (type === 'chips') state.chips = Math.min(state.chips + amount, ctx.rules.gamble.maxChips);
else state.events.buffs.push({ type: 'global_mult', value: amount, remaining: offer.seconds });
}
/** Take offer number `index` from the board. Only one contract can be active. */
export function acceptContract(state, ctx, index) {
const c = state.contracts;
if (c.active || !c.offers[index]) return false;
const [offer] = c.offers.splice(index, 1);
c.active = { offer, progress: 0, remaining: offer.seconds };
return true;
}
export function abandonContract(state) {
if (!state.contracts.active) return false;
state.contracts.active = null;
return true;
}
export function stepContracts(state, ctx, dt, sold, d, mods, rng, offline, totals) {
if (offline) return;
const c = state.contracts;
const cfg = ctx.rules.contracts;
if (c.active) {
c.active.progress += sold;
c.active.remaining -= dt;
if (c.active.progress >= c.active.offer.target) {
grantReward(state, ctx, c.active.offer);
totals.notices.push({ kind: 'contract_done', reward: c.active.offer.reward, seconds: c.active.offer.seconds });
state.stats.contractsDone += 1;
c.active = null;
} else if (c.active.remaining <= 0) {
totals.notices.push({ kind: 'contract_failed' });
c.active = null;
}
}
state.contractTimer += dt;
if (state.contractTimer >= cfg.offerEverySeconds) {
state.contractTimer = 0;
c.offers.push(makeOffer(state, ctx, d, mods, rng));
if (c.offers.length > cfg.maxOffers) c.offers.shift(); // the oldest offer expires
}
}
// --------------------------------------------------------------------- events
function pickEventType(cfg, rng) {
const total = cfg.types.reduce((sum, t) => sum + t.weight, 0);
let roll = rng() * total;
for (const type of cfg.types) {
roll -= type.weight;
if (roll < 0) return type;
}
return cfg.types[0];
}
export function stepEvents(state, ctx, dt, d, rng, offline, totals) {
const ev = state.events;
const cfg = ctx.rules.events;
for (const buff of ev.buffs) buff.remaining -= dt;
ev.buffs = ev.buffs.filter((buff) => buff.remaining > 0);
if (offline) return;
if (ev.current) {
ev.current.remaining -= dt;
if (ev.current.remaining <= 0) ev.current = null;
}
state.eventTimer -= dt;
if (state.eventTimer > 0 || ev.current) return;
state.eventTimer = cfg.minSeconds + rng() * (cfg.maxSeconds - cfg.minSeconds);
const type = pickEventType(cfg, rng);
if (type.auto) {
ev.buffs.push({ type: type.buff.type, value: type.buff.value, remaining: type.buff.seconds });
totals.notices.push({ kind: 'event_incident', type: type.id });
} else {
ev.current = { type: type.id, remaining: cfg.claimSeconds };
}
}
/** Claim the opportunity currently on offer. */
export function claimEvent(state, ctx, d) {
const ev = state.events;
if (!ev.current) return false;
const cfg = ctx.rules.events;
const type = cfg.types.find((t) => t.id === ev.current.type);
if (type.buff) ev.buffs.push({ type: type.buff.type, value: type.buff.value, remaining: type.buff.seconds });
else state.funds += salesEstimate(state, d).income * cfg.windfallIncomeSeconds;
state.stats.eventsClaimed += 1;
ev.current = null;
return true;
}