Files
Battle-for-Tismo/client/js/modals/budget.js
T
adrien 91dfedf7ec Gave regions their own economies and removed the global market
- Each nation has a currency and each region a private-sector cash basket, borrowing from its central bank at the bank's rate; the government may always pay, going into central-bank debt when its budget runs red.
- Resources are bought from reachable stores only: the region itself first, then same-nation regions, then foreign ones, paid for out of private cash or, for government orders, the treasury. Drawn national stores are paid to their owning region.
- A grid in deficit imports power from a reachable surplus grid instead of a market fallback; downgrading a building no longer refunds anything.
- The monthly budget lists each resource's economic value, expands to a resource-to-source tree and keeps a cash figure beside it.
- Regions report what their people went short of over the day; starved research buildings and power-starved works raise a private daily notice.
- The city panel gains a Resources tab: reserves per resource, their market value and the private sector's daily consumption.
2026-09-22 06:41:58 +02:00

284 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The budget table shared by the nation and city panels. Every row is an item
// and its hourly amount, income in green and every cost in red, with the
// projected total on the last row. Building types are rows that expand to their
// per-city cost. `breakdown` mirrors the shape both the national and the
// regional economy use, so one renderer serves them both.
import { percent, signedMoney, money, SOURCE_LABELS } from "./format.js";
import { BUDGET_EXPENSE_CATEGORIES, resourceById } from "../../../shared/data.js";
import { groupDigits } from "../../../shared/text_format.js";
import { currencySymbol } from "../currency.js";
// Draws the table into `$list`. `expanded` is the set of open building types,
// mutated in place; `onToggle` is called after a caret flips so the caller can
// redraw. Regions pass `showUnits: false`: unit upkeep is funded nationally.
export function renderBudget($list, breakdown = {}, { expanded = new Set(), onToggle = null, showUnits = true } = {}) {
if (!$list || !$list.length) return;
const income = breakdown.income || { rate: 0, base: 0, modifier: 0, total: 0, sources: [], regions: [] };
const incomeSources = income.sources || [];
const building = breakdown.buildingUpkeep || { total: 0, groups: [], sources: [] };
const buildingGroups = building.groups || [];
const units = breakdown.unitUpkeep || { total: 0, groups: [], sources: [] };
const unitSources = units.sources || [];
const campaigns = breakdown.campaignUpkeep || { total: 0, sources: [] };
const campaignSources = campaigns.sources || [];
const transport = breakdown.transportUpkeep || { total: 0, sources: [] };
const transportSources = transport.sources || [];
const resourceMoney = breakdown.resourceMoney || [];
const net = breakdown.net || 0;
$list.empty();
const $body = $("<tbody></tbody>");
$body.append(sectionRow("Income"));
$body.append(
moneyRow(
"Tax income",
income.total,
"income",
`Collecting ${(income.rate * 100).toFixed(3)}% of GDP per hour — ${(
income.rate *
365 *
24 *
100
).toFixed(0)}% a year`
)
);
if (incomeSources.length === 0) {
$body.append(detailRow("No bonuses", 0, "income-detail"));
}
for (const source of incomeSources) {
const where = source.cityName ? ` in ${source.cityName}` : "";
$body.append(
detailRow(
`${source.label}${where} (${SOURCE_LABELS[source.kind] || source.kind}) ${percent(source.effect)}`,
source.amount,
"income-detail"
)
);
}
$body.append(sectionRow("Expenses"));
if (buildingGroups.length === 0) {
$body.append(detailRow("No buildings", 0, "expense-detail"));
}
for (const group of buildingGroups) {
const key = `building:${group.index}`;
const isExpanded = expanded.has(key);
$body.append(groupRow(group.label, -group.amount, key, isExpanded, expanded, onToggle));
if (isExpanded) {
for (const city of group.cities) {
$body.append(
detailRow(
`${group.label} in ${city.cityName} — level ${city.level}`,
-city.amount,
"expense-detail"
)
);
}
}
}
if (showUnits) {
if (unitSources.length === 0) {
$body.append(detailRow("No units", 0, "expense-detail"));
}
for (const source of unitSources) {
$body.append(
detailRow(`${source.label} ×${source.count}`, -source.amount, "expense-detail")
);
}
for (const source of campaignSources) {
$body.append(
detailRow(`${source.label} ×${source.count}`, -source.amount, "expense-detail")
);
}
for (const source of transportSources) {
$body.append(
detailRow(`${source.label} ×${source.count}`, -source.amount, "expense-detail")
);
}
}
// What the nation spent buying each resource, and earned selling it to other
// countries, over the last day, shown as an hourly average.
if (resourceMoney.length > 0) {
$body.append(sectionRow("Resource trade"));
for (const entry of resourceMoney) {
const proto = resourceById(entry.id);
const name = proto ? proto.name : entry.id;
if (entry.bought > 0) {
$body.append(detailRow(`Bought ${name}`, -entry.bought / 24, "expense-detail"));
}
if (entry.sold > 0) {
$body.append(detailRow(`Sold ${name}`, entry.sold / 24, "income-detail"));
}
}
}
const $table = $("<table class='budget-table'></table>");
$table.append($body);
$table.append($("<tfoot></tfoot>").append(totalRow("TOTAL", net)));
$list.append($table);
}
// Renders a whole month's actual budget: the tax and resource sales earned, the
// money each expense category spent, and the resource-trade memo. `report` is
// the compact shape the snapshot ships (category ids, not labels). The cash
// spent is the headline; when a category consumed materials worth more than the
// cash it paid (the stores covered the rest), the row notes the full market
// value. The treasury itself lives on the panel's subtitle, not in the table.
export function renderMonthlyBudget($list, report, { expanded = new Set(), onToggle = null } = {}) {
if (!$list || !$list.length) return;
$list.empty();
if (!report) return;
const $body = $("<tbody></tbody>");
$body.append(sectionRow("Income"));
$body.append(monthRow("Tax income", report.income, "income", null));
if (report.sales > 0) {
$body.append(monthRow("Resource sales", report.sales, "income-detail", null));
}
$body.append(sectionRow("Expenses"));
const byId = new Map((report.expenses || []).map((entry) => [entry[0], entry]));
const sections = [];
for (const category of BUDGET_EXPENSE_CATEGORIES) {
const entry = byId.get(category.id);
if (!entry) continue;
const last = sections[sections.length - 1];
if (!last || last.group !== category.group) {
sections.push({ group: category.group, rows: [] });
}
sections[sections.length - 1].rows.push({
id: category.id,
label: category.label,
cash: entry[1],
economic: entry[2],
buys: entry[3] || [],
});
}
for (const section of sections) {
$body.append(subsectionRow(section.group));
for (const row of section.rows) {
// The rightmost column is the month's true material cost. When the stores
// covered part of it only the smaller cash figure left the treasury, so
// that is noted under the name. Expanding names each resource bought and
// the buildings, units or works that consumed it.
const valued = row.economic > row.cash + 1;
const note = valued ? `cash ${money(-row.cash)}` : null;
const isOpen = expanded.has(row.id);
const expandable = row.buys.length > 0;
const $tr = monthRow(row.label, -row.economic, "expense-detail", note);
if (expandable) {
const $name = $tr.find(".budget-name");
$name.addClass("clickable");
$name.prepend($("<span class='budget-caret'></span>").text(isOpen ? "▾" : "▸"));
$name.on("click", () => {
if (expanded.has(row.id)) expanded.delete(row.id);
else expanded.add(row.id);
if (onToggle) onToggle(row.id);
});
}
$body.append($tr);
if (isOpen) {
for (const buy of row.buys) {
const [resource, amount, sources] = buy;
const proto = resourceById(resource);
const name = proto ? proto.name : resource;
$body.append(monthRow(name, -amount, "expense-detail budget-buy", null));
for (const [source, sourceAmount] of sources || []) {
$body.append(
monthRow(source, -sourceAmount, "expense-detail budget-source", null)
);
}
}
}
}
}
const trade = report.resourceTrade || [];
if (trade.length > 0) {
$body.append(sectionRow("Resource trade"));
for (const [id, bought, sold] of trade) {
const proto = resourceById(id);
const name = proto ? proto.name : id;
if (bought > 0) $body.append(monthRow(`Bought ${name}`, -bought, "expense-detail", null));
if (sold > 0) $body.append(monthRow(`Sold ${name}`, sold, "income-detail", null));
}
}
const totalNote = report.totalEconomic > report.totalCash + 1
? `materials valued at ${currencySymbol()}${groupDigits(Math.round(report.totalEconomic))}`
: null;
const $table = $("<table class='budget-table'></table>");
$table.append($body);
$table.append($("<tfoot></tfoot>").append(monthTotalRow("TOTAL", report.net, totalNote)));
$list.append($table);
}
function monthRow(label, value, cls, note) {
const $tr = $("<tr></tr>").addClass(cls);
const $name = $("<td class='budget-name'></td>").text(label);
if (note) $name.append($("<div class='budget-note'></div>").text(note));
$tr.append($name, $("<td class='budget-value'></td>").text(money(value)));
return $tr;
}
function monthTotalRow(label, value, note) {
const $tr = $("<tr class='budget-total'></tr>");
const $name = $("<td class='budget-name'></td>").text(label);
if (note) $name.append($("<div class='budget-note'></div>").text(note));
$tr.append($name, $("<td class='budget-value'></td>").text(money(value)));
return $tr;
}
function subsectionRow(label) {
return $("<tr class='budget-subsection'></tr>").append(
$("<td colspan='2'></td>").text(label)
);
}
function sectionRow(label) {
return $("<tr class='budget-section'></tr>").append(
$("<td colspan='2'></td>").text(label)
);
}
function moneyRow(label, value, cls, note) {
const $tr = $("<tr></tr>").addClass(cls);
const $name = $("<td class='budget-name'></td>").text(label);
if (note) $name.append($("<div class='budget-note'></div>").text(note));
$tr.append($name, $("<td class='budget-value'></td>").text(signedMoney(value)));
return $tr;
}
function detailRow(label, value, cls) {
const $tr = $("<tr></tr>").addClass(cls);
$tr.append($("<td class='budget-name'></td>").text(label));
$tr.append(
$("<td class='budget-value'></td>").text(value === 0 ? "—" : signedMoney(value))
);
return $tr;
}
function groupRow(label, value, key, isExpanded, expanded, onToggle) {
const $tr = $("<tr class='budget-group'></tr>");
const $name = $("<td class='budget-name clickable'></td>");
$name.append($("<span class='budget-caret'></span>").text(isExpanded ? "▾" : "▸"));
$name.append(document.createTextNode(` ${label}`));
$name.on("click", () => {
if (expanded.has(key)) expanded.delete(key);
else expanded.add(key);
if (onToggle) onToggle(key);
});
$tr.append($name, $("<td class='budget-value'></td>").text(signedMoney(value)));
return $tr;
}
function totalRow(label, value) {
const $tr = $("<tr class='budget-total'></tr>");
$tr.append($("<td class='budget-name'></td>").text(label));
$tr.append($("<td class='budget-value'></td>").text(signedMoney(value)));
return $tr;
}