- The monthly budget breaks each expense into the resource bought, its quantity and the average price paid: tile improvements already did, and now every upkeep row (buildings, units, works, transport) does too. - Private producers settle their daily wear from their own cash and borrow the shortfall from the central bank, so their cash never goes negative. - Producers fund their own construction and upgrades on the same credit; the treasury pays only for public works, roads and railways, and no private purchase is filed as a public tile-improvement expense. - The server settles the world until every commodity holds within 10% over a trailing seven days instead of a fixed 150, capped by --warmup (0 skips it). - The shipped month report carries its label again, so the budget header and pager name the month.
431 lines
18 KiB
JavaScript
431 lines
18 KiB
JavaScript
// Production buildings as independent private-sector agents. Every resource
|
|
// building -- a steel mill, a mine, a chip foundry, a power plant -- is not
|
|
// merely a region's asset: it keeps its own cash from what it sells, watches
|
|
// how its own demand moves, and reinvests that cash in upgrading itself. A
|
|
// converter makes only what it can sell and the grid pays its plants for the
|
|
// power it delivers (see `_settleGridPower`), so no agent is forced to run at
|
|
// a permanent loss.
|
|
//
|
|
// The daily tick rolls each agent's sales into a short window, then lets an
|
|
// agent whose recent sales beat its older ones -- or that is selling out -- buy
|
|
// the steel and high-tech for its next level and start the work. An upgrade
|
|
// raises throughput exponentially but the extra output costs power linearly
|
|
// (see RESOURCE_BUILDING_UPGRADE in shared/data/resources.js), and an agent may
|
|
// only have one upgrade under way at a time.
|
|
|
|
import { key, parseKey } from "../hex.js";
|
|
import { RESOURCE_RULES } from "../data/resources.js";
|
|
import {
|
|
RESOURCE_BUILDING_UPGRADE,
|
|
RESOURCE_EFFICIENCY_EFFECT,
|
|
resourceBuildingOutputAt,
|
|
resourceBuildingInputAt,
|
|
resourceBuildingUpgradeMoneyCost,
|
|
tileImprovementById,
|
|
isResourceTileBuilding,
|
|
} from "../data.js";
|
|
import { HOURS_PER_DAY } from "./constants.js";
|
|
|
|
function emptyBuildingAgent() {
|
|
return { level: 0, cash: 0, debt: 0, salesToday: 0, sales: [], upgrading: null };
|
|
}
|
|
|
|
export const industryMethods = {
|
|
// ------------------------------------------------------- agent state --
|
|
|
|
// Every resource building trades: a material producer sells its good, a
|
|
// power plant sells the power its grid delivers.
|
|
_isProductionAgent(proto) {
|
|
return isResourceTileBuilding(proto);
|
|
},
|
|
|
|
_ensureBuildingAgent(k) {
|
|
if (!this.buildingAgents) this.buildingAgents = new Map();
|
|
let agent = this.buildingAgents.get(k);
|
|
if (!agent) {
|
|
agent = emptyBuildingAgent();
|
|
this.buildingAgents.set(k, agent);
|
|
}
|
|
return agent;
|
|
},
|
|
|
|
// Drops the agent of a tile whose building is gone.
|
|
_removeBuildingAgent(k) {
|
|
if (this.buildingAgents) this.buildingAgents.delete(k);
|
|
},
|
|
|
|
buildingAgentAt(coords) {
|
|
if (!this.buildingAgents) return null;
|
|
return this.buildingAgents.get(key(coords.x, coords.y)) || null;
|
|
},
|
|
|
|
resourceBuildingLevel(k) {
|
|
const agent = this.buildingAgents ? this.buildingAgents.get(k) : null;
|
|
return agent ? agent.level : 0;
|
|
},
|
|
|
|
// The output per day of the building on `k`, compounded by its level.
|
|
resourceBuildingOutputAt(k, proto) {
|
|
return resourceBuildingOutputAt(proto, this.resourceBuildingLevel(k));
|
|
},
|
|
|
|
// The operating input of the building on `k` at its level. `field` names the
|
|
// proto field to scale (`energyPerDay` or `fuelEnergyPerDay`).
|
|
resourceBuildingInputAt(k, proto, field = "energyPerDay") {
|
|
return resourceBuildingInputAt(proto, this.resourceBuildingLevel(k), field);
|
|
},
|
|
|
|
// The nation's output-per-input bonus for one commodity, from its efficiency
|
|
// research. Energy drawn to make that commodity is divided by `1 + bonus`.
|
|
// Read from the technology modifiers alone, so seeding never primes the
|
|
// cached building-modifier totals with a half-built world.
|
|
resourceEfficiency(civ, resource) {
|
|
if (civ < 0) return 0;
|
|
const stat = RESOURCE_EFFICIENCY_EFFECT[resource];
|
|
if (!stat) return 0;
|
|
let cached = this._resourceEfficiencyCache ? this._resourceEfficiencyCache.get(civ) : null;
|
|
if (!cached || cached.version !== this._modifiersVersion) {
|
|
if (!this._resourceEfficiencyCache) this._resourceEfficiencyCache = new Map();
|
|
cached = { version: this._modifiersVersion, values: this.getCivTechnologyModifiers(civ) };
|
|
this._resourceEfficiencyCache.set(civ, cached);
|
|
}
|
|
return Math.max(0, cached.values[stat] || 0);
|
|
},
|
|
|
|
// ------------------------------------------------------------ money --
|
|
|
|
// A buyer's payment reaches the producing building, into its own cash, in the
|
|
// currency of the nation it belongs to whatever currency it was paid in.
|
|
_creditBuildingCash(k, code, amount) {
|
|
if (!(amount > 0)) return;
|
|
const owner = this.tileImprovementOwner.get(k);
|
|
if (owner === undefined || owner < 0) return;
|
|
const agent = this._ensureBuildingAgent(k);
|
|
const value = amount * this.currencyValueOf(code) / this.currencyValue(owner);
|
|
agent.cash = (agent.cash || 0) + value;
|
|
},
|
|
|
|
// Records that a production agent sold `qty` of its output during the day,
|
|
// feeding the demand trend that decides whether it grows.
|
|
_recordBuildingSale(k, qty) {
|
|
if (!(qty > 0)) return;
|
|
const agent = this._ensureBuildingAgent(k);
|
|
agent.salesToday = (agent.salesToday || 0) + qty;
|
|
},
|
|
|
|
// A payer that draws on one production building's own cash.
|
|
_buildingPayer(k) {
|
|
const payer = { total: 0 };
|
|
payer.charge = (cost) => {
|
|
if (!(cost > 0)) return;
|
|
this._chargeBuildingAccount(k, cost);
|
|
payer.total += cost;
|
|
};
|
|
return payer;
|
|
},
|
|
|
|
// Charges one production building, spending its cash and borrowing whatever
|
|
// is missing from the national central bank at the bank's rate. Like a region,
|
|
// an agent never goes into the red: it takes on a loan instead, which the
|
|
// daily tick compounds and repays from later sales.
|
|
_chargeBuildingAccount(k, amount) {
|
|
if (!(amount > 0)) return;
|
|
const agent = this._ensureBuildingAgent(k);
|
|
const cash = agent.cash || 0;
|
|
if (cash >= amount) {
|
|
agent.cash = cash - amount;
|
|
return;
|
|
}
|
|
agent.cash = 0;
|
|
agent.debt = (agent.debt || 0) + (amount - cash);
|
|
},
|
|
|
|
// Charges one production building for something the grid delivered to it,
|
|
// from its own cash, borrowing from the central bank where the cash falls
|
|
// short, so the money a supplier receives is real.
|
|
_chargeBuildingCash(k, code, cost) {
|
|
if (!(cost > 0)) return;
|
|
const owner = this.tileImprovementOwner.get(k);
|
|
if (owner === undefined || owner < 0) return;
|
|
const value = cost * this.currencyValueOf(code) / this.currencyValue(owner);
|
|
this._chargeBuildingAccount(k, value);
|
|
},
|
|
|
|
// Pays a grid's plants for `kwh` of power worth `money`, split by how much
|
|
// each contributed, and credits each with the sale its demand trend reads.
|
|
_payGridProducers(component, money, kwh) {
|
|
const producers = (component && component.producers) || [];
|
|
let total = 0;
|
|
for (const producer of producers) total += producer.output || 0;
|
|
if (!(total > 0)) {
|
|
if (money > 0 && component.civ >= 0) this._creditRegions(component.civ, money);
|
|
return;
|
|
}
|
|
const code = this.currencyOf(component.civ).code;
|
|
for (const producer of producers) {
|
|
const share = (producer.output || 0) / total;
|
|
if (money > 0) this._creditBuildingCash(producer.k, code, money * share);
|
|
if (kwh > 0) this._recordBuildingSale(producer.k, kwh * share);
|
|
}
|
|
},
|
|
|
|
// Everything the nation's production buildings hold, in the nation's currency.
|
|
getPrivateBuildingCash(civ) {
|
|
if (!this.buildingAgents) return 0;
|
|
let total = 0;
|
|
for (const [k, agent] of this.buildingAgents) {
|
|
if (this.tileImprovementOwner.get(k) !== civ) continue;
|
|
total += agent.cash || 0;
|
|
}
|
|
return total;
|
|
},
|
|
|
|
// What the nation's production buildings owe the central bank in total.
|
|
getPrivateBuildingDebt(civ) {
|
|
if (!this.buildingAgents) return 0;
|
|
let total = 0;
|
|
for (const [k, agent] of this.buildingAgents) {
|
|
if (this.tileImprovementOwner.get(k) !== civ) continue;
|
|
total += agent.debt || 0;
|
|
}
|
|
return total;
|
|
},
|
|
|
|
// A nation's production agents as plain objects, for the Economy tab's
|
|
// separate private-sector row and the tile panel.
|
|
getProductionAgents(civ) {
|
|
const result = [];
|
|
if (!this.buildingAgents) return result;
|
|
for (const [k, agent] of this.buildingAgents) {
|
|
if (this.tileImprovementOwner.get(k) !== civ) continue;
|
|
const id = this.tileImprovements.get(k);
|
|
const proto = id ? tileImprovementById(id) : null;
|
|
if (!this._isProductionAgent(proto)) continue;
|
|
const coords = parseKey(k);
|
|
const site = this.constructionSites ? this.constructionSites.get(k) : null;
|
|
const upgradingSite = site && site.kind === "upgrade" ? site : null;
|
|
const job = upgradingSite || agent.upgrading;
|
|
result.push({
|
|
coords: [coords.x, coords.y],
|
|
name: proto.name,
|
|
resource: proto.resource,
|
|
level: agent.level,
|
|
cash: agent.cash || 0,
|
|
debt: agent.debt || 0,
|
|
outputPerDay: resourceBuildingOutputAt(proto, agent.level),
|
|
upgrading: !!job,
|
|
upgradeDaysLeft: job
|
|
? Math.max(0, Math.ceil((job.totalHours - job.elapsedHours) / HOURS_PER_DAY))
|
|
: 0,
|
|
});
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// The steel and high-tech one upgrade level needs, scaled off its money cost
|
|
// like any other construction. Only an advanced producer needs the high-tech.
|
|
_upgradeMaterialCost(proto, level) {
|
|
const money = resourceBuildingUpgradeMoneyCost(proto, level);
|
|
return {
|
|
steel: money * RESOURCE_RULES.steelPerBudgetEuro,
|
|
hightech: proto.advanced ? RESOURCE_RULES.constructionHighTechPerLevel * (level + 1) : 0,
|
|
};
|
|
},
|
|
|
|
// How much of its capacity a converter should run at. Its output follows what
|
|
// it has actually been selling across the whole watch window, not its nameplate
|
|
// capacity: a building ramps up only as demand is sustained and eases off again
|
|
// when demand fades, so it never burns power on goods it cannot move. A
|
|
// brand-new building has no history and runs flat out.
|
|
_converterProductionScale(k, proto, agent) {
|
|
const capacity = resourceBuildingOutputAt(proto, agent ? agent.level : 0);
|
|
if (!(capacity > 0)) return 1;
|
|
// A converter that cannot cover its power bill at today's prices does not run
|
|
// at all; idling beats making a loss.
|
|
if (!this._converterProfitable(k, proto, agent)) return 0;
|
|
const sales = (agent && agent.sales) || [];
|
|
let expected = capacity;
|
|
if (sales.length > 0) {
|
|
expected = sales.reduce((sum, value) => sum + value, 0) / sales.length;
|
|
}
|
|
const target = expected * 2;
|
|
const store = this.resourceStock ? this.resourceStock.get(k) : null;
|
|
const stock = (store || {})[proto.resource] || 0;
|
|
if (stock <= target) return 1;
|
|
// Overstocked: make only what has been selling, but never switch off.
|
|
return Math.max(0.1, Math.min(1, expected / capacity));
|
|
},
|
|
|
|
// Whether a converter can cover its power bill at today's market prices. Making
|
|
// a material that sells for less than the energy it burns is a loss the private
|
|
// agent will not take, so it idles for the day instead. Efficiency research
|
|
// cuts the power each unit needs, so it only ever helps.
|
|
_converterProfitable(k, proto, agent) {
|
|
if (!proto || proto.resource === "energy") return true;
|
|
const level = agent ? agent.level : 0;
|
|
const output = resourceBuildingOutputAt(proto, level);
|
|
if (!(output > 0)) return true;
|
|
const owner = this.tileImprovementOwner.get(k);
|
|
const efficiency = 1 + this.resourceEfficiency(owner, proto.resource);
|
|
const energy = resourceBuildingInputAt(proto, level, "energyPerDay") / efficiency;
|
|
if (!(energy > 0)) return true;
|
|
const revenue = output * this.getResourcePrice(proto.resource);
|
|
const bill = energy * this.getResourcePrice("energy");
|
|
return revenue >= bill;
|
|
},
|
|
|
|
// What the next level's materials would cost at today's delivered market
|
|
// price, in the agent's national currency. The delivered cost folds in a
|
|
// nominal two-tile haul, so the quoted bill is not beaten by a distant
|
|
// supplier's freight.
|
|
_upgradeMaterialValue(civ, materials) {
|
|
const value = materials.steel * this._marketUnitCost("steel") +
|
|
materials.hightech * this._marketUnitCost("hightech");
|
|
return value / this.currencyValue(civ);
|
|
},
|
|
|
|
// ------------------------------------------------------- daily tick --
|
|
|
|
// One hour of industry, and once a day the demand read that may start a new
|
|
// upgrade. The upgrade itself is a construction site: it gathers materials,
|
|
// builds and becomes the next level through the shared site tick.
|
|
_tickIndustry() {
|
|
if (!this.buildingAgents || this.buildingAgents.size === 0) return;
|
|
if (this.totalHours % HOURS_PER_DAY === 0) this._tickBuildingDecisions();
|
|
},
|
|
|
|
_tickBuildingDecisions() {
|
|
// Roll today's sales into the trend window before reading it.
|
|
for (const agent of this.buildingAgents.values()) {
|
|
agent.sales.push(agent.salesToday || 0);
|
|
if (agent.sales.length > RESOURCE_BUILDING_UPGRADE.salesWindow) agent.sales.shift();
|
|
agent.salesToday = 0;
|
|
}
|
|
for (const [k, agent] of this.buildingAgents) {
|
|
if (this.constructionSites && this.constructionSites.has(k)) continue;
|
|
if (agent.level >= RESOURCE_BUILDING_UPGRADE.maxLevel) continue;
|
|
const id = this.tileImprovements.get(k);
|
|
const proto = id ? tileImprovementById(id) : null;
|
|
if (!this._isProductionAgent(proto)) continue;
|
|
if (!this._buildingDemandGrowing(agent, this.resourceBuildingOutputAt(k, proto))) {
|
|
continue;
|
|
}
|
|
this._startBuildingUpgrade(k, proto, agent);
|
|
}
|
|
},
|
|
|
|
// Whether the agent should grow: its recent sales have to beat its older ones,
|
|
// or it has to be selling out (demand at least matching what it can make).
|
|
_buildingDemandGrowing(agent, capacity = 0) {
|
|
const sales = agent.sales || [];
|
|
if (sales.length < 2) return false;
|
|
const half = Math.floor(sales.length / 2);
|
|
const older = sales.slice(0, half);
|
|
const recent = sales.slice(half);
|
|
const average = (list) => list.reduce((sum, value) => sum + value, 0) / list.length;
|
|
const before = average(older);
|
|
const now = average(recent);
|
|
if (now > before * (1 + RESOURCE_BUILDING_UPGRADE.demandGrowth)) return true;
|
|
return capacity > 0 &&
|
|
now >= capacity * RESOURCE_BUILDING_UPGRADE.sellThrough &&
|
|
now >= before;
|
|
},
|
|
|
|
// Commit an agent to its next level. It hands its cash to a construction
|
|
// site, which gathers the materials, builds, and raises the level on
|
|
// completion like any other work. Only one upgrade may run at a time.
|
|
_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 materials = this._upgradeMaterialCost(proto, level);
|
|
const quoted = this._upgradeMaterialValue(owner, materials) *
|
|
RESOURCE_BUILDING_UPGRADE.cashMargin;
|
|
if ((agent.cash || 0) < quoted) return false;
|
|
const coords = parseKey(k);
|
|
if (this._buildingMaterialAvailability(owner, coords, "steel") < materials.steel) return false;
|
|
if (materials.hightech > 0 &&
|
|
this._buildingMaterialAvailability(owner, coords, "hightech") < materials.hightech) {
|
|
return false;
|
|
}
|
|
const money = resourceBuildingUpgradeMoneyCost(proto, level);
|
|
// A private work: the agent keeps its cash and the site draws on it (then a
|
|
// central-bank loan) as it buys, so an upgrade is never a public expense.
|
|
const site = this._openConstructionSite({
|
|
civ: owner,
|
|
coords,
|
|
proto,
|
|
kind: "upgrade",
|
|
targetLevel: level + 1,
|
|
money,
|
|
buildCost: money,
|
|
level,
|
|
isPrivate: true,
|
|
});
|
|
this._kickoffSite(site);
|
|
this._tileImprovementVersion += 1;
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// The amount of `id` an agent could lay hands on for an upgrade: its own
|
|
// store for free, the nation's city stores, and the nearby producers' surplus.
|
|
_buildingMaterialAvailability(civ, coords, id) {
|
|
const k = key(coords.x, coords.y);
|
|
let available = (this.resourceStock.get(k) || {})[id] || 0;
|
|
for (const city of this.cities) {
|
|
if (city.civ !== civ) continue;
|
|
available += this.getCityResourceStock(city)[id] || 0;
|
|
}
|
|
const city = this._nearestCity(civ, coords);
|
|
if (!city) return available;
|
|
const nodes = this._resourceNodes();
|
|
const allowSea = this._cityMarketAccess(city);
|
|
for (const supplier of this._nearbySuppliers(city, id, nodes, allowSea)) {
|
|
if (supplier.kind !== "building") continue;
|
|
if (key(supplier.coords.x, supplier.coords.y) === k) continue;
|
|
available += this._nodeSurplus(supplier, id);
|
|
}
|
|
return available;
|
|
},
|
|
|
|
// Spends the agent's cash on the upgrade's materials: its own store first
|
|
// (already its own), then the nation's regions, then the reachable market.
|
|
_buyBuildingMaterials(k, coords, civ, materials) {
|
|
const own = this._ensureResourceStock(k);
|
|
const ownSteel = Math.min(materials.steel, own.steel || 0);
|
|
const ownTech = Math.min(materials.hightech, own.hightech || 0);
|
|
if (ownSteel > 0) own.steel -= ownSteel;
|
|
if (ownTech > 0) own.hightech -= ownTech;
|
|
let needSteel = materials.steel - ownSteel;
|
|
let needTech = materials.hightech - ownTech;
|
|
const payer = this._buildingPayer(k);
|
|
if (needSteel > 0) {
|
|
needSteel -= this._drawFromNearest(civ, coords, { steel: needSteel, hightech: 0 }, payer).steel;
|
|
}
|
|
if (needTech > 0) {
|
|
needTech -= this._drawFromNearest(civ, coords, { steel: 0, hightech: needTech }, payer).hightech;
|
|
}
|
|
if (needSteel > 0 || needTech > 0) {
|
|
const city = this._nearestCity(civ, coords);
|
|
if (city) {
|
|
const nodes = this._resourceNodes();
|
|
const allowSea = this._cityMarketAccess(city);
|
|
const haul = { steel: 0, hightech: 0 };
|
|
if (needSteel > 0) {
|
|
needSteel -= this._procureFromNeighbours(
|
|
city, "steel", needSteel, haul, nodes, { payer, allowSea }
|
|
);
|
|
}
|
|
if (needTech > 0) {
|
|
needTech -= this._procureFromNeighbours(
|
|
city, "hightech", needTech, haul, nodes, { payer, allowSea }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return needSteel <= 1e-6 && needTech <= 1e-6;
|
|
},
|
|
};
|