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; }