// Quarterly budget ledger: a two-page accounting book. The left page sets sector // budgets, the right page sets tax rates and shows the Treasury summary and a // four-quarter projection. Nothing is applied until the player signs. import { t, fmt, getLang } from "../i18n/i18n.js"; import { previewBudget, setBudget, confirmBudget, qualityFromRatio, taxRevenue, TAX_IDS, } from "../engine/index.js"; import { $ } from "./desk.js"; const ALLOC_STEP = 0.05; const TAX_STEP = 0.5; // Projection rows: label key, field on the summary, digits, suffix const PROJ_ROWS = [ ["econ.debt", (p) => p.debtRatio, 1, ""], ["econ.deficit", (p) => p.deficit, 1, ""], ["econ.growth", (p) => p.growth, 1, ""], ["econ.unemployment", (p) => p.unemployment, 1, ""], ["ind.opinion", (p) => p.indicators.opinion, 0, ""], ["ind.stability", (p) => p.indicators.stability, 0, ""], ["ind.economy", (p) => p.indicators.economy, 0, ""], ]; export function createLedger(root, scenario, { onConfirm }) { let openKey = null; let ctx = null; // { state, bal } let draft = null; let base = null; let queued = false; const sectorIds = (state) => Object.keys(state.sectors); function open(state, bal) { ctx = { state, bal }; base = { alloc: Object.fromEntries(Object.entries(state.sectors).map(([id, s]) => [id, s.alloc])), taxes: { ...state.economy.taxes }, }; draft = structuredClone(base); const allocMax = Math.min(bal.limits.allocMaxRatio, bal.limits.sliderMaxRatio); const sectorRows = sectorIds(state).map((id) => { const s = state.sectors[id]; const max = Math.max(s.need * allocMax, s.alloc); return `
`; }).join(""); const taxRows = TAX_IDS.map((id) => { const max = bal.tax[id].peak * bal.limits.taxMaxFactorOfPeak; return `
`; }).join(""); root.innerHTML = `

${t("ledger.title")}

${t("ledger.spending")}. ${t("ledger.spendingHint")}

${sectorRows}
${t("ledger.totalAlloc")}

${t("ledger.revenue")}

${t("ledger.revenueHint")}

${taxRows}

${t("ledger.summary")}

${t("ledger.projection")}

`; root.hidden = false; refresh(); } function close() { root.hidden = true; root.innerHTML = ""; openKey = null; ctx = null; } // Cheap parts (values next to sliders) run immediately; the preview is coalesced per frame. function refresh() { const { state, bal } = ctx; let total = 0; for (const id of sectorIds(state)) { const s = state.sectors[id]; const v = draft.alloc[id]; total += v; const row = $(root, `[data-sector="${id}"]`); $(row, ".lrow__val").textContent = fmt(v, 2); const target = qualityFromRatio(v / s.need, bal); const diff = target - s.quality; const q = $(row, ".lrow__q"); q.textContent = diff > 1 ? "▲" : diff < -1 ? "▼" : "="; q.className = `lrow__q ${diff > 1 ? "up" : diff < -1 ? "down" : "flat"}`; q.title = t("ledger.quality", { now: Math.round(s.quality), target: Math.round(target) }); row.classList.toggle("is-changed", Math.abs(v - base.alloc[id]) > 1e-9); } $(root, "[data-total]").textContent = `${fmt(total, 2)} %`; for (const id of TAX_IDS) { const row = $(root, `[data-tax="${id}"]`); $(row, ".lrow__val").textContent = `${fmt(draft.taxes[id], 1)} %`; $(row, ".lrow__yield").textContent = t("ledger.yield", { n: fmt(taxRevenue(id, draft.taxes[id], bal), 1) }); row.classList.toggle("is-changed", Math.abs(draft.taxes[id] - base.taxes[id]) > 1e-9); } if (!queued) { queued = true; requestAnimationFrame(() => { queued = false; if (ctx) renderPreview(); }); } } function renderPreview() { const { state, bal } = ctx; const pv = previewBudget(state, bal, draft); if (!pv.ok) return; const f = pv.fiscal; const gdp = state.economy.gdp; const bn = (pct) => fmt(Math.abs((pct / 100) * gdp), 1); const balance = -f.deficit; // positive = surplus $(root, "[data-summary]").innerHTML = `
${t("ledger.sumRevenue")}
${fmt(f.revenue, 1)} %
${t("ledger.sumSectors")}
${fmt(f.allocTotal, 1)} %
${t("ledger.sumInterest")}
${fmt(f.interest, 1)} %
${t("ledger.sumSpending")}
${fmt(f.spending, 1)} %
${t(balance >= 0 ? "ledger.sumSurplus" : "ledger.sumDeficit")}
${fmt(Math.abs(balance), 1)} % ${t("ledger.perYear", { n: bn(balance), currency: scenario.currency })}
`; const months = (i) => (i === 0 ? t("ledger.now") : t("ledger.months", { n: i * 3 })); $(root, ".proj thead").innerHTML = `${pv.points.map((_, i) => `${months(i)}`).join("")}`; $(root, ".proj tbody").innerHTML = PROJ_ROWS.map(([label, get, digits, suffix]) => `${t(label)}${pv.points.map((p) => `${fmt(get(p), digits)}${suffix}`).join("")}`).join(""); } function syncSliders() { for (const id of sectorIds(ctx.state)) $(root, `#alloc-${id}`).value = draft.alloc[id]; for (const id of TAX_IDS) $(root, `#tax-${id}`).value = draft.taxes[id]; } root.addEventListener("input", (e) => { const input = e.target; if (!ctx || input.type !== "range") return; const v = Number(input.value); if (input.id.startsWith("alloc-")) draft.alloc[input.id.slice(6)] = v; else if (input.id.startsWith("tax-")) draft.taxes[input.id.slice(4)] = v; refresh(); }); root.addEventListener("click", (e) => { const b = e.target.closest("button[data-act]"); if (!b || !ctx) return; if (b.dataset.act === "reset") { draft = structuredClone(base); syncSliders(); refresh(); } else { const { state, bal } = ctx; const res = setBudget(state, bal, draft); if (!res.ok) { $(root, "[data-error]").hidden = false; return; } confirmBudget(state); close(); onConfirm(); } }); function update(state, bal) { if (!(state.pending.review && !state.pending.profile && !state.over)) { if (openKey !== null) close(); return; } const key = `${state.day}:${getLang()}`; if (key !== openKey) { openKey = key; open(state, bal); } } return { update }; }