1129 lines
46 KiB
JavaScript
1129 lines
46 KiB
JavaScript
// Simple-economy overrides for GameState. When a game runs the simple model,
|
|
// `configure()` assigns these methods onto the instance, so they shadow the
|
|
// hard-economy mixins on the prototype without changing them. The hard model
|
|
// needs no override module: it is the existing mixins untouched.
|
|
//
|
|
// Instance-level assignment is safe because the economy model is fixed for the
|
|
// life of a game and the browser never runs a live GameState. Name every
|
|
// override after the method it replaces, so the diff is obvious.
|
|
//
|
|
// Decision (02-resource-pool-and-production.md): every nation keeps **one
|
|
// resource pool** holding the four storable goods, available in every region.
|
|
// Energy is deliberately **not** pooled: it stays a per-region flow so
|
|
// `03-construction.md` can key construction speed to the region's available
|
|
// power. Production buildings are public -- their output lands in the owner's
|
|
// pool -- and the private-agent accounting (cash, debt, sales, upgrades) is
|
|
// retired by overriding the agent hooks to no-ops. The daily resource tick
|
|
// itself is the hard one, re-pointed at the pool: `_ensureResourceStock` now
|
|
// returns the owner's pool, `getCityResourceStock` hands out that same pool,
|
|
// and the sourcing searches are overridden to find nothing, so every draw is a
|
|
// single subtraction from the pool.
|
|
//
|
|
// Decision (03-construction.md): an order starts building at once. Its whole
|
|
// steel/high-tech bill is drawn from the pool the moment it is placed -- a bill
|
|
// the pool cannot cover is refused rather than gathered over days -- and there
|
|
// is no materials phase, no energy-buy phase and no trade-graph lookup. The
|
|
// construction or training clock is set from the spare power of the region the
|
|
// work stands in: `regionAvailableEnergy` is the region's plant output less what
|
|
// its cities, buildings and converters already draw. A guaranteed baseline
|
|
// covers a region whose grid has nothing to spare, so a starter city on any map
|
|
// raises a modern-infantry unit in the 45-hour reference time and no work ever
|
|
// divides by zero; spare power above the baseline only makes a work quicker.
|
|
// Money is not spent
|
|
// internally on construction (the single tax and the market cover that in
|
|
// `05-`/`06-`), so the treasury gates nothing. Every opening region is guaranteed
|
|
// at least one plant -- a fuel-free one where the hard seeding left it without --
|
|
// so the speed rule never starts at zero.
|
|
//
|
|
// The private demand-driven upgrade stays retired (02-); `_startBuildingUpgrade`
|
|
// is overridden so that if an upgrade is ever undertaken it is a public,
|
|
// immediate, energy-driven work rather than an agent-funded one.
|
|
//
|
|
// Decision (07-procurement.md): every consumer sources from the one pool. The
|
|
// sourcing search is emptied (`_nearbySuppliers`/`_resourceNodes` find nothing,
|
|
// `_procureFromNeighbours` returns zero) and the draw helpers
|
|
// (`_drawFromNearest`/`_drawFromCities`) are re-pointed at the pool, so upkeep,
|
|
// city consumption, combat, repairs and unit supply all take a single
|
|
// subtraction -- no bidding, no distance, no freight, never a higher price for a
|
|
// shortfall. The construction-site and queued-build gather passes are overridden
|
|
// to the pool too, so even a stray call from the hard daily tick cannot rebuild
|
|
// the trade graph. The day's ledger (`resourceSpend`, consumption/production
|
|
// totals) is still filled by the hard tick; only the sourcing changed.
|
|
|
|
import { key, parseKey } from "../hex.js";
|
|
import {
|
|
RESOURCE_IDS,
|
|
STORABLE_RESOURCE_IDS,
|
|
RESOURCE_MARKET_BASE,
|
|
SIMPLE_ECONOMY,
|
|
simpleConstructionHours,
|
|
MARKET_START_STOCK,
|
|
MARKET_LOTS,
|
|
marketPriceFor,
|
|
EFFECT_TAX_INCOME,
|
|
TAXES,
|
|
tileImprovementById,
|
|
isResourceTileBuilding,
|
|
resourceBuildingUpgradeMoneyCost,
|
|
} from "../data.js";
|
|
import { buildingBuildCost } from "../rules.js";
|
|
import {
|
|
cityEnergyPerDay,
|
|
foodNeedPerDay,
|
|
steelNeedPerDay,
|
|
luxuryNeedPerDay,
|
|
tileFoodOutput,
|
|
tileImprovementResourceCost,
|
|
protoUpkeep,
|
|
} from "../resources.js";
|
|
import { TRAINING_QUEUE_LIMIT, TILE_IMPROVEMENT_HP, HOURS_PER_DAY } from "./constants.js";
|
|
|
|
// A fresh pool of the four storable goods. Mirrors the hard economy's
|
|
// `emptyStock`; the simple model never owns per-tile stock.
|
|
function emptyPool() {
|
|
return { steel: 0, food: 0, luxury: 0, hightech: 0 };
|
|
}
|
|
|
|
// The bill a construction site or queue entry reports. Shaped like the hard
|
|
// one's `emptyResources` so the snapshot and the panels keep their fields.
|
|
function emptySiteResources() {
|
|
return { steel: 0, hightech: 0, energy: 0 };
|
|
}
|
|
|
|
export const simpleEconomyMethods = {
|
|
// -------------------------------------------------------------- pool --
|
|
|
|
// Builds the pool collection and the market series exactly as the hard
|
|
// initialiser does, but seeds one pool per nation instead of a store for
|
|
// every city and resource tile. No `_seedCityResources` call: there is no
|
|
// per-tile stock in the simple economy.
|
|
_initResources() {
|
|
this.resourcePools = new Map();
|
|
// Kept as an empty map so the hard methods that guard on `resourceStock`
|
|
// (the tick, unit supply) still run and the ones that iterate it find
|
|
// nothing to do. Nothing is ever stored here in simple mode.
|
|
this.resourceStock = new Map();
|
|
this.resourcePrices = new Map();
|
|
this.resourceMarketStats = new Map();
|
|
for (const id of RESOURCE_IDS) {
|
|
this.resourcePrices.set(id, RESOURCE_MARKET_BASE[id]);
|
|
this.resourceMarketStats.set(id, { supply: 0, demand: 0, price: RESOURCE_MARKET_BASE[id] });
|
|
}
|
|
this.resourceMonthlyPrices = new Map();
|
|
for (const id of RESOURCE_IDS) this.resourceMonthlyPrices.set(id, [RESOURCE_MARKET_BASE[id]]);
|
|
this.resourceDailyPrices = new Map();
|
|
for (const id of RESOURCE_IDS) this.resourceDailyPrices.set(id, [RESOURCE_MARKET_BASE[id]]);
|
|
this.resourceLinks = [];
|
|
this._resourceShortages = new Map();
|
|
this._resourceGraphCache = null;
|
|
this._relayStock = new Map();
|
|
this._famine = new Set();
|
|
this._resourceVersion = 0;
|
|
this.resourceTrade = new Map();
|
|
this.resourceExternal = new Map();
|
|
this._resourceAlerts = new Map();
|
|
this.resourceSpend = new Map();
|
|
this.cityRepairQueue = new Map();
|
|
this.tileRepairDebt = new Map();
|
|
for (let civ = 0; civ < this.civilisations.length; civ++) {
|
|
const pool = this._ensurePool(civ);
|
|
for (const id of STORABLE_RESOURCE_IDS) pool[id] = SIMPLE_ECONOMY.startingPool[id] || 0;
|
|
}
|
|
this._initSimpleMarket();
|
|
},
|
|
|
|
// The one pool every region of a nation draws on, created on first use.
|
|
_ensurePool(civ) {
|
|
if (!this.resourcePools) this.resourcePools = new Map();
|
|
let pool = this.resourcePools.get(civ);
|
|
if (!pool) {
|
|
pool = emptyPool();
|
|
this.resourcePools.set(civ, pool);
|
|
}
|
|
return pool;
|
|
},
|
|
|
|
// A nation's whole resource pool. Callers that used to ask a city for its
|
|
// store now ask the owner for the pool.
|
|
getResourcePool(civ) {
|
|
return this._ensurePool(civ);
|
|
},
|
|
|
|
// Takes up to `amount` of one good out of a nation's pool and returns what
|
|
// was actually drawn: the single procurement primitive of the simple
|
|
// economy. Never returns negative and never bids a price up.
|
|
_drawFromPool(civ, id, amount) {
|
|
const pool = this._ensurePool(civ);
|
|
const available = Math.max(0, pool[id] || 0);
|
|
const take = Math.min(available, Math.max(0, amount));
|
|
if (take > 0) pool[id] = available - take;
|
|
return take;
|
|
},
|
|
|
|
// The public producers write here. The hard tick adds each work's output to
|
|
// `_ensureResourceStock(k)`, so mapping that onto the owner's pool is all it
|
|
// takes for every producer -- food from the land, materials from the
|
|
// converters -- to feed the shared pool.
|
|
_ensureResourceStock(k) {
|
|
const coords = parseKey(k);
|
|
const city = this.cityAt(coords);
|
|
let civ = city ? city.civ : this.tileImprovementOwner.get(k);
|
|
if (civ === undefined || civ === null || civ < 0) civ = this.civAt(coords);
|
|
if (civ === undefined || civ === null || civ < 0) return emptyPool();
|
|
return this._ensurePool(civ);
|
|
},
|
|
|
|
// The read every surviving consumer makes: a city's stock is its owner's
|
|
// pool, so a region has exactly what the nation holds.
|
|
getCityResourceStock(city) {
|
|
return this._ensurePool(city.civ);
|
|
},
|
|
|
|
// ------------------------------------------------- global market (06) --
|
|
|
|
// The one world stock of each storable good, opening full. A single shared
|
|
// stock, not a per-player order book: every nation trades against the same
|
|
// figures, so the price one player moves is the price the next one pays.
|
|
// Energy is not traded -- it stays a per-region flow -- so only the four
|
|
// storable goods are stocked.
|
|
_initSimpleMarket() {
|
|
this.marketStock = new Map();
|
|
for (const id of STORABLE_RESOURCE_IDS) this.marketStock.set(id, MARKET_START_STOCK);
|
|
this._marketVersion = (this._marketVersion || 0) + 1;
|
|
},
|
|
|
|
// The world price of a good straight from the shared stock. A good with no
|
|
// stock (energy) keeps its base price. Computed rather than memoised: it is
|
|
// one Map lookup and a pow, and deriving it keeps the stock the single source
|
|
// of truth, so no stale cache can lag a trade.
|
|
marketPrice(id) {
|
|
const stock = this.marketStock ? this.marketStock.get(id) : undefined;
|
|
if (stock === undefined) return RESOURCE_MARKET_BASE[id] || 1;
|
|
return marketPriceFor(RESOURCE_MARKET_BASE[id], stock);
|
|
},
|
|
|
|
// Every surviving caller -- upkeep valuation, construction quotes, food
|
|
// synthesis, the panels -- reads the market price in the simple economy, so
|
|
// one override keeps them all in step with the stock. Closes the loop with
|
|
// the daily tick: a trade that drains the stock raises what the next order
|
|
// costs and what the stores are worth.
|
|
getResourcePrice(id) {
|
|
return this.marketPrice(id);
|
|
},
|
|
|
|
// The simple market block of the daily tick. Prices come from the world
|
|
// stock, not from the day's supply and demand, so the hard drift has nothing
|
|
// to do: the stock only moves when a player trades.
|
|
_updateMarketPrices(_market) {},
|
|
|
|
// A nation's share of the market, for its Resources panel: the world stock
|
|
// and price of each buyable good, how much of it the nation holds, and the
|
|
// lots it may trade in.
|
|
getMarketView(civ) {
|
|
const pool = this._ensurePool(civ);
|
|
return STORABLE_RESOURCE_IDS.map((id) => ({
|
|
id,
|
|
price: this.marketPrice(id),
|
|
stock: this.marketStock ? (this.marketStock.get(id) || 0) : 0,
|
|
pool: pool[id] || 0,
|
|
lots: MARKET_LOTS.slice(),
|
|
}));
|
|
},
|
|
|
|
// Buys or sells one lot of one good at the world price. Atomic: every check
|
|
// runs before any figure moves, so a refused trade leaves the treasury, the
|
|
// pool and the stock exactly as they were, and there is no overdraft to hide
|
|
// a shortfall. `qty` must be one of the three lots; the server re-validates
|
|
// whatever the client sends.
|
|
requestMarketTrade(civ, resource, side, qty) {
|
|
if (!this._validCiv(civ)) return false;
|
|
if (side !== "buy" && side !== "sell") return false;
|
|
if (!STORABLE_RESOURCE_IDS.includes(resource)) return false;
|
|
const amount = Number(qty);
|
|
if (!MARKET_LOTS.includes(amount)) return false;
|
|
if (!this.marketStock || !this.marketStock.has(resource)) return false;
|
|
const price = this.marketPrice(resource);
|
|
if (!(price > 0) || !Number.isFinite(price)) return false;
|
|
const pool = this._ensurePool(civ);
|
|
const stock = this.marketStock.get(resource) || 0;
|
|
const treasury = this.getBudget(civ);
|
|
if (side === "buy") {
|
|
const cost = amount * price;
|
|
// The world must hold the goods and the nation must hold the money; a
|
|
// near miss is refused whole rather than part-filled.
|
|
if (stock + 1e-9 < amount) return false;
|
|
if (treasury + 1e-9 < cost) return false;
|
|
this.budgets.set(civ, treasury - cost);
|
|
pool[resource] = (pool[resource] || 0) + amount;
|
|
this.marketStock.set(resource, stock - amount);
|
|
this._recordBudgetCash(civ, "provisioning", -cost);
|
|
this._recordBudgetResource(civ, resource, cost, 0);
|
|
this._recordBudgetCategoryResource(civ, "provisioning", resource, cost, "market", amount);
|
|
} else {
|
|
if ((pool[resource] || 0) + 1e-9 < amount) return false;
|
|
const revenue = amount * price;
|
|
pool[resource] = (pool[resource] || 0) - amount;
|
|
this.marketStock.set(resource, stock + amount);
|
|
this.budgets.set(civ, treasury + revenue);
|
|
this._recordBudgetCash(civ, "sell", revenue);
|
|
this._recordBudgetResource(civ, resource, 0, revenue);
|
|
}
|
|
this._marketVersion += 1;
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// ------------------------------------------------- money and taxes (05) --
|
|
|
|
// A simple nation keeps one treasury in one currency. The central bank, its
|
|
// reserves, the FX desk and the private-sector region cash baskets are all
|
|
// hard-economy machinery: the simple model still creates the collection
|
|
// shapes so the serialisers can walk them, but seeds no money anywhere except
|
|
// the fixed opening treasury (`_initBudgets`), so every basket reads empty.
|
|
_initMoney() {
|
|
this.regionCash = new Map();
|
|
this.regionDebt = new Map();
|
|
this.centralBankDebt = new Map();
|
|
this.centralBankRate = new Map();
|
|
this.centralBankReserves = new Map();
|
|
this.currencyValues = new Map();
|
|
this._currencyDemand = new Map();
|
|
this._currencyCivMap = null;
|
|
this._fxSubsidySpent = new Map();
|
|
for (let civ = 0; civ < this.civilisations.length; civ++) {
|
|
this.centralBankRate.set(civ, 0);
|
|
this.centralBankReserves.set(civ, new Map());
|
|
this.centralBankDebt.set(civ, new Map());
|
|
this.currencyValues.set(civ, 1);
|
|
this._currencyDemand.set(civ, 0);
|
|
}
|
|
},
|
|
|
|
// A nation opens with a fixed euro reserve, not the hard model's tenth of
|
|
// GDP: money buys only market goods, so the opening figure is day-one buying
|
|
// power rather than a working balance.
|
|
_initBudgets() {
|
|
for (let i = 0; i < this.civilisations.length; i++) {
|
|
this.budgets.set(i, SIMPLE_ECONOMY.startingTreasury);
|
|
}
|
|
},
|
|
|
|
// The one currency: every nation's prices are quoted in euros, at par.
|
|
currencyOf(_civ) {
|
|
return SIMPLE_ECONOMY.euro;
|
|
},
|
|
|
|
currencyValue(_civ) {
|
|
return 1;
|
|
},
|
|
|
|
currencyValueOf(_code) {
|
|
return 1;
|
|
},
|
|
|
|
exchangeRate(_fromCiv, _toCiv) {
|
|
return 1;
|
|
},
|
|
|
|
// No central bank work runs in the simple tick: nothing borrows, no currency
|
|
// floats with demand and no region repatriates foreign cash.
|
|
_tickMoney() {},
|
|
|
|
requestSetInterestRate(_civ, _rate) {
|
|
return false;
|
|
},
|
|
|
|
// ------------------------------------------------------ taxes (05) --
|
|
|
|
// The day's one revenue: every inhabitant pays `taxPerInhabitant`, scaled by
|
|
// twice their region's derived approval (a 0..2 factor, clamped) and the
|
|
// nation's tax-income modifier. Booked once per day, after the daily ledger
|
|
// reset, so the tax ledger always holds the last full day's take.
|
|
_tickApprovalTaxes() {
|
|
const factorCap = SIMPLE_ECONOMY.approvalTaxFactor;
|
|
for (let civ = 0; civ < this.civilisations.length; civ++) {
|
|
const modifier = this.taxIncomeModifier(civ);
|
|
let total = 0;
|
|
for (const city of this.cities) {
|
|
if (city.civ !== civ) continue;
|
|
const population = Math.max(0, this.getCityPopulation(city));
|
|
const approval = Math.max(0, Math.min(1, this.getCityApproval(city)));
|
|
const factor = Math.min(factorCap, approval * factorCap);
|
|
const income = population * SIMPLE_ECONOMY.taxPerInhabitant * factor * modifier;
|
|
if (!(income > 0) || !Number.isFinite(income)) continue;
|
|
total += income;
|
|
this._bookCityApprovalTax(city, income);
|
|
}
|
|
if (!(total > 0)) continue;
|
|
this.budgets.set(civ, this.getBudget(civ) + total);
|
|
this._recordBudgetCash(civ, "taxIncome", total);
|
|
let day = this.taxLedger.get(civ);
|
|
if (!day) {
|
|
day = { income: 0 };
|
|
this.taxLedger.set(civ, day);
|
|
}
|
|
day.income += total;
|
|
}
|
|
},
|
|
|
|
// The slice of the day's tax a single region raised, for the region tables.
|
|
_bookCityApprovalTax(city, income) {
|
|
let entry = this.cityTaxLedger.get(city.id);
|
|
if (!entry) {
|
|
entry = { cityId: city.id, income: 0 };
|
|
this.cityTaxLedger.set(city.id, entry);
|
|
}
|
|
entry.income += income;
|
|
},
|
|
|
|
// The one modifier on the approval tax. No government, building or
|
|
// technology grants EFFECT_TAX_INCOME yet, so this is 1; the hook is here so
|
|
// a later policy can move it.
|
|
taxIncomeModifier(civ) {
|
|
const bonus = this.getCivModifiers(civ)[EFFECT_TAX_INCOME] || 0;
|
|
return Math.max(0, 1 + bonus);
|
|
},
|
|
|
|
// The taxes a nation collected over the last day: one "income" figure, in
|
|
// place of the hard economy's sales/export/import kinds.
|
|
getTaxTake(civ) {
|
|
const day = this.taxLedger ? this.taxLedger.get(civ) : null;
|
|
return { sales: 0, export: 0, import: 0, income: day ? day.income : 0 };
|
|
},
|
|
|
|
getCityTaxTake(city) {
|
|
const region = this.cityTaxLedger ? this.cityTaxLedger.get(city.id) : null;
|
|
return { sales: 0, export: 0, import: 0, income: region ? region.income : 0 };
|
|
},
|
|
|
|
// There is no inter-region trade to tax and no player-set rate: the one tax
|
|
// is the approval tax above, so a trade-tax booking or a tax control is a
|
|
// hard-only concern.
|
|
_recordTax(_civ, _kind, _resource, _amount, _city) {},
|
|
|
|
requestSetTaxRate(_civ, _kind, _rate, _resource) {
|
|
return false;
|
|
},
|
|
|
|
// The client renders the tax shape it knows, so it is returned with zeroed
|
|
// rates and no overrides rather than omitted; the tab itself is hidden in
|
|
// `09-`. The server refuses any attempt to move a rate.
|
|
getTaxConfigView(_civ) {
|
|
const view = {};
|
|
for (const kind of TAXES.kinds) view[kind] = { rate: 0, resources: {} };
|
|
return view;
|
|
},
|
|
|
|
// ------------------------------------------------- no internal money --
|
|
|
|
// Money never moves inside the state in the simple economy, so no spend can
|
|
// touch the treasury. Construction, training, upkeep and repair draw from the
|
|
// resource pool; the only money movements are the daily tax in and the market
|
|
// in `06-`.
|
|
_spendBudget(_civ, _amount, _category) {
|
|
return true;
|
|
},
|
|
|
|
// The hard upkeep figure is money plus the market value of materials. Money
|
|
// upkeep is retired, so only the material figure a caller adds remains.
|
|
getPlayerUpkeep(_civ) {
|
|
return 0;
|
|
},
|
|
|
|
_campaignUpkeepSources(_civ) {
|
|
return [];
|
|
},
|
|
|
|
// Propaganda is paid for in culture: a campaign drains its hourly rate from
|
|
// the national culture stock each day. A nation that cannot fund a full day
|
|
// loses its campaigns rather than running them on credit, which the hard
|
|
// economy's treasury overdraft would have allowed.
|
|
_tickEconomy() {
|
|
for (let civ = 0; civ < this.civilisations.length; civ++) {
|
|
const upkeep = this.getCampaignUpkeep(civ) * HOURS_PER_DAY;
|
|
if (!(upkeep > 0)) continue;
|
|
const culture = this.getCulture(civ);
|
|
if (culture >= upkeep) {
|
|
this.culture.set(civ, culture - upkeep);
|
|
continue;
|
|
}
|
|
this.culture.set(civ, 0);
|
|
this.campaigns = this.campaigns.filter((campaign) => campaign.civ !== civ);
|
|
this._touchPolitics();
|
|
}
|
|
},
|
|
|
|
// The two campaign cost hooks, retargeted from money to culture. The opinion
|
|
// shift still scales with spend because `_campaignSpendFactor` reads these.
|
|
_campaignDefaultCost() {
|
|
return SIMPLE_ECONOMY.campaign.defaultCulturePerHour;
|
|
},
|
|
|
|
_campaignMaxCost() {
|
|
return SIMPLE_ECONOMY.campaign.maxCulturePerHour;
|
|
},
|
|
|
|
// --------------------------------------------------- public production --
|
|
|
|
// Production buildings are public works, not private agents: the simple
|
|
// economy never creates an agent for one. This gates the agent creation in
|
|
// `_seedResourceBuildings` and the private branches of the site/money code.
|
|
_isProductionAgent(_proto) {
|
|
return false;
|
|
},
|
|
|
|
// A public converter runs at its nameplate output. The hard model throttles
|
|
// one that cannot cover its power bill; the simple model is not run for a
|
|
// private profit, so only grid power (above) limits it.
|
|
_converterProductionScale(_k, _proto, _agent) {
|
|
return 1;
|
|
},
|
|
|
|
_converterProfitable(_k, _proto, _agent) {
|
|
return true;
|
|
},
|
|
|
|
// Retire the private-agent money hooks: no agent cash, no debt, no central
|
|
// bank loan. They are no-ops rather than absent so the hard callers that
|
|
// reach them keep running.
|
|
_creditBuildingCash(_k, _code, _amount) {},
|
|
_chargeBuildingCash(_k, _code, _cost) {},
|
|
_chargeBuildingAccount(_k, _amount) {},
|
|
getPrivateBuildingCash(_civ) {
|
|
return 0;
|
|
},
|
|
getPrivateBuildingDebt(_civ) {
|
|
return 0;
|
|
},
|
|
getProductionAgents(_civ) {
|
|
return [];
|
|
},
|
|
|
|
// No private upgrades: the demand-driven reinvestment of the hard model has
|
|
// no owner to serve.
|
|
_tickIndustry() {},
|
|
|
|
// ------------------------------------------------------- procurement --
|
|
|
|
// Every good in the simple economy lives in the pool, so there is nothing to
|
|
// search for. These overrides empty the sourcing search so no trade graph,
|
|
// trade radius or supplier bid can run, and point the draws at the pool.
|
|
_nearbySuppliers() {
|
|
return [];
|
|
},
|
|
|
|
_resourceNodes() {
|
|
return [];
|
|
},
|
|
|
|
// With no per-tile stores there is no route to price: the trade graph is
|
|
// empty. Its only callers (`_nearbySuppliers`, `_resourceCandidateEdges`) are
|
|
// already retired above, but returning early keeps the walk unreachable.
|
|
_tradeGraph() {
|
|
return [];
|
|
},
|
|
|
|
// The hard consumers call this to buy a shortfall after exhausting the city
|
|
// store. In simple mode the store *is* the pool, so the goods are already
|
|
// gone or they never existed: returning zero means a shortfall simply stalls
|
|
// the work instead of triggering a bid.
|
|
_procureFromNeighbours(_city, _id, _amount, _cityStore, _nodes, _options) {
|
|
return 0;
|
|
},
|
|
|
|
// The materials a government or region draws are pulled straight from the
|
|
// pool. Kept to the hard signatures so construction, combat, repairs and
|
|
// unit supply all land here unchanged.
|
|
_drawFromNearest(civ, _coords, wanted, _payer = null) {
|
|
return {
|
|
steel: this._drawFromPool(civ, "steel", wanted.steel || 0),
|
|
hightech: this._drawFromPool(civ, "hightech", wanted.hightech || 0),
|
|
};
|
|
},
|
|
|
|
// Returns what is still missing, like the hard version, but the source is
|
|
// the pool rather than the nation's city stores.
|
|
_drawFromCities(civ, id, amount, _payer = null) {
|
|
const drawn = this._drawFromPool(civ, id, amount);
|
|
return Math.max(0, amount - drawn);
|
|
},
|
|
|
|
// With no freight and no currency conversion, a good is worth its world
|
|
// price. The hard model folds in a nominal two-tile haul; the simple model
|
|
// quotes the market directly.
|
|
_marketUnitCost(id) {
|
|
return this.getResourcePrice(id);
|
|
},
|
|
|
|
// The hard model pays a region for material drawn from its store and lets the
|
|
// treasury carry the bill. The simple model has neither private region cash
|
|
// nor internal money: the good comes out of the pool and nothing is settled.
|
|
_payRegionForMaterial(_city, _id, _amount, _payer) {},
|
|
|
|
// A tile work's daily gather is a hard-economy phase: a simple site opened
|
|
// already building with its whole bill paid. Kept to the hard signature and
|
|
// drawn from the pool best-effort, so a stray call still finishes rather than
|
|
// stalling on a supplier that does not exist. Energy is a regional flow and is
|
|
// never pooled, so it is never drawn here.
|
|
_gatherSiteMaterials(site) {
|
|
if (site.phase !== "materials") return;
|
|
for (const id of ["steel", "hightech"]) {
|
|
const missing = Math.max(0, (site.needed[id] || 0) - (site.bought[id] || 0));
|
|
if (!(missing > 0)) continue;
|
|
site.bought[id] = (site.bought[id] || 0) + this._drawFromPool(site.civ, id, missing);
|
|
}
|
|
const stillMissing = ["steel", "hightech"].some(
|
|
(id) => (site.needed[id] || 0) - (site.bought[id] || 0) > 1e-9
|
|
);
|
|
site.stalled = stillMissing;
|
|
if (!stillMissing) {
|
|
site.phase = "construction";
|
|
site.elapsedHours = 0;
|
|
this._emitChanged();
|
|
}
|
|
},
|
|
|
|
// A site's materials come from the owner's pool, not a city store, and no
|
|
// budget is charged: the pool already paid at order time. Energy is a flow.
|
|
_siteBuy(site, id, amount) {
|
|
if (id === "energy") return 0;
|
|
return this._drawFromPool(site.civ, id, amount);
|
|
},
|
|
|
|
// A queued building or unit starts constructing at once, so its gather pass
|
|
// draws the whole remaining bill from the pool in one go and flips the entry.
|
|
_gatherBuildEntry(city, entry) {
|
|
for (const id of ["steel", "hightech"]) {
|
|
const missing = Math.max(0, (entry.needed[id] || 0) - (entry.bought[id] || 0));
|
|
if (!(missing > 0)) continue;
|
|
entry.bought[id] = (entry.bought[id] || 0) + this._drawFromPool(city.civ, id, missing);
|
|
}
|
|
const stillMissing = ["steel", "hightech"].some(
|
|
(id) => (entry.needed[id] || 0) - (entry.bought[id] || 0) > 1e-9
|
|
);
|
|
if (!stillMissing) entry.phase = "construction";
|
|
entry.stalled = stillMissing;
|
|
return !stillMissing;
|
|
},
|
|
|
|
// The pool is available everywhere, so a work's material is never stranded.
|
|
_siteUnreachableMaterials(_site) {
|
|
return [];
|
|
},
|
|
|
|
// A public upgrade's materials are the owner's pool, checked whole so an
|
|
// underfunded upgrade never starts half-paid.
|
|
_buyBuildingMaterials(_k, _coords, civ, materials) {
|
|
const steel = Math.max(0, materials.steel || 0);
|
|
const hightech = Math.max(0, materials.hightech || 0);
|
|
return this._payPoolMaterials(civ, { steel, hightech });
|
|
},
|
|
|
|
// The owner's pool is the whole availability for an upgrade's material.
|
|
_buildingMaterialAvailability(civ, _coords, id) {
|
|
return this._ensurePool(civ)[id] || 0;
|
|
},
|
|
|
|
// ------------------------------------------------------------ energy --
|
|
|
|
// No regional power trading: energy is a per-region flow in the simple
|
|
// economy, so each grid balances what its own plants make against what its
|
|
// own cities and industry draw. The hard tick reads `imports` when it folds
|
|
// the day's balance, so they are zeroed rather than left undefined.
|
|
_tradeGridPower(ledger) {
|
|
for (const component of ledger.values()) {
|
|
component.imports = 0;
|
|
component.exports = 0;
|
|
component.importCost = 0;
|
|
}
|
|
},
|
|
|
|
// Power is not bought or sold for money in the simple model; the physical
|
|
// balance the ledger already computed is all that matters. Consumers are
|
|
// billed in `05-money-and-taxes.md`'s single tax, not per kWh.
|
|
_settleGridPower(_ledger) {},
|
|
|
|
// ------------------------------------------------------ scenario seed --
|
|
|
|
// The hard scenario fills every region with a year of its own consumption.
|
|
// The simple scenario seeds the one pool each nation opens with; the warm-up
|
|
// may have spent the initial seed before the scenario is applied, so it is
|
|
// reset here to the fixed working stock.
|
|
_seedStartingReserves() {
|
|
for (let civ = 0; civ < this.civilisations.length; civ++) {
|
|
const pool = this._ensurePool(civ);
|
|
for (const id of STORABLE_RESOURCE_IDS) pool[id] = SIMPLE_ECONOMY.startingPool[id] || 0;
|
|
}
|
|
},
|
|
|
|
// ------------------------------------------------------- migration (08) --
|
|
|
|
// Decision (08-migrations.md): only *economic* migration is retired. The
|
|
// income-chasing flows and the start-of-game equalize pass are the two
|
|
// mechanisms that move people between regions for money, so they are the two
|
|
// no-ops below. Political migration -- expelling a people, an immigration
|
|
// policy, airport traffic -- is not economic and stays, exactly as in the hard
|
|
// model, so `_tickMigration` is left untouched. The snapshot fields
|
|
// (`migrations`, `migrationGraph`) keep their shape: the hard tick still
|
|
// fills them, and with no policies and no airports they simply open empty
|
|
// rather than missing.
|
|
//
|
|
// People leaving a poor region for a rich one: finds nothing to do.
|
|
_migrateForIncome() {},
|
|
|
|
// The opening levelling the hard model runs when the world is placed, which
|
|
// redistributes each nation's people so every region starts at the same
|
|
// income per head. Without it a simple game keeps the scenario's raw spread;
|
|
// population follows natural growth and politics, not GDP.
|
|
_equalizeRegionIncomes() {},
|
|
|
|
// ---------------------------------------------------------- summary --
|
|
|
|
// What the nation holds, needs, makes and consumes. The stock is the pool
|
|
// once, not the pool summed once per city as the hard summary would do; the
|
|
// production is the public works' output plus the land's harvest.
|
|
getCivResourceSummary(civ) {
|
|
const pool = this._ensurePool(civ);
|
|
const stock = {
|
|
steel: pool.steel || 0,
|
|
food: pool.food || 0,
|
|
luxury: pool.luxury || 0,
|
|
hightech: pool.hightech || 0,
|
|
};
|
|
const need = emptyPool();
|
|
let energyNeed = 0;
|
|
for (const city of this.cities) {
|
|
if (city.civ !== civ) continue;
|
|
const economy = this.getCityEconomy(city);
|
|
const population = economy.population;
|
|
need.food += foodNeedPerDay(population);
|
|
need.steel += steelNeedPerDay(population);
|
|
need.luxury += luxuryNeedPerDay(population);
|
|
energyNeed += cityEnergyPerDay(economy.gdp);
|
|
}
|
|
const production = emptyPool();
|
|
let energySupply = 0;
|
|
for (const [k, id] of this.tileImprovements) {
|
|
if (this.tileImprovementOwner.get(k) !== civ) continue;
|
|
const proto = tileImprovementById(id);
|
|
if (!isResourceTileBuilding(proto)) continue;
|
|
if (proto.resource === "energy") {
|
|
energySupply += this.resourceBuildingOutputAt(k, proto);
|
|
} else {
|
|
production[proto.resource] += this.resourceBuildingOutputAt(k, proto);
|
|
}
|
|
}
|
|
const foodMultipliers = new Map();
|
|
for (const coords of this._territoryByCiv.get(civ) || []) {
|
|
const k = key(coords.x, coords.y);
|
|
if ((this.tilePopulation.get(k) || 0) <= 0) continue;
|
|
const city = this.regionCityAt(coords);
|
|
let multiplier = 1;
|
|
if (city) {
|
|
multiplier = foodMultipliers.get(city.id);
|
|
if (multiplier === undefined) {
|
|
multiplier = this._regionFoodMultiplier(city);
|
|
foodMultipliers.set(city.id, multiplier);
|
|
}
|
|
}
|
|
production.food += tileFoodOutput(this.tiles[k]) * multiplier;
|
|
}
|
|
const upkeep = this.getCivUpkeepResources(civ);
|
|
const consumed = {
|
|
food: need.food + (upkeep.food || 0),
|
|
steel: need.steel + (upkeep.steel || 0),
|
|
luxury: need.luxury + (upkeep.luxury || 0),
|
|
hightech: upkeep.hightech || 0,
|
|
energy: energyNeed + (upkeep.energy || 0),
|
|
};
|
|
return {
|
|
stock,
|
|
need,
|
|
production,
|
|
energy: { need: energyNeed, supply: energySupply },
|
|
upkeep,
|
|
consumed,
|
|
trade: this._civResourceTrade(civ),
|
|
external: this._civExternalTrade(civ),
|
|
prices: Object.fromEntries(RESOURCE_IDS.map((id) => [id, this.getResourcePrice(id)])),
|
|
};
|
|
},
|
|
|
|
// The delivery graph is a hard-economy overlay: there are no routes to draw
|
|
// when every region shares one pool. Returning an empty graph also keeps the
|
|
// snapshot off the trade-graph walk entirely.
|
|
_serializeResourceGraph(_viewerCiv) {
|
|
return { nodes: [], links: [], edges: [] };
|
|
},
|
|
|
|
// ------------------------------------------------- construction (03) --
|
|
|
|
// The spare power of a city's region, in kWh a day: what its plants make less
|
|
// what its cities, buildings and converters already draw. Region-based rather
|
|
// than grid-based, because the simple economy has no regional power market; a
|
|
// tile outside every region reads zero. Answers the construction-speed
|
|
// question and (in `09-`) explains it to the player.
|
|
//
|
|
// The city Resources tab and the build/site clocks all read this, and the
|
|
// snapshot does so for every city on every 10 Hz broadcast, so the region walk
|
|
// is memoised against the versions that can move a power balance: the region's
|
|
// tiles (territory/regions), its works (improvements/warfare), its buildings
|
|
// and its people (modifiers/population).
|
|
regionAvailableEnergy(city) {
|
|
if (!city) return 0;
|
|
const stamp = `${this._territoryVersion}:${this._regionVersion}:` +
|
|
`${this._improvementVersion}:${this._tileImprovementVersion}:` +
|
|
`${this._modifiersVersion}:${this._populationVersion}:${this._gdpEpoch}`;
|
|
if (!this._regionEnergyCache) this._regionEnergyCache = new Map();
|
|
const cached = this._regionEnergyCache.get(city.id);
|
|
if (cached && cached.stamp === stamp) return cached.value;
|
|
const value = this._computeRegionAvailableEnergy(city);
|
|
this._regionEnergyCache.set(city.id, { stamp, value });
|
|
return value;
|
|
},
|
|
|
|
_computeRegionAvailableEnergy(city) {
|
|
// A city's own people and its buildings are the first draw on its region.
|
|
let committed = cityEnergyPerDay(this.getCityEconomy(city).gdp);
|
|
for (const [index, level] of Object.entries(city.buildings || {})) {
|
|
if (level <= 0) continue;
|
|
const proto = this.protoBuildings[Number(index)];
|
|
if (proto) committed += this.buildingUpkeepResources(proto, level).energy;
|
|
}
|
|
let supply = 0;
|
|
for (const coords of this.regionTiles(city)) {
|
|
const k = key(coords.x, coords.y);
|
|
const id = this.tileImprovements.get(k);
|
|
if (!id) continue;
|
|
const proto = tileImprovementById(id);
|
|
if (!proto) continue;
|
|
if (isResourceTileBuilding(proto)) {
|
|
if (proto.resource === "energy") {
|
|
supply += this.resourceBuildingOutputAt(k, proto);
|
|
} else {
|
|
committed += this.resourceBuildingInputAt(k, proto, "energyPerDay");
|
|
}
|
|
// A plant's own fuel is a draw on the grid, as in the hard ledger.
|
|
committed += this.resourceBuildingInputAt(k, proto, "fuelEnergyPerDay");
|
|
}
|
|
// Every work's operating power rides on the grid, as in the hard ledger.
|
|
committed += protoUpkeep(proto, proto.buildCost || 0).energy;
|
|
}
|
|
return Math.max(0, supply - committed);
|
|
},
|
|
|
|
// The base hours a work of `cost` takes in `city`'s region at today's spare
|
|
// power. `kind` is "unit" for training, anything else for a building, tile
|
|
// work or upgrade -- which the hard economy runs ten times quicker. The
|
|
// arithmetic lives in the data module, where the guaranteed baseline keeps
|
|
// even a blacked-out starter region at the reference time, and so the browser
|
|
// quotes the same figure.
|
|
_simpleConstructionHours(city, cost, kind) {
|
|
return simpleConstructionHours(cost, this.regionAvailableEnergy(city), kind);
|
|
},
|
|
|
|
// Pays a construction bill out of the nation's pool. Atomic: a bill the pool
|
|
// cannot cover is refused whole, so an order can never start half-funded.
|
|
_payPoolMaterials(civ, materials) {
|
|
const steel = Math.max(0, materials.steel || 0);
|
|
const hightech = Math.max(0, materials.hightech || 0);
|
|
const pool = this._ensurePool(civ);
|
|
if ((pool.steel || 0) + 1e-9 < steel) return false;
|
|
if ((pool.hightech || 0) + 1e-9 < hightech) return false;
|
|
pool.steel = (pool.steel || 0) - steel;
|
|
pool.hightech = (pool.hightech || 0) - hightech;
|
|
return true;
|
|
},
|
|
|
|
// Draws a tile work's bill from the pool best-effort. A tile already carries
|
|
// at most one work, so a partial draw is a slow-down the player sees, not a
|
|
// way to spam cheap works.
|
|
_paySiteMaterials(civ, materials) {
|
|
this._drawFromPool(civ, "steel", Math.max(0, materials.steel || 0));
|
|
this._drawFromPool(civ, "hightech", Math.max(0, materials.hightech || 0));
|
|
},
|
|
|
|
// Opens a site already under construction: the whole bill is paid from the
|
|
// pool now, the clock is set from the region's spare power, and there is
|
|
// nothing to gather, haul or buy each day. The hard economy's reserved
|
|
// budget, locked prices and haul route are all left out.
|
|
_openConstructionSite({
|
|
civ, coords, proto, kind = "build", targetLevel = 0, buildCost, level = 0,
|
|
}) {
|
|
const k = key(coords.x, coords.y);
|
|
const materials = kind === "upgrade"
|
|
? this.constructionResourceCost(proto, level, buildCost)
|
|
: tileImprovementResourceCost(proto, buildCost);
|
|
this._paySiteMaterials(civ, materials);
|
|
const city = this.cityAt(coords) || this.regionCityAt(coords) || this._nearestCity(civ, coords);
|
|
const cost = buildCost !== null && buildCost !== undefined
|
|
? buildCost
|
|
: ((proto && proto.buildCost) || 0);
|
|
const site = {
|
|
coords: { x: coords.x, y: coords.y },
|
|
id: proto.id,
|
|
civ,
|
|
kind,
|
|
targetLevel,
|
|
private: false,
|
|
phase: "construction",
|
|
budget: 0,
|
|
prices: {},
|
|
needed: emptySiteResources(),
|
|
bought: emptySiteResources(),
|
|
elapsedHours: 0,
|
|
totalHours: Math.max(1,
|
|
this._simpleConstructionHours(city, cost, "building") *
|
|
this.constructionSpeedMultiplier(civ)),
|
|
cityId: null,
|
|
path: [],
|
|
stalled: false,
|
|
budgetRaises: 0,
|
|
lastBuyDay: -1,
|
|
};
|
|
this.constructionSites.set(k, site);
|
|
this._emitChanged();
|
|
return site;
|
|
},
|
|
|
|
// A simple site has nothing to gather or buy; it is already building.
|
|
_kickoffSite(_site) {},
|
|
|
|
// Simple construction is paid from the material pool, not the treasury, so no
|
|
// money is reserved for a site. Kept as a no-op so the hard callers that reach
|
|
// it in simple mode move nothing.
|
|
_reserveSiteBudget(_civ, _amount, _category) {
|
|
return 0;
|
|
},
|
|
|
|
// ------------------------------------------------------ orders (03) --
|
|
|
|
// Queues one unit. Its material bill is drawn from the pool up front and it
|
|
// starts training at once: no money, no production-capacity gate, no gather
|
|
// phase. The clock comes from the city's spare power, scaled by approval and
|
|
// the usual construction-speed research, exactly as the hard order is.
|
|
requestTrain(cityId, protoIndex, free = false) {
|
|
const city = this.findCity(cityId);
|
|
if (!city) return false;
|
|
if (protoIndex < 0 || protoIndex >= this.protoUnits.length) return false;
|
|
const proto = this.protoUnits[protoIndex];
|
|
if (proto.requiresBuilding && !this.hasCityBuilding(city, proto.requiresBuilding)) return false;
|
|
if (proto.requiresTechnology && !this.hasTechnology(city.civ, proto.requiresTechnology)) return false;
|
|
if (
|
|
Array.isArray(proto.requiresTechnologies) &&
|
|
!proto.requiresTechnologies.every((id) => this.hasTechnology(city.civ, id))
|
|
) {
|
|
return false;
|
|
}
|
|
if (free) {
|
|
this._spawnTrainedUnit(city, proto);
|
|
this._visibilityDirty = true;
|
|
this._emitChanged();
|
|
return true;
|
|
}
|
|
const queue = this.training.get(cityId);
|
|
if (queue && queue.length >= TRAINING_QUEUE_LIMIT) return false;
|
|
if (proto.cost <= 0) {
|
|
this._spawnTrainedUnit(city, proto);
|
|
this._visibilityDirty = true;
|
|
this._emitChanged();
|
|
return true;
|
|
}
|
|
const materials = this.constructionResourceCost(proto, 0, proto.cost);
|
|
if (!this._payPoolMaterials(city.civ, materials)) return false;
|
|
const entry = {
|
|
kind: "unit",
|
|
protoIndex,
|
|
phase: "construction",
|
|
budget: 0,
|
|
budgetCategory: "training",
|
|
prices: {},
|
|
needed: emptySiteResources(),
|
|
bought: emptySiteResources(),
|
|
elapsedHours: 0,
|
|
totalHours: Math.max(1,
|
|
this._simpleConstructionHours(city, proto.cost, "unit") *
|
|
this.constructionApprovalMultiplier(city) *
|
|
this.constructionSpeedMultiplier(city.civ)),
|
|
lastGatherDay: -1,
|
|
};
|
|
if (queue) queue.push(entry);
|
|
else this.training.set(cityId, [entry]);
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// Queues one level of a city building, paid from the pool and started at
|
|
// once, with the level priced off whatever the queue will leave behind.
|
|
requestBuild(cityId, protoIndex, free = false) {
|
|
const city = this.findCity(cityId);
|
|
if (!city || protoIndex < 0 || protoIndex >= this.protoBuildings.length) return false;
|
|
const proto = this.protoBuildings[protoIndex];
|
|
if (proto.coastal && !this.isCoastalCity(city)) return false;
|
|
if (proto.requiresBuilding && !this.hasCityBuilding(city, proto.requiresBuilding)) return false;
|
|
if (proto.requiresTechnology && !this.hasTechnology(city.civ, proto.requiresTechnology)) return false;
|
|
if (free) {
|
|
const level =
|
|
this.getCityBuildingLevel(city, protoIndex) +
|
|
this._pendingBuildingLevels(cityId, protoIndex);
|
|
city.buildings[protoIndex] = level + 1;
|
|
this._clearTileGdpCache();
|
|
this._touchModifiers();
|
|
this._emitChanged();
|
|
return true;
|
|
}
|
|
const queue = this.training.get(cityId);
|
|
if (queue && queue.length >= TRAINING_QUEUE_LIMIT) return false;
|
|
const level =
|
|
this.getCityBuildingLevel(city, protoIndex) +
|
|
this._pendingBuildingLevels(cityId, protoIndex);
|
|
const cost = buildingBuildCost(proto, level);
|
|
if (cost <= 0) {
|
|
city.buildings[protoIndex] = level + 1;
|
|
this._clearTileGdpCache();
|
|
this._touchModifiers();
|
|
this._emitChanged();
|
|
return true;
|
|
}
|
|
const materials = this.constructionResourceCost(proto, level, cost);
|
|
if (!this._payPoolMaterials(city.civ, materials)) return false;
|
|
const entry = {
|
|
kind: "building",
|
|
protoIndex,
|
|
level,
|
|
phase: "construction",
|
|
budget: 0,
|
|
prices: {},
|
|
needed: emptySiteResources(),
|
|
bought: emptySiteResources(),
|
|
elapsedHours: 0,
|
|
totalHours: Math.max(1,
|
|
this._simpleConstructionHours(city, cost, "building") *
|
|
this.constructionApprovalMultiplier(city) *
|
|
this.constructionSpeedMultiplier(city.civ)),
|
|
lastGatherDay: -1,
|
|
};
|
|
if (queue) queue.push(entry);
|
|
else this.training.set(cityId, [entry]);
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// Lays a military or resource tile improvement. Like a city order it pays the
|
|
// pool at once and starts building, with the clock from the region's spare
|
|
// power; there is no GDP-capacity gate, and a producer is public (it never
|
|
// opens a private agent).
|
|
requestBuildTileImprovement(civ, coords, id) {
|
|
if (!this._validCiv(civ) || !coords) return false;
|
|
if (!Number.isFinite(coords.x) || !Number.isFinite(coords.y)) return false;
|
|
const proto = tileImprovementById(id);
|
|
if (!proto) return false;
|
|
const k = key(coords.x, coords.y);
|
|
const tile = this.tiles[k];
|
|
if (!tile || tile.terrainClass !== "Land") return false;
|
|
if (this.civAt(coords) !== civ) return false;
|
|
if (this.tileImprovements.has(k) || this.constructionSiteAt(coords)) return false;
|
|
if (proto.coastal && !this.isCoastalCoords(coords)) return false;
|
|
if (proto.resource && this.cityAt(coords)) return false;
|
|
if (proto.requiresTechnology && !this.hasTechnology(civ, proto.requiresTechnology)) return false;
|
|
this._openConstructionSite({
|
|
civ, coords, proto, kind: "build", buildCost: proto.buildCost,
|
|
});
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// A public upgrade of a production building: immediate and energy-driven like
|
|
// any other simple work, funded by the pool, never by a private agent. The
|
|
// daily demand-driven decision that would call this stays retired (02-).
|
|
_startBuildingUpgrade(k, proto, agent) {
|
|
const owner = this.tileImprovementOwner.get(k);
|
|
if (owner === undefined || owner < 0) return false;
|
|
if (this.constructionSites && this.constructionSites.has(k)) return false;
|
|
const level = agent.level;
|
|
const coords = parseKey(k);
|
|
this._openConstructionSite({
|
|
civ: owner,
|
|
coords,
|
|
proto,
|
|
kind: "upgrade",
|
|
targetLevel: level + 1,
|
|
buildCost: resourceBuildingUpgradeMoneyCost(proto, level),
|
|
level,
|
|
});
|
|
this._tileImprovementVersion += 1;
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// --------------------------------------------------- region plant seed --
|
|
|
|
// Gives every city region that opened without a plant one fuel-free producer,
|
|
// placed deterministically on a valid owned land tile, so the energy-driven
|
|
// construction rule always starts from a non-zero basis.
|
|
_seedSimpleRegionPlants() {
|
|
const ids = SIMPLE_ECONOMY.construction.seedPlantIds;
|
|
let added = false;
|
|
for (const city of this.cities) {
|
|
if (this._regionHasPowerPlant(city)) continue;
|
|
const coords = this._pickSimplePlantTile(city);
|
|
if (!coords) continue;
|
|
const proto = ids
|
|
.map((id) => tileImprovementById(id))
|
|
.find((candidate) => candidate && !candidate.coastal);
|
|
if (!proto) continue;
|
|
const k = key(coords.x, coords.y);
|
|
this.tileImprovements.set(k, proto.id);
|
|
this.tileImprovementOwner.set(k, city.civ);
|
|
this.tileImprovementHp.set(k, TILE_IMPROVEMENT_HP[proto.id] || 1000);
|
|
// A plant beside the network gets a one-tile road spur, so it is reachable
|
|
// exactly as a hard-seeded work is.
|
|
if (!this.roads.has(k) && !this.railways.has(k) && this._touchesNetwork(coords)) {
|
|
this.roads.add(k);
|
|
}
|
|
added = true;
|
|
}
|
|
// The seeded plants change what each region can spare; drop the memoised
|
|
// power balances so the next read rebuilds them.
|
|
if (added) {
|
|
this._improvementVersion += 1;
|
|
this._regionEnergyCache = null;
|
|
}
|
|
},
|
|
|
|
_regionHasPowerPlant(city) {
|
|
for (const coords of this.regionTiles(city)) {
|
|
const id = this.tileImprovements.get(key(coords.x, coords.y));
|
|
if (!id) continue;
|
|
const proto = tileImprovementById(id);
|
|
if (isResourceTileBuilding(proto) && proto.resource === "energy") return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Whether a tile sits beside a road or railway.
|
|
_touchesNetwork(coords) {
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const nk = key(neighbour.x, neighbour.y);
|
|
if (this.roads.has(nk) || this.railways.has(nk)) return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// The first owned land tile in the region with room for a plant. Iterated in
|
|
// `regionTiles` order, which is deterministic for a given world, so a replay
|
|
// places the same plants.
|
|
_pickSimplePlantTile(city) {
|
|
for (const coords of this.regionTiles(city)) {
|
|
if (this.cityAt(coords)) continue;
|
|
if (!this._isLand(coords)) continue;
|
|
if (this.civAt(coords) !== city.civ) continue;
|
|
const k = key(coords.x, coords.y);
|
|
if (this.tileImprovements.has(k)) continue;
|
|
if (this.constructionSiteAt(coords)) continue;
|
|
return coords;
|
|
}
|
|
return null;
|
|
},
|
|
};
|