feat: Doing more polishing (M8b)
This commit is contained in:
+25
-16
@@ -51,12 +51,15 @@ 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 waiting = new Set(state.inbox.map((e) => e.id));
|
||||
// the last few categories seen, answered or still in the tray
|
||||
const recent = [...state.cardHistory, ...state.inbox].slice(-bal.cards.recentWindow).map((h) => h.category);
|
||||
|
||||
const pool = [];
|
||||
const weights = [];
|
||||
for (const card of cards) {
|
||||
if (card.followUpOnly) continue;
|
||||
if (card.followUpOnly || waiting.has(card.id)) continue;
|
||||
if (card.scenarios && !card.scenarios.includes(state.scenarioId)) continue;
|
||||
const last = lastAnswered.get(card.id);
|
||||
if (last !== undefined) {
|
||||
if (card.once !== false) continue;
|
||||
@@ -70,28 +73,34 @@ export function drawCard(state, bal, cards) {
|
||||
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.
|
||||
// Called when a new week starts (state.pending.weeklyCard): draws the week's proposal and puts it
|
||||
// at the bottom of the in-tray (state.inbox). A due follow-up always comes first. With no eligible
|
||||
// card the week is simply empty. Proposals pile up without stopping the clock; the game only waits
|
||||
// once the tray holds bal.cards.inboxMax of them (pending.inboxFull), until one is answered.
|
||||
export function ensureCard(state, bal, cards) {
|
||||
if (!state.pending.weeklyCard || state.pendingCard) return null;
|
||||
if (!state.pending.weeklyCard) return null;
|
||||
state.pending.weeklyCard = false;
|
||||
const byId = indexCards(cards);
|
||||
const waiting = new Set(state.inbox.map((e) => e.id));
|
||||
|
||||
let card = null;
|
||||
const due = state.followUps.findIndex((f) => f.due <= state.day);
|
||||
const due = state.followUps.findIndex((f) => f.due <= state.day && !waiting.has(f.id));
|
||||
if (due >= 0) card = byId.get(state.followUps.splice(due, 1)[0].id) ?? null;
|
||||
card ??= drawCard(state, bal, cards);
|
||||
if (!card) return null;
|
||||
|
||||
if (!card) {
|
||||
state.pending.weeklyCard = false;
|
||||
return null;
|
||||
}
|
||||
state.pendingCard = { id: card.id, day: state.day };
|
||||
state.inbox.push({ id: card.id, day: state.day, category: card.category });
|
||||
if (state.inbox.length >= bal.cards.inboxMax) state.pending.inboxFull = true;
|
||||
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);
|
||||
// Answers the proposal `id` (default: the one on top of the tray, the oldest).
|
||||
export function answerCard(state, bal, cards, choice, id) {
|
||||
if (choice !== "yes" && choice !== "no") return { ok: false };
|
||||
const at = id === undefined ? 0 : state.inbox.findIndex((e) => e.id === id);
|
||||
const entry = state.inbox[at];
|
||||
if (!entry) return { ok: false };
|
||||
const card = indexCards(cards).get(entry.id);
|
||||
if (!card) return { ok: false };
|
||||
|
||||
const option = card[choice];
|
||||
@@ -101,7 +110,7 @@ export function answerCard(state, bal, cards, choice) {
|
||||
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;
|
||||
state.inbox.splice(at, 1);
|
||||
state.pending.inboxFull = state.inbox.length >= bal.cards.inboxMax;
|
||||
return { ok: true, card };
|
||||
}
|
||||
|
||||
@@ -47,11 +47,12 @@ export function createGame({ scenario, balance, seed = Date.now(), countryName,
|
||||
world: { cycle: 0 }, // global business cycle, see world.js
|
||||
flags: {},
|
||||
cardHistory: [],
|
||||
pendingCard: null,
|
||||
inbox: [], // proposals waiting in the tray, oldest first: { id, day, category }
|
||||
followUps: [],
|
||||
// Day 0 opens with the pronoun question, the initial budget review and a first card.
|
||||
// Any true flag here pauses time (see advanceDay).
|
||||
pending: { profile: !PRONOUNS.includes(pronouns), weeklyCard: true, review: true, alert: false },
|
||||
// Any true flag here pauses time (see advanceDay). weeklyCard is momentary: the caller answers it
|
||||
// at once with ensureCard(); inboxFull stays until the tray is below its limit.
|
||||
pending: { profile: !PRONOUNS.includes(pronouns), weeklyCard: true, review: true, alert: false, inboxFull: false },
|
||||
over: null,
|
||||
// days in each danger zone; emergency = an unscheduled budget session is open (see advanceDay)
|
||||
risk: { revolution: 0, coup: 0, impeachment: 0, defaultZone: false, emergency: false, lastEmergency: -999 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Shared constants and tiny helpers. No game logic here.
|
||||
|
||||
export const STATE_VERSION = 2;
|
||||
export const STATE_VERSION = 3;
|
||||
|
||||
export const IND_IDS = [
|
||||
"order", "stability", "civilRights", "opinion", "economy",
|
||||
|
||||
@@ -5,11 +5,11 @@ export function createClock() {
|
||||
}
|
||||
|
||||
// Feed it the real elapsed ms; it returns how many in-game days are due.
|
||||
// dt is capped so a background tab doesn't fast-forward the game on return, and
|
||||
// floored at 0: the first animation frame can carry a timestamp slightly before
|
||||
// the one the caller recorded, and a negative step would pre-load a whole day.
|
||||
export function consumeClock(clock, dtMs, msPerDay, speed) {
|
||||
clock.acc += Math.max(0, Math.min(dtMs, 1000)) * speed;
|
||||
// dt is capped (capMs) so a frozen tab doesn't fast-forward the game without limit when it wakes
|
||||
// up, and floored at 0: the first animation frame can carry a timestamp slightly before the one the
|
||||
// caller recorded, and a negative step would pre-load a whole day.
|
||||
export function consumeClock(clock, dtMs, msPerDay, speed, capMs = 1000) {
|
||||
clock.acc += Math.max(0, Math.min(dtMs, capMs)) * speed;
|
||||
const days = Math.floor(clock.acc / msPerDay);
|
||||
clock.acc -= days * msPerDay;
|
||||
return days;
|
||||
|
||||
@@ -9,7 +9,7 @@ const DEV_KEYS = ["growth", "unemployment", "inflation", "debt", "deficit", "tax
|
||||
const HISTORY_SERIES = ["debtRatio", "deficit", ...IND_IDS, ...VAR_IDS];
|
||||
const OVER_TYPES = ["term", "revolution", "coup", "default", "impeachment"];
|
||||
const ALERT_KINDS = ["revolution", "coup", "impeachment", "default"];
|
||||
const PENDING_KINDS = ["profile", "weeklyCard", "review", "alert"];
|
||||
const PENDING_KINDS = ["profile", "weeklyCard", "review", "alert", "inboxFull"];
|
||||
|
||||
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
const isNum = (v, lo = -Infinity, hi = Infinity) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi;
|
||||
@@ -73,7 +73,11 @@ export function restoreState(s, { scenarios, balance, cards }) {
|
||||
if (!isObj(h) || typeof h.id !== "string" || typeof h.category !== "string" || !isInt(h.day, 0, balance.time.termDays) || (h.choice !== "yes" && h.choice !== "no")) return fail();
|
||||
}
|
||||
if (!Array.isArray(s.followUps) || !s.followUps.every((f) => isObj(f) && typeof f.id === "string" && isNum(f.due))) return fail();
|
||||
if (s.pendingCard !== null && !(isObj(s.pendingCard) && typeof s.pendingCard.id === "string" && isInt(s.pendingCard.day, 0, balance.time.termDays))) return fail();
|
||||
if (!Array.isArray(s.inbox) || s.inbox.length > balance.cards.inboxMax) return fail();
|
||||
for (const e of s.inbox) {
|
||||
if (!isObj(e) || typeof e.id !== "string" || !isInt(e.day, 0, balance.time.termDays)) return fail();
|
||||
if (e.category !== undefined && typeof e.category !== "string") return fail();
|
||||
}
|
||||
|
||||
// Pauses, alerts and endings
|
||||
if (!isObj(s.pending)) return fail();
|
||||
@@ -95,7 +99,8 @@ export function restoreState(s, { scenarios, balance, cards }) {
|
||||
|
||||
// Repairs: content that changed since the save was made
|
||||
s.followUps = s.followUps.filter((f) => cardIds.has(f.id));
|
||||
if (s.pendingCard && !cardIds.has(s.pendingCard.id)) s.pendingCard = null;
|
||||
s.inbox = s.inbox.filter((e) => cardIds.has(e.id));
|
||||
s.pending.inboxFull = s.inbox.length >= balance.cards.inboxMax;
|
||||
s.pending.alert = s.alerts.length > 0;
|
||||
return { ok: true, state: s };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user