Production buildings are now independent private-sector agents: they keep what their output sells for, run only to the demand they can actually sell, and spend that cash on steel and high-tech upgrades when their demand grows. Grid power is a paid private flow between plants and their consumers, and regions synthesise the food their own harvest and the reachable market cannot supply. Island regions no longer famine: a region with no reachable supplier grows its whole shortfall from energy, and grid power now feeds cities and food before the converters, so a famine never waits on a factory. Bundles the in-progress tree cleanup that was already present in the working tree.
375 lines
15 KiB
JavaScript
375 lines
15 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 { ECONOMY } from "../data/economy.js";
|
|
import { RESOURCE_RULES } from "../data/resources.js";
|
|
import {
|
|
RESOURCE_BUILDING_UPGRADE,
|
|
resourceBuildingOutputAt,
|
|
resourceBuildingInputAt,
|
|
resourceBuildingUpgradeMoneyCost,
|
|
tileImprovementById,
|
|
isResourceTileBuilding,
|
|
} from "../data.js";
|
|
import { HOURS_PER_DAY } from "./constants.js";
|
|
|
|
function emptyBuildingAgent() {
|
|
return { level: 0, cash: 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);
|
|
},
|
|
|
|
// ------------------------------------------------------------ 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 agent = this._ensureBuildingAgent(k);
|
|
const payer = { total: 0 };
|
|
payer.charge = (cost) => {
|
|
if (!(cost > 0)) return;
|
|
// An agent never mints money: a bill it cannot cover leaves it overdrawn,
|
|
// to be paid off from the next sales.
|
|
agent.cash = (agent.cash || 0) - cost;
|
|
payer.total += cost;
|
|
};
|
|
return payer;
|
|
},
|
|
|
|
// Charges one production building for something the grid delivered to it,
|
|
// from its own cash. Like `_buildingPayer`, it may go overdrawn rather than
|
|
// fail, 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 agent = this._ensureBuildingAgent(k);
|
|
const value = cost * this.currencyValueOf(code) / this.currencyValue(owner);
|
|
agent.cash = (agent.cash || 0) - 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;
|
|
},
|
|
|
|
// 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);
|
|
result.push({
|
|
coords: [coords.x, coords.y],
|
|
name: proto.name,
|
|
resource: proto.resource,
|
|
level: agent.level,
|
|
cash: agent.cash || 0,
|
|
outputPerDay: resourceBuildingOutputAt(proto, agent.level),
|
|
upgrading: !!agent.upgrading,
|
|
upgradeDaysLeft: agent.upgrading
|
|
? Math.max(0, Math.ceil((agent.upgrading.totalHours - agent.upgrading.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. It runs flat out while
|
|
// its store is thin -- so it always keeps up with demand -- and only eases off
|
|
// once it is sitting on more stock than it has been selling, making roughly
|
|
// what it sells 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;
|
|
const sales = (agent && agent.sales) || [];
|
|
let expected = capacity;
|
|
if (sales.length > 0) {
|
|
const half = Math.max(1, Math.floor(sales.length / 2));
|
|
const recent = sales.slice(-half);
|
|
expected = recent.reduce((sum, value) => sum + value, 0) / recent.length;
|
|
}
|
|
const target = Math.max(capacity, 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));
|
|
},
|
|
|
|
// 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 every agent's upgrade, and once a day the demand read that may
|
|
// start a new one.
|
|
_tickIndustry() {
|
|
if (!this.buildingAgents || this.buildingAgents.size === 0) return;
|
|
for (const [k, agent] of this.buildingAgents) {
|
|
if (!agent.upgrading) continue;
|
|
agent.upgrading.elapsedHours += 1;
|
|
if (agent.upgrading.elapsedHours >= agent.upgrading.totalHours) {
|
|
this._completeBuildingUpgrade(k, agent);
|
|
}
|
|
}
|
|
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 (agent.upgrading) 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: buy the materials from its own stores and
|
|
// the reachable market with its own cash, then start the 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;
|
|
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;
|
|
}
|
|
if (!this._buyBuildingMaterials(k, coords, owner, materials)) return false;
|
|
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(owner));
|
|
const money = resourceBuildingUpgradeMoneyCost(proto, level);
|
|
const totalHours = capacity > 0
|
|
? Math.max(1, (money / capacity) * this.constructionSpeedMultiplier(owner))
|
|
: 1;
|
|
agent.upgrading = { elapsedHours: 0, totalHours, targetLevel: level + 1 };
|
|
this._tileImprovementVersion += 1;
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
_completeBuildingUpgrade(k, agent) {
|
|
agent.level = agent.upgrading ? agent.upgrading.targetLevel : agent.level + 1;
|
|
agent.upgrading = null;
|
|
// The extra output lifts the region's GDP, so the cached production is stale.
|
|
this._gdpPerCapitaCache.clear();
|
|
this._touchModifiers();
|
|
this._tileImprovementVersion += 1;
|
|
this._emitChanged();
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
};
|