201 lines
8.0 KiB
JavaScript
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,
|
|
};
|
|
},
|
|
};
|