Files
Battle-for-Tismo/shared/game_state/statuses.js
T
adrien e7ee5dbf0e Added the resource economy, its works and their icons
- 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.
2026-09-22 05:27:11 +02:00

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;
},
};