- Tile improvements (fortifications, radar towers, coastal cannons), trenches, zone of control, support-unit capture and launch transports, with parabolic barrage arcs drawn between attacker and target. - Focused research that pours points straight into the chosen technology, with per-technology progress kept when the focus changes, plus repeatable levels. - Popularity reworked to integer points, war and pillage effects, minority policies and month-over-month arrows on the nation summary. - City progress bars, square improvement buttons, a technology tab of wide short cards with progress bars, transport cargo readout and double-click to open a city panel.
234 lines
8.6 KiB
JavaScript
234 lines
8.6 KiB
JavaScript
// Territory: which civilisation owns each land tile, how ownership spreads
|
|
// from cities, and how it is redrawn when a city changes hands.
|
|
|
|
import { key, parseKey } from "../hex.js";
|
|
import { NEWS_CAPTURE } from "../data/relations.js";
|
|
import { POPULARITY } from "../data/politics.js";
|
|
import { tileImprovementById } from "../data/improvements.js";
|
|
import { serializeCells } from "./helpers.js";
|
|
|
|
export const territoryMethods = {
|
|
civAt(coords) {
|
|
const k = key(coords.x, coords.y);
|
|
return this.territory.has(k) ? this.territory.get(k) : -1;
|
|
},
|
|
|
|
terrainTypeAt(coords) {
|
|
const tile = this.tiles[key(coords.x, coords.y)];
|
|
return tile ? tile.terrainType : "Unknown";
|
|
},
|
|
|
|
getTilePopulation(coords) {
|
|
return Math.round(this.tilePopulation.get(key(coords.x, coords.y)) || 0);
|
|
},
|
|
|
|
getTileInfo(coords) {
|
|
const city = this.cityAt(coords);
|
|
return {
|
|
coords,
|
|
owner: this.civAt(coords),
|
|
terrainType: this.terrainTypeAt(coords),
|
|
population: this.getTilePopulation(coords),
|
|
improvements: city ? city.improvements : [],
|
|
};
|
|
},
|
|
|
|
_recomputeTerritory() {
|
|
this.territory = new Map();
|
|
this._gdpPerCapitaCache = new Map();
|
|
const ownerByCell = new Map();
|
|
let frontier = [];
|
|
for (const city of this.cities) {
|
|
const k = key(city.coords.x, city.coords.y);
|
|
ownerByCell.set(k, city.civ);
|
|
this.territory.set(k, city.civ);
|
|
frontier.push(city.coords);
|
|
}
|
|
while (frontier.length > 0) {
|
|
const next = [];
|
|
for (const coords of frontier) {
|
|
const owner = ownerByCell.get(key(coords.x, coords.y));
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const nk = key(neighbour.x, neighbour.y);
|
|
if (ownerByCell.has(nk) || !this._isLand(neighbour)) continue;
|
|
ownerByCell.set(nk, owner);
|
|
this.territory.set(nk, owner);
|
|
next.push(neighbour);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
this._rebuildRegionIndex();
|
|
this._indexTerritory();
|
|
},
|
|
|
|
// The tiles a nation's fortifications anchor: a fort's own tile and its
|
|
// neighbours. Even if an enemy takes the nearby city, these tiles stay.
|
|
_fortifiedControl(civ) {
|
|
const anchored = new Set();
|
|
for (const [k, id] of this.tileImprovements) {
|
|
const proto = tileImprovementById(id);
|
|
if (!proto || !proto.permanentControl) continue;
|
|
if (this.tileImprovementOwner.get(k) !== civ) continue;
|
|
anchored.add(k);
|
|
for (const neighbour of this._neighbours(parseKey(k))) {
|
|
anchored.add(key(neighbour.x, neighbour.y));
|
|
}
|
|
}
|
|
return anchored;
|
|
},
|
|
|
|
// Subdivides each nation's territory into the regions of its own cities: a
|
|
// multi-source BFS grown from every city, but never crossing into another
|
|
// nation's land. Ties between equally close cities are settled by city order,
|
|
// so the result is deterministic and identical on both sides of the wire.
|
|
_rebuildRegionIndex() {
|
|
this._regionVersion += 1;
|
|
this.tileRegion = new Map();
|
|
this._regionByCity = new Map();
|
|
const cityByCell = new Map();
|
|
let frontier = [];
|
|
for (const city of this.cities) {
|
|
const k = key(city.coords.x, city.coords.y);
|
|
cityByCell.set(k, city);
|
|
this.tileRegion.set(k, city.id);
|
|
frontier.push(city.coords);
|
|
}
|
|
while (frontier.length > 0) {
|
|
const next = [];
|
|
for (const coords of frontier) {
|
|
const city = cityByCell.get(key(coords.x, coords.y));
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const nk = key(neighbour.x, neighbour.y);
|
|
if (cityByCell.has(nk) || !this._isLand(neighbour)) continue;
|
|
if (this.territory.get(nk) !== city.civ) continue;
|
|
cityByCell.set(nk, city);
|
|
this.tileRegion.set(nk, city.id);
|
|
next.push(neighbour);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
for (const [k, cityId] of this.tileRegion) {
|
|
if (!this._regionByCity.has(cityId)) this._regionByCity.set(cityId, []);
|
|
this._regionByCity.get(cityId).push(parseKey(k));
|
|
}
|
|
},
|
|
|
|
// The city whose region the tile belongs to, or null on unclaimed land.
|
|
regionCityAt(coords) {
|
|
const id = this.tileRegion.get(key(coords.x, coords.y));
|
|
return id !== undefined ? this._cityById.get(id) || null : null;
|
|
},
|
|
|
|
// The land tiles a city's region covers that are still owned by the city's
|
|
// civilisation. A freshly captured city only taxes the land that came with it.
|
|
regionTiles(city) {
|
|
const tiles = this._regionByCity.get(city.id) || [];
|
|
return tiles.filter((coords) => this.civAt(coords) === city.civ);
|
|
},
|
|
|
|
_indexTerritory() {
|
|
this._territoryVersion += 1;
|
|
this._territoryByCiv = new Map();
|
|
for (const [k, owner] of this.territory) {
|
|
if (!this._territoryByCiv.has(owner)) this._territoryByCiv.set(owner, []);
|
|
this._territoryByCiv.get(owner).push(parseKey(k));
|
|
}
|
|
this._serializedTerritory = serializeCells(this.territory);
|
|
this._gdpPerCapitaCache = new Map();
|
|
// Which tiles a nation holds changes whose people count toward approval,
|
|
// so a territory redraw invalidates the memoised approval figures.
|
|
this._touchPolitics();
|
|
},
|
|
|
|
// Rebuilds the owner index, the regions, the serialised territory and the
|
|
// GDP-per-capita cache after `this.territory` changes outside a full
|
|
// recompute.
|
|
_reindexTerritory() {
|
|
this._rebuildRegionIndex();
|
|
this._indexTerritory();
|
|
},
|
|
|
|
// Transfers `city` to `newCiv` and, with it, every tile of the old owner's
|
|
// territory that is strictly closer (through land) to the captured city than
|
|
// to any of their remaining cities. Ties stay with the old owner.
|
|
_captureCity(city, newCiv) {
|
|
const oldCiv = city.civ;
|
|
if (oldCiv === newCiv) return false;
|
|
const region = (this._territoryByCiv.get(oldCiv) || []).slice();
|
|
const claim = this._nearestCityOwner(city, oldCiv, newCiv);
|
|
// Fortifications hold a permanent area of control: their tile and the ring
|
|
// around it stay with the builder even when the city nearby changes hands.
|
|
const anchored = this._fortifiedControl(oldCiv);
|
|
city.civ = newCiv;
|
|
for (const coords of region) {
|
|
const k = key(coords.x, coords.y);
|
|
if (anchored.has(k)) continue;
|
|
if (claim.get(k) === newCiv) this.territory.set(k, newCiv);
|
|
}
|
|
// The captured city's own tile always changes hands, ties or not.
|
|
this.territory.set(key(city.coords.x, city.coords.y), newCiv);
|
|
this.training.delete(city.id);
|
|
this._reindexTerritory();
|
|
this._rebuildCityIndex();
|
|
this._visibilityDirty = true;
|
|
// Aircraft caught on the captured airfield change allegiance with it.
|
|
this._captureCityAircraft(city, newCiv);
|
|
// The city's buildings now serve a different nation, so its modifier
|
|
// totals changed hands.
|
|
this._touchModifiers();
|
|
// Taking a city heartens the conqueror's people and demoralises the losers.
|
|
this._shiftOwnPopularity(newCiv, POPULARITY.cityCapture);
|
|
this._shiftOwnPopularity(oldCiv, -POPULARITY.cityLoss);
|
|
this._addNews({ type: NEWS_CAPTURE, city: city.name, from: oldCiv, to: newCiv });
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// Multi-source BFS over land from the captured city and every remaining city
|
|
// of the old owner. Each tile records the civ of the nearest city; a tile
|
|
// reached at the same distance from both sides is credited to the old owner.
|
|
_nearestCityOwner(captured, oldCiv, newCiv) {
|
|
const claim = new Map();
|
|
const visited = new Set();
|
|
let frontier = [];
|
|
const sources = [{ coords: captured.coords, civ: newCiv }];
|
|
for (const city of this.cities) {
|
|
if (city !== captured && city.civ === oldCiv) {
|
|
sources.push({ coords: city.coords, civ: oldCiv });
|
|
}
|
|
}
|
|
for (const source of sources) {
|
|
const k = key(source.coords.x, source.coords.y);
|
|
if (visited.has(k)) continue;
|
|
visited.add(k);
|
|
claim.set(k, source.civ);
|
|
frontier.push(source.coords);
|
|
}
|
|
while (frontier.length > 0) {
|
|
const nextFrontier = [];
|
|
const nextClaim = new Map();
|
|
for (const coords of frontier) {
|
|
const civ = claim.get(key(coords.x, coords.y));
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const nk = key(neighbour.x, neighbour.y);
|
|
if (visited.has(nk) || !this._isLand(neighbour)) continue;
|
|
if (nextClaim.has(nk)) {
|
|
if (nextClaim.get(nk) === newCiv && civ === oldCiv) nextClaim.set(nk, oldCiv);
|
|
continue;
|
|
}
|
|
nextClaim.set(nk, civ);
|
|
nextFrontier.push(neighbour);
|
|
}
|
|
}
|
|
for (const [k, civ] of nextClaim) {
|
|
claim.set(k, civ);
|
|
visited.add(k);
|
|
}
|
|
frontier = nextFrontier;
|
|
}
|
|
return claim;
|
|
},
|
|
};
|