Marching into unexplored or enemy land reveals a tile every step, and each reveal rebuilt the whole-map fog mesh: about 1.8M vertices and 50 MB on a 160x160 map, regenerated at the rebuild throttle. Walking through already-explored home land reveals nothing, which is why the stutter only showed up on a border crossing, with a single unit. - buildKnowledge now takes a row range and builds only the rows on screen; the row table stays full length so the existing row-banded draw keeps working. - _flushFog rebuilds on a visibility change or a pan to a new row band, still coalesced, and the draw clamps to the band that was built. - A mode/zoom change schedules the band rebuild instead of building the whole mesh, and uniform interior tiles skip the per-vertex blur kernel.
174 lines
7.0 KiB
JavaScript
174 lines
7.0 KiB
JavaScript
// The map-mode switch: terrain, political, and the economic colour modes. It
|
|
// owns which fill the map is showing, keeps the effective political flag in
|
|
// step with the camera zoom (terrain mode still turns political when far out),
|
|
// and drives the economic fill and its legend. The political names themselves
|
|
// live in ./political.js; this file only decides what is on screen.
|
|
|
|
import { HEX_W } from "../../../shared/hex.js";
|
|
import { POLITICAL_ZOOM } from "./constants.js";
|
|
import { ECONOMIC_GRADIENT, ECONOMIC_MODES, economicColour, isEconomicMode } from "./economic.js";
|
|
import { NEUTRAL_LAND, POLITICAL_WATER } from "./political.js";
|
|
import { isWaterTile } from "./textures.js";
|
|
|
|
export const MAP_MODES = ["terrain", "political", "gdp", "population"];
|
|
|
|
export const modeMethods = {
|
|
// Selects a mode and repaints. Terrain mode keeps the far-out auto-switch, so
|
|
// "political" forces the political map at any zoom while "terrain" only shows
|
|
// it once the camera has pulled back.
|
|
setMapMode(mode) {
|
|
if (!MAP_MODES.includes(mode) || mode === this.mapMode) return;
|
|
this.mapMode = mode;
|
|
this._syncModeState();
|
|
this._repaintMode();
|
|
},
|
|
|
|
// Installs the per-tile figures an economic mode paints. Called by the game
|
|
// screen whenever a snapshot arrives while such a mode is active. A cheap
|
|
// signature skips the rebuild when nothing moved.
|
|
setEconomicData(kind, values, min, max) {
|
|
this.economicKind = kind;
|
|
this.economicValues = values instanceof Map ? values : new Map(values || []);
|
|
this.economicRange = { min: min || 0, max: max || 0 };
|
|
// The scale a mode reads its figures through (the population mode is
|
|
// logarithmic). Kept on the view so both renderers agree.
|
|
this.economicScale = (ECONOMIC_MODES[kind] || {}).scale || null;
|
|
const signature = this._economicSignature();
|
|
if (signature === this._economicSignatureDone) return;
|
|
this._economicSignatureDone = signature;
|
|
if (this.economic) this._refreshMapColours();
|
|
},
|
|
|
|
_economicSignature() {
|
|
let sum = 0;
|
|
for (const value of this.economicValues.values()) sum += value;
|
|
return `${this.economicKind}:${this.economicValues.size}:${Math.round(sum)}`;
|
|
},
|
|
|
|
// Political shows when the user asked for it, or while terrain mode is
|
|
// zoomed far enough out. Economic modes are exclusive with it.
|
|
_effectivePolitical() {
|
|
return this.mapMode === "political" ||
|
|
(this.mapMode === "terrain" && this.camera.zoom <= POLITICAL_ZOOM);
|
|
},
|
|
|
|
_isEconomic() {
|
|
return isEconomicMode(this.mapMode);
|
|
},
|
|
|
|
// Recomputes the two effective flags and reflects them on the world element.
|
|
// The renderers read `political` / `economic` off the view. Returns whether
|
|
// either flag actually changed, so callers can skip a repaint otherwise.
|
|
_syncModeState() {
|
|
const political = this._effectivePolitical();
|
|
const economic = this._isEconomic();
|
|
const changed = political !== this.political || economic !== this.economic;
|
|
// Touch the DOM classes only on a real change: this runs on every camera
|
|
// move, where the flags almost never move.
|
|
if (political !== this.political) this.$world.toggleClass("political", political);
|
|
if (economic !== this.economic) this.$world.toggleClass("economic", economic);
|
|
this.political = political;
|
|
this.economic = economic;
|
|
return changed;
|
|
},
|
|
|
|
// Called from the camera whenever the zoom changes: terrain mode may have
|
|
// crossed into or out of the far-out political map.
|
|
_updatePoliticalMode() {
|
|
if (!this._syncModeState()) return;
|
|
this._repaintMode();
|
|
},
|
|
|
|
// A mode or zoom-threshold change: the whole surface flips between relief and
|
|
// flat, so the fills, borders, fog and selected-region overlay all repaint.
|
|
_repaintMode() {
|
|
this._refreshMapColours();
|
|
if (this.glRenderer && this.topology) {
|
|
// The fog only needs the rows on screen, so leave it to the frame loop's
|
|
// coalesced band rebuild instead of building the whole mesh here.
|
|
this._fogDirty = true;
|
|
this._fogBuiltAt = 0;
|
|
this._fogBand = undefined;
|
|
this.glRenderer.buildRegionOverlay(this);
|
|
}
|
|
},
|
|
|
|
// Repaints the active fills, the borders over them and the labels/legend that
|
|
// go with them. This is the per-territory-change path, so it deliberately
|
|
// leaves the fog and region overlay alone: their geometry only depends on the
|
|
// mode, not on who owns which tile.
|
|
_refreshMapColours() {
|
|
if (this.glRenderer && this.topology) {
|
|
this.glRenderer.buildPolitical(this);
|
|
this.glRenderer.buildEconomic(this);
|
|
this.glRenderer.buildBorders(this);
|
|
} else if (!this.glRenderer && this.topology) {
|
|
this._rebuildDomTerrain();
|
|
}
|
|
if (this.political) this._renderPoliticalLabels();
|
|
else if (this.$politicalLabels) this.$politicalLabels.empty();
|
|
this._updateLegend();
|
|
},
|
|
|
|
// Rebuilds every materialised chunk so a change of mode or territory colour
|
|
// is reflected in the DOM terrain. Only the chunks near the camera exist, so
|
|
// this stays cheap.
|
|
_rebuildDomTerrain() {
|
|
if (!this._chunks) return;
|
|
this._destroyAllChunks();
|
|
this._chunkDirty = true;
|
|
this._chunkView = { x: null, y: null, zoom: null };
|
|
this._syncChunks();
|
|
},
|
|
|
|
// Shows the gradient scale while an economic mode is active, with the range's
|
|
// low, middle and high figures for the current mode.
|
|
_updateLegend() {
|
|
const $legend = this.$legend;
|
|
if (!$legend || !$legend.length) return;
|
|
const meta = ECONOMIC_MODES[this.mapMode];
|
|
if (!this.economic || !meta) {
|
|
$legend.addClass("hidden");
|
|
return;
|
|
}
|
|
$legend.removeClass("hidden");
|
|
$legend.find("#map-legend-title").text(meta.label);
|
|
const { min, max } = this.economicRange;
|
|
$legend.find("#map-legend-min").text(meta.format(min));
|
|
$legend.find("#map-legend-mid").text(meta.format(this._economicMidpoint(min, max, meta.scale)));
|
|
$legend.find("#map-legend-max").text(meta.format(max));
|
|
$legend.find("#map-legend-bar")
|
|
.css("background", `linear-gradient(to right, ${ECONOMIC_GRADIENT.join(", ")})`);
|
|
},
|
|
|
|
// The value whose scaled position is the midpoint of the ramp, so a
|
|
// logarithmic legend still labels the middle colour honestly.
|
|
_economicMidpoint(min, max, scale) {
|
|
if (scale && max > min) {
|
|
const mid = (scale(min) + scale(max)) / 2;
|
|
return Math.expm1(mid);
|
|
}
|
|
return (min + max) / 2;
|
|
},
|
|
|
|
// Paints one DOM terrain tile with its economic colour. Water and tiles with
|
|
// no figure stay neutral; the atlas mask still clips the hexagon.
|
|
_applyEconomicColour(div, tile, k) {
|
|
let colour = POLITICAL_WATER;
|
|
if (!isWaterTile(tile)) {
|
|
const value = this.economicValues.get(k);
|
|
colour = value === undefined
|
|
? NEUTRAL_LAND
|
|
: economicColour(
|
|
value, this.economicRange.min, this.economicRange.max,
|
|
ECONOMIC_GRADIENT, this.economicScale
|
|
);
|
|
}
|
|
div.style.backgroundImage = "none";
|
|
div.style.backgroundColor = colour;
|
|
const maskPosition = `${-tile.col * HEX_W}px 0`;
|
|
div.style.maskPosition = maskPosition;
|
|
div.style.webkitMaskPosition = maskPosition;
|
|
},
|
|
};
|