First commit, done on M6 from the PLAN.MD
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// Weekly decision cards: drawing, eligibility and applying the player's answer.
|
||||
//
|
||||
// Card: { id, category, doc, sender, weight?, once?, cooldownDays?, followUpOnly?,
|
||||
// conditions?: { min:{stat:n}, max:{stat:n}, flags:[], notFlags:[], dayMin, dayMax },
|
||||
// title, body, yes: Option, no: Option }
|
||||
// Option: { label, effects: [...see modifiers.js], setFlags?, clearFlags?, followUp?: { id, inDays } }
|
||||
// Condition stats: any indicator, growth/unemployment/inflation, "debt" (% of GDP),
|
||||
// "deficit" (% of GDP) and "q:<sector>" (sector quality).
|
||||
|
||||
import { IND_IDS, VAR_IDS, clamp } from "./state.js";
|
||||
import { computeFiscal } from "./economy.js";
|
||||
import { applyEffect } from "./modifiers.js";
|
||||
import { pickWeighted } from "./rng.js";
|
||||
|
||||
export const indexCards = (cards) => new Map(cards.map((c) => [c.id, c]));
|
||||
|
||||
function statValue(state, key, fiscal) {
|
||||
if (IND_IDS.includes(key)) return state.indicators[key];
|
||||
if (VAR_IDS.includes(key)) return state.economy[key];
|
||||
if (key === "debt") return fiscal.debtRatio;
|
||||
if (key === "deficit") return fiscal.deficit;
|
||||
if (key.startsWith("q:")) return state.sectors[key.slice(2)]?.quality ?? NaN;
|
||||
return NaN; // unknown key: never eligible (content tests catch typos)
|
||||
}
|
||||
|
||||
function meetsConditions(card, state, fiscal) {
|
||||
const c = card.conditions;
|
||||
if (!c) return true;
|
||||
if (c.dayMin !== undefined && state.day < c.dayMin) return false;
|
||||
if (c.dayMax !== undefined && state.day > c.dayMax) return false;
|
||||
for (const [k, v] of Object.entries(c.min ?? {})) if (!(statValue(state, k, fiscal) >= v)) return false;
|
||||
for (const [k, v] of Object.entries(c.max ?? {})) if (!(statValue(state, k, fiscal) <= v)) return false;
|
||||
for (const f of c.flags ?? []) if (!state.flags[f]) return false;
|
||||
for (const f of c.notFlags ?? []) if (state.flags[f]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A sector (or indicator) in trouble makes cards about it more likely.
|
||||
function boostFor(card, state, bal) {
|
||||
const b = bal.cards.boost[card.category];
|
||||
if (!b) return 1;
|
||||
const v = b.sector ? state.sectors[b.sector].quality : state.indicators[b.indicator];
|
||||
const below = bal.cards.boostBelow;
|
||||
return 1 + bal.cards.boostMax * clamp((below - v) / below, 0, 1);
|
||||
}
|
||||
|
||||
export function drawCard(state, bal, cards) {
|
||||
const fiscal = computeFiscal(state, bal);
|
||||
const lastAnswered = new Map();
|
||||
for (const h of state.cardHistory) lastAnswered.set(h.id, h.day);
|
||||
const recent = state.cardHistory.slice(-bal.cards.recentWindow).map((h) => h.category);
|
||||
|
||||
const pool = [];
|
||||
const weights = [];
|
||||
for (const card of cards) {
|
||||
if (card.followUpOnly) continue;
|
||||
const last = lastAnswered.get(card.id);
|
||||
if (last !== undefined) {
|
||||
if (card.once !== false) continue;
|
||||
if (state.day - last < (card.cooldownDays ?? bal.cards.defaultCooldown)) continue;
|
||||
}
|
||||
if (!meetsConditions(card, state, fiscal)) continue;
|
||||
const seen = recent.filter((c) => c === card.category).length;
|
||||
pool.push(card);
|
||||
weights.push((card.weight ?? 1) * boostFor(card, state, bal) * Math.pow(bal.cards.recentPenalty, seen));
|
||||
}
|
||||
return pickWeighted(state, pool, weights);
|
||||
}
|
||||
|
||||
// Called once a weekly pause is pending: puts the card to answer in state.pendingCard.
|
||||
// A due follow-up always comes first. With no eligible card the week is simply empty.
|
||||
export function ensureCard(state, bal, cards) {
|
||||
if (!state.pending.weeklyCard || state.pendingCard) return null;
|
||||
const byId = indexCards(cards);
|
||||
|
||||
let card = null;
|
||||
const due = state.followUps.findIndex((f) => f.due <= state.day);
|
||||
if (due >= 0) card = byId.get(state.followUps.splice(due, 1)[0].id) ?? null;
|
||||
card ??= drawCard(state, bal, cards);
|
||||
|
||||
if (!card) {
|
||||
state.pending.weeklyCard = false;
|
||||
return null;
|
||||
}
|
||||
state.pendingCard = { id: card.id, day: state.day };
|
||||
return card;
|
||||
}
|
||||
|
||||
export function answerCard(state, bal, cards, choice) {
|
||||
if (!state.pendingCard || (choice !== "yes" && choice !== "no")) return { ok: false };
|
||||
const card = indexCards(cards).get(state.pendingCard.id);
|
||||
if (!card) return { ok: false };
|
||||
|
||||
const option = card[choice];
|
||||
for (const effect of option.effects ?? []) applyEffect(state, bal, effect);
|
||||
for (const f of option.setFlags ?? []) state.flags[f] = state.day;
|
||||
for (const f of option.clearFlags ?? []) delete state.flags[f];
|
||||
if (option.followUp) state.followUps.push({ id: option.followUp.id, due: state.day + option.followUp.inDays });
|
||||
|
||||
state.cardHistory.push({ id: card.id, category: card.category, day: state.day, choice });
|
||||
state.pendingCard = null;
|
||||
state.pending.weeklyCard = false;
|
||||
return { ok: true, card };
|
||||
}
|
||||
Reference in New Issue
Block a user