257 lines
12 KiB
JavaScript
257 lines
12 KiB
JavaScript
import { MAP_H, MAP_W, SIZE } from "./mapUtils.js";
|
|
|
|
const EMPTY_ARRAY_KEYS = new Set([
|
|
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
|
|
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters",
|
|
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
|
|
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
|
|
"mainRivers", "tributaryRivers", "smallStreams", "prefectureBorder", "regionalPrefectureBorders",
|
|
"adminBorders", "externalGateways", "prefectureRegions",
|
|
]);
|
|
|
|
const PATH_ARRAY_KEYS = new Set([
|
|
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
|
|
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
|
|
"mainRivers", "tributaryRivers", "smallStreams",
|
|
]);
|
|
|
|
const SEGMENT_ARRAY_KEYS = new Set([
|
|
"prefectureBorder", "regionalPrefectureBorders", "adminBorders",
|
|
]);
|
|
|
|
const POINT_ARRAY_KEYS = new Set([
|
|
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
|
|
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters",
|
|
"externalGateways", "prefectureRegions",
|
|
]);
|
|
|
|
const NEGATIVE_ONE_FIELDS = new Set([
|
|
"adminId",
|
|
"prefectureRegionId",
|
|
"regionId",
|
|
"municipalityId",
|
|
"naturalCompartmentId",
|
|
"watershedId",
|
|
]);
|
|
|
|
function isCellField(value) {
|
|
return ArrayBuffer.isView(value) && typeof value.length === "number" && value.length === SIZE;
|
|
}
|
|
|
|
function defaultForField(name, Constructor) {
|
|
if (name === "sea") return 1;
|
|
if (name === "elevation") return 0.08;
|
|
if (NEGATIVE_ONE_FIELDS.has(name)) return -1;
|
|
if (Constructor === Float32Array || Constructor === Float64Array) return 0;
|
|
return 0;
|
|
}
|
|
|
|
function worldIndex(world, x, y) {
|
|
if (!world || x < 0 || y < 0 || x >= world.width || y >= world.height) return -1;
|
|
return y * world.width + x;
|
|
}
|
|
|
|
function copyViewportField(name, source, world, camera, viewWidth, viewHeight) {
|
|
const Constructor = source.constructor;
|
|
const out = new Constructor(viewWidth * viewHeight);
|
|
const fallback = defaultForField(name, Constructor);
|
|
if (fallback !== 0) out.fill(fallback);
|
|
|
|
const cx = Math.round(camera.x || 0);
|
|
const cy = Math.round(camera.y || 0);
|
|
for (let y = 0; y < viewHeight; y++) {
|
|
for (let x = 0; x < viewWidth; x++) {
|
|
const src = worldIndex(world, cx + x, cy + y);
|
|
if (src >= 0) out[y * viewWidth + x] = source[src];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function inViewportPoint(p, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
return p && p.x >= -margin && p.y >= -margin && p.x < viewWidth + margin && p.y < viewHeight + margin;
|
|
}
|
|
|
|
function transformPointObject(point, camera, originX, originY) {
|
|
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
|
|
return {
|
|
...point,
|
|
x: point.x + originX - camera.x,
|
|
y: point.y + originY - camera.y,
|
|
worldX: point.x + originX,
|
|
worldY: point.y + originY,
|
|
};
|
|
}
|
|
|
|
function transformPointArray(items, camera, originX, originY, margin = 36, preserveIndexes = false, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
const mapped = (items || []).map((item) => transformPointObject(item, camera, originX, originY));
|
|
return preserveIndexes ? mapped : mapped.filter((item) => inViewportPoint(item, margin, viewWidth, viewHeight));
|
|
}
|
|
|
|
function transformTuple(tuple, camera, originX, originY) {
|
|
if (!Array.isArray(tuple) || tuple.length < 2) return null;
|
|
return [tuple[0] + originX - camera.x, tuple[1] + originY - camera.y];
|
|
}
|
|
|
|
function tupleInside(tuple, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
return tuple && tuple[0] >= -margin && tuple[1] >= -margin && tuple[0] < viewWidth + margin && tuple[1] < viewHeight + margin;
|
|
}
|
|
|
|
function splitTransformedPath(path, camera, originX, originY, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
const chunks = [];
|
|
let current = [];
|
|
for (const tuple of path || []) {
|
|
const p = transformTuple(tuple, camera, originX, originY);
|
|
const inside = tupleInside(p, margin, viewWidth, viewHeight);
|
|
if (inside) {
|
|
current.push([Math.round(p[0]), Math.round(p[1])]);
|
|
} else if (current.length >= 2) {
|
|
chunks.push(current);
|
|
current = [];
|
|
} else {
|
|
current = [];
|
|
}
|
|
}
|
|
if (current.length >= 2) chunks.push(current);
|
|
return chunks;
|
|
}
|
|
|
|
function transformPaths(paths, camera, originX, originY, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
const out = [];
|
|
for (const path of paths || []) out.push(...splitTransformedPath(path, camera, originX, originY, 0, viewWidth, viewHeight));
|
|
return out;
|
|
}
|
|
|
|
function segmentIntersectsViewport(seg, margin = 4, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
if (!seg || seg.length < 2) return false;
|
|
const xs = [seg[0][0], seg[1][0]];
|
|
const ys = [seg[0][1], seg[1][1]];
|
|
return Math.max(...xs) >= -margin && Math.min(...xs) <= viewWidth + margin && Math.max(...ys) >= -margin && Math.min(...ys) <= viewHeight + margin;
|
|
}
|
|
|
|
function transformSegments(segments, camera, originX, originY, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
return (segments || [])
|
|
.map((seg) => [transformTuple(seg?.[0], camera, originX, originY), transformTuple(seg?.[1], camera, originX, originY)])
|
|
.filter((seg) => segmentIntersectsViewport(seg, 4, viewWidth, viewHeight));
|
|
}
|
|
|
|
|
|
function copySourceMapViewportField(source, camera, originX, originY, viewWidth, viewHeight) {
|
|
if (!ArrayBuffer.isView(source) || typeof source.length !== "number" || source.length !== SIZE) return source;
|
|
const out = new source.constructor(viewWidth * viewHeight);
|
|
const cx = Math.round(camera.x || 0);
|
|
const cy = Math.round(camera.y || 0);
|
|
for (let y = 0; y < viewHeight; y++) {
|
|
for (let x = 0; x < viewWidth; x++) {
|
|
const sx = cx + x - originX;
|
|
const sy = cy + y - originY;
|
|
if (sx >= 0 && sy >= 0 && sx < MAP_W && sy < MAP_H) out[y * viewWidth + x] = source[sy * MAP_W + sx];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function transformTransportDebug(debug, camera, originX, originY, viewport = null, viewWidth = MAP_W, viewHeight = MAP_H) {
|
|
if (!debug?.layers) return debug;
|
|
const layers = { ...debug.layers };
|
|
for (const [key, value] of Object.entries(layers)) {
|
|
if (ArrayBuffer.isView(value) && value.length === SIZE) layers[key] = copySourceMapViewportField(value, camera, originX, originY, viewWidth, viewHeight);
|
|
}
|
|
// The original transport-debug potential layers are fixed-map arrays. Once the
|
|
// viewport pans into patched world cells, synthesize equivalent viewport-sized
|
|
// debug fields from the current world-backed fields so the color overlay moves
|
|
// with the terrain instead of staying tied to the initial source map.
|
|
if (viewport) {
|
|
const n = viewWidth * viewHeight;
|
|
const make = (fn) => {
|
|
const out = new Float32Array(n);
|
|
for (let i = 0; i < n; i++) out[i] = fn(i);
|
|
return out;
|
|
};
|
|
const sea = viewport.sea || new Uint8Array(n);
|
|
const slope = viewport.slope || new Float32Array(n);
|
|
const plain = viewport.plain || new Float32Array(n);
|
|
const pop = viewport.populationDensity || viewport.settlementScore || new Float32Array(n);
|
|
const road = viewport.roadInfluence || new Float32Array(n);
|
|
const rail = viewport.railInfluence2 || viewport.stationInfluence || new Float32Array(n);
|
|
layers.expresswayPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.72 + pop[i] * 0.42 + plain[i] * 0.22 - slope[i] * 0.52)));
|
|
layers.nationalRoadPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.58 + pop[i] * 0.55 + plain[i] * 0.18 - slope[i] * 0.38)));
|
|
layers.railPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, rail[i] * 0.72 + pop[i] * 0.38 + plain[i] * 0.26 - slope[i] * 0.72)));
|
|
layers.slopeSeaPenalty = make((i) => sea[i] ? 1 : Math.max(0, Math.min(1, slope[i] * 1.35)));
|
|
}
|
|
if (Array.isArray(layers.components)) {
|
|
layers.components = layers.components.map((component) => ({
|
|
...component,
|
|
cells: (component.cells || [])
|
|
.map((cell) => transformTuple(cell, camera, originX, originY))
|
|
.filter((cell) => tupleInside(cell, 0, viewWidth, viewHeight))
|
|
.map(([x, y]) => [Math.round(x), Math.round(y)]),
|
|
})).filter((component) => component.cells.length);
|
|
}
|
|
if (Array.isArray(layers.repairedSegments)) {
|
|
layers.repairedSegments = layers.repairedSegments.flatMap((repair) => transformPaths([repair.path || []], camera, originX, originY, viewWidth, viewHeight).map((path) => ({ ...repair, path })));
|
|
}
|
|
if (Array.isArray(layers.unservedSettlements)) layers.unservedSettlements = transformPointArray(layers.unservedSettlements, camera, originX, originY, 36, false, viewWidth, viewHeight);
|
|
return { ...debug, layers };
|
|
}
|
|
|
|
function buildEmptyViewportFromSource(sourceMap, world, camera, viewWidth, viewHeight) {
|
|
const viewport = { ...sourceMap };
|
|
viewport.width = viewWidth;
|
|
viewport.height = viewHeight;
|
|
viewport.worldCamera = { x: camera.x, y: camera.y };
|
|
viewport.worldOrigin = { x: world.originX, y: world.originY };
|
|
viewport.generatedRects = world.generatedRects || [];
|
|
for (const key of EMPTY_ARRAY_KEYS) if (Array.isArray(viewport[key])) viewport[key] = [];
|
|
return viewport;
|
|
}
|
|
|
|
export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MAP_H, options = {}) {
|
|
const sourceMap = world?.sourceMap || {};
|
|
const normalizedCamera = {
|
|
x: Math.round(camera?.x || 0),
|
|
y: Math.round(camera?.y || 0),
|
|
};
|
|
const viewport = buildEmptyViewportFromSource(sourceMap, world, normalizedCamera, viewWidth, viewHeight);
|
|
|
|
for (const [name, value] of Object.entries(world?.fields || {})) {
|
|
viewport[name] = copyViewportField(name, value, world, normalizedCamera, viewWidth, viewHeight);
|
|
}
|
|
|
|
if (options.light) {
|
|
// Light/fast redraws are used while panning and zooming. Do not leave
|
|
// source-space debug vectors on the viewport; in borders-debug this made
|
|
// natural compartment lines appear fixed on screen while the map moved.
|
|
viewport.adminDebug = null;
|
|
viewport.transportDebug = null;
|
|
return viewport;
|
|
}
|
|
|
|
const originX = world?.originX || 0;
|
|
const originY = world?.originY || 0;
|
|
for (const key of POINT_ARRAY_KEYS) {
|
|
if (!Array.isArray(sourceMap[key])) continue;
|
|
viewport[key] = transformPointArray(sourceMap[key], normalizedCamera, originX, originY, 36, key === "adminCenters", viewWidth, viewHeight);
|
|
}
|
|
for (const key of PATH_ARRAY_KEYS) if (Array.isArray(sourceMap[key])) viewport[key] = transformPaths(sourceMap[key], normalizedCamera, originX, originY, viewWidth, viewHeight);
|
|
for (const key of SEGMENT_ARRAY_KEYS) if (Array.isArray(sourceMap[key])) viewport[key] = transformSegments(sourceMap[key], normalizedCamera, originX, originY, viewWidth, viewHeight);
|
|
|
|
if (sourceMap.adminDebug) {
|
|
viewport.adminDebug = {
|
|
...sourceMap.adminDebug,
|
|
compartmentBorders: transformSegments(sourceMap.adminDebug.compartmentBorders || [], normalizedCamera, originX, originY, viewWidth, viewHeight),
|
|
lowlandAdminSeeds: transformPointArray(sourceMap.adminDebug.lowlandAdminSeeds || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
|
|
};
|
|
}
|
|
if (sourceMap.transportDebug) viewport.transportDebug = transformTransportDebug(sourceMap.transportDebug, normalizedCamera, originX, originY, viewport, viewWidth, viewHeight);
|
|
if (sourceMap.neighborPrefectureDetails) {
|
|
viewport.neighborPrefectureDetails = {
|
|
...sourceMap.neighborPrefectureDetails,
|
|
cities: transformPointArray(sourceMap.neighborPrefectureDetails.cities || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
|
|
adminCenters: transformPointArray(sourceMap.neighborPrefectureDetails.adminCenters || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
|
|
roads: transformPaths(sourceMap.neighborPrefectureDetails.roads || [], normalizedCamera, originX, originY, viewWidth, viewHeight),
|
|
};
|
|
}
|
|
|
|
return viewport;
|
|
}
|