Files
Battle-for-Tismo/shared/game_state/budget.js
T

201 lines
8.0 KiB
JavaScript

// The monthly budget ledger. Money moves in and out of a nation's treasury all
// day; this keeps a running total for the calendar month, filed under the
// expense categories the budget screen shows, so the panel can report what the
// current month has cost so far and what each past month cost.
//
// Two figures are kept per category. `cash` is the money that actually left the
// treasury; `economic` is the market value of the materials the category
// consumed, whether they were drawn from the nation's stores or bought -- the
// upkeep rows consume stock for free, so their economic cost is the truer
// measure of what they took. Income and resource sales are the positive side.
//
// On every calendar-month rollover the live ledger is frozen into the history
// and a fresh one begins, so the panel can page back through the last two years.
import { BUDGET_EXPENSE_CATEGORIES } from "../data/economy.js";
import { MONTH_NAMES } from "../game_clock.js";
// How many completed months the history keeps. Two years is enough to compare a
// season with the one before it without growing the snapshot without bound.
export const BUDGET_HISTORY_MONTHS = 24;
function emptyBudgetLedger() {
// Tax income is booked under the kind of trade tax that raised it; `income` is
// kept as the running total the report reads.
const cash = { income: 0, sell: 0, taxSales: 0, taxExport: 0, taxImport: 0 };
const economic = {};
for (const category of BUDGET_EXPENSE_CATEGORIES) {
cash[category.id] = 0;
economic[category.id] = 0;
}
// Money spent per category, broken down by the resource it bought, so an
// expense row can expand to show what it consumed and what it paid.
const resourceByCategory = {};
for (const category of BUDGET_EXPENSE_CATEGORIES) resourceByCategory[category.id] = {};
return { cash, economic, resourceBuy: {}, resourceSell: {}, resourceByCategory };
}
export const budgetMethods = {
_initBudgetHistory() {
this.budgetLedger = new Map();
for (let civ = 0; civ < this.civilisations.length; civ++) {
this.budgetLedger.set(civ, emptyBudgetLedger());
}
// Frozen months, oldest first: [{ monthIndex, records: [ledger, ...] }].
this.budgetHistory = [];
},
_budgetRecord(civ) {
if (!this.budgetLedger) this._initBudgetHistory();
let record = this.budgetLedger.get(civ);
if (!record) {
record = emptyBudgetLedger();
this.budgetLedger.set(civ, record);
}
return record;
},
// A signed cash movement for the month: positive is money into the treasury
// (tax, a resource sale, a refund), negative is money spent. Recorded once per
// category so the report can total them.
_recordBudgetCash(civ, category, amount) {
if (civ < 0 || !(amount !== 0)) return;
const record = this._budgetRecord(civ);
record.cash[category] = (record.cash[category] || 0) + amount;
},
// The market value of the materials one category consumed, whatever their
// source, so an upkeep row can show what it cost even when the stores paid.
_recordBudgetEconomic(civ, category, amount) {
if (civ < 0 || !(amount > 0)) return;
const record = this._budgetRecord(civ);
record.economic[category] = (record.economic[category] || 0) + amount;
},
// The money one expense category spent on one resource, so the row can be
// expanded to show what it bought. `source` names what consumed it -- a
// building, unit, work or transport type -- and is optional.
_recordBudgetCategoryResource(civ, category, resource, amount, source = null) {
if (civ < 0 || !(amount > 0)) return;
const record = this._budgetRecord(civ);
const byResource = record.resourceByCategory[category]
|| (record.resourceByCategory[category] = {});
let entry = byResource[resource];
if (!entry || typeof entry === "number") {
entry = { total: typeof entry === "number" ? entry : 0, sources: {} };
byResource[resource] = entry;
}
entry.total += amount;
if (source) entry.sources[source] = (entry.sources[source] || 0) + amount;
},
// The money a nation spent buying and earned selling one resource to the
// world market or another country, for the month's resource-trade memo.
_recordBudgetResource(civ, resource, bought, sold) {
if (civ < 0) return;
const record = this._budgetRecord(civ);
if (bought > 0) record.resourceBuy[resource] = (record.resourceBuy[resource] || 0) + bought;
if (sold > 0) record.resourceSell[resource] = (record.resourceSell[resource] || 0) + sold;
},
// Freezes the month that just ended and starts a new ledger. Called from the
// monthly rollover with the month index that is being closed.
_rolloverBudget(previousIndex) {
if (!this.budgetLedger) return;
const records = [];
for (let civ = 0; civ < this.civilisations.length; civ++) {
records.push(this.budgetLedger.get(civ) || emptyBudgetLedger());
}
this.budgetHistory.push({ monthIndex: previousIndex, records });
while (this.budgetHistory.length > BUDGET_HISTORY_MONTHS) this.budgetHistory.shift();
this.budgetLedger = new Map();
for (let civ = 0; civ < this.civilisations.length; civ++) {
this.budgetLedger.set(civ, emptyBudgetLedger());
}
},
// Every month the panel may show, oldest first, with the live month last.
budgetMonthReports(civ) {
const reports = [];
for (const entry of this.budgetHistory || []) {
reports.push(this._budgetReport(civ, entry.monthIndex, entry.records[civ], false));
}
const currentIndex = this._monthIndexOf(this.totalHours);
reports.push(this._budgetReport(
civ,
currentIndex,
this.budgetLedger ? this.budgetLedger.get(civ) : null,
true
));
return reports;
},
_budgetReport(civ, monthIndex, record, isCurrent) {
const cash = (record && record.cash) || {};
const economic = (record && record.economic) || {};
const expenses = [];
let totalCash = 0;
let totalEconomic = 0;
const resourceByCategory = (record && record.resourceByCategory) || {};
for (const category of BUDGET_EXPENSE_CATEGORIES) {
const spent = Math.max(0, -(cash[category.id] || 0));
const cost = Math.max(0, economic[category.id] || 0, spent);
if (spent === 0 && cost === 0) continue;
// Each buy is [resource, money, [[source, money], ...]]: what the
// category bought and which buildings, units or works consumed it.
const buys = Object.entries(resourceByCategory[category.id] || {})
.map(([resource, detail]) => {
if (typeof detail === "number") return [resource, detail, []];
const sources = Object.entries(detail.sources || {})
.filter(([, amount]) => amount > 0)
.sort((a, b) => b[1] - a[1]);
return [resource, detail.total || 0, sources];
})
.filter(([, amount]) => amount > 0)
.sort((a, b) => b[1] - a[1]);
expenses.push({
id: category.id,
label: category.label,
group: category.group,
cash: spent,
economic: cost,
buys,
});
totalCash += spent;
totalEconomic += cost;
}
const resourceBuy = (record && record.resourceBuy) || {};
const resourceSell = (record && record.resourceSell) || {};
const resourceIds = new Set([...Object.keys(resourceBuy), ...Object.keys(resourceSell)]);
const resourceTrade = Array.from(resourceIds)
.map((id) => ({
id,
bought: Math.max(0, resourceBuy[id] || 0),
sold: Math.max(0, resourceSell[id] || 0),
}))
.filter((entry) => entry.bought > 0 || entry.sold > 0);
const taxes = {
sales: cash.taxSales || 0,
export: cash.taxExport || 0,
import: cash.taxImport || 0,
};
const income = taxes.sales + taxes.export + taxes.import;
const sales = cash.sell || 0;
const year = Math.floor(monthIndex / 12);
const month = monthIndex % 12;
return {
monthIndex,
isCurrent,
label: `${MONTH_NAMES[month]} ${year}`,
income,
taxes,
sales,
expenses,
resourceTrade,
totalCash,
totalEconomic,
net: income + sales - totalCash,
};
},
};