The simple economy is now the default and the old simulation is frozen as the hard model, selectable per game. A nation keeps one resource pool available in every region, fed by public production works rather than private per-region stores, and construction and training begin at once, paced by the region's spare power instead of gathering materials over days. Money is euro only: a per-inhabitant daily tax funds a treasury spent in a global market, where buy and sell lots trade against the world stock and move its price. The hard economy keeps its per-nation currencies, central banks and exchange rates. Simple games skip the price-settling warmup and economic migration; the snapshot carries the economy model, the market and the pool figures the client draws. Added the matching test suites and the design briefs under docs/simple_economy.
614 lines
25 KiB
JavaScript
614 lines
25 KiB
JavaScript
// Authoritative game model. It owns the generated world, cities, units,
|
|
// territory, population, the economy, combat and diplomacy, and is completely
|
|
// free of any rendering or transport concerns, so the Node server can own it
|
|
// and both sides can share the data catalogue. Clients never run this class
|
|
// against a live game; they render snapshots of it and send orders back.
|
|
//
|
|
// The behaviour is split across the modules in ./game_state/ and composed onto
|
|
// the prototype below. Everything still lives on the one GameState object, so
|
|
// methods can freely call each other through `this`; the split is purely about
|
|
// keeping each concern in a file small enough to read.
|
|
|
|
import { key } from "./hex.js";
|
|
import { HexPathfinder } from "./hex_pathfinder.js";
|
|
import { randomSeed, Random } from "./rng.js";
|
|
import {
|
|
PROTO_UNITS,
|
|
BUILDINGS,
|
|
GOVERNMENTS,
|
|
TECHNOLOGIES,
|
|
MAP_CONFIG,
|
|
DEFAULT_ECONOMY_MODEL,
|
|
normalizeEconomyModel,
|
|
} from "./data.js";
|
|
import {
|
|
MAX_NEWS,
|
|
UNIT_VISION,
|
|
TERRITORY_VISION,
|
|
HOURS_PER_DAY,
|
|
DAYS_PER_YEAR,
|
|
HOURS_PER_YEAR,
|
|
CONTROLLED_SPEED_MULTIPLIER,
|
|
DEFAULT_UNIT_SPEED,
|
|
MIN_STEP_COST,
|
|
TRAINING_QUEUE_LIMIT,
|
|
DEFENDER_DEFENSE_MULTIPLIER,
|
|
} from "./game_state/constants.js";
|
|
import { worldMethods } from "./game_state/world.js";
|
|
import { territoryMethods } from "./game_state/territory.js";
|
|
import { economyMethods } from "./game_state/economy.js";
|
|
import { movementMethods } from "./game_state/movement.js";
|
|
import { combatMethods } from "./game_state/combat.js";
|
|
import { airMethods } from "./game_state/air.js";
|
|
import { statusMethods } from "./game_state/statuses.js";
|
|
import { diplomacyMethods } from "./game_state/diplomacy.js";
|
|
import { treatyMethods } from "./game_state/treaties.js";
|
|
import { wmdMethods } from "./game_state/wmd.js";
|
|
import { intelligenceMethods } from "./game_state/intelligence.js";
|
|
import { politicsMethods } from "./game_state/politics.js";
|
|
import { orderMethods } from "./game_state/orders.js";
|
|
import { visibilityMethods } from "./game_state/visibility.js";
|
|
import { serializationMethods } from "./game_state/serialization.js";
|
|
import { simpleEconomyMethods } from "./game_state/simple_economy.js";
|
|
import { monthlyMethods } from "./game_state/monthly.js";
|
|
import { warfareMethods } from "./game_state/warfare.js";
|
|
import { resourceMethods } from "./game_state/resources.js";
|
|
import { siteMethods } from "./game_state/sites.js";
|
|
import { industryMethods } from "./game_state/industry.js";
|
|
import { budgetMethods } from "./game_state/budget.js";
|
|
import { moneyMethods } from "./game_state/money.js";
|
|
import { taxMethods } from "./game_state/taxes.js";
|
|
import { scenarioMethods } from "./game_state/scenario.js";
|
|
|
|
// Shared empty result for `unitsAt`, so an empty tile does not allocate.
|
|
const EMPTY_UNITS = [];
|
|
|
|
export {
|
|
MAX_NEWS,
|
|
UNIT_VISION,
|
|
TERRITORY_VISION,
|
|
HOURS_PER_DAY,
|
|
DAYS_PER_YEAR,
|
|
HOURS_PER_YEAR,
|
|
CONTROLLED_SPEED_MULTIPLIER,
|
|
DEFAULT_UNIT_SPEED,
|
|
MIN_STEP_COST,
|
|
TRAINING_QUEUE_LIMIT,
|
|
DEFENDER_DEFENSE_MULTIPLIER,
|
|
};
|
|
|
|
export class GameState {
|
|
constructor() {
|
|
this.mapConfig = MAP_CONFIG;
|
|
this.civilisations = [];
|
|
this.protoUnits = PROTO_UNITS.slice();
|
|
this.protoBuildings = BUILDINGS.slice();
|
|
this.governments = GOVERNMENTS.slice();
|
|
this.technologies = TECHNOLOGIES.slice();
|
|
|
|
this.seed = 0;
|
|
this.topology = null;
|
|
this.tiles = {};
|
|
this.terrainStats = null;
|
|
this.landCells = [];
|
|
this.totalHours = 0;
|
|
|
|
this._configured = false;
|
|
// Set by the server for a local test game: construction and training may be
|
|
// issued for free and instantly. Never trusted from a remote client.
|
|
this.testing = false;
|
|
// Which economy model the game runs: "simple" (the default) or "hard", the
|
|
// original per-region resource simulation. Fixed for the life of a game and
|
|
// shipped in the snapshot so the client renders the matching UI.
|
|
this.economyModel = DEFAULT_ECONOMY_MODEL;
|
|
this._generator = null;
|
|
this._pathfinder = new HexPathfinder();
|
|
this._minMovementCost = 1.0;
|
|
this._maxStepLength = 1.0;
|
|
this._listeners = [];
|
|
|
|
this._resetCollections();
|
|
}
|
|
|
|
// Recreates every per-game collection, index and counter. Shared by the
|
|
// constructor and `configure`, so a fresh instance and a restarted game
|
|
// always start from exactly the same shape.
|
|
_resetCollections() {
|
|
this.cities = [];
|
|
this.units = [];
|
|
this.territory = new Map();
|
|
// Which city's region each land tile belongs to: the nearest city, used to
|
|
// attribute population, GDP and tax to the city that develops the land.
|
|
this.tileRegion = new Map();
|
|
this._regionByCity = new Map();
|
|
// The per-city owned tile lists behind `regionTiles`, rebuilt when the
|
|
// territory or region map changes (see territory.js).
|
|
this._regionTilesCache = null;
|
|
// Tiles carrying a road (pre-generated city network, plus anything the
|
|
// players build).
|
|
this.roads = new Set();
|
|
// Tiles carrying a railway. A tile holds a road or a railway, never both.
|
|
this.railways = new Set();
|
|
// War-time tile works: trenches dug by infantry, and military improvements
|
|
// (fortifications, radar towers, coastal cannons) with their owner and
|
|
// remaining hit points. A tile works queue per civilisation advances them.
|
|
this.trenches = new Set();
|
|
this.tileImprovements = new Map();
|
|
this.tileImprovementOwner = new Map();
|
|
this.tileImprovementHp = new Map();
|
|
// Tile improvements under construction, as economic nodes keyed by tile: the
|
|
// site gathers materials and builds before the improvement stands. Each
|
|
// holds its own budget and the prices locked when the work was decided.
|
|
this.constructionSites = new Map();
|
|
// Money left over from a finished work, held on its tile as the
|
|
// improvement's own cash.
|
|
this.improvementCash = new Map();
|
|
// Production buildings as independent private-sector agents, keyed by tile:
|
|
// each keeps its own cash, upgrade level and sales history.
|
|
this.buildingAgents = new Map();
|
|
this._tileImprovementVersion = 0;
|
|
// Bumped whenever the transport network changes, so the serialised road and
|
|
// railway lists are rebuilt only when something actually moved.
|
|
this._improvementVersion = 0;
|
|
this.tilePopulation = new Map();
|
|
// Per-tile production-per-head multiplier, lowered by pillaging (default 1).
|
|
this.tileProductionPenalty = new Map();
|
|
// Absolute production per head lost to fighting on a tile, and the hour its
|
|
// recovery may begin. A tile leaves both maps once it has recovered.
|
|
this.tileBattleProductionDeficit = new Map();
|
|
this.tileBattleRecoveryStart = new Map();
|
|
// Tiles crippled by a spy's sabotage, each mapped to the hour its week ends.
|
|
this.tileSabotage = new Map();
|
|
// Espionage: reports each nation's spies have brought home (per civ, capped),
|
|
// the chat log comms spying reads, and the spy satellites in orbit.
|
|
this.spyReports = new Map();
|
|
this._nextSpyReportId = 1;
|
|
this.messages = [];
|
|
this.satellites = [];
|
|
this.budgets = new Map();
|
|
this.training = new Map();
|
|
this.culture = new Map();
|
|
this.government = new Map();
|
|
this.researched = new Map();
|
|
// Focused research: each nation works on one technology at a time and every
|
|
// point it creates flows straight into that technology. There is no bank: a
|
|
// nation with no focus simply does not advance. `techProgress` keeps the
|
|
// points committed to every technology the nation has started, so switching
|
|
// focus never loses work already done, and `repeatCounts` counts how many
|
|
// times each repeatable technology has been researched.
|
|
this.focusTechnology = new Map();
|
|
this.techProgress = new Map();
|
|
this.repeatCounts = new Map();
|
|
this.explored = new Map();
|
|
this.visible = new Map();
|
|
// Diplomatic relations, stored as a set of canonical "a:b" keys of pairs
|
|
// at war. Every other pair is at peace by default.
|
|
this.wars = new Set();
|
|
// One record per war, keyed like `wars`: when it started and the military
|
|
// and civilian casualties each side has suffered so far.
|
|
this.conflicts = new Map();
|
|
// Diplomatic treaties: offers waiting for an answer, the agreements in
|
|
// force, and the ones that ended (broken, rejected, or made obsolete).
|
|
this.treaties = [];
|
|
this.treatyProposals = [];
|
|
this.treatyHistory = [];
|
|
this._nextTreatyId = 1;
|
|
this._nextTreatyProposalId = 1;
|
|
this._treatyHistoryLimit = 40;
|
|
this.news = [];
|
|
|
|
// Politics: the ethnic makeup of every tile, the opinions each ethnicity
|
|
// holds of every other ethnicity and government, the war crime grudges a
|
|
// government carries, its active propaganda campaigns and minority
|
|
// policies, and small caches/indexes the daily politics tick leans on.
|
|
this.tileEthnicity = new Map();
|
|
this.ethnicOpinions = new Map();
|
|
this.govOpinions = new Map();
|
|
this.grudges = new Map();
|
|
this.campaigns = [];
|
|
this.policies = new Map();
|
|
// The day's migration flows, rebuilt each migration tick: civ -> { inbound,
|
|
// outbound } as Map<ethnicity, people>.
|
|
this.migrations = new Map();
|
|
// Signed migration change over the last day, per scope: tile, city and
|
|
// civ, each a Map<key, Map<ethnicity, people>>. Drives the up/down arrows.
|
|
this._migrationDeltas = { civ: new Map(), city: new Map(), tile: new Map() };
|
|
this._nextCampaignId = 1;
|
|
this._nextPolicyId = 1;
|
|
this._rebellionCooldown = new Map();
|
|
this._politicsVersion = 0;
|
|
// Monotonic counters for the large per-tile collections. Snapshots ship
|
|
// them so a viewer can tell which collections actually changed and rebuild
|
|
// only those, and the server can omit the unchanged ones from the wire.
|
|
this._territoryVersion = 0;
|
|
this._regionVersion = 0;
|
|
this._populationVersion = 0;
|
|
this._ethnicityVersion = 0;
|
|
this._visibleVersion = 0;
|
|
// Each land tile's opening production per head, the constant natural growth
|
|
// is measured from. Static once the world exists.
|
|
this._productionBaselineVersion = 0;
|
|
this._serializedProductionBaselineCache = null;
|
|
this.tileProductionBaseline = new Map();
|
|
// Approval is expensive to weight over every tile and is read far more
|
|
// often than politics changes, so both the per-city and per-nation results
|
|
// are memoised against the politics version (and cleared on territory
|
|
// changes). The full approval matrix the snapshot ships is cached too.
|
|
this._approvalCache = new Map();
|
|
this._civApprovalCache = new Map();
|
|
this._approvalMatrixCache = null;
|
|
// Per-civ and per-city ethnic make-ups, rebuilt only when the population,
|
|
// ethnicity, territory or migration deltas move (see politics.js).
|
|
this._civEthnicMakeupCache = new Map();
|
|
this._cityEthnicMakeupCache = new Map();
|
|
// The full per-region income-per-head map `_migrateForIncome` builds each
|
|
// day, kept until a counter its walk reads moves (see politics.js).
|
|
this._regionIncomeCache = null;
|
|
|
|
this._nextUnitId = 1;
|
|
this._nextCityId = 1;
|
|
this._nextNewsId = 1;
|
|
this._cityNamesUsed = new Map();
|
|
this._usedCityNames = new Set();
|
|
this._unitById = new Map();
|
|
this._unitsByTile = new Map();
|
|
// Per-civ "tiles held by hostile troops", rebuilt the moment a unit is
|
|
// spawned, moved or killed, or a war begins; read many times a day.
|
|
this._hostileOccupiedCache = new Map();
|
|
// The memoised battlefield scan (see combat.js `_battleTiles`). The counter
|
|
// moves whenever a unit joins or leaves a tile, or changes side, so the
|
|
// many reads per snapshot share one walk over the army.
|
|
this._battleVersion = 0;
|
|
this._battleTilesCache = null;
|
|
this._battleIdsCache = null;
|
|
// Per-(city, mode) trade reachability and the port-access memo. The graph is
|
|
// expensive to walk (sea lanes run any distance, so a walk floods the ocean),
|
|
// so it is kept across the day's sourcing passes *and* across days, and only
|
|
// thrown away when the transport network itself changes. `_tradeGraphStamp`
|
|
// records which network version the cache holds; roads, railways, storage
|
|
// buildings and territory each bump their own counter, so any change picks a
|
|
// fresh stamp and rebuilds lazily. Unit movement only matters while a war is
|
|
// on (a hostile camp severs a route), see `_invalidateUnitCaches`.
|
|
this._tradeGraphCache = null;
|
|
this._tradeGraphStamp = null;
|
|
this._marketAccessCache = null;
|
|
// Per-city populations, read once for the day's surplus checks.
|
|
this._nodePopulationCache = null;
|
|
this._cityById = new Map();
|
|
this._cityByCoords = new Map();
|
|
this._cityAdjacent = new Map();
|
|
this._territoryByCiv = new Map();
|
|
this._clearTileGdpCache();
|
|
// Per-civ population/GDP sums, memoised against the GDP generation,
|
|
// population version and territory version (see `_basePlayerAggregates`).
|
|
this._baseAggregatesCache = new Map();
|
|
// Memoised nation-wide modifier totals, invalidated by _touchModifiers.
|
|
this._civModifierCache = new Map();
|
|
// Per-city building modifiers and per-nation government/tech modifiers, read
|
|
// once per populated tile during the daily economy walk; both invalidated by
|
|
// `_modifiersVersion`.
|
|
this._cityBuildingModifierCache = new Map();
|
|
this._nationalModifierCache = new Map();
|
|
// Per-region port production and food bonuses, likewise keyed to the
|
|
// modifier version (buildings and ports only change through it).
|
|
this._regionSeaBonusCache = new Map();
|
|
// Per-nation road/railway tile counts, keyed to the network and territory.
|
|
this._transportCountsCache = new Map();
|
|
this._modifiersVersion = 0;
|
|
// Per-broadcast memo of `getCityEconomy`, active only while the server is
|
|
// assembling one snapshot (see beginSnapshotCache). Kept separate from the
|
|
// simulation paths so an hourly tick can never read a stale figure.
|
|
this._cityEconomyCache = new Map();
|
|
this._cityEconomyCacheActive = false;
|
|
// Per-civ factors that map generation picked so every nation starts with
|
|
// the same population and production per head (see _balanceStartingEconomy).
|
|
this._populationBalance = new Map();
|
|
this._productionBalance = new Map();
|
|
this._serializedTerritory = [];
|
|
this._serializedExplored = new Map();
|
|
this._serializedVisible = new Map();
|
|
this._serializedTileEthnicityCache = null;
|
|
this._neighbourCache = new Map();
|
|
this._visibilityDirty = true;
|
|
// Month-over-month baseline for the nation summary bar; null until the
|
|
// world exists and January 2000 has a month to compare against.
|
|
this.monthlyStats = null;
|
|
this._monthIndexNow = 0;
|
|
this._currentMonthStats = null;
|
|
}
|
|
|
|
onChanged(fn) {
|
|
this._listeners.push(fn);
|
|
}
|
|
|
|
_emitChanged() {
|
|
for (const fn of this._listeners) fn();
|
|
}
|
|
|
|
// Scopes the per-broadcast city-economy memo. The server brackets one
|
|
// `serializeShared` plus every viewer's stats in these calls; outside them
|
|
// `getCityEconomy` always derives a fresh figure, so an hourly simulation
|
|
// tick can never read a value cached for a snapshot.
|
|
beginSnapshotCache() {
|
|
this._cityEconomyCache.clear();
|
|
this._cityEconomyCacheActive = true;
|
|
}
|
|
|
|
endSnapshotCache() {
|
|
this._cityEconomyCacheActive = false;
|
|
this._cityEconomyCache.clear();
|
|
}
|
|
|
|
isConfigured() {
|
|
return this._configured;
|
|
}
|
|
|
|
// Whether this game runs the simple economy. Branch on this rather than
|
|
// comparing `economyModel` to a string at each call site.
|
|
isSimpleEconomy() {
|
|
return this.economyModel === DEFAULT_ECONOMY_MODEL;
|
|
}
|
|
|
|
configure(civs, worldSeed = 0, options = {}) {
|
|
// The model is fixed before the world is built, because world and scenario
|
|
// seeding differ per model. Unknown values coerce to the default. Assigning
|
|
// the simple overrides as own properties shadows the hard mixins on the
|
|
// prototype; dropping any previous ones first makes a reconfigure from
|
|
// simple back to hard fall through to the untouched prototype again.
|
|
this.economyModel = normalizeEconomyModel(options.economyModel);
|
|
for (const name of Object.keys(simpleEconomyMethods)) delete this[name];
|
|
if (this.isSimpleEconomy()) Object.assign(this, simpleEconomyMethods);
|
|
this.civilisations = civs.slice();
|
|
// The map seeds one small island per nation (up to the map's island
|
|
// budget), and those islands are the first capitals, so generation has to
|
|
// know how many nations are playing. The figure travels in the snapshot so
|
|
// the browser rebuilds the very same islands.
|
|
this.mapConfig = { ...this.mapConfig, nationCount: this.civilisations.length };
|
|
this.seed = worldSeed !== 0 ? worldSeed | 0 : randomSeed();
|
|
// The small islands become the first capitals, so a nation's position in
|
|
// this list decides whether it starts island-bound. Shuffle the countries
|
|
// from the world seed so the same few nations are not always the ones
|
|
// seeded onto the islands. Players pick their nation from this same order,
|
|
// and the snapshot ships it, so client and server stay in step.
|
|
new Random((this.seed ^ 0x1b873593) >>> 0).shuffle(this.civilisations);
|
|
// Combat rolls (a city losing a building level to a bombardment) draw from
|
|
// their own generator, seeded from the world so a game is reproducible.
|
|
this._combatRng = new Random((this.seed ^ 0x5bd1e995) >>> 0);
|
|
this._buildTerrain();
|
|
|
|
this._resetCollections();
|
|
for (let i = 0; i < this.civilisations.length; i++) {
|
|
this.culture.set(i, 0);
|
|
this.government.set(i, 0);
|
|
this.researched.set(i, new Set());
|
|
this.focusTechnology.set(i, -1);
|
|
this.techProgress.set(i, new Map());
|
|
this.repeatCounts.set(i, new Map());
|
|
}
|
|
this.totalHours = 0;
|
|
this._configured = true;
|
|
|
|
this._placeSettlements();
|
|
this._initResources();
|
|
this._initMoney();
|
|
this._initTaxes();
|
|
this._refreshStatuses();
|
|
this._revealAll();
|
|
this._refreshVisibility();
|
|
this._initMonthly();
|
|
this._initNaturalProductionGrowth();
|
|
this._visibilityDirty = false;
|
|
this._emitChanged();
|
|
}
|
|
|
|
// ------------------------------------------------------------- lookups --
|
|
|
|
findUnit(unitId) {
|
|
return this._unitById.get(unitId) || null;
|
|
}
|
|
|
|
findCity(cityId) {
|
|
return this._cityById.get(cityId) || null;
|
|
}
|
|
|
|
unitProto(unit) {
|
|
const index = unit.proto;
|
|
if (index < 0 || index >= this.protoUnits.length) return null;
|
|
return this.protoUnits[index];
|
|
}
|
|
|
|
cityAt(coords) {
|
|
return this._cityByCoords.get(key(coords.x, coords.y)) || null;
|
|
}
|
|
|
|
// A seeded [0, 1) roll for the simulation's combat effects. Reproducible from
|
|
// the world seed, so replaying a game gives the same bombardment results.
|
|
_random() {
|
|
if (!this._combatRng) this._combatRng = new Random((this.seed ^ 0x5bd1e995) >>> 0);
|
|
return this._combatRng.float();
|
|
}
|
|
|
|
// Every unit standing on `coords`. Friendly units may stack, so a tile can
|
|
// hold more than one. Backed by a per-tile index so the many supply, siege and
|
|
// battle checks that ask "who is on this tile" never scan the whole army.
|
|
unitsAt(coords) {
|
|
const bucket = this._unitsByTile && this._unitsByTile.get(key(coords.x, coords.y));
|
|
if (bucket) return bucket;
|
|
return EMPTY_UNITS;
|
|
}
|
|
|
|
_indexUnit(unit) {
|
|
if (!this._unitsByTile) this._unitsByTile = new Map();
|
|
this._invalidateUnitCaches();
|
|
const k = key(unit.coords.x, unit.coords.y);
|
|
let bucket = this._unitsByTile.get(k);
|
|
if (!bucket) {
|
|
bucket = [];
|
|
this._unitsByTile.set(k, bucket);
|
|
}
|
|
bucket.push(unit);
|
|
}
|
|
|
|
_unindexUnit(unit) {
|
|
if (!this._unitsByTile) return;
|
|
this._invalidateUnitCaches();
|
|
const k = key(unit.coords.x, unit.coords.y);
|
|
const bucket = this._unitsByTile.get(k);
|
|
if (!bucket) return;
|
|
const index = bucket.indexOf(unit);
|
|
if (index >= 0) bucket.splice(index, 1);
|
|
if (bucket.length === 0) this._unitsByTile.delete(k);
|
|
}
|
|
|
|
// Drops the memos that depend on where units stand: which tiles hostile
|
|
// troops hold, the trade routes they sever and whether a port is blockaded.
|
|
_invalidateSpatialCaches() {
|
|
if (this._hostileOccupiedCache) this._hostileOccupiedCache.clear();
|
|
if (this._tradeGraphCache) {
|
|
this._tradeGraphCache.clear();
|
|
this._tradeGraphStamp = null;
|
|
}
|
|
if (this._marketAccessCache) this._marketAccessCache.clear();
|
|
}
|
|
|
|
// The narrower drop for a unit spawning, moving or dying. Hostile-held tiles
|
|
// and port access depend on where troops stand, so those memos go. The trade
|
|
// graph does not: it only reads unit positions through `_hostileOccupied`,
|
|
// which is empty unless a war is on, so a move in peacetime leaves the cached
|
|
// walks valid. While a war is on, any move may sever a route, so drop it then.
|
|
_invalidateUnitCaches() {
|
|
// A unit joining or leaving a tile can start or end a battle, so the
|
|
// memoised battlefield scan goes too. `unit.civ` changes (a capture) bump
|
|
// it from the capture helpers instead.
|
|
this._battleVersion = (this._battleVersion || 0) + 1;
|
|
if (this._hostileOccupiedCache) this._hostileOccupiedCache.clear();
|
|
if (this._marketAccessCache) this._marketAccessCache.clear();
|
|
if (this._tradeGraphCache && this.wars && this.wars.size > 0) {
|
|
this._tradeGraphCache.clear();
|
|
this._tradeGraphStamp = null;
|
|
}
|
|
}
|
|
|
|
// Moves a unit to a new tile and keeps the per-tile index in step.
|
|
_moveUnitTo(unit, coords) {
|
|
this._unindexUnit(unit);
|
|
unit.coords = coords;
|
|
this._indexUnit(unit);
|
|
}
|
|
|
|
unitAt(coords) {
|
|
return this.unitsAt(coords)[0] || null;
|
|
}
|
|
|
|
// The hostile units standing on `coords` (when seen by `civ`).
|
|
enemyUnitsAt(coords, civ) {
|
|
return this.unitsAt(coords).filter((unit) => unit.civ !== civ);
|
|
}
|
|
|
|
advanceHour() {
|
|
this.advanceMovement(1.0);
|
|
this.tickHour();
|
|
}
|
|
|
|
// Runs `days` of the world as fast as the machine allows, using the very same
|
|
// `advanceHour` the live clock runs, so a fresh game can open settled instead
|
|
// of at a cold start. Nothing changes the transport network while the settle
|
|
// runs, so the trade-graph cache (see `_tradeGraph`) is simply reused across
|
|
// every day; a settle would otherwise re-flood every sea lane daily.
|
|
warmUp(days) {
|
|
const hours = Math.max(0, Math.floor(days)) * HOURS_PER_DAY;
|
|
if (hours <= 0) return;
|
|
try {
|
|
for (let hour = 0; hour < hours; hour++) this.advanceHour();
|
|
} finally {
|
|
this._resetSettleCaches();
|
|
}
|
|
}
|
|
|
|
// The caches a settle leaves stale: the trade graph is kept across the run
|
|
// (nothing alters the network), so it is dropped only once the settle ends.
|
|
_resetSettleCaches() {
|
|
this._tradeGraphCache = null;
|
|
this._tradeGraphStamp = null;
|
|
this._marketAccessCache = null;
|
|
}
|
|
|
|
tickHour() {
|
|
this.totalHours += 1;
|
|
// Natural growth is a whole number of days, so the cached production figures
|
|
// only go stale when a new day begins.
|
|
if (this.totalHours % HOURS_PER_DAY === 0) this._clearTileGdpCache();
|
|
this._tickMonthly();
|
|
this._tickTraining();
|
|
this._tickAirUnits();
|
|
this._tickStrikes();
|
|
this._tickSabotage();
|
|
this._tickSatellites();
|
|
this._tickGarrisonHealing();
|
|
// Popularity drifts toward neutral every hour, not just daily.
|
|
this._tickOpinionDrift();
|
|
// Stale treaty offers fall out of the receiver's inbox.
|
|
this._expireTreatyProposals();
|
|
if (this._visibilityDirty) {
|
|
this._refreshVisibility();
|
|
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();
|
|
this._tickProductionRecovery();
|
|
this._tickMoney();
|
|
this._tickEconomy();
|
|
this._tickResources();
|
|
this._tickRepairs();
|
|
this._tickUnitSupply();
|
|
this._refreshStatuses();
|
|
this._applyDailyStatusEffects();
|
|
this._resolveCombatRound();
|
|
}
|
|
this._tickBuildings();
|
|
this._tickConstructionSites();
|
|
this._tickIndustry();
|
|
this._tickEmbark();
|
|
this._tickCoastalCannons();
|
|
// A unit no longer sharing a tile with an enemy is not an invader any more.
|
|
for (const unit of this.units) {
|
|
if (unit.invader && !this._hasHostileOn(unit.coords, unit.civ)) unit.invader = false;
|
|
}
|
|
this._emitChanged();
|
|
}
|
|
}
|
|
|
|
// Compose the behaviour mixins onto the prototype. Each module owns one concern
|
|
// and may call methods from any other through `this`.
|
|
Object.assign(
|
|
GameState.prototype,
|
|
worldMethods,
|
|
territoryMethods,
|
|
economyMethods,
|
|
movementMethods,
|
|
combatMethods,
|
|
airMethods,
|
|
statusMethods,
|
|
diplomacyMethods,
|
|
treatyMethods,
|
|
wmdMethods,
|
|
intelligenceMethods,
|
|
politicsMethods,
|
|
orderMethods,
|
|
visibilityMethods,
|
|
serializationMethods,
|
|
monthlyMethods,
|
|
warfareMethods,
|
|
resourceMethods,
|
|
siteMethods,
|
|
industryMethods,
|
|
budgetMethods,
|
|
moneyMethods,
|
|
taxMethods,
|
|
scenarioMethods
|
|
);
|