Files
Battle-for-Tismo/client/js/map_view/constants.js
T
adrien b2ab4d76eb Sped up large battles and trimmed snapshot traffic
- The bombardment overlay was rebuilt every animation frame, and each barrage
  scanned the whole army to find its target (O(strikes x units)). It now
  redraws at most at the renderer's 24 fps, reuses its SVG/DOM nodes, and
  resolves garrisons from one per-snapshot tile index, so a high-refresh
  display no longer makes a big battle worse.
- Route truncation had the same shape: every step of every route rescanned
  every unit. The snapshot now buckets the army by tile once.
- Idle units no longer have identical left/top rewritten every frame, and the
  whole-map fog buffer is rebuilt less often.
- Snapshot traffic: cityStats and visible are delta-encoded like territory and
  population, city stats carry a content-hash version, tile ethnicity is
  memoised, and a city's economy is derived once per broadcast instead of
  twice. A per-peer civ change now forces a full snapshot, since the visible
  set is per-viewer.
2026-09-21 02:23:04 +02:00

119 lines
5.9 KiB
JavaScript

// Tuning constants for the world map view.
import { HEX_H } from "../../../shared/hex.js";
import { EDGE_SEGMENTS } from "./distortion.js";
export const RESYNC_HOURS = 0.75;
export const PREDICTION_GRACE_HOURS = 2.0;
// Moving units change the visible set constantly. The fog is a whole-map buffer
// in WebGL, so rebuilding it on every snapshot spike built up; the latest one is
// flushed from the frame loop at most this often. Each rebuild walks the entire
// map, so the interval is kept well above the render's 24 fps but short enough
// that revealing a newly explored tile is not noticeable.
export const FOG_REBUILD_MS = 300;
// Bombardment arcs, travelling shells and target rings are DOM/SVG overlays, not
// GPU geometry, so they are not covered by the WebGL renderer's own 24 fps
// throttle. Rebuilding them every animation frame scales with the army (and
// runs 2x-3x faster on a high-refresh display), so the overlay is redrawn at
// most this often and immediately whenever its contents change.
export const TARGET_REFRESH_MS = 1000 / 24;
export const DRAG_THRESHOLD = 5;
export const ZOOM_MIN = 0.2;
export const ZOOM_MAX = 2.0;
// The camera looks north from an orthographic elevation of 70° above the
// ground. That foreshortens the north-south axis by sin(70°) ~= 0.94 while the
// east-west axis is untouched: a gentle 3D tilt that keeps the current
// orientation. It is applied to the terrain, roads, borders, fog and ground
// overlays; units, cities and labels stay upright on top.
export const CAMERA_ELEVATION_DEG = 45;
export const CAMERA_TILT = Math.sin((CAMERA_ELEVATION_DEG * Math.PI) / 180);
// How far the tallest terrain (normalised height 1, see map_view/heightmap.js)
// is lifted off the ground plane, in world pixels. The projection turns a lift
// into a northward screen shift of `lift * cos(elevation)`, while the ground
// distance between a tile and its northern neighbour is
// `HEX_H * sin(elevation) = HEX_H * CAMERA_TILT`. A tile already reaches half a
// hex north of its centre, so keeping the lift under half that spacing (here at
// 45%) means even a height-1 ridge stops short of the next tile's centre: no
// tile north of a peak is eclipsed, and the painter's-order layers below need
// no depth buffer.
export const HEIGHT_LIFT = HEX_H * CAMERA_TILT * 1; // set by a human
// The normalised-height band where a mountain's cap texture (rock/snow) fades
// in: below `low` the slopes stay grass, above `high` the cap is at full
// strength and in between it blends. Tune these to move the snow line.
export const MOUNTAIN_ROCK_LOW = 0.3;
export const MOUNTAIN_ROCK_HIGH = 0.8;
// Grass shifts towards a drier yellow-green as the ground climbs, standing in
// for the old hill overlay art. Below `low` it keeps its colour, above `high`
// it is shifted by `strength` (0..1), and in between it blends. The tint is the
// target hue; the shader renormalises it so the luminance is preserved.
export const GRASS_DRY_LOW = 0.08;
export const GRASS_DRY_HIGH = 0.45;
export const GRASS_DRY_STRENGTH = 0.8;
export const GRASS_DRY_TINT = [1.2, 1.0, 0.45];
// Coastal wavelets: the baked coast field spans COAST_RANGE tiles and sets how
// strongly the ripples run near land, while `pulse` is how fast each wavelet
// comes and goes (cycles/s).
export const COAST_RANGE = 6;
export const COAST_WAVE_PULSE = 0.35;
export const COAST_WAVE_STRENGTH = 0.8;
// The baked Voronoi map repeats every WATER_REPEAT world pixels and holds
// `WATER_BUMP_CELLS` cells across it, so a cell is about
// WATER_REPEAT / WATER_BUMP_CELLS world pixels — a few map pixels. Those are
// the glinting wavelets; WATER_BUMP is how far they tilt the surface.
export const WATER_REPEAT = 32;
export const WATER_BUMP_CELLS = 8;
export const WATER_BUMP = 0.5;
// Levels of detail for the terrain mesh, from the closest zoom to the furthest.
// `segments` is the wavy-edge subdivision (1 draws straight hexes), `relief`
// scales the normal maps' surface detail so distant terrain does not shimmer
// (the height-gradient lighting stays on at every level), and `subdivisions`
// splits each fan triangle in four to sample the height field more finely. The
// coarsest level's `minZoom` is -Infinity so every zoom maps to a level.
export const TERRAIN_LODS = [
{ minZoom: 0.6, segments: EDGE_SEGMENTS, subdivisions: 1, relief: 1.0 },
{ minZoom: 0.3, segments: 2, subdivisions: 1, relief: 0.55 },
{ minZoom: -Infinity, segments: 1, subdivisions: 0, relief: 0.0 },
];
// The level of detail that serves a camera zoom.
export function terrainLod(zoom) {
for (let i = 0; i < TERRAIN_LODS.length; i++) {
if (zoom >= TERRAIN_LODS[i].minZoom) return i;
}
return TERRAIN_LODS.length - 1;
}
// Past this zoom the map stops being a terrain map and becomes a political one:
// each country's territory is painted in its colour and its name is drawn once
// per contiguous holding. It matches the threshold between the two coarsest
// terrain levels of detail, where tile art is already sub-pixel.
export const POLITICAL_ZOOM = 0.5;
export function politicalMode(zoom) {
return zoom <= POLITICAL_ZOOM;
}
// A battle spreads the two stacks to either side of the tile centre, with the
// battle icon between them. Side offset is in pixels; step is the spacing
// between the (half-size) units of one stack.
export const BATTLE_SIDE_OFFSET = 15;
export const BATTLE_STACK_STEP = 9;
// The detailed view is built from square blocks of tiles. Only the blocks that
// intersect the viewport (plus CHUNK_MARGIN blocks of slack on every side) are
// materialised, so panning costs the same no matter how big the map is. Tiles
// are pooled and recycled as blocks leave the view.
export const CHUNK_SIZE = 8;
export const CHUNK_MARGIN = 1;
// Built chunks are kept detached in an LRU cache so panning back over ground
// already visited reattaches them instead of rebuilding hundreds of tiles. The
// cap bounds how many detached tile nodes can be held at once.
export const CHUNK_CACHE_LIMIT = 160;