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.
This commit is contained in:
@@ -1349,6 +1349,9 @@ input:focus, select:focus { border-color: var(--accent); }
|
||||
.budget-table tr.budget-group .budget-name.clickable:hover { color: var(--accent); }
|
||||
.budget-caret { display: inline-block; width: 12px; color: var(--muted); }
|
||||
.budget-note { margin-top: 2px; color: var(--muted); font-size: 11px; }
|
||||
.budget-table tr.budget-buy .budget-name { padding-left: 26px; }
|
||||
.budget-table tr.budget-source .budget-name { padding-left: 44px; color: var(--muted); }
|
||||
.budget-table tr.budget-source .budget-value { color: var(--muted); }
|
||||
.budget-table tfoot .budget-total td {
|
||||
padding-top: 10px;
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.25);
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
<button class="tab active" data-tab="train">Train</button>
|
||||
<button class="tab" data-tab="buildings">Buildings</button>
|
||||
<button class="tab" data-tab="people">People</button>
|
||||
<button class="tab" data-tab="resources">Resources</button>
|
||||
<button class="tab" data-tab="budget">Budget</button>
|
||||
<button class="tab" data-tab="economy">Economy</button>
|
||||
</div>
|
||||
@@ -244,6 +245,10 @@
|
||||
<div id="city-people-sub" class="modal-sub"></div>
|
||||
<div id="city-people-list" class="modal-body"></div>
|
||||
</div>
|
||||
<div class="tab-page" id="city-tab-resources">
|
||||
<div id="city-resources-sub" class="modal-sub"></div>
|
||||
<div id="city-resources-list" class="modal-body"></div>
|
||||
</div>
|
||||
<div class="tab-page" id="city-tab-budget">
|
||||
<div id="city-budget-sub" class="modal-sub"></div>
|
||||
<div id="city-budget-list" class="modal-body"></div>
|
||||
@@ -351,6 +356,8 @@
|
||||
<div id="economy-sub" class="modal-sub"></div>
|
||||
<div id="economy-subtabs" class="subtabs"></div>
|
||||
<div id="economy-summary" class="modal-body"></div>
|
||||
<div id="economy-consumption" class="modal-body hidden"></div>
|
||||
<div id="economy-currency" class="modal-body hidden"></div>
|
||||
<div id="economy-central-bank" class="modal-body hidden"></div>
|
||||
<div id="economy-rates" class="modal-body hidden"></div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,7 @@ export class GameScreen {
|
||||
this._conflicts = [];
|
||||
this._news = [];
|
||||
this._seenNews = null;
|
||||
this._seenAlerts = null;
|
||||
this._entered = false;
|
||||
// Detects buildings and units the local player finishes between snapshots;
|
||||
// technologies are picked up from the news feed instead.
|
||||
@@ -141,6 +142,7 @@ export class GameScreen {
|
||||
this.$screen.removeClass("active");
|
||||
this.feed.clear();
|
||||
this._seenNews = null;
|
||||
this._seenAlerts = null;
|
||||
this.completions.reset();
|
||||
this.bombing.reset();
|
||||
this.airBombing.reset();
|
||||
@@ -229,6 +231,7 @@ export class GameScreen {
|
||||
this._conflicts = state.conflicts || [];
|
||||
this._news = state.news || [];
|
||||
this._updateFeed(this._news);
|
||||
this._updateResourceAlerts(state.viewerStats);
|
||||
|
||||
this.map.setCatalogue(this.civilisations, this.protoUnits);
|
||||
// Mirror the viewer's repeatable-research bonuses used by prediction.
|
||||
|
||||
@@ -1637,6 +1637,10 @@ export const panelMethods = {
|
||||
debt: cityStats.debt || 0,
|
||||
currencies: stats.currencies || [],
|
||||
approval: cityStats.approval,
|
||||
// What the region's people went short of over the last day.
|
||||
shortage: cityStats.shortage || {},
|
||||
// The region's reserves, their value and its daily consumption.
|
||||
resources: cityStats.resources || null,
|
||||
// The region's tax base and approval price an upgrade's expected income.
|
||||
base: cityStats.base || 0,
|
||||
// Construction technology shortens the displayed build times.
|
||||
@@ -1671,8 +1675,10 @@ export const panelMethods = {
|
||||
cityStats.ethnicMakeup || [],
|
||||
cityStats.approval,
|
||||
this._taxMultiplier(cityStats.approval),
|
||||
this.civilisations
|
||||
this.civilisations,
|
||||
cityStats.shortage || {}
|
||||
);
|
||||
this.cityModal.renderResources(cityStats.resources || null);
|
||||
}
|
||||
this.cityModal.refreshBuildings(
|
||||
stats.budget || 0,
|
||||
@@ -1773,6 +1779,8 @@ export const panelMethods = {
|
||||
exchangeRates: stats.exchangeRates || [],
|
||||
centralBank: stats.centralBank || { rate: 0, reserves: [] },
|
||||
privateCash: stats.privateCash || 0,
|
||||
// Where the viewer's own currency sits: treasury, regions, foreign banks.
|
||||
currencyHoldings: stats.currencyHoldings || null,
|
||||
cities: (this.snapshot && this.snapshot.cityStats) || [],
|
||||
};
|
||||
},
|
||||
@@ -1895,6 +1903,30 @@ export const panelMethods = {
|
||||
this.feed.push(describeNews(entry, this.civilisations));
|
||||
},
|
||||
|
||||
// Pops the viewer's own daily resource-shortage notices into the corner feed.
|
||||
// Each building, region and resource is announced once per game day.
|
||||
_updateResourceAlerts(stats) {
|
||||
const alerts = (stats && stats.resourceAlerts) || [];
|
||||
if (this._seenAlerts === null) {
|
||||
this._seenAlerts = new Set(alerts.map((alert) => this._alertSignature(alert)));
|
||||
return;
|
||||
}
|
||||
for (const alert of alerts) {
|
||||
const signature = this._alertSignature(alert);
|
||||
if (this._seenAlerts.has(signature)) continue;
|
||||
this._seenAlerts.add(signature);
|
||||
const proto = resourceById(alert.resource);
|
||||
const name = proto ? proto.name : alert.resource;
|
||||
this.feed.push(
|
||||
`${alert.building} in ${alert.region} is missing ${name}`, { alert: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_alertSignature(alert) {
|
||||
return `${alert.day || 0}|${alert.building}|${alert.region}|${alert.resource}`;
|
||||
},
|
||||
|
||||
_finances(stats) {
|
||||
if (!stats) return null;
|
||||
// The month reports page back through the stored budget digests; the region
|
||||
|
||||
+23
-10
@@ -160,12 +160,15 @@ export function renderMonthlyBudget($list, report, { expanded = new Set(), onTog
|
||||
for (const section of sections) {
|
||||
$body.append(subsectionRow(section.group));
|
||||
for (const row of section.rows) {
|
||||
const note = row.economic > row.cash + 1
|
||||
? `materials valued at ${currencySymbol()}${groupDigits(Math.round(row.economic))}`
|
||||
: null;
|
||||
// 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.cash, "expense-detail", note);
|
||||
const $tr = monthRow(row.label, -row.economic, "expense-detail", note);
|
||||
if (expandable) {
|
||||
const $name = $tr.find(".budget-name");
|
||||
$name.addClass("clickable");
|
||||
@@ -178,10 +181,16 @@ export function renderMonthlyBudget($list, report, { expanded = new Set(), onTog
|
||||
}
|
||||
$body.append($tr);
|
||||
if (isOpen) {
|
||||
for (const [resource, amount] of row.buys) {
|
||||
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", null));
|
||||
$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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,9 +207,12 @@ export function renderMonthlyBudget($list, report, { expanded = new Set(), onTog
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
$table.append($("<tfoot></tfoot>").append(monthTotalRow("TOTAL", report.net, totalNote)));
|
||||
$list.append($table);
|
||||
}
|
||||
|
||||
@@ -212,10 +224,11 @@ function monthRow(label, value, cls, note) {
|
||||
return $tr;
|
||||
}
|
||||
|
||||
function monthTotalRow(label, value) {
|
||||
function monthTotalRow(label, value, note) {
|
||||
const $tr = $("<tr class='budget-total'></tr>");
|
||||
$tr.append($("<td class='budget-name'></td>").text(label));
|
||||
$tr.append($("<td class='budget-value'></td>").text(money(value)));
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+119
-8
@@ -3,11 +3,10 @@
|
||||
// city's production queue, so the train tab shows both kinds of order.
|
||||
|
||||
import { groupDigits, hours } from "../../../shared/text_format.js";
|
||||
import { ECONOMY, BUILDINGS, EFFECT_BUDGET_INCOME, effectValue, resourceById } from "../../../shared/data.js";
|
||||
import { ECONOMY, BUILDINGS, EFFECT_BUDGET_INCOME, effectValue, resourceById, RESOURCE_IDS } from "../../../shared/data.js";
|
||||
import { constructionResourceCost, formatResourceAmount } from "../../../shared/resources.js";
|
||||
import {
|
||||
buildingBuildCost,
|
||||
buildingRefund,
|
||||
buildingUpkeep,
|
||||
} from "../../../shared/rules.js";
|
||||
import { buildingEffectText, signedMoney, quotedBuildCost } from "./format.js";
|
||||
@@ -36,6 +35,9 @@ export class CityModal {
|
||||
this.$budgetSub = $("#city-budget-sub");
|
||||
this.$budgetList = $("#city-budget-list");
|
||||
|
||||
this.$resourcesSub = $("#city-resources-sub");
|
||||
this.$resourcesList = $("#city-resources-list");
|
||||
|
||||
this.$economySub = $("#city-economy-sub");
|
||||
this.$economyList = $("#city-economy-list");
|
||||
|
||||
@@ -97,14 +99,22 @@ export class CityModal {
|
||||
});
|
||||
this.$budgetSub.text(`Region net +${currencySymbol()}0/h`);
|
||||
this.renderBudget(null);
|
||||
this.renderPeople(view.ethnicMakeup || [], view.approval, view.taxMultiplier, view.civs || []);
|
||||
this.renderPeople(
|
||||
view.ethnicMakeup || [],
|
||||
view.approval,
|
||||
view.taxMultiplier,
|
||||
view.civs || [],
|
||||
view.shortage || {}
|
||||
);
|
||||
this.renderResources(view.resources);
|
||||
this.renderEconomy(view.cash || {}, view.cashValue || 0, view.currencies || [], view.debt || 0);
|
||||
this.$modal.removeClass("hidden");
|
||||
}
|
||||
|
||||
// The region's people: their ethnic makeup and their government approval,
|
||||
// which decides how much tax they pay and how fast they build.
|
||||
renderPeople(makeup, approval, taxMultiplier, civs) {
|
||||
// which decides how much tax they pay and how fast they build. `shortage`
|
||||
// reports the resources they went short of over the last day and by how much.
|
||||
renderPeople(makeup, approval, taxMultiplier, civs, shortage = {}) {
|
||||
if (!this.$peopleList || !this.$peopleList.length) return;
|
||||
this.$peopleList.empty();
|
||||
const value = approval === undefined || approval === null ? 0.5 : approval;
|
||||
@@ -116,6 +126,7 @@ export class CityModal {
|
||||
} else {
|
||||
this.$peopleList.append($("<div class='desc'></div>").text("No population data."));
|
||||
}
|
||||
this._renderShortages(shortage || {});
|
||||
if (taxMultiplier !== undefined && taxMultiplier < 0.999) {
|
||||
this.$peopleList.append(
|
||||
$("<div class='desc politics-penalty'></div>").text(
|
||||
@@ -126,6 +137,107 @@ export class CityModal {
|
||||
}
|
||||
}
|
||||
|
||||
// What the region's people went without over the last day: each resource they
|
||||
// could not buy enough of, the amount it left them short, and the share of
|
||||
// the day's need that was.
|
||||
_renderShortages(shortage) {
|
||||
const entries = Object.entries(shortage);
|
||||
this.$peopleList.append($("<div class='subhead'></div>").text("Resource shortages"));
|
||||
if (entries.length === 0) {
|
||||
this.$peopleList.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"No shortages: the region met all of its people's and industry's needs."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const $table = $("<table class='resources-table economy-table'></table>");
|
||||
const $head = $("<thead></thead>").append(
|
||||
$("<tr></tr>")
|
||||
.append($("<th></th>").text("Resource"))
|
||||
.append($("<th class='numeric'></th>").text("Short by"))
|
||||
.append($("<th class='numeric'></th>").text("Share of need"))
|
||||
);
|
||||
const $body = $("<tbody></tbody>");
|
||||
for (const [id, entry] of entries) {
|
||||
const proto = resourceById(id);
|
||||
const name = proto ? proto.name : id;
|
||||
const amount = typeof entry === "number" ? entry : entry.amount;
|
||||
const fraction = typeof entry === "number" ? 0 : entry.fraction;
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text(name))
|
||||
.append(
|
||||
$("<td class='numeric'></td>").text(formatResourceAmount(id, amount || 0))
|
||||
)
|
||||
.append(
|
||||
$("<td class='numeric'></td>").text(`${Math.round((fraction || 0) * 100)}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
$table.append($head, $body);
|
||||
this.$peopleList.append($table);
|
||||
this.$peopleList.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"Shortages cut approval and slow the region's economy."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// The region's material reserves: how much of each resource it holds, what
|
||||
// that reserve is worth at today's market, and how much its private sector
|
||||
// draws down each day. Energy is a flow, so it has no reserve of its own.
|
||||
renderResources(resources) {
|
||||
if (!this.$resourcesList || !this.$resourcesList.length) return;
|
||||
this.$resourcesList.empty();
|
||||
const info = resources || {};
|
||||
const stock = info.stock || {};
|
||||
const consumption = info.consumption || {};
|
||||
const value = info.value || 0;
|
||||
if (this.$resourcesSub && this.$resourcesSub.length) {
|
||||
this.$resourcesSub.text(
|
||||
`Reserves worth ${currencySymbol()}${groupDigits(Math.round(value))}`
|
||||
);
|
||||
}
|
||||
this.$resourcesList.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"The region's private sector holds these reserves and draws them down as " +
|
||||
"its people and buildings consume them. Energy is a flow -- it has no reserve."
|
||||
)
|
||||
);
|
||||
const $table = $("<table class='resources-table economy-table'></table>");
|
||||
const $head = $("<thead></thead>").append(
|
||||
$("<tr></tr>")
|
||||
.append($("<th></th>").text("Resource"))
|
||||
.append($("<th class='numeric'></th>").text("Reserve"))
|
||||
.append($("<th class='numeric'></th>").text("Consumption/day"))
|
||||
.append($("<th class='numeric'></th>").text("Value"))
|
||||
);
|
||||
const $body = $("<tbody></tbody>");
|
||||
for (const id of RESOURCE_IDS) {
|
||||
const proto = resourceById(id);
|
||||
const name = proto ? proto.name : id;
|
||||
const storable = proto ? proto.storable : false;
|
||||
const held = stock[id] || 0;
|
||||
const price = this.prices[id] || 0;
|
||||
const row = $("<tr></tr>").append($("<td></td>").text(name));
|
||||
row.append(
|
||||
$("<td class='numeric'></td>").text(storable ? formatResourceAmount(id, held) : "—")
|
||||
);
|
||||
row.append(
|
||||
$("<td class='numeric'></td>").text(formatResourceAmount(id, consumption[id] || 0))
|
||||
);
|
||||
row.append(
|
||||
$("<td class='numeric'></td>").text(
|
||||
storable ? `${currencySymbol()}${groupDigits(Math.round(held * price))}` : "—"
|
||||
)
|
||||
);
|
||||
$body.append(row);
|
||||
}
|
||||
$table.append($head, $body);
|
||||
this.$resourcesList.append($table);
|
||||
}
|
||||
|
||||
// The region's private-sector cash: a basket of currencies it holds. Regions
|
||||
// are the game's private economic agents, so this is their own money, not the
|
||||
// government treasury.
|
||||
@@ -419,9 +531,8 @@ export class CityModal {
|
||||
this._setBuildTooltip(row.$free, row.proto, level, nextLevel + 1, region);
|
||||
}
|
||||
if (level > 0) {
|
||||
row.$downgrade
|
||||
.text(`Downgrade +${currencySymbol()}${groupDigits(Math.round(buildingRefund(row.proto, level)))}`)
|
||||
.prop("disabled", false);
|
||||
// Downgrading returns nothing, so the button names no price.
|
||||
row.$downgrade.text("Downgrade").prop("disabled", false);
|
||||
} else {
|
||||
row.$downgrade.text("Not built").prop("disabled", true);
|
||||
}
|
||||
|
||||
+210
-12
@@ -5,7 +5,16 @@ import {
|
||||
governmentChangeCost,
|
||||
prerequisitesMet,
|
||||
} from "../../../shared/rules.js";
|
||||
import { POLICIES, PROPAGANDA, policyById, TECH_THEMES, technologyCost, RESOURCES } from "../../../shared/data.js";
|
||||
import {
|
||||
POLICIES,
|
||||
PROPAGANDA,
|
||||
policyById,
|
||||
TECH_THEMES,
|
||||
technologyCost,
|
||||
RESOURCES,
|
||||
RESOURCE_IDS,
|
||||
resourceById,
|
||||
} from "../../../shared/data.js";
|
||||
import { formatResourceAmount } from "../../../shared/resources.js";
|
||||
import { renderMonthlyBudget } from "./budget.js";
|
||||
import { effectsText, money, quotedMoney } from "./format.js";
|
||||
@@ -26,10 +35,12 @@ export class NationModal {
|
||||
this.$economySub = $("#economy-sub");
|
||||
this.$economySubtabs = $("#economy-subtabs");
|
||||
this.$economySummary = $("#economy-summary");
|
||||
this.$economyConsumption = $("#economy-consumption");
|
||||
this.$economyCurrency = $("#economy-currency");
|
||||
this.$economyCentralBank = $("#economy-central-bank");
|
||||
this.$economyRates = $("#economy-rates");
|
||||
// Which Economy sub-tab is open: the private sector, the central bank or
|
||||
// the exchange rates.
|
||||
// Which Economy sub-tab is open: the private sector, the trade table, the
|
||||
// viewer's own currency, the central bank or the exchange rates.
|
||||
this.economyView = "summary";
|
||||
// The central-bank interest input is built once and only its value updated,
|
||||
// so typing is not interrupted by the snapshot refresh.
|
||||
@@ -189,13 +200,17 @@ export class NationModal {
|
||||
if (this.$resourceTradeList) this.$resourceTradeList.toggleClass("hidden", view !== "leaders");
|
||||
}
|
||||
|
||||
// The Economy tab's sub-tabs: the private sector, the central bank and the
|
||||
// exchange-rate table.
|
||||
// The Economy tab's sub-tabs: the private sector, the day's consumption and
|
||||
// trade, the viewer's own currency and where it is held, the central bank and
|
||||
// the exchange-rate table.
|
||||
_buildEconomySubtabs() {
|
||||
if (!this.$economySubtabs || !this.$economySubtabs.length) return;
|
||||
this.$economySubtabs.empty();
|
||||
const currency = (this.money && this.money.currency) || null;
|
||||
const views = [
|
||||
{ id: "summary", label: "Private sector" },
|
||||
{ id: "consumption", label: "Consumption & trade" },
|
||||
{ id: "currency", label: currency ? `${currency.symbol} ${currency.name}` : "Currency" },
|
||||
{ id: "centralbank", label: "Central bank" },
|
||||
{ id: "rates", label: "Exchange rates" },
|
||||
];
|
||||
@@ -217,6 +232,10 @@ export class NationModal {
|
||||
this.$economySubtabs.find(`.subtab[data-view="${view}"]`).addClass("active");
|
||||
}
|
||||
if (this.$economySummary) this.$economySummary.toggleClass("hidden", view !== "summary");
|
||||
if (this.$economyConsumption) {
|
||||
this.$economyConsumption.toggleClass("hidden", view !== "consumption");
|
||||
}
|
||||
if (this.$economyCurrency) this.$economyCurrency.toggleClass("hidden", view !== "currency");
|
||||
if (this.$economyCentralBank) {
|
||||
this.$economyCentralBank.toggleClass("hidden", view !== "centralbank");
|
||||
}
|
||||
@@ -227,11 +246,190 @@ export class NationModal {
|
||||
|
||||
_renderEconomy(money) {
|
||||
this.money = money || this.money || {};
|
||||
if (this.economyView === "centralbank") this._renderCentralBank(this.money);
|
||||
if (this.economyView === "consumption") this._renderConsumption();
|
||||
else if (this.economyView === "currency") this._renderCurrencyHoldings(this.money);
|
||||
else if (this.economyView === "centralbank") this._renderCentralBank(this.money);
|
||||
else if (this.economyView === "rates") this._renderExchangeRates(this.money);
|
||||
else this._renderPrivateSector(this.money);
|
||||
}
|
||||
|
||||
// The day's resource consumption and the trade that crossed the nation's
|
||||
// border: what each neighbour and the world market sold us, and what we sold
|
||||
// them, with the money it moved.
|
||||
_renderConsumption() {
|
||||
if (!this.$economyConsumption || !this.$economyConsumption.length) return;
|
||||
if (this.$economySub && this.$economySub.length) {
|
||||
this.$economySub.text("Daily consumption and cross-border trade");
|
||||
}
|
||||
this.$economyConsumption.empty();
|
||||
const summary = this.resources && this.resources.summary;
|
||||
if (!summary) {
|
||||
this.$economyConsumption.append(
|
||||
$("<div class='desc'></div>").text("No resource data yet.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const consumed = summary.consumed || {};
|
||||
const external = summary.external || { buy: {}, sell: {} };
|
||||
const $table = $("<table class='resources-table economy-table'></table>");
|
||||
const $head = $("<thead></thead>").append(
|
||||
$("<tr></tr>")
|
||||
.append($("<th></th>").text("Resource"))
|
||||
.append($("<th class='numeric'></th>").text("Consumed / day"))
|
||||
.append($("<th class='numeric'></th>").text("Bought abroad"))
|
||||
.append($("<th class='numeric'></th>").text("Spent"))
|
||||
.append($("<th class='numeric'></th>").text("Sold abroad"))
|
||||
.append($("<th class='numeric'></th>").text("Earned"))
|
||||
);
|
||||
const $body = $("<tbody></tbody>");
|
||||
let totalSpent = 0;
|
||||
let totalEarned = 0;
|
||||
for (const id of RESOURCE_IDS) {
|
||||
const proto = resourceById(id);
|
||||
const name = proto ? proto.name : id;
|
||||
const buy = (external.buy && external.buy[id]) || { qty: 0, value: 0 };
|
||||
const sell = (external.sell && external.sell[id]) || { qty: 0, value: 0 };
|
||||
totalSpent += buy.value || 0;
|
||||
totalEarned += sell.value || 0;
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text(name))
|
||||
.append(
|
||||
$("<td class='numeric'></td>").text(
|
||||
formatResourceAmount(id, consumed[id] || 0)
|
||||
)
|
||||
)
|
||||
.append($("<td class='numeric'></td>").text(
|
||||
buy.qty > 0 ? formatResourceAmount(id, buy.qty) : "—"
|
||||
))
|
||||
.append($("<td class='numeric'></td>").text(
|
||||
buy.value > 0 ? this._amount(buy.value) : "—"
|
||||
))
|
||||
.append($("<td class='numeric'></td>").text(
|
||||
sell.qty > 0 ? formatResourceAmount(id, sell.qty) : "—"
|
||||
))
|
||||
.append($("<td class='numeric'></td>").text(
|
||||
sell.value > 0 ? this._amount(sell.value) : "—"
|
||||
))
|
||||
);
|
||||
}
|
||||
$table.append($head, $body);
|
||||
$table.append(
|
||||
$("<tfoot></tfoot>").append(
|
||||
$("<tr class='budget-total'></tr>")
|
||||
.append($("<td colspan='3'></td>").text("Trade balance"))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(totalSpent)))
|
||||
.append($("<td></td>"))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(totalEarned)))
|
||||
)
|
||||
);
|
||||
this.$economyConsumption.append($table);
|
||||
this.$economyConsumption.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"Bought and sold counts only trade across the border — foreign " +
|
||||
"regions reached over the trade network. Neighbours at home are not " +
|
||||
"a foreign sale."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Where every unit of the viewer's currency is stored: the treasury, the
|
||||
// nation's regions, and the foreign regions and central banks that hold it.
|
||||
_renderCurrencyHoldings(money) {
|
||||
if (!this.$economyCurrency || !this.$economyCurrency.length) return;
|
||||
const holdings = money.currencyHoldings;
|
||||
const currency = money.currency || { name: "Currency", symbol: "¤", code: "" };
|
||||
if (this.$economySub && this.$economySub.length) {
|
||||
this.$economySub.text(
|
||||
`Holdings of the ${currency.name} (${currency.code})`
|
||||
);
|
||||
}
|
||||
this.$economyCurrency.empty();
|
||||
if (!holdings) {
|
||||
this.$economyCurrency.append(
|
||||
$("<div class='desc'></div>").text("No currency data yet.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const $table = $("<table class='resources-table economy-table'></table>");
|
||||
const $body = $("<tbody></tbody>");
|
||||
$body.append(
|
||||
$("<tr class='budget-section'></tr>").append(
|
||||
$("<td colspan='2'></td>").text("Treasury")
|
||||
)
|
||||
);
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text("Government"))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(holdings.treasury || 0)))
|
||||
);
|
||||
if (holdings.treasuryDebt > 0) {
|
||||
$body.append(
|
||||
$("<tr class='expense-detail'></tr>")
|
||||
.append($("<td></td>").text("Overdraft (debt)"))
|
||||
.append($("<td class='numeric'></td>").text(`−${this._amount(holdings.treasuryDebt)}`))
|
||||
);
|
||||
}
|
||||
$body.append(
|
||||
$("<tr class='budget-section'></tr>").append(
|
||||
$("<td colspan='2'></td>").text(`Your regions (${(holdings.own || []).length})`)
|
||||
)
|
||||
);
|
||||
for (const region of holdings.own || []) {
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text(region.name))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(region.amount)))
|
||||
);
|
||||
}
|
||||
if ((holdings.foreign || []).length > 0) {
|
||||
$body.append(
|
||||
$("<tr class='budget-section'></tr>").append(
|
||||
$("<td colspan='2'></td>").text("Held by foreign regions")
|
||||
)
|
||||
);
|
||||
for (const region of holdings.foreign) {
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text(`${region.name} (${region.civName})`))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(region.amount)))
|
||||
);
|
||||
}
|
||||
}
|
||||
if ((holdings.banks || []).length > 0) {
|
||||
$body.append(
|
||||
$("<tr class='budget-section'></tr>").append(
|
||||
$("<td colspan='2'></td>").text("Central-bank reserves")
|
||||
)
|
||||
);
|
||||
for (const bank of holdings.banks) {
|
||||
$body.append(
|
||||
$("<tr></tr>")
|
||||
.append($("<td></td>").text(`Central bank of ${bank.name}`))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(bank.amount)))
|
||||
);
|
||||
}
|
||||
}
|
||||
$table.append($body);
|
||||
const $foot = $("<tfoot></tfoot>").append(
|
||||
$("<tr class='budget-total'></tr>")
|
||||
.append($("<td></td>").text("Total in circulation"))
|
||||
.append($("<td class='numeric'></td>").text(this._amount(holdings.total || 0)))
|
||||
);
|
||||
$table.append($foot);
|
||||
this.$economyCurrency.append($table);
|
||||
this.$economyCurrency.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"Foreign holdings are what exports earned; a foreign central bank " +
|
||||
"holds your money as a reserve."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_amount(value) {
|
||||
return `${currencySymbol()}${groupDigits(Math.round(value || 0))}`;
|
||||
}
|
||||
|
||||
// The aggregate private-sector cash the nation's regions hold, and each
|
||||
// region's own reserve.
|
||||
_renderPrivateSector(money) {
|
||||
@@ -498,8 +696,8 @@ export class NationModal {
|
||||
}
|
||||
|
||||
// The Resources tab: the nation's stores, what it consumes and makes, what it
|
||||
// imports and exports, beside the global market's price and the world's supply
|
||||
// and demand. Energy is a daily flow, so it has no store: its row reads the
|
||||
// imports and exports, beside the reference price and the world's supply and
|
||||
// demand. Energy is a daily flow, so it has no store: its row reads the
|
||||
// cities' demand against the power plants' output instead.
|
||||
_renderResources(info) {
|
||||
if (!this.$resourcesList || !this.$resourcesList.length) return;
|
||||
@@ -508,7 +706,7 @@ export class NationModal {
|
||||
if (this.$resourcesSub && this.$resourcesSub.length) {
|
||||
this.$resourcesSub.text(
|
||||
"Stores, daily consumption, production and cross-nation trade, with the " +
|
||||
"global market's price. Energy is delivered as a flow and cannot be stored."
|
||||
"reference price. Energy is delivered as a flow and cannot be stored."
|
||||
);
|
||||
}
|
||||
if (!summary) {
|
||||
@@ -594,9 +792,9 @@ export class NationModal {
|
||||
this.$resourcesList.append(
|
||||
$("<div class='desc'></div>").text(
|
||||
"Consumption is the people's needs plus the materials buildings, units and works " +
|
||||
"wear out. Exports and imports count yesterday's deliveries between nations; the " +
|
||||
"global market is not a partner. Prices drift with the world's supply and demand, " +
|
||||
"and a city buys from the cheapest source it can reach, then from the market."
|
||||
"wear out. Exports and imports count yesterday's deliveries between nations. " +
|
||||
"Prices drift with the world's supply and demand, and a region buys from its own " +
|
||||
"stores first, then from reachable home regions, then from foreign ones."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -365,6 +365,8 @@ export class GameState {
|
||||
this._visibilityDirty = false;
|
||||
}
|
||||
if (this.totalHours % HOURS_PER_DAY === 0) {
|
||||
// A fresh day of shortage notices: this day's are published to the owners.
|
||||
if (this._resourceAlerts) this._resourceAlerts = new Map();
|
||||
this._tickPopulation();
|
||||
this._tickPolitics();
|
||||
this._tickBattleTiles();
|
||||
|
||||
@@ -71,13 +71,20 @@ export const budgetMethods = {
|
||||
},
|
||||
|
||||
// The money one expense category spent on one resource, so the row can be
|
||||
// expanded to show what it bought.
|
||||
_recordBudgetCategoryResource(civ, category, resource, amount) {
|
||||
// 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] = {});
|
||||
byResource[resource] = (byResource[resource] || 0) + amount;
|
||||
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
|
||||
@@ -132,7 +139,16 @@ export const budgetMethods = {
|
||||
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({
|
||||
|
||||
@@ -873,8 +873,8 @@ export const economyMethods = {
|
||||
_spendBudget(civ, amount, category = null) {
|
||||
if (amount <= 0) return true;
|
||||
this.budgets.set(civ, this.getBudget(civ) - amount);
|
||||
// The money does not vanish: it is paid into the nation's regions, the
|
||||
// private sector that supplied the goods, labour or services.
|
||||
// The money does not vanish: it is paid into the nation's own regions, the
|
||||
// private sector that supplied the labour or service.
|
||||
this._creditRegions(civ, amount);
|
||||
if (category) this._recordBudgetCash(civ, category, -amount);
|
||||
return true;
|
||||
@@ -978,10 +978,17 @@ export const economyMethods = {
|
||||
const cultureRate = modifiers[EFFECT_CULTURE] || 0;
|
||||
if (scienceRate === 0 && cultureRate === 0) continue;
|
||||
// Science buildings spend a unit of high-tech for each research point
|
||||
// they make, so a nation short of chips researches only as far as its
|
||||
// store reaches. Government research needs no high-tech.
|
||||
// they make, so a region short of chips researches only as far as its
|
||||
// store reaches, and reports the shortfall. Government research needs no
|
||||
// high-tech.
|
||||
const buildingRate = this.getCivBuildingModifiers(civ)[EFFECT_RESEARCH] || 0;
|
||||
const effectiveBuilding = this._spendHighTechForResearch(civ, buildingRate);
|
||||
let effectiveBuilding = 0;
|
||||
for (const city of this.cities) {
|
||||
if (city.civ !== civ) continue;
|
||||
const cityRate = this.getCityBuildingModifiers(city)[EFFECT_RESEARCH] || 0;
|
||||
if (cityRate <= 0) continue;
|
||||
effectiveBuilding += this._spendCityHighTechForResearch(civ, city, cityRate);
|
||||
}
|
||||
this._addResearch(civ, scienceRate - buildingRate + effectiveBuilding);
|
||||
this.culture.set(civ, this.getCulture(civ) + cultureRate);
|
||||
}
|
||||
@@ -1107,22 +1114,48 @@ export const economyMethods = {
|
||||
steel: targetHeal * repair.unitSteelPerHp,
|
||||
hightech: proto.advanced ? targetHeal * repair.unitHighTechPerHp : 0,
|
||||
};
|
||||
const fromStock = this._drawFromNearest(city.civ, city.coords, wanted);
|
||||
const payer = this._treasuryPayer(city.civ);
|
||||
const fromStock = this._drawFromNearest(city.civ, city.coords, wanted, payer);
|
||||
const shortfall = {
|
||||
steel: wanted.steel - fromStock.steel,
|
||||
hightech: wanted.hightech - fromStock.hightech,
|
||||
};
|
||||
const cost = this._marketUnitCost("steel") * shortfall.steel +
|
||||
this._marketUnitCost("hightech") * shortfall.hightech;
|
||||
const steelCost = this._marketUnitCost("steel") * shortfall.steel;
|
||||
const techCost = this._marketUnitCost("hightech") * shortfall.hightech;
|
||||
const paid = 1;
|
||||
if (cost > 0) {
|
||||
// The government can always pay, borrowing if the treasury is dry.
|
||||
this._spendBudget(city.civ, cost * paid, "units");
|
||||
this._recordResourceSpend(city.civ, "upkeep", cost * paid, {
|
||||
steel: steelCost * paid,
|
||||
hightech: techCost * paid,
|
||||
const v = this.currencyValue(city.civ);
|
||||
let spentSteel = fromStock.steel * this.getResourcePrice("steel") / v;
|
||||
let spentTech = fromStock.hightech * this.getResourcePrice("hightech") / v;
|
||||
let paid = 1;
|
||||
if (shortfall.steel > 0 || shortfall.hightech > 0) {
|
||||
// The replacement steel and high-tech come from reachable regions,
|
||||
// bought by the treasury. Whatever is not available is not healed.
|
||||
const nodes = this._resourceNodes();
|
||||
const haul = { steel: 0, hightech: 0 };
|
||||
let gotSteel = 0;
|
||||
let gotTech = 0;
|
||||
if (shortfall.steel > 0) {
|
||||
const before = payer.total;
|
||||
gotSteel = this._procureFromNeighbours(
|
||||
city, "steel", shortfall.steel, haul, nodes, { payer }
|
||||
);
|
||||
spentSteel += payer.total - before;
|
||||
}
|
||||
if (shortfall.hightech > 0) {
|
||||
const before = payer.total;
|
||||
gotTech = this._procureFromNeighbours(
|
||||
city, "hightech", shortfall.hightech, haul, nodes, { payer }
|
||||
);
|
||||
spentTech += payer.total - before;
|
||||
}
|
||||
if (gotSteel + 1e-6 < shortfall.steel || gotTech + 1e-6 < shortfall.hightech) {
|
||||
paid = 0;
|
||||
}
|
||||
}
|
||||
if (spentSteel + spentTech > 0) {
|
||||
this._recordBudgetCash(city.civ, "units", -(spentSteel + spentTech));
|
||||
this._recordBudgetCategoryResource(city.civ, "units", "steel", spentSteel);
|
||||
this._recordBudgetCategoryResource(city.civ, "units", "hightech", spentTech);
|
||||
this._recordResourceSpend(city.civ, "upkeep", spentSteel + spentTech, {
|
||||
steel: spentSteel,
|
||||
hightech: spentTech,
|
||||
});
|
||||
}
|
||||
const gained = targetHeal * paid;
|
||||
|
||||
+68
-21
@@ -161,21 +161,6 @@ export const moneyMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
// Pays the world market's producers -- every region outside the buyer's nation
|
||||
// -- so an import does not drain money out of the world.
|
||||
_payWorld(buyerCiv, amount, code) {
|
||||
if (!(amount > 0)) return;
|
||||
const others = this.cities.filter((city) => city.civ !== buyerCiv);
|
||||
if (others.length === 0) {
|
||||
this._creditRegions(buyerCiv, amount, code);
|
||||
return;
|
||||
}
|
||||
const shares = this._populationShares(others);
|
||||
for (let i = 0; i < others.length; i++) {
|
||||
this._creditRegionCash(others[i], code, amount * shares[i]);
|
||||
}
|
||||
},
|
||||
|
||||
// Each region's share of a set's total population, defaulting to an even split
|
||||
// when there are no people at all.
|
||||
_populationShares(cities) {
|
||||
@@ -261,22 +246,25 @@ export const moneyMethods = {
|
||||
return total;
|
||||
},
|
||||
|
||||
// A region buys `globalValue` (in global units) worth of `targetCiv`'s
|
||||
// A buyer acquires `globalValue` (in global units) worth of `targetCiv`'s
|
||||
// currency: first from what it already holds, then from that nation's central
|
||||
// bank, paying with its own national currency (borrowing from its own bank if
|
||||
// it is short). The target's central bank keeps the buyer's currency as a
|
||||
// foreign reserve. Returns the amount of the target currency that ends up
|
||||
// handed to the seller.
|
||||
_regionAcquireCurrency(buyerCity, targetCiv, globalValue) {
|
||||
//
|
||||
// `charge` overrides who pays the national-currency cost: a region does by
|
||||
// default, but a government buys with the treasury. `useHeld` is false for the
|
||||
// treasury, which holds no basket of its own.
|
||||
_regionAcquireCurrency(buyerCity, targetCiv, globalValue, charge = null, useHeld = true) {
|
||||
if (!(globalValue > 0)) return { code: this.currencyOf(targetCiv).code, amount: 0 };
|
||||
const target = this.currencyOf(targetCiv);
|
||||
const buyer = this.currencyOf(buyerCity.civ);
|
||||
const vTarget = this.currencyValue(targetCiv);
|
||||
const vBuyer = this.currencyValue(buyerCity.civ);
|
||||
const needTarget = globalValue / vTarget;
|
||||
const basket = this.regionCash.get(buyerCity.id);
|
||||
if (!basket) return null;
|
||||
const heldTarget = basket.get(target.code) || 0;
|
||||
const basket = useHeld ? this.regionCash.get(buyerCity.id) : null;
|
||||
const heldTarget = basket ? basket.get(target.code) || 0 : 0;
|
||||
const fromHeld = Math.min(heldTarget, needTarget);
|
||||
const remaining = needTarget - fromHeld;
|
||||
if (remaining > 0) {
|
||||
@@ -285,7 +273,8 @@ export const moneyMethods = {
|
||||
// own central bank if it is short; the seller's bank holds that currency
|
||||
// as a reserve and issues its own, taking on the matching debt so no money
|
||||
// is conjured from nothing.
|
||||
this._chargeRegionCash(buyerCity, buyer.code, costBuyer);
|
||||
if (charge) charge(costBuyer);
|
||||
else this._chargeRegionCash(buyerCity, buyer.code, costBuyer);
|
||||
this._addReserve(targetCiv, buyer.code, costBuyer);
|
||||
this._addCentralBankDebt(targetCiv, target.code, remaining);
|
||||
}
|
||||
@@ -373,6 +362,64 @@ export const moneyMethods = {
|
||||
return total;
|
||||
},
|
||||
|
||||
// Every place a nation's own currency is stored: its treasury, its own
|
||||
// regions' cash, any foreign region still holding some, and every central
|
||||
// bank that keeps it as a reserve. Foreign holdings are what exports earned.
|
||||
getCurrencyHoldings(civ) {
|
||||
const currency = this.currencyOf(civ);
|
||||
const code = currency.code;
|
||||
const own = [];
|
||||
const foreign = [];
|
||||
let ownTotal = 0;
|
||||
let foreignTotal = 0;
|
||||
for (const city of this.cities) {
|
||||
const amount = this.getRegionCash(city).get(code) || 0;
|
||||
if (!(amount > 0)) continue;
|
||||
const host = this.civilisations[city.civ];
|
||||
const entry = {
|
||||
cityId: city.id,
|
||||
name: city.name,
|
||||
civ: city.civ,
|
||||
civName: host ? host.name : "",
|
||||
amount,
|
||||
};
|
||||
if (city.civ === civ) {
|
||||
own.push(entry);
|
||||
ownTotal += amount;
|
||||
} else {
|
||||
foreign.push(entry);
|
||||
foreignTotal += amount;
|
||||
}
|
||||
}
|
||||
const banks = [];
|
||||
let bankTotal = 0;
|
||||
for (let c = 0; c < this.civilisations.length; c++) {
|
||||
const amount = this.getCentralBankReserves(c).get(code) || 0;
|
||||
if (!(amount > 0)) continue;
|
||||
const host = this.civilisations[c];
|
||||
banks.push({ civ: c, name: host ? host.name : "", amount });
|
||||
bankTotal += amount;
|
||||
}
|
||||
const budget = this.getBudget(civ) || 0;
|
||||
const treasury = Math.max(0, budget);
|
||||
const treasuryDebt = Math.max(0, -budget);
|
||||
return {
|
||||
code,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name,
|
||||
value: this.currencyValue(civ),
|
||||
treasury,
|
||||
treasuryDebt,
|
||||
own,
|
||||
ownTotal,
|
||||
foreign,
|
||||
foreignTotal,
|
||||
banks,
|
||||
bankTotal,
|
||||
total: treasury + ownTotal + foreignTotal + bankTotal,
|
||||
};
|
||||
},
|
||||
|
||||
// --------------------------------------------------- daily tick --
|
||||
|
||||
// The day's monetary work: debts to the central bank accrue interest, regions
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ECONOMY } from "../data/economy.js";
|
||||
import { GOVERNMENTS } from "../data/governments.js";
|
||||
import { PILLAGE, pillageSteps } from "../data/combat.js";
|
||||
import { transportImprovement } from "../data/improvements.js";
|
||||
import { buildingBuildCost, buildingRefund } from "../rules.js";
|
||||
import { buildingBuildCost } from "../rules.js";
|
||||
import { tileImprovementResourceCost } from "../resources.js";
|
||||
import { TRAINING_QUEUE_LIMIT } from "./constants.js";
|
||||
|
||||
@@ -42,7 +42,7 @@ export const orderMethods = {
|
||||
if (capacity <= 0) return false;
|
||||
if (!this._spendBudget(city.civ, proto.cost, "training")) return false;
|
||||
const materials = this.constructionResourceCost(proto, 0, proto.cost);
|
||||
if (!this._payConstructionResources(city.civ, city.coords, materials, "training")) {
|
||||
if (!this._payConstructionResources(city.civ, city.coords, materials, "training", proto.name)) {
|
||||
this._refundBudget(city.civ, proto.cost, "training");
|
||||
return false;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export const orderMethods = {
|
||||
if (capacity <= 0) return false;
|
||||
if (!this._spendBudget(city.civ, cost, "construction")) return false;
|
||||
const materials = this.constructionResourceCost(proto, level, cost);
|
||||
if (!this._payConstructionResources(city.civ, city.coords, materials)) {
|
||||
if (!this._payConstructionResources(city.civ, city.coords, materials, "construction", proto.name)) {
|
||||
this._refundBudget(city.civ, cost, "construction");
|
||||
return false;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export const orderMethods = {
|
||||
if (!free) {
|
||||
if (!this._spendBudget(civ, improvement.buildCost, "tilebuild")) return false;
|
||||
const materials = tileImprovementResourceCost(improvement);
|
||||
if (!this._payConstructionResources(civ, coords, materials, "tilebuild")) {
|
||||
if (!this._payConstructionResources(civ, coords, materials, "tilebuild", improvement.name)) {
|
||||
this._refundBudget(civ, improvement.buildCost, "tilebuild");
|
||||
return false;
|
||||
}
|
||||
@@ -191,12 +191,10 @@ export const orderMethods = {
|
||||
requestDemolish(cityId, protoIndex) {
|
||||
const city = this.findCity(cityId);
|
||||
if (!city || protoIndex < 0 || protoIndex >= this.protoBuildings.length) return false;
|
||||
const proto = this.protoBuildings[protoIndex];
|
||||
const civ = city.civ;
|
||||
const level = this.getCityBuildingLevel(city, protoIndex);
|
||||
if (level <= 0) return false;
|
||||
const refund = buildingRefund(proto, level);
|
||||
if (refund > 0) this._refundBudget(civ, refund, "construction");
|
||||
// Downgrading refunds nothing: the money and materials already sunk into
|
||||
// the level are lost.
|
||||
if (level <= 1) delete city.buildings[protoIndex];
|
||||
else city.buildings[protoIndex] = level - 1;
|
||||
this._gdpPerCapitaCache.clear();
|
||||
|
||||
+663
-261
File diff suppressed because it is too large
Load Diff
@@ -151,6 +151,8 @@ export const serializationMethods = {
|
||||
constructionSpeed: 0, railSpeed: 0,
|
||||
resources: null,
|
||||
breakdown: null,
|
||||
currencyHoldings: null,
|
||||
resourceAlerts: [],
|
||||
};
|
||||
}
|
||||
const headline = this._civHeadline(viewerCiv);
|
||||
@@ -162,6 +164,9 @@ export const serializationMethods = {
|
||||
railSpeed: modifiers[EFFECT_RAIL_SPEED] || 0,
|
||||
// The nation's stores, daily needs and production, plus today's prices.
|
||||
resources: this.getCivResourceSummary(viewerCiv),
|
||||
// The day's notices that the viewer's own producing buildings went short
|
||||
// of a material or of grid power.
|
||||
resourceAlerts: this._resourceAlertsFor(viewerCiv),
|
||||
budget: this.getBudget(viewerCiv),
|
||||
// The upkeep figure the HUD nets against income: propaganda money plus
|
||||
// the market value of the materials buildings, units and works wear out.
|
||||
@@ -188,6 +193,9 @@ export const serializationMethods = {
|
||||
exchangeRates: this._serializeExchangeRates(viewerCiv),
|
||||
centralBank: this._serializeCentralBank(viewerCiv),
|
||||
privateCash: this.getPrivateSectorCash(viewerCiv),
|
||||
// Where every unit of the viewer's currency is stored: its treasury, its
|
||||
// regions, and foreign regions and central banks that hold it.
|
||||
currencyHoldings: this.getCurrencyHoldings(viewerCiv),
|
||||
governmentDebt: this.getGovernmentDebt(viewerCiv),
|
||||
// How far the headline figures have grown, against January 2000 in the
|
||||
// first year and year-on-year afterwards.
|
||||
@@ -419,6 +427,12 @@ export const serializationMethods = {
|
||||
cash: this._serializeRegionCash(city),
|
||||
cashValue: this.getRegionCashValue(city),
|
||||
debt: this.getRegionDebt(city),
|
||||
// What the region's people went short of over the last day, so the
|
||||
// People tab can report it.
|
||||
shortage: this.getRegionShortages(city),
|
||||
// The region's reserves, their market value and the private sector's
|
||||
// daily consumption, for the Resources tab.
|
||||
resources: this.getRegionResourceInfo(city),
|
||||
// The ethnicity of the region's people, for the circle graph.
|
||||
ethnicMakeup: this.getCityEthnicMakeup(city),
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export const warfareMethods = {
|
||||
if (ECONOMY.productionCapacity(gdp) <= 0) return false;
|
||||
if (!this._spendBudget(civ, proto.buildCost, "tilebuild")) return false;
|
||||
const materials = tileImprovementResourceCost(proto, proto.buildCost);
|
||||
if (!this._payConstructionResources(civ, coords, materials, "tilebuild")) {
|
||||
if (!this._payConstructionResources(civ, coords, materials, "tilebuild", proto.name)) {
|
||||
this._refundBudget(civ, proto.buildCost, "tilebuild");
|
||||
return false;
|
||||
}
|
||||
|
||||
+23
-3
@@ -111,13 +111,33 @@ export class BudgetTest extends TestCase {
|
||||
this.assertApprox(steel[1], 300);
|
||||
}
|
||||
|
||||
test_expense_rows_name_the_buildings_that_consumed_a_resource() {
|
||||
const state = smallState();
|
||||
state._recordBudgetCash(0, "buildings", -500);
|
||||
state._recordBudgetCategoryResource(0, "buildings", "steel", 300, "Barracks");
|
||||
state._recordBudgetCategoryResource(0, "buildings", "steel", 100, "Factory");
|
||||
const report = state.budgetMonthReports(0).at(-1);
|
||||
const buildings = report.expenses.find((entry) => entry.id === "buildings");
|
||||
const steel = buildings.buys.find((entry) => entry[0] === "steel");
|
||||
this.assertApprox(steel[1], 400);
|
||||
const sources = new Map(steel[2]);
|
||||
this.assertApprox(sources.get("Barracks"), 300);
|
||||
this.assertApprox(sources.get("Factory"), 100);
|
||||
}
|
||||
|
||||
test_upkeep_records_the_resources_it_bought() {
|
||||
const state = smallState();
|
||||
// Empty the stores so the day's upkeep has to be bought on the market.
|
||||
// Empty the viewer's stores and stock a foreign region, so the day's upkeep
|
||||
// has to be bought across the border.
|
||||
for (const city of state.cities) {
|
||||
const store = state.getCityResourceStock(city);
|
||||
store.steel = 0;
|
||||
store.hightech = 0;
|
||||
if (city.civ === 0) {
|
||||
store.steel = 0;
|
||||
store.hightech = 0;
|
||||
} else {
|
||||
store.steel = 1.0e9;
|
||||
store.hightech = 1.0e9;
|
||||
}
|
||||
}
|
||||
state.budgets.set(0, 1.0e15);
|
||||
for (let i = 0; i < HOURS_PER_DAY; i++) state.tickHour();
|
||||
|
||||
@@ -206,11 +206,19 @@ export class GameStateBuildingsTest extends TestCase {
|
||||
state.budgets.set(0, BIG_BUDGET);
|
||||
const index = this.addBuilding(state, gdpBuilding({ baseCost: 100, costAlpha: 2.0 }));
|
||||
const city = cityOf(state, 0);
|
||||
// Stock the steel so the whole bill is drawn from the stores and the two
|
||||
// orders can be compared cleanly.
|
||||
for (const own of state.cities) {
|
||||
if (own.civ === 0) state.getCityResourceStock(own).steel = 1.0e9;
|
||||
}
|
||||
const before = state.getBudget(0);
|
||||
this.assertTrue(state.requestBuild(city.id, index));
|
||||
const first = before - state.getBudget(0);
|
||||
const mid = state.getBudget(0);
|
||||
this.assertTrue(state.requestBuild(city.id, index));
|
||||
// 100 for the first level, 200 for the second.
|
||||
this.assertApprox(before - state.getBudget(0), 300);
|
||||
// The second level (money and steel) is priced off the level the first
|
||||
// leaves behind, so it costs twice the first.
|
||||
this.assertApprox(mid - state.getBudget(0), first * 2, 1e-6);
|
||||
this.advanceUntilIdle(state);
|
||||
this.assertEqual(state.getCityBuildingLevel(city, index), 2);
|
||||
}
|
||||
@@ -229,6 +237,10 @@ export class GameStateBuildingsTest extends TestCase {
|
||||
state.budgets.set(0, BIG_BUDGET);
|
||||
const index = this.addBuilding(state, gdpBuilding());
|
||||
const city = cityOf(state, 0);
|
||||
// Stock the steel so the material bill is never the limit being tested.
|
||||
for (const own of state.cities) {
|
||||
if (own.civ === 0) state.getCityResourceStock(own).steel = 1.0e12;
|
||||
}
|
||||
for (let i = 0; i < 20; i++) this.assertTrue(state.requestBuild(city.id, index));
|
||||
this.advanceUntilIdle(state);
|
||||
this.assertEqual(state.getCityBuildingLevel(city, index), 20);
|
||||
@@ -242,7 +254,7 @@ export class GameStateBuildingsTest extends TestCase {
|
||||
this.assertFalse(state.requestBuild(cityOf(state, 0).id, 9999));
|
||||
}
|
||||
|
||||
test_request_demolish_refunds_and_removes_the_building() {
|
||||
test_request_demolish_removes_the_building_without_a_refund() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, BIG_BUDGET);
|
||||
const index = this.addBuilding(state, gdpBuilding());
|
||||
@@ -253,7 +265,7 @@ export class GameStateBuildingsTest extends TestCase {
|
||||
const before = state.getBudget(0);
|
||||
this.assertTrue(state.requestDemolish(city.id, index));
|
||||
this.assertEqual(state.getCityBuildingLevel(city, index), 1);
|
||||
this.assertGreater(state.getBudget(0), before, "refunded");
|
||||
this.assertApprox(state.getBudget(0), before, 1e-6, "downgrading refunds nothing");
|
||||
this.assertTrue(state.requestDemolish(city.id, index));
|
||||
this.assertEqual(state.getCityBuildingLevel(city, index), 0);
|
||||
this.assertFalse(state.requestDemolish(city.id, index), "nothing left to demolish");
|
||||
|
||||
@@ -243,10 +243,22 @@ export class GameStateTest extends TestCase {
|
||||
const state = smallState();
|
||||
const city = state.cities[0];
|
||||
grantBuilding(state, city, "barracks");
|
||||
// Stock the materials so the bill is the training cost plus the material
|
||||
// value the state buys from its regions.
|
||||
for (const own of state.cities) {
|
||||
if (own.civ !== city.civ) continue;
|
||||
const stock = state.getCityResourceStock(own);
|
||||
stock.steel = 1.0e9;
|
||||
stock.hightech = 1.0e9;
|
||||
}
|
||||
const proto = state.protoUnits[0];
|
||||
const materials = state.constructionResourceCost(proto, 0, proto.cost);
|
||||
const materialValue =
|
||||
materials.steel * state.getResourcePrice("steel") +
|
||||
materials.hightech * state.getResourcePrice("hightech");
|
||||
const before = state.getBudget(city.civ);
|
||||
this.assertTrue(state.requestTrain(city.id, 0));
|
||||
this.assertApprox(state.getBudget(city.civ), before - proto.cost);
|
||||
this.assertApprox(before - state.getBudget(city.civ), proto.cost + materialValue, 1e-3);
|
||||
const queue = state.getTraining(city.id);
|
||||
this.assertSize(queue, 1);
|
||||
this.assertEqual(queue[0].kind, "unit");
|
||||
@@ -257,6 +269,13 @@ export class GameStateTest extends TestCase {
|
||||
const city = state.cities[0];
|
||||
grantBuilding(state, city, "barracks");
|
||||
state.budgets.set(city.civ, 1.0e15);
|
||||
// Stock the materials so the queue limit, not the stores, is what caps it.
|
||||
for (const own of state.cities) {
|
||||
if (own.civ !== city.civ) continue;
|
||||
const stock = state.getCityResourceStock(own);
|
||||
stock.steel = 1.0e12;
|
||||
stock.hightech = 1.0e12;
|
||||
}
|
||||
for (let i = 0; i < TRAINING_QUEUE_LIMIT; i++) {
|
||||
this.assertTrue(state.requestTrain(city.id, 0), `order ${i} accepted`);
|
||||
}
|
||||
|
||||
+14
-11
@@ -19,8 +19,9 @@ export class ImprovementsTest extends TestCase {
|
||||
test_building_a_road_costs_money_and_resources_and_lays_the_tile() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, BIG_BUDGET);
|
||||
// Stock enough steel that only the electricity is bought on the market, so
|
||||
// the money bill is the build cost plus the construction energy.
|
||||
// Stock enough steel that the whole material bill is drawn from the stores;
|
||||
// the treasury pays the build cost plus the steel it buys from the region.
|
||||
// Construction electricity is a grid draw, not a purchase, so it is free.
|
||||
for (const city of state.cities) {
|
||||
if (city.civ === 0) state.getCityResourceStock(city).steel = 1e9;
|
||||
}
|
||||
@@ -32,12 +33,12 @@ export class ImprovementsTest extends TestCase {
|
||||
this.assertTrue(state.roads.has(k), "the road is on the tile");
|
||||
this.assertFalse(state.railways.has(k), "it is not a railway");
|
||||
const materials = tileImprovementResourceCost(TRANSPORT_BY_ID.road);
|
||||
const energyCost = materials.energy * state.getResourcePrice("energy");
|
||||
const steelValue = materials.steel * state.getResourcePrice("steel");
|
||||
this.assertApprox(
|
||||
before - state.getBudget(0),
|
||||
TRANSPORT_BY_ID.road.buildCost + energyCost,
|
||||
TRANSPORT_BY_ID.road.buildCost + steelValue,
|
||||
1e-3,
|
||||
"money plus the construction electricity"
|
||||
"the build cost plus the steel the region supplied"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,8 +197,9 @@ export class ImprovementsTest extends TestCase {
|
||||
test_a_tile_work_costs_steel_energy_and_high_tech() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1.0e15);
|
||||
// Enough stores that only the electricity is bought, so the money bill is
|
||||
// the build cost plus the construction energy.
|
||||
// Enough stores that the whole material bill is drawn from them; the
|
||||
// treasury pays the build cost plus the steel and high-tech it buys from the
|
||||
// region. Construction electricity comes off the grid, so it is not bought.
|
||||
for (const city of state.cities) {
|
||||
if (city.civ !== 0) continue;
|
||||
const stock = state.getCityResourceStock(city);
|
||||
@@ -213,16 +215,17 @@ export class ImprovementsTest extends TestCase {
|
||||
const radar = TILE_IMPROVEMENT_BY_ID.radar_tower;
|
||||
const materials = tileImprovementResourceCost(radar);
|
||||
this.assertGreater(materials.steel, 0, "steel is spent");
|
||||
this.assertGreater(materials.energy, 0, "energy is spent");
|
||||
this.assertGreater(materials.energy, 0, "energy is drawn");
|
||||
this.assertGreater(materials.hightech, 0, "high-tech is spent");
|
||||
const before = state.getBudget(0);
|
||||
this.assertTrue(state.requestBuildTileImprovement(0, coords, "radar_tower"));
|
||||
const energyCost = materials.energy * state.getResourcePrice("energy");
|
||||
const steelValue = materials.steel * state.getResourcePrice("steel");
|
||||
const techValue = materials.hightech * state.getResourcePrice("hightech");
|
||||
this.assertApprox(
|
||||
before - state.getBudget(0),
|
||||
radar.buildCost + energyCost,
|
||||
radar.buildCost + steelValue + techValue,
|
||||
1e-3,
|
||||
"money plus the construction electricity"
|
||||
"the build cost plus the materials the region supplied"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+115
-5
@@ -277,6 +277,64 @@ export class CityModalTest extends TestCase {
|
||||
teardownDom(env);
|
||||
}
|
||||
}
|
||||
|
||||
async test_resources_tab_shows_reserves_value_and_consumption() {
|
||||
const env = await setupDom();
|
||||
try {
|
||||
const modal = new CityModal();
|
||||
modal.show(
|
||||
{ id: 1, name: "Lyon" },
|
||||
{
|
||||
budget: 1.0e9,
|
||||
gdp: 1.0e9,
|
||||
resources: {
|
||||
stock: { food: 1000, steel: 2000, luxury: 0, hightech: 50 },
|
||||
consumption: { food: 100, steel: 200, luxury: 10, hightech: 2, energy: 2500 },
|
||||
value: 123456,
|
||||
},
|
||||
}
|
||||
);
|
||||
env.$("#modal-city .tab[data-tab=resources]").click();
|
||||
this.assertTrue(env.$("#city-tab-resources").hasClass("active"));
|
||||
this.assertTrue(
|
||||
env.$("#city-resources-sub").text().includes("123 456"),
|
||||
"the reserve value shows"
|
||||
);
|
||||
const text = env.$("#city-resources-list").text();
|
||||
this.assertTrue(text.includes("Food"), "food is listed");
|
||||
this.assertTrue(text.includes("Steel"), "steel is listed");
|
||||
this.assertTrue(text.includes("Energy"), "energy is listed");
|
||||
this.assertTrue(text.includes("2.5 MWh"), "the daily power draw is shown");
|
||||
} finally {
|
||||
teardownDom(env);
|
||||
}
|
||||
}
|
||||
|
||||
async test_people_tab_reports_resource_shortages() {
|
||||
const env = await setupDom();
|
||||
try {
|
||||
const modal = new CityModal();
|
||||
modal.show(
|
||||
{ id: 1, name: "Lyon" },
|
||||
{
|
||||
budget: 1.0e9,
|
||||
gdp: 1.0e9,
|
||||
ethnicMakeup: [],
|
||||
approval: 0.5,
|
||||
shortage: { steel: { fraction: 0.4, amount: 1200 } },
|
||||
}
|
||||
);
|
||||
env.$("#modal-city .tab[data-tab=people]").click();
|
||||
this.assertTrue(env.$("#city-tab-people").hasClass("active"));
|
||||
const text = env.$("#city-people-list").text();
|
||||
this.assertTrue(text.includes("Resource shortages"), "the shortage section shows");
|
||||
this.assertTrue(text.includes("Steel"), "the short resource is named");
|
||||
this.assertTrue(text.includes("1200"), "the amount it is short by is shown");
|
||||
this.assertTrue(text.includes("40%"), "the share of need is shown");
|
||||
} finally {
|
||||
teardownDom(env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NationModalTest extends TestCase {
|
||||
@@ -550,7 +608,10 @@ export class NationModalTest extends TestCase {
|
||||
totalEconomic: 900,
|
||||
net: 545,
|
||||
expenses: [
|
||||
["buildings", 400, 700, [["steel", 250], ["hightech", 150]]],
|
||||
["buildings", 400, 700, [
|
||||
["steel", 250, [["Barracks", 250]]],
|
||||
["hightech", 150, [["Laboratory", 150]]],
|
||||
]],
|
||||
["units", 150, 200, []],
|
||||
],
|
||||
resourceTrade: [["steel", 220, 0], ["food", 0, 90]],
|
||||
@@ -567,8 +628,8 @@ export class NationModalTest extends TestCase {
|
||||
this.assertTrue(text.includes("+¤1 035"), "the month's tax income is shown");
|
||||
this.assertTrue(text.includes("Resource sales"), "resource sales are shown");
|
||||
this.assertTrue(text.includes("Building upkeep"), "a building category is listed");
|
||||
this.assertTrue(text.includes("−¤400"), "the building cash is listed");
|
||||
this.assertTrue(text.includes("materials valued at ¤700"), "the market value is noted");
|
||||
this.assertTrue(text.includes("−¤700"), "the rightmost column is the market value");
|
||||
this.assertTrue(text.includes("cash −¤400"), "the cash the treasury paid is noted");
|
||||
this.assertTrue(text.includes("Unit upkeep"), "a unit category is listed");
|
||||
this.assertTrue(text.includes("Bought Steel"), "a bought resource is listed");
|
||||
this.assertTrue(text.includes("Sold Food"), "a sold resource is listed");
|
||||
@@ -577,11 +638,13 @@ export class NationModalTest extends TestCase {
|
||||
this.assertTrue(env.$("#budget-sub").text().includes("Treasury"));
|
||||
this.assertTrue(env.$("#budget-sub").text().includes("January 2000"), "the month is named");
|
||||
|
||||
// Expanding an upkeep row names the resources it bought.
|
||||
// Expanding an upkeep row names the resources it bought and the buildings
|
||||
// that consumed them.
|
||||
env.$("#budget-list tr .budget-name.clickable").first().click();
|
||||
const expanded = env.$("#budget-list").text();
|
||||
this.assertTrue(expanded.includes("Steel"), "the expanded row names steel");
|
||||
this.assertTrue(expanded.includes("−¤250"), "and what it paid for it");
|
||||
this.assertTrue(expanded.includes("Barracks"), "and the building that used it");
|
||||
|
||||
// The pager steps back to the previous month's digest.
|
||||
env.$("#budget-months .budget-month-nav").first().click();
|
||||
@@ -755,10 +818,39 @@ export class NationModalTest extends TestCase {
|
||||
},
|
||||
privateCash: 5000,
|
||||
cities: [{ id: 1, civ: 0, name: "Paris", cashValue: 5000 }],
|
||||
currencyHoldings: {
|
||||
code: "FRF",
|
||||
symbol: "₣",
|
||||
name: "French Franc",
|
||||
value: 1,
|
||||
treasury: 1000,
|
||||
treasuryDebt: 0,
|
||||
own: [{ cityId: 1, name: "Paris", civ: 0, civName: "France", amount: 5000 }],
|
||||
ownTotal: 5000,
|
||||
foreign: [{ cityId: 2, name: "London", civ: 1, civName: "Britain", amount: 700 }],
|
||||
foreignTotal: 700,
|
||||
banks: [{ civ: 1, name: "Britain", amount: 1234 }],
|
||||
bankTotal: 1234,
|
||||
total: 7934,
|
||||
},
|
||||
};
|
||||
const resourcesInfo = {
|
||||
summary: {
|
||||
stock: {},
|
||||
need: {},
|
||||
production: {},
|
||||
consumed: { steel: 1200, food: 10, luxury: 0, hightech: 0, energy: 0 },
|
||||
external: {
|
||||
buy: { steel: { qty: 300, value: 180_000 } },
|
||||
sell: { food: { qty: 40, value: 12_000 } },
|
||||
},
|
||||
prices: {},
|
||||
},
|
||||
market: [],
|
||||
};
|
||||
modal.show(
|
||||
GOVERNMENTS, 0, TECHNOLOGIES, new Set(), 1.0e9, 1.0e15, 1.0e6,
|
||||
{ months: [], cities: [] }, null, [], null, null, null, money
|
||||
{ months: [], cities: [] }, null, [], null, null, resourcesInfo, money
|
||||
);
|
||||
env.$("#modal-nation .tab[data-tab=economy]").click();
|
||||
this.assertTrue(env.$("#tab-economy").hasClass("active"));
|
||||
@@ -775,6 +867,24 @@ export class NationModalTest extends TestCase {
|
||||
env.$("#economy-subtabs .subtab[data-view=rates]").click();
|
||||
this.assertTrue(env.$("#economy-rates").text().includes("British Pound"), "the rate table shows");
|
||||
this.assertTrue(env.$("#economy-rates").text().includes("0.8500"), "the rate is shown");
|
||||
|
||||
env.$("#economy-subtabs .subtab[data-view=consumption]").click();
|
||||
const consumption = env.$("#economy-consumption").text();
|
||||
this.assertTrue(consumption.includes("Steel"), "the consumed resource is listed");
|
||||
this.assertTrue(consumption.includes("1200"), "its daily consumption is shown");
|
||||
this.assertTrue(consumption.includes("180 000"), "what was spent abroad is shown");
|
||||
this.assertTrue(consumption.includes("40"), "what was sold abroad is shown");
|
||||
|
||||
env.$("#economy-subtabs .subtab[data-view=currency]").click();
|
||||
const currency = env.$("#economy-currency").text();
|
||||
this.assertTrue(
|
||||
env.$("#economy-sub").text().includes("French Franc"),
|
||||
"the currency is named"
|
||||
);
|
||||
this.assertTrue(currency.includes("Paris"), "a home region holding it is listed");
|
||||
this.assertTrue(currency.includes("London"), "a foreign region holding it is listed");
|
||||
this.assertTrue(currency.includes("Central bank of Britain"), "a foreign bank is listed");
|
||||
this.assertTrue(currency.includes("7 934"), "the total in circulation is shown");
|
||||
} finally {
|
||||
teardownDom(env);
|
||||
}
|
||||
|
||||
+67
-11
@@ -39,6 +39,30 @@ export class MoneyTest extends TestCase {
|
||||
this.assertApprox(state.getBudget(0), budgetBefore - 1_000_000, 1e-6);
|
||||
}
|
||||
|
||||
test_a_government_purchase_pays_the_foreign_seller() {
|
||||
const state = smallState();
|
||||
const buyer = cityOf(state, 0);
|
||||
const seller = cityOf(state, 1);
|
||||
const sellerCode = state.currencyOf(1).code;
|
||||
// Empty every home store so only the foreign region can supply.
|
||||
for (const city of state.cities) {
|
||||
if (city.civ === 0) state.getCityResourceStock(city).food = 0;
|
||||
}
|
||||
state.getCityResourceStock(seller).food = 1.0e9;
|
||||
const before = state.getRegionCash(seller).get(sellerCode) || 0;
|
||||
const payer = state._treasuryPayer(0);
|
||||
const bought = state._procureFromNeighbours(
|
||||
buyer, "food", 1000, { food: 0 }, state._resourceNodes(), { payer }
|
||||
);
|
||||
this.assertGreater(bought, 0, "the foreign region sold food");
|
||||
this.assertGreater(
|
||||
state.getRegionCash(seller).get(sellerCode) || 0,
|
||||
before,
|
||||
"the foreign seller was paid"
|
||||
);
|
||||
this.assertGreater(payer.total, 0, "the treasury paid for it");
|
||||
}
|
||||
|
||||
test_taxes_are_taken_from_the_regions() {
|
||||
const state = smallState();
|
||||
const code = state.currencyOf(0).code;
|
||||
@@ -60,14 +84,45 @@ export class MoneyTest extends TestCase {
|
||||
this.assertApprox(this.totalMoney(state), before, 1, "the spend moved money, it did not mint it");
|
||||
}
|
||||
|
||||
test_a_market_purchase_neither_creates_nor_destroys_money() {
|
||||
test_a_cross_border_purchase_neither_creates_nor_destroys_money() {
|
||||
const state = smallState();
|
||||
state.setCurrencyValue = () => {};
|
||||
const city = cityOf(state, 0);
|
||||
const buyer = cityOf(state, 0);
|
||||
const seller = cityOf(state, 1);
|
||||
for (const city of state.cities) {
|
||||
if (city.civ === 0) state.getCityResourceStock(city).food = 0;
|
||||
}
|
||||
state.getCityResourceStock(seller).food = 1.0e9;
|
||||
const before = this.totalMoney(state);
|
||||
state._chargeRegionCash(city, state.currencyOf(0).code, 2_000_000);
|
||||
state._payWorld(0, 2_000_000, state.currencyOf(0).code);
|
||||
this.assertApprox(this.totalMoney(state), before, 1, "the import stayed in the world");
|
||||
state._procureFromNeighbours(
|
||||
buyer, "food", 1000, { food: 0 }, state._resourceNodes()
|
||||
);
|
||||
this.assertApprox(this.totalMoney(state), before, 1, "the trade moved money, it did not mint it");
|
||||
}
|
||||
|
||||
test_currency_holdings_add_up_to_the_total_in_circulation() {
|
||||
const state = smallState();
|
||||
const code = state.currencyOf(0).code;
|
||||
const city = cityOf(state, 0);
|
||||
state._creditRegionCash(city, code, 1_000);
|
||||
const holdings = state.getCurrencyHoldings(0);
|
||||
const own = holdings.own.find((entry) => entry.cityId === city.id);
|
||||
this.assertNotNull(own, "the region is listed");
|
||||
this.assertApprox(own.amount, state.getRegionCash(city).get(code));
|
||||
const expected =
|
||||
holdings.treasury + holdings.ownTotal + holdings.foreignTotal + holdings.bankTotal;
|
||||
this.assertApprox(holdings.total, expected, 1e-6);
|
||||
}
|
||||
|
||||
test_currency_holdings_list_foreign_central_bank_reserves() {
|
||||
const state = smallState();
|
||||
const code = state.currencyOf(0).code;
|
||||
state._addReserve(1, code, 5_000);
|
||||
const holdings = state.getCurrencyHoldings(0);
|
||||
const bank = holdings.banks.find((entry) => entry.civ === 1);
|
||||
this.assertNotNull(bank, "the foreign central bank is listed");
|
||||
this.assertApprox(bank.amount, 5_000);
|
||||
this.assertApprox(holdings.bankTotal, 5_000);
|
||||
}
|
||||
|
||||
test_every_civilisation_has_a_currency() {
|
||||
@@ -211,14 +266,15 @@ export class MoneyTest extends TestCase {
|
||||
test_a_broke_region_buys_by_borrowing() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
const portIndex = state.protoBuildings.findIndex((b) => b.id === "port");
|
||||
const coastal = state.cities.find((c) => c.civ === 0 && state.isCoastalCity(c));
|
||||
if (!coastal) return;
|
||||
coastal.buildings[portIndex] = 1;
|
||||
this.assertTrue(state._civMarketAccess(0), "a port opens the market");
|
||||
const nodes = state._resourceNodes();
|
||||
const supplier = state._nearbySuppliers(city, "food", nodes)[0];
|
||||
this.assertNotNull(supplier, "a supplier is reachable");
|
||||
// Leave one supplier with a surplus and empty the buyer's purse.
|
||||
for (const [, stock] of state.resourceStock) stock.food = 0;
|
||||
supplier.stock.food = 1.0e9;
|
||||
state.getRegionCash(city).set(state.currencyOf(0).code, 0);
|
||||
const store = state.getCityResourceStock(city);
|
||||
const bought = state._buyMarketMaterial(city, "food", 100, store);
|
||||
const bought = state._procureFromNeighbours(city, "food", 100, store, nodes);
|
||||
this.assertGreater(bought, 0, "the purchase still goes through");
|
||||
this.assertGreater(state.getRegionDebt(city), 0, "it was funded by borrowing");
|
||||
}
|
||||
|
||||
@@ -372,6 +372,13 @@ export class PoliticsModelTest extends TestCase {
|
||||
state.budgets.set(1, 1e15);
|
||||
for (const city of state.cities) {
|
||||
state.getRegionCash(city).set(state.currencyOf(city.civ).code, 1e15);
|
||||
// With no global market, money alone does not feed anyone: stock the
|
||||
// stores so no shortage can drown out the museum.
|
||||
const stock = state.getCityResourceStock(city);
|
||||
stock.food = 1e15;
|
||||
stock.steel = 1e15;
|
||||
stock.luxury = 1e15;
|
||||
stock.hightech = 1e15;
|
||||
}
|
||||
const museum = BUILDINGS.findIndex((b) => b.id === "museum");
|
||||
const city = state.cities.find((c) => c.civ === 0);
|
||||
|
||||
@@ -104,7 +104,7 @@ export class EconomyOrderEdgesTest extends TestCase {
|
||||
this.assertTrue(state.requestBuild(city.id, 0), "levels keep rising without a cap");
|
||||
}
|
||||
|
||||
test_demolishing_removes_one_level_and_refunds() {
|
||||
test_demolishing_removes_one_level_without_refunding() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
state.budgets.set(0, 1e15);
|
||||
@@ -113,7 +113,7 @@ export class EconomyOrderEdgesTest extends TestCase {
|
||||
const budget = state.getBudget(0);
|
||||
this.assertTrue(state.requestDemolish(city.id, 0));
|
||||
this.assertEqual(state.getCityBuildingLevel(city, 0), 0);
|
||||
this.assertGreater(state.getBudget(0), budget, "the refund raises the budget");
|
||||
this.assertApprox(state.getBudget(0), budget, 1e-6, "downgrading refunds nothing");
|
||||
}
|
||||
|
||||
test_cancelling_an_out_of_range_queue_entry_fails() {
|
||||
|
||||
+241
-16
@@ -8,7 +8,7 @@ import {
|
||||
EFFECT_RESEARCH,
|
||||
technologyCost,
|
||||
} from "../shared/data.js";
|
||||
import { DAYS_PER_YEAR } from "../shared/game_state/constants.js";
|
||||
import { DAYS_PER_YEAR, HOURS_PER_DAY } from "../shared/game_state/constants.js";
|
||||
import {
|
||||
formatEnergy,
|
||||
formatResourceAmount,
|
||||
@@ -128,6 +128,135 @@ export class ResourceModelTest extends TestCase {
|
||||
this.assertApprox(steel.sold, 400);
|
||||
}
|
||||
|
||||
test_external_trade_ledger_counts_buying_and_selling() {
|
||||
const state = smallState();
|
||||
state._recordExternalBuy(0, "steel", 100, 1_000);
|
||||
state._recordExternalSell(0, "food", 40, 500);
|
||||
const summary = state.getCivResourceSummary(0);
|
||||
this.assertApprox(summary.external.buy.steel.qty, 100);
|
||||
this.assertApprox(summary.external.buy.steel.value, 1_000);
|
||||
this.assertApprox(summary.external.sell.food.qty, 40);
|
||||
this.assertApprox(summary.external.sell.food.value, 500);
|
||||
}
|
||||
|
||||
test_the_day_s_shortfalls_are_bought_abroad() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1.0e15);
|
||||
for (const city of state.cities) {
|
||||
if (city.civ !== 0) continue;
|
||||
const stock = state.getCityResourceStock(city);
|
||||
stock.food = 0;
|
||||
stock.steel = 0;
|
||||
stock.luxury = 0;
|
||||
}
|
||||
for (let i = 0; i < HOURS_PER_DAY; i++) state.advanceHour();
|
||||
const summary = state.getCivResourceSummary(0);
|
||||
const bought = Object.values(summary.external.buy)
|
||||
.reduce((sum, entry) => sum + entry.qty, 0);
|
||||
this.assertGreater(bought, 0, "the day's shortfalls were bought across the border");
|
||||
const spent = Object.values(summary.external.buy)
|
||||
.reduce((sum, entry) => sum + entry.value, 0);
|
||||
this.assertGreater(spent, 0, "and the money it cost is recorded");
|
||||
}
|
||||
|
||||
test_a_region_reports_what_it_ran_short_of() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
state._resourceShortages.set(city.id, { steel: 0.5 });
|
||||
const shortages = state.getRegionShortages(city);
|
||||
this.assertTrue(shortages.steel, "steel is reported");
|
||||
this.assertApprox(shortages.steel.fraction, 0.5);
|
||||
const population = state.getCityEconomy(city).population;
|
||||
this.assertApprox(shortages.steel.amount, 0.5 * steelNeedPerDay(population));
|
||||
}
|
||||
|
||||
test_the_snapshot_ships_each_region_s_shortages() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
state._resourceShortages.set(city.id, { food: 0.25 });
|
||||
const snapshot = state.snapshot(0);
|
||||
const stats = snapshot.cityStats.find((entry) => entry.id === city.id);
|
||||
this.assertTrue(stats.shortage.food, "the shortage is shipped");
|
||||
this.assertApprox(stats.shortage.food.fraction, 0.25);
|
||||
}
|
||||
|
||||
test_a_research_building_without_high_tech_raises_a_notice() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
grantBuilding(state, city, "research_lab");
|
||||
for (const own of state.cities) {
|
||||
if (own.civ === 0) state.getCityResourceStock(own).hightech = 0;
|
||||
}
|
||||
state._tickBuildings();
|
||||
const alert = state._resourceAlertsFor(0)
|
||||
.find((entry) => entry.resource === "hightech" && entry.region === city.name);
|
||||
this.assertNotNull(alert, "the lab's missing high-tech is reported");
|
||||
this.assertTrue(alert.building.includes("Research lab"), "the building is named");
|
||||
}
|
||||
|
||||
test_a_power_starved_work_raises_a_notice() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1e15);
|
||||
const city = cityOf(state, 0);
|
||||
const tile = freeLandTile(state, 0);
|
||||
const k = placeResourceBuilding(state, tile, "steel_mill", 0);
|
||||
const components = new Map([
|
||||
[k, 0],
|
||||
[key(city.coords.x, city.coords.y), 0],
|
||||
]);
|
||||
const ledger = new Map([[0, { civ: 0, industrialScale: 0.5 }]]);
|
||||
state._noteGridPowerShortages(ledger, components);
|
||||
const alert = state._resourceAlertsFor(0)
|
||||
.find((entry) => entry.building === "Steel mill" && entry.resource === "energy");
|
||||
this.assertNotNull(alert, "the idle work is reported");
|
||||
this.assertEqual(alert.region, city.name);
|
||||
}
|
||||
|
||||
test_region_resource_info_reports_reserves_value_and_consumption() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
const store = state.getCityResourceStock(city);
|
||||
store.food = 1000;
|
||||
store.steel = 2000;
|
||||
store.hightech = 50;
|
||||
store.luxury = 10;
|
||||
const info = state.getRegionResourceInfo(city);
|
||||
this.assertEqual(info.stock.food, 1000);
|
||||
this.assertEqual(info.stock.steel, 2000);
|
||||
const expected = 1000 * state.getResourcePrice("food") +
|
||||
2000 * state.getResourcePrice("steel") +
|
||||
50 * state.getResourcePrice("hightech") +
|
||||
10 * state.getResourcePrice("luxury");
|
||||
this.assertApprox(info.value, expected);
|
||||
const population = state.getCityEconomy(city).population;
|
||||
this.assertApprox(info.consumption.food, foodNeedPerDay(population));
|
||||
this.assertApprox(info.consumption.steel, steelNeedPerDay(population));
|
||||
this.assertApprox(info.consumption.luxury, luxuryNeedPerDay(population));
|
||||
this.assertTrue(info.consumption.energy > 0, "grid demand draws power");
|
||||
}
|
||||
|
||||
test_city_stats_ship_the_region_resources() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
state.getCityResourceStock(city).steel = 42;
|
||||
const stats = state.snapshot(0).cityStats.find((entry) => entry.id === city.id);
|
||||
this.assertNotNull(stats.resources, "the region's reserves travel in the snapshot");
|
||||
this.assertEqual(stats.resources.stock.steel, 42);
|
||||
this.assertTrue(stats.resources.value > 0, "the reserve carries a value");
|
||||
}
|
||||
|
||||
test_shortage_notices_are_private_to_the_owner() {
|
||||
const state = smallState();
|
||||
state._noteResourceShortage(0, "University", "Paris", "hightech");
|
||||
const mine = state.snapshot(0).viewerStats.resourceAlerts;
|
||||
const theirs = state.snapshot(1).viewerStats.resourceAlerts;
|
||||
this.assertSize(mine, 1, "the owner sees the notice");
|
||||
this.assertSize(theirs, 0, "another nation does not");
|
||||
this.assertEqual(mine[0].building, "University");
|
||||
this.assertEqual(mine[0].resource, "hightech");
|
||||
this.assertGreaterOrEqual(mine[0].day, 0);
|
||||
}
|
||||
|
||||
test_national_gdp_per_capita_matches_the_regions() {
|
||||
const state = smallState();
|
||||
const snapshot = state.snapshot(0);
|
||||
@@ -145,29 +274,64 @@ export class ResourceModelTest extends TestCase {
|
||||
test_construction_draws_steel_from_the_city_before_the_market() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
const code = state.currencyOf(0).code;
|
||||
const stock = state.getCityResourceStock(city);
|
||||
stock.steel = 1000;
|
||||
const cashBefore = state.getRegionCash(city).get(code) || 0;
|
||||
const before = state.getBudget(0);
|
||||
const ok = state._payConstructionResources(0, city.coords, { steel: 400, hightech: 0 });
|
||||
this.assertTrue(ok);
|
||||
this.assertApprox(stock.steel, 600, 1e-6, "steel came out of the store");
|
||||
this.assertApprox(state.getBudget(0), before, 1e-6, "no market bill was needed");
|
||||
// The region is not charged: the state buys the steel from it.
|
||||
const value = 400 * state.getResourcePrice("steel") / state.currencyValue(0);
|
||||
this.assertApprox(state.getBudget(0), before - value, 1e-3, "the state paid for it");
|
||||
this.assertApprox(
|
||||
(state.getRegionCash(city).get(code) || 0) - cashBefore,
|
||||
value,
|
||||
1e-3,
|
||||
"the region received the money"
|
||||
);
|
||||
}
|
||||
|
||||
test_construction_buys_the_shortfall_on_the_market() {
|
||||
test_drawing_a_national_store_pays_the_region() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
const code = state.currencyOf(0).code;
|
||||
state.getCityResourceStock(city).steel = 1000;
|
||||
const cashBefore = state.getRegionCash(city).get(code) || 0;
|
||||
const before = state.getBudget(0);
|
||||
const payer = state._treasuryPayer(0);
|
||||
const drawn = state._drawFromNearest(
|
||||
0, city.coords, { steel: 400, hightech: 0 }, payer
|
||||
);
|
||||
this.assertApprox(drawn.steel, 400);
|
||||
const value = 400 * state.getResourcePrice("steel") / state.currencyValue(0);
|
||||
this.assertApprox(payer.total, value, 1e-3, "the treasury carried the bill");
|
||||
this.assertApprox(state.getBudget(0), before - value, 1e-3, "the state paid for it");
|
||||
this.assertApprox(
|
||||
(state.getRegionCash(city).get(code) || 0) - cashBefore,
|
||||
value,
|
||||
1e-3,
|
||||
"the region's private sector received it"
|
||||
);
|
||||
}
|
||||
|
||||
test_construction_buys_the_shortfall_from_a_reachable_region() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1e15);
|
||||
const city = cityOf(state, 0);
|
||||
// Every city of the nation must be empty, or the draw would find another
|
||||
// store before it reaches the market.
|
||||
const seller = cityOf(state, 1);
|
||||
// Every city of the nation must be empty, or the draw would find their
|
||||
// stores before it reaches a foreign region.
|
||||
for (const own of state.cities) {
|
||||
if (own.civ === 0) state.getCityResourceStock(own).steel = 0;
|
||||
}
|
||||
const price = state.getResourcePrice("steel");
|
||||
state.getCityResourceStock(seller).steel = 1.0e9;
|
||||
const before = state.getBudget(0);
|
||||
const ok = state._payConstructionResources(0, city.coords, { steel: 1000, hightech: 0 });
|
||||
this.assertTrue(ok);
|
||||
this.assertApprox(before - state.getBudget(0), 1000 * price, 1e-3);
|
||||
this.assertTrue(ok, "the steel was found across the border");
|
||||
this.assertLess(state.getBudget(0), before, "the treasury paid for it");
|
||||
this.assertLess(state.getCityResourceStock(seller).steel, 1.0e9, "the seller's store shrank");
|
||||
}
|
||||
|
||||
test_research_spends_high_tech() {
|
||||
@@ -253,6 +417,65 @@ export class ResourceModelTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
test_materials_come_from_home_regions_before_foreign_ones() {
|
||||
const state = smallState();
|
||||
const buyer = cityOf(state, 0);
|
||||
const land = state.topology.neighbours(buyer.coords.x, buyer.coords.y)
|
||||
.filter((n) => state._isLand(n));
|
||||
this.assertGreaterOrEqual(land.length, 2, "the city has land neighbours");
|
||||
for (const n of land) state.roads.add(key(n.x, n.y));
|
||||
// Two reachable suppliers, home and foreign, both with a surplus. Home wins.
|
||||
const homeNode = { coords: land[0], civ: 0, stock: { food: 1.0e9 }, kind: "building" };
|
||||
const foreignNode = { coords: land[1], civ: 1, stock: { food: 1.0e9 }, kind: "building" };
|
||||
const store = state.getCityResourceStock(buyer);
|
||||
const bought = state._procureFromNeighbours(
|
||||
buyer, "food", 1000, store, [homeNode, foreignNode]
|
||||
);
|
||||
this.assertGreater(bought, 0, "the order was filled");
|
||||
this.assertApprox(homeNode.stock.food, 1.0e9 - 1000, 1e-6, "the home region supplied it");
|
||||
this.assertApprox(foreignNode.stock.food, 1.0e9, 1e-6, "the foreign region was left alone");
|
||||
}
|
||||
|
||||
test_without_a_market_unreachable_goods_are_not_bought() {
|
||||
const state = smallState();
|
||||
const buyer = cityOf(state, 0);
|
||||
for (const [, stock] of state.resourceStock) stock.food = 0;
|
||||
state.roads.clear();
|
||||
state.railways.clear();
|
||||
const store = state.getCityResourceStock(buyer);
|
||||
const bought = state._procureFromNeighbours(
|
||||
buyer, "food", 1000, store, state._resourceNodes(), { allowSea: false }
|
||||
);
|
||||
this.assertEqual(bought, 0, "no trade route, no goods");
|
||||
this.assertEqual(store.food, 0, "and nothing arrives");
|
||||
}
|
||||
|
||||
test_power_flows_between_reachable_grids() {
|
||||
const state = smallState();
|
||||
const city = cityOf(state, 0);
|
||||
const neighbour = state.topology.neighbours(city.coords.x, city.coords.y)
|
||||
.find((n) => state._isLand(n) && state.civAt(n) === 0);
|
||||
this.assertNotNull(neighbour, "the city has an owned land neighbour");
|
||||
const nk = key(neighbour.x, neighbour.y);
|
||||
state.roads.add(nk);
|
||||
const components = new Map([
|
||||
[key(city.coords.x, city.coords.y), 0],
|
||||
[nk, 1],
|
||||
]);
|
||||
const ledger = new Map([
|
||||
[0, {
|
||||
civ: 0, supply: 0, demand: 1000, imports: 0,
|
||||
cityEnergy: 1000, converterEnergy: 0, foodEnergy: 0,
|
||||
}],
|
||||
[1, {
|
||||
civ: 0, supply: 5000, demand: 0, imports: 0,
|
||||
cityEnergy: 0, converterEnergy: 0, foodEnergy: 0,
|
||||
}],
|
||||
]);
|
||||
state._tradeGridPower(ledger, components);
|
||||
this.assertApprox(ledger.get(0).imports, 1000, 1e-6, "the deficit grid imported the power");
|
||||
}
|
||||
|
||||
test_foreign_trade_runs_on_the_transport_network() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1e15);
|
||||
@@ -480,6 +703,12 @@ export class ResourceModelTest extends TestCase {
|
||||
test_fusion_needs_its_technology() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1e15);
|
||||
for (const own of state.cities) {
|
||||
if (own.civ !== 0) continue;
|
||||
const stock = state.getCityResourceStock(own);
|
||||
stock.steel = 1.0e12;
|
||||
stock.hightech = 1.0e12;
|
||||
}
|
||||
const tile = freeLandTile(state, 0);
|
||||
this.assertFalse(
|
||||
state.requestBuildTileImprovement(0, tile, "fusion_power_plant"),
|
||||
@@ -613,22 +842,18 @@ export class ResourceModelTest extends TestCase {
|
||||
this.assertFalse(inland && state.hasCityBuilding(inland, "port"), "an inland city does not");
|
||||
}
|
||||
|
||||
test_a_city_without_a_port_has_no_foreign_market() {
|
||||
test_a_city_without_a_port_has_no_sea_lane() {
|
||||
const state = smallState();
|
||||
state.budgets.set(0, 1e15);
|
||||
const city = cityOf(state, 0);
|
||||
const store = state.getCityResourceStock(city);
|
||||
const portIndex = state.protoBuildings.findIndex((b) => b.id === "port");
|
||||
for (const own of state.cities) {
|
||||
if (own.civ === 0) delete own.buildings[portIndex];
|
||||
}
|
||||
this.assertFalse(state._civMarketAccess(0), "no working port, no market");
|
||||
this.assertEqual(state._buyMarketMaterial(city, "food", 100, store), 0, "nothing can be bought");
|
||||
this.assertFalse(state._civMarketAccess(0), "no working port, no sea trade");
|
||||
|
||||
const coastal = state.cities.find((c) => c.civ === 0 && state.isCoastalCity(c));
|
||||
this.assertNotNull(coastal, "the nation has a coastal city");
|
||||
coastal.buildings[portIndex] = 1;
|
||||
this.assertTrue(state._civMarketAccess(0), "the port opens the market");
|
||||
this.assertGreater(state._buyMarketMaterial(city, "food", 100, store), 0, "goods now arrive");
|
||||
this.assertTrue(state._civMarketAccess(0), "the port opens the sea lane");
|
||||
}
|
||||
|
||||
test_air_defense_spends_ammunition_when_it_fires() {
|
||||
|
||||
Reference in New Issue
Block a user