145 lines
5.1 KiB
JavaScript
145 lines
5.1 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 };
|
|
totals.notices.push({ kind: 'event_opportunity', type: type.id });
|
|
}
|
|
}
|
|
|
|
/** 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;
|
|
}
|