- run_tests.js discovers and runs each *_test.js in its own worker process (--jobs N to cap the pool, --file <name> for a single suite) - GameState caches the deterministic terrain, city sites and road network by (seed, mapConfig, civ count), rebuilding only the mutable per-game state - the snapshot ships mapConfig and the client rebuilds terrain from it, so the DOM tests can use the small map fixture and a non-default config renders right - shortestPaths can stop once every target city is settled
133 lines
4.6 KiB
JavaScript
133 lines
4.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";
|
|
|
|
// 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) {
|
|
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;
|
|
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 = () => {};
|
|
|
|
this._setupCameraInput();
|
|
}
|
|
}
|
|
|
|
Object.assign(
|
|
MapView.prototype,
|
|
terrainMethods,
|
|
entityMethods,
|
|
motionMethods,
|
|
inputMethods
|
|
);
|