Files
Battle-for-Tismo/shared/game_state/visibility.js
T

150 lines
5.2 KiB
JavaScript

// Fog of war and the grid walkers. Explored cells stay known; visible cells
// are the ones currently watched by territory and units.
import { key, parseKey } from "../hex.js";
import { UNIT_VISION, TERRITORY_VISION } from "./constants.js";
import { tileImprovementById } from "../data/improvements.js";
import { serializeSet } from "./helpers.js";
export const visibilityMethods = {
isExplored(civ, coords) {
const set = this.explored.get(civ);
return set ? set.has(key(coords.x, coords.y)) : false;
},
isVisible(civ, coords) {
const set = this.visible.get(civ);
return set ? set.has(key(coords.x, coords.y)) : false;
},
canEnter(civ, proto, coords) {
if (!proto || !this.isExplored(civ, coords)) return false;
const tile = this.tiles[key(coords.x, coords.y)];
if (!tile) return false;
return proto.traversableTerrains.includes(tile.terrainClass);
},
_refreshVisibility() {
this.visible = new Map();
for (let i = 0; i < this.civilisations.length; i++) this.visible.set(i, new Set());
for (const [civ, cells] of this._territoryByCiv) {
for (const coords of cells) this._reveal(civ, coords, TERRITORY_VISION);
}
for (const unit of this.units) {
const proto = this.unitProto(unit);
this._reveal(unit.civ, unit.coords, (proto && proto.vision) || UNIT_VISION);
}
// Fortifications and radar towers watch their own neighbourhood too.
for (const [k, id] of this.tileImprovements) {
const proto = tileImprovementById(id);
if (!proto || !proto.vision) continue;
const owner = this.tileImprovementOwner.get(k);
if (owner === undefined || owner < 0) continue;
this._reveal(owner, parseKey(k), Math.max(UNIT_VISION, proto.vision));
}
// The visible sets only move here, so this counter lets a delta snapshot
// omit the (large) per-viewer visible list until the next refresh.
this._visibleVersion += 1;
},
// The enemy units a nation cannot see: a submarine stays hidden unless one of
// the viewer's naval units sits on its tile or directly beside it, and a spy
// is hidden from every other nation outright. A spectator (viewer -1) sees
// everything.
_hiddenEnemyUnits(viewerCiv) {
const hidden = new Set();
if (viewerCiv < 0) return hidden;
for (const unit of this.units) {
const proto = this.unitProto(unit);
if (!proto || unit.civ === viewerCiv) continue;
if (proto.spy) {
hidden.add(unit.id);
continue;
}
if (proto.submarine && !this._submarineDetectedBy(unit.coords, viewerCiv)) {
hidden.add(unit.id);
}
}
return hidden;
},
_submarineDetectedBy(coords, viewerCiv) {
for (const spot of [coords, ...this._neighbours(coords)]) {
for (const other of this.unitsAt(spot)) {
if (other.civ !== viewerCiv) continue;
const proto = this.unitProto(other);
if (proto && proto.naval) return true;
}
}
return false;
},
_revealAll() { for (let i = 0; i < this.civilisations.length; i++) {
this.explored.set(i, new Set(Object.keys(this.tiles)));
}
},
_reveal(civ, center, radius) {
if (!this.explored.has(civ)) this.explored.set(civ, new Set());
if (!this.visible.has(civ)) this.visible.set(civ, new Set());
const exploredCells = this.explored.get(civ);
const visibleCells = this.visible.get(civ);
let discovered = false;
for (const coords of this._cellsInRadius(center, radius)) {
const k = key(coords.x, coords.y);
if (!exploredCells.has(k)) {
exploredCells.add(k);
discovered = true;
}
visibleCells.add(k);
}
if (discovered) this._serializedExplored.delete(civ);
},
_cachedExplored(civ) {
if (!this._serializedExplored.has(civ)) {
this._serializedExplored.set(civ, serializeSet(this.explored.get(civ) || new Set()));
}
return this._serializedExplored.get(civ);
},
// The viewer's currently visible tiles, flattened for the wire. The whole
// visible set is rebuilt only on a visibility refresh, which bumps
// `_visibleVersion`, so an unchanged fog is not re-walked on every broadcast.
_cachedVisible(civ) {
const cached = this._serializedVisible.get(civ);
if (cached && cached.version === this._visibleVersion) return cached.value;
const value = serializeSet(this.visible.get(civ) || new Set());
this._serializedVisible.set(civ, { version: this._visibleVersion, value });
return value;
},
_cellsInRadius(center, radius) {
const result = [center];
const visited = new Set([key(center.x, center.y)]);
let frontier = [center];
for (let step = 0; step < radius; step++) {
const next = [];
for (const coords of frontier) {
for (const neighbour of this._neighbours(coords)) {
const k = key(neighbour.x, neighbour.y);
if (visited.has(k)) continue;
visited.add(k);
next.push(neighbour);
result.push(neighbour);
}
}
frontier = next;
}
return result;
},
_neighbours(coords) {
const k = key(coords.x, coords.y);
if (this._neighbourCache.has(k)) return this._neighbourCache.get(k);
const result = this.topology.neighbours(coords.x, coords.y);
this._neighbourCache.set(k, result);
return result;
},
};