Spaces:
Paused
Paused
File size: 7,673 Bytes
e3658fa 34ea8bc 816885c 34ea8bc 8f89fac 34ea8bc 8f89fac e3658fa 34ea8bc e3658fa 34ea8bc 6db3533 34ea8bc df20f1d e3658fa df20f1d c310233 e3658fa c310233 8f89fac 34ea8bc 4559046 8f89fac 4559046 8f89fac 34ea8bc e3658fa 34ea8bc ac7d42f d228ca7 8f89fac 03f71c7 8f89fac 34ea8bc 8f89fac 34ea8bc 8f89fac 34ea8bc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | // @ts-check
/* ============================================================
Fog of war: a coarse grid tracks which cells are currently in sight of
an owner's unit/building (recomputed fresh every tick) and which have
ever been seen (permanent, once set). Both sides get their own grid β
state.fog for the player, state.fogAI for the AI β so neither is
omniscient; the same createFog/updateFog serves both (updateFog takes
the owner). See engine/ai.js for how the AI's grid gates its decisions.
Deliberately scoped to units/buildings only β charted surface deposits
(data.js) stay visible regardless, treated as known map knowledge, not
battlefield intel. Hidden caches (map.js) are the exception: they only
count as known once their cell has been explored (isNodeDiscovered).
There's no "remembered snapshot" of enemy positions once they leave
vision β they simply stop rendering (or, for the AI, stop being read).
============================================================ */
"use strict";
import { UNITS, BUILDINGS } from "./entities.js";
import { sampleTerrain, sideMod, TERRAIN } from "./map.js";
export const FOG_CELL_SIZE = 40;
// Uphill concealment (docs/improvement-proposals.md "A real LOS pass: low ground can't see onto
// high ground"): a source whose own tile is NOT high ground only reveals a high-ground CELL out
// to this fraction of its normal sight radius β a mesa is a stat pad (extra sight/damage for its
// holder) AND a genuine intelligence blind spot for anyone still down in the lowland, not a free
// look from a comfortable distance. combat.js's acquisition (nearestEnemy/spreadEnemy) imports
// this same constant so "what a lowland unit can see" and "what it can shoot at" agree.
export const UPHILL_FRAC = 0.55;
/** @param {GameMap} map @returns {Fog} */
export function createFog(map) {
const cols = Math.ceil(map.width / FOG_CELL_SIZE);
const rows = Math.ceil(map.height / FOG_CELL_SIZE);
return { cols, rows, explored: new Uint8Array(cols * rows), visible: new Uint8Array(cols * rows) };
}
function cellOf(fog, wx, wy) {
return { cx: Math.floor(wx / FOG_CELL_SIZE), cy: Math.floor(wy / FOG_CELL_SIZE) };
}
function inBounds(fog, cx, cy) {
return cx >= 0 && cy >= 0 && cx < fog.cols && cy < fog.rows;
}
/** @param {Fog} fog @param {number} wx @param {number} wy @returns {boolean} */
export function isVisibleAt(fog, wx, wy) {
const { cx, cy } = cellOf(fog, wx, wy);
return inBounds(fog, cx, cy) && fog.visible[cy * fog.cols + cx] === 1;
}
/** @param {Fog} fog @param {number} wx @param {number} wy @returns {boolean} */
export function isExploredAt(fog, wx, wy) {
const { cx, cy } = cellOf(fog, wx, wy);
return inBounds(fog, cx, cy) && fog.explored[cy * fog.cols + cx] === 1;
}
// Ordinary charted deposits are always known; a hidden cache (map.js) only
// exists for the player once they've scouted its cell. The permanent explored
// memory doubles as the discovery record, so a found cache stays on the map
// even after the scout moves on. Used by render/minimap/input alike so what
// shows, what the minimap dots, and what a right-click can target all agree.
/** @param {Fog} fog @param {ResourceNode} node @returns {boolean} */
export function isNodeDiscovered(fog, node) {
return !node.hidden || isExploredAt(fog, node.x, node.y);
}
// World point at the centre of the nearest never-explored cell to (fromX, fromY),
// or null when the whole grid has been explored. A row-major scan with a strict
// `<` keeps the pick deterministic (first cell wins a distance tie), so it's safe
// to call from the sim. Shared by the Ranger's scout mode (heads for the nearest
// dark ground to reveal it) and the AI's hunt for a hidden Command Center (sweeps
// unexplored ground when it can see no enemy). Cheap enough at this grid
// resolution to call on demand, and only ever when a new target is needed.
/** @param {Fog} fog @param {number} fromX @param {number} fromY */
export function nearestUnexploredPoint(fog, fromX, fromY) {
let best = null, bestD = Infinity;
for (let cy = 0; cy < fog.rows; cy++) {
for (let cx = 0; cx < fog.cols; cx++) {
if (fog.explored[cy * fog.cols + cx]) continue;
const wx = cx * FOG_CELL_SIZE + FOG_CELL_SIZE / 2;
const wy = cy * FOG_CELL_SIZE + FOG_CELL_SIZE / 2;
const d = (wx - fromX) * (wx - fromX) + (wy - fromY) * (wy - fromY);
if (d < bestD) { bestD = d; best = { x: wx, y: wy }; }
}
}
return best;
}
// `terrain` is the map's terrain grid (engine/map.js generateTerrain β cols/rows/cell all shared
// 1:1 with this fog grid, both 40px cells, per TERRAIN_CELL_SIZE's own comment) or undefined for a
// map-less state; `srcHigh` is whether the SOURCE's own tile is high ground (type 2), computed
// once by the caller (updateFog) rather than re-sampled per candidate cell here. A source not on
// high ground reveals a high-ground cell only out to UPHILL_FRAC of `sight` β everything else
// (open/rough ground, or a source that's itself on the high ground) reveals at the full radius.
function reveal(fog, x, y, sight, terrain, srcHigh) {
const { cx, cy } = cellOf(fog, x, y);
const reach = Math.ceil(sight / FOG_CELL_SIZE);
const sightSq = sight * sight; // compare squared distances β no per-cell sqrt (Math.hypot)
const uphillCap = sight * UPHILL_FRAC;
const uphillSq = uphillCap * uphillCap;
const gyMax = Math.min(fog.rows - 1, cy + reach), gxMax = Math.min(fog.cols - 1, cx + reach);
for (let gy = Math.max(0, cy - reach); gy <= gyMax; gy++) {
const row = gy * fog.cols; // hoisted out of the inner loop
const dy = gy * FOG_CELL_SIZE + FOG_CELL_SIZE / 2 - y;
for (let gx = Math.max(0, cx - reach); gx <= gxMax; gx++) {
const dx = gx * FOG_CELL_SIZE + FOG_CELL_SIZE / 2 - x;
const idx = row + gx;
// Uphill concealment: a high-ground cell (terrain type 2) seen from a NON-high-ground
// source is compared against the shorter uphill radius instead of the full one.
const limitSq = (!srcHigh && terrain && terrain.type[idx] === 2) ? uphillSq : sightSq;
if (dx * dx + dy * dy > limitSq) continue;
fog.visible[idx] = 1;
fog.explored[idx] = 1;
}
}
}
// Recomputes `visible` from scratch each call (cheap at this grid
// resolution) and folds newly-seen cells permanently into `explored`.
/** @param {State} state @param {Fog} fog @param {string} owner */
export function updateFog(state, fog, owner) {
fog.visible.fill(0);
// A world's sight modifier scales every reveal radius (see PLANET_MODIFIERS).
// Optional-chained so the fog tests' map-less stubs read the default 1.
const sightMult = sideMod(state, owner, "sightMult"); // per-side on an asymmetric world (updateFog is already called per owner)
// A source standing on high ground sees farther (terrain sightMult) AND is exempt from the
// uphill-concealment cap below (srcHigh) β ONE sampleTerrain call per source serves both. OPEN
// ground and the map-less test stubs read TERRAIN[0] (sightMult 1, never high).
const terr = state.map?.terrain;
const srcTerrain = (x, y) => (terr ? sampleTerrain(terr, x, y) : TERRAIN[0]);
for (const u of state.units.values()) {
if (u.owner !== owner) continue;
const t = srcTerrain(u.x, u.y);
reveal(fog, u.x, u.y, UNITS[u.type].sight * sightMult * t.sightMult, terr, t === TERRAIN[2]);
}
for (const b of state.buildings.values()) {
if (b.owner !== owner) continue;
const t = srcTerrain(b.x, b.y);
reveal(fog, b.x, b.y, BUILDINGS[b.type].sight * sightMult * t.sightMult, terr, t === TERRAIN[2]);
}
}
|