// 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")); }, _statusHpLossPerDay(def, inCity) { if (inCity && typeof def.cityHpLossPerDay === "number") return def.cityHpLossPerDay; return typeof def.hpLossPerDay === "number" ? def.hpLossPerDay : 0; }, };