Files
Battle-for-Tismo/client/js/map_view.js
T

180 lines
6.6 KiB
JavaScript

// The world map view. It owns no game logic: the server generates and
// simulates the world, and this class rebuilds the terrain once from the shared
// seed and mirrors units, cities, territory and fog from each snapshot, then
// turns pointer input into selection and move orders. All rendering is done
// with ordinary DOM elements, as required.
//
// The behaviour is split across the modules in ./map_view/ and composed onto the
// prototype below, so each concern (terrain, entities, motion, input) stays in
// its own file.
import { terrainMethods } from "./map_view/terrain.js";
import { entityMethods } from "./map_view/entities.js";
import { motionMethods } from "./map_view/motion.js";
import { inputMethods } from "./map_view/input.js";
import { createGLMapRenderer } from "./map_view/webgl.js";
// Re-exported for tests and callers that use the border geometry directly.
export { inwardOffset } from "./map_view/utils.js";
export class MapView {
constructor($viewport, $world, layers, options = {}) {
this.$viewport = $viewport;
this.$world = $world;
this.$terrain = layers.terrain;
this.$roads = layers.roads && layers.roads.length
? layers.roads
: $("#layer-roads");
this.$borders = layers.borders;
this.$fog = layers.fog;
this.$paths = layers.paths;
// Bombardment target lines and rings, drawn over the terrain and paths but
// under the unit and city icons.
this.$targets = layers.targets && layers.targets.length
? layers.targets
: $("#layer-targets");
this.$entities = layers.entities;
// The simplified map used when zoomed out. It is created on demand so
// callers (and tests) that predate it keep working unchanged.
this.$overview = layers.overview && layers.overview.length
? layers.overview
: $("#layer-overview");
if (!this.$overview.length) {
this.$overview = $('<div id="layer-overview" class="layer overview"></div>');
this.$world.prepend(this.$overview);
}
// City names live on their own layer above the entities so they are never
// hidden behind a unit or city marker.
this.$labels = layers.labels && layers.labels.length
? layers.labels
: $("#layer-labels");
if (!this.$labels.length) {
this.$labels = $('<div id="layer-labels" class="layer city-label-layer"></div>');
this.$world.append(this.$labels);
}
this.seed = -1;
// Signature of the terrain currently built, so a snapshot with the same
// seed and config does not rebuild the world.
this._terrainSignature = null;
this.mapConfig = null;
this.topology = null;
this.tiles = {};
this.terrainStats = null;
this.explored = new Set();
this.visible = new Set();
this.territory = new Map();
// Pre-generated road tiles, keyed "x,y".
this.roads = new Set();
this.civilisations = [];
this.localCiv = -1;
this.protoUnits = [];
// Battlefield tiles, keyed "x,y" and mapped to { defender, attacker }.
this._battles = new Map();
this.camera = { x: 0, y: 0, zoom: 1.0 };
this._centered = false;
this._selectedUnitIds = new Set();
this._selectedTile = null;
this._unitViews = new Map();
this._unitMotion = new Map();
this._unitData = new Map();
this._cityViews = new Map();
this._cityCiv = new Map();
this._battleViews = new Map();
// In-flight pathfinding job, cancelled when a newer request supersedes it.
this._pathToken = null;
this._maxStepLength = 1.0;
this._territorySignature = "";
this._roadsSignature = null;
this._exploredCount = -1;
this._visibleSignature = null;
// Cylindrical wrap state. `_period` is the horizontal pixel period (0 on a
// flat map); every entity is shifted to the copy nearest the camera.
this._period = 0;
this._worldWidth = 0;
this._worldHeight = 0;
this._worldMinX = 0;
this._worldMinY = 0;
// Chunked detailed layers: only the chunks near the viewport exist as DOM.
// The tile wrappers are pooled so panning recycles nodes instead of
// allocating a fresh one for every tile that scrolls in.
this._chunkSize = 0;
this._chunkCols = 0;
this._chunkRows = 0;
this._chunks = new Map();
this._chunkCache = new Map();
this._chunkCacheLimit = 0;
this._chunkOriginCache = new Map();
this._chunkView = { x: null, y: null, zoom: null };
this._chunkDirty = true;
this._hexPool = [];
this._borderPool = [];
this._roadPool = [];
this._wrapperPool = [];
this.lod = false;
this.onUnitSelected = () => {};
this.onCitySelected = () => {};
this.onTileRequested = () => {};
this.onMoveOrdered = () => {};
this.onAttackOrdered = () => {};
this.onScheduleOrdered = () => {};
this.onStackMenu = () => {};
// The map (terrain, roads, borders, fog, overview) is drawn on a WebGL
// canvas when the browser provides a context; the icon layers stay DOM.
// "dom" forces the classic renderer (tests, benchmark, troubleshooting).
this.glRenderer = null;
this.glCanvas = null;
this.rendererMode = options.renderer || "auto";
if (this.rendererMode !== "dom") this._setupGLRenderer();
this._setupCameraInput();
}
_setupGLRenderer() {
// The viewport element survives between games; drop any canvas a previous
// MapView left behind so contexts do not accumulate.
const previous = this.$viewport[0].querySelector("#map-canvas");
if (previous) previous.remove();
const canvas = document.createElement("canvas");
canvas.id = "map-canvas";
const world = this.$world[0];
if (world.parentNode) world.parentNode.insertBefore(canvas, world);
const renderer = createGLMapRenderer(canvas);
if (!renderer) {
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
return;
}
// A CPU rasteriser (no usable GPU) can be slower than the DOM renderer.
// In auto mode, keep the DOM path there; "webgl" forces the canvas.
if (this.rendererMode === "auto" && renderer.softwareRenderer()) {
renderer.dispose();
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
return;
}
this.glRenderer = renderer;
this.glCanvas = canvas;
this.$world.addClass("gl-render");
}
// Draws the map canvas from the current view state, if WebGL is active. Safe
// to call every frame: the renderer skips untouched camera/content state.
_glRender() {
if (!this.glRenderer) return;
this.glRenderer.render(this, this.$viewport.width(), this.$viewport.height());
}
}
Object.assign(
MapView.prototype,
terrainMethods,
entityMethods,
motionMethods,
inputMethods
);