// 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), "q:" (sector quality), "ratio:" (budget / need) // and "tax:" (rate). 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; if (key.startsWith("ratio:")) { const s = state.sectors[key.slice(6)]; return s ? s.alloc / s.need : NaN; } if (key.startsWith("tax:")) return state.economy.taxes[key.slice(4)] ?? 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 }; }