Files
Battle-for-Tismo/client/js/map_view/modes.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

220 lines
8.9 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, mapToLocal } 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";
import { makeSegment } from "./utils.js";
import { RESOURCES } from "../../../shared/data/resources.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)}`;
},
// ------------------------------------------------- delivery graph ------
// Replaces the delivery graph shipped in the snapshot. It is only drawn in
// the economic modes, so a political or terrain map never reveals it.
setResourceGraph(graph) {
if (!graph) return;
this.resourceGraph = {
nodes: Array.isArray(graph.nodes) ? graph.nodes : [],
links: Array.isArray(graph.links) ? graph.links : [],
};
this._renderResourceGraph();
},
// Draws one line per delivery leg and a dot on every storage node. The layer
// shares the world's coordinate system, so pan and zoom move it for free.
_renderResourceGraph() {
if (!this.$resources || !this.$resources.length) return;
const graph = this.resourceGraph || { nodes: [], links: [] };
if (!this.economic || graph.links.length === 0) {
if (this.$resources[0].firstChild) this.$resources.empty();
return;
}
const colours = Object.fromEntries(RESOURCES.map((r) => [r.id, r.colour]));
const fragment = document.createDocumentFragment();
for (const link of graph.links) {
const [fromX, fromY, toX, toY, resource] = link;
const a = mapToLocal(fromX, fromY);
const b = mapToLocal(toX, toY);
fragment.appendChild(makeSegment(a, b, colours[resource] || "#ffffff", 3));
}
for (const node of graph.nodes) {
const point = mapToLocal(node[0], node[1]);
const dot = document.createElement("div");
dot.className = "resource-node";
dot.style.left = `${point.x}px`;
dot.style.top = `${point.y}px`;
fragment.appendChild(dot);
}
this.$resources.empty()[0].appendChild(fragment);
},
// 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 rebuild instead of building the whole mesh here. Clearing the
// cover makes the next flush treat it as an escape, so it runs at once.
this._fogDirty = true;
this._fogBuiltAt = 0;
this._fogCover = undefined;
this._fogChanged = null;
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._renderResourceGraph();
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;
},
};