- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep. - Resource-producing tile works built outside cities; every nation opens with the works its economy needs. - Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated.
130 lines
4.9 KiB
JavaScript
130 lines
4.9 KiB
JavaScript
// Supply: encirclement and siege detection. A tile is cut off when enemy units
|
|
// sit on every land tile beside it and it has no working port to bring supplies
|
|
// in by sea; a port is itself blockaded when enemy warships hold every sea tile
|
|
// around its city.
|
|
|
|
import { key } from "../hex.js";
|
|
import { STATUS_ENCIRCLED, STATUS_BESIEGED } from "../data/statuses.js";
|
|
|
|
export const statusMethods = {
|
|
// Recomputes the status of every unit and city. A unit cut off is encircled;
|
|
// a city cut off is besieged. The check is local -- the ring of land tiles
|
|
// around the occupant and its own port -- so it never walks the whole map.
|
|
_refreshStatuses() {
|
|
for (const unit of this.units) {
|
|
unit.statuses = this._isEncircled(unit.coords, unit.civ) ? [STATUS_ENCIRCLED] : [];
|
|
}
|
|
for (const city of this.cities) {
|
|
city.statuses = this._isEncircled(city.coords, city.civ) ? [STATUS_BESIEGED] : [];
|
|
}
|
|
},
|
|
|
|
// Whether a hostile unit stands next to `coords`. Kept as the cheap trigger
|
|
// for callers that only care whether a fight is near.
|
|
_hasAdjacentHostile(coords, civ) {
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
if (this.unitsAt(neighbour).some((unit) => unit.civ !== civ)) return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// A tile is encircled when no enemy warship can break the ring: it has no
|
|
// working port, and every land tile beside it is held by an enemy unit. A
|
|
// tile with no land neighbours has no ring to close, so it is not encircled.
|
|
_isEncircled(coords, civ) {
|
|
if (this._hasSeaSupply(coords, civ)) return false;
|
|
return this._landRingBlocked(coords, civ);
|
|
},
|
|
|
|
_landRingBlocked(coords, civ) {
|
|
let land = 0;
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const tile = this.tiles[key(neighbour.x, neighbour.y)];
|
|
if (!tile || tile.terrainClass !== "Land") continue;
|
|
land += 1;
|
|
if (!this.unitsAt(neighbour).some((unit) => unit.civ !== civ)) return false;
|
|
}
|
|
return land > 0;
|
|
},
|
|
|
|
// A friendly city's port supplies its own tile by sea, unless the port is
|
|
// blockaded.
|
|
_hasSeaSupply(coords, civ) {
|
|
const city = this.cityAt(coords);
|
|
if (!city || city.civ !== civ) return false;
|
|
if (!this.hasCityBuilding(city, "port")) return false;
|
|
return !this._isPortBlockaded(city);
|
|
},
|
|
|
|
// A port is blockaded when enemy warships hold every sea tile around the city.
|
|
_isPortBlockaded(city) {
|
|
let sea = 0;
|
|
let held = 0;
|
|
for (const neighbour of this._neighbours(city.coords)) {
|
|
const tile = this.tiles[key(neighbour.x, neighbour.y)];
|
|
if (!tile || tile.terrainClass !== "Sea") continue;
|
|
sea += 1;
|
|
if (this.unitsAt(neighbour).some(
|
|
(unit) => unit.civ !== city.civ && this._isNaval(unit))) {
|
|
held += 1;
|
|
}
|
|
}
|
|
return sea > 0 && held === sea;
|
|
},
|
|
|
|
_isNaval(unit) {
|
|
const proto = this.unitProto(unit);
|
|
// Aircraft may fly over the sea, but only a hull counts as naval supply.
|
|
return !!(proto && !proto.air && (proto.traversableTerrains || []).includes("Sea"));
|
|
},
|
|
|
|
// Land connected components for one civ, with enemy-occupied and enemy-held
|
|
// city tiles acting as walls. Each component remembers how many of the civ's
|
|
// cities it contains. Used by the unit supply rule to find a clear route to
|
|
// an allied city.
|
|
_landComponents(civ) {
|
|
const byTile = new Map();
|
|
const cityCountByComp = new Map();
|
|
const totalCities = this.cities.filter((city) => city.civ === civ).length;
|
|
let component = 0;
|
|
for (const start of this.landCells) {
|
|
const startKey = key(start.x, start.y);
|
|
if (byTile.has(startKey) || !this._landPassable(start, civ)) continue;
|
|
const id = component++;
|
|
byTile.set(startKey, id);
|
|
let cities = 0;
|
|
let frontier = [start];
|
|
while (frontier.length > 0) {
|
|
const next = [];
|
|
for (const coords of frontier) {
|
|
const city = this.cityAt(coords);
|
|
if (city && city.civ === civ) cities += 1;
|
|
for (const neighbour of this._neighbours(coords)) {
|
|
const nk = key(neighbour.x, neighbour.y);
|
|
if (byTile.has(nk) || !this._landPassable(neighbour, civ)) continue;
|
|
byTile.set(nk, id);
|
|
next.push(neighbour);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
cityCountByComp.set(id, cities);
|
|
}
|
|
return { byTile, cityCountByComp, totalCities };
|
|
},
|
|
|
|
_landPassable(coords, civ) {
|
|
const tile = this.tiles[key(coords.x, coords.y)];
|
|
if (!tile || tile.terrainClass !== "Land") return false;
|
|
if (this.unitsAt(coords).some((unit) => unit.civ !== civ)) return false;
|
|
const city = this.cityAt(coords);
|
|
if (city && city.civ !== civ) return false;
|
|
return true;
|
|
},
|
|
|
|
_statusHpLossPerDay(def, inCity) {
|
|
if (inCity && typeof def.cityHpLossPerDay === "number") return def.cityHpLossPerDay;
|
|
return typeof def.hpLossPerDay === "number" ? def.hpLossPerDay : 0;
|
|
},
|
|
};
|