map/mapHumanPatch.js
2026-05-28 19:37:28 +09:00

1177 lines
50 KiB
JavaScript

import { clamp, hash2, MinHeap, pickEntities } from "./mapUtils.js";
import { LANDUSE } from "./landuseCodes.js";
import { generateEntityName } from "./names.js";
const POINT_LAYER_KEYS = [
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
"interchanges", "industrialZones", "logisticsParks", "newTowns",
];
const PATH_LAYER_KEYS = [
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
];
const SEGMENT_LAYER_KEYS = ["adminBorders"];
const FLOAT_FIELD_KEYS = [
"settlementScore", "populationDensity", "stationInfluence", "roadInfluence", "railInfluence2", "villageInfluence",
];
const INT_FIELD_DEFAULTS = new Map([
["adminId", -1],
["municipalityId", -1],
]);
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 rectWidth(rect) {
return Math.max(0, Math.floor(rect.x1) - Math.floor(rect.x0));
}
function rectHeight(rect) {
return Math.max(0, Math.floor(rect.y1) - Math.floor(rect.y0));
}
function rectArea(rect) {
return rectWidth(rect) * rectHeight(rect);
}
function insideRect(x, y, rect) {
return rect && x >= rect.x0 && y >= rect.y0 && x < rect.x1 && y < rect.y1;
}
function distanceToRectEdge(x, y, rect) {
return Math.min(x - rect.x0, y - rect.y0, rect.x1 - 1 - x, rect.y1 - 1 - y);
}
function expandRect(rect, margin, world) {
return {
x0: Math.max(0, rect.x0 - margin),
y0: Math.max(0, rect.y0 - margin),
x1: Math.min(world.width, rect.x1 + margin),
y1: Math.min(world.height, rect.y1 + margin),
};
}
function worldToSourcePoint(world, x, y) {
return { x: x - world.originX, y: y - world.originY };
}
function pointWorldX(world, p) {
if (Number.isFinite(p?.worldX)) return p.worldX;
return (p?.x || 0) + (world?.originX || 0);
}
function pointWorldY(world, p) {
if (Number.isFinite(p?.worldY)) return p.worldY;
return (p?.y || 0) + (world?.originY || 0);
}
function tupleWorldX(world, tuple) {
return (tuple?.[0] || 0) + (world?.originX || 0);
}
function tupleWorldY(world, tuple) {
return (tuple?.[1] || 0) + (world?.originY || 0);
}
function ensureSourceArray(sourceMap, key) {
if (!Array.isArray(sourceMap[key])) sourceMap[key] = [];
return sourceMap[key];
}
function ensureField(world, key, Constructor = Float32Array, fallback = 0) {
if (!world.fields[key] || world.fields[key].length !== world.width * world.height) {
world.fields[key] = new Constructor(world.width * world.height);
if (fallback !== 0) world.fields[key].fill(fallback);
}
return world.fields[key];
}
function seeded(seed, x, y, salt = 0) {
return hash2((x | 0) + salt * 8191, (y | 0) - salt * 131, seed >>> 0);
}
function localId(rect, x, y, salt = 0) {
return `${rect.x0}:${rect.y0}:${x}:${y}:${salt}`;
}
function makeName(seed, rect, x, y, entity, usedNames, salt = 0) {
const id = localId(rect, x, y, salt);
const name = generateEntityName(seed, id, entity, null, usedNames);
if (name) usedNames?.add(name);
return name || `隨ャ${Math.max(1, Math.floor(seeded(seed, x, y, salt) * 99))}逕コ`;
}
function isLand(world, x, y) {
const i = worldIndex(world, x, y);
return i >= 0 && !world.fields.sea?.[i];
}
function seaNeighbors(world, x, y, radius = 1) {
let count = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (!dx && !dy) continue;
const i = worldIndex(world, x + dx, y + dy);
if (i >= 0 && world.fields.sea?.[i]) count++;
}
}
return count;
}
function candidateScore(world, x, y, seed) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) return -Infinity;
const elevation = world.fields.elevation?.[i] || 0;
const slope = world.fields.slope?.[i] || 0;
const plain = world.fields.plain?.[i] || 0;
const agriculture = world.fields.agriculture?.[i] || 0;
const river = world.fields.river?.[i] || 0;
const floodplain = world.fields.floodplain?.[i] || 0;
const coast = seaNeighbors(world, x, y, 2) > 0 ? 0.26 : 0;
const lowland = Math.max(0, 1 - Math.abs(elevation - 0.33) * 2.0);
const noise = seeded(seed, x, y, 47) * 0.18;
return plain * 0.72 + agriculture * 0.72 + floodplain * 0.45 + river * 0.30 + coast + lowland * 0.38 - slope * 1.35 + noise;
}
function collectLandCandidates(world, rect, seed, stride = 3) {
const candidates = [];
for (let y = rect.y0 + 2; y < rect.y1 - 2; y += stride) {
for (let x = rect.x0 + 2; x < rect.x1 - 2; x += stride) {
const score = candidateScore(world, x, y, seed);
if (score > 0.28) candidates.push({ x, y, score });
}
}
return candidates;
}
function collectPortCandidates(world, rect, seed) {
const out = [];
for (let y = rect.y0 + 2; y < rect.y1 - 2; y += 2) {
for (let x = rect.x0 + 2; x < rect.x1 - 2; x += 2) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
const seaN = seaNeighbors(world, x, y, 2);
if (seaN < 3) continue;
const slope = world.fields.slope?.[i] || 0;
const suit = world.fields.portSuitability?.[i] || 0;
const score = seaN * 0.09 + suit * 1.10 + (1 - slope) * 0.35 + seeded(seed, x, y, 71) * 0.20;
if (score > 0.55) out.push({ x, y, score });
}
}
return out;
}
function usedNameSet(sourceMap) {
const out = new Set();
for (const key of ["villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations", "industrialZones", "logisticsParks", "newTowns", "adminCenters"]) {
for (const p of sourceMap[key] || []) if (p?.name) out.add(p.name);
}
return out;
}
function hideAdminCentersInside(sourceMap, world, rect) {
if (!Array.isArray(sourceMap.adminCenters)) return 0;
let hidden = 0;
for (const p of sourceMap.adminCenters) {
if (!p || p.patchHidden) continue;
const wx = pointWorldX(world, p);
const wy = pointWorldY(world, p);
if (!insideRect(wx, wy, rect)) continue;
p.patchHidden = true;
p.patchHiddenAt = Date.now();
p.x = -100000;
p.y = -100000;
hidden++;
}
return hidden;
}
function pointLayerPreservedCount(sourceMap, world, userRect) {
let preserved = 0;
for (const key of [...POINT_LAYER_KEYS, "adminCenters"]) {
const arr = sourceMap[key];
if (!Array.isArray(arr)) continue;
for (const p of arr) {
if (!p || p.patchHidden) continue;
const wx = pointWorldX(world, p);
const wy = pointWorldY(world, p);
if (!insideRect(wx, wy, userRect)) preserved++;
}
}
return preserved;
}
function portStillValid(world, p, rect) {
const wx = Math.round(pointWorldX(world, p));
const wy = Math.round(pointWorldY(world, p));
const land = nearestLand(world, wx, wy, rect, 5);
return !!land && seaNeighbors(world, land.x, land.y, 2) >= 2;
}
function prunePointLayers(sourceMap, world, rects) {
let removed = 0;
let invalidPortsRemoved = 0;
const userRect = rects.writeRect || rects.userRect;
const blendRect = rects.coreRect || rects.blendRect;
for (const key of POINT_LAYER_KEYS) {
const arr = sourceMap[key];
if (!Array.isArray(arr)) continue;
const kept = [];
for (const p of arr) {
const wx = pointWorldX(world, p);
const wy = pointWorldY(world, p);
const inBlend = insideRect(wx, wy, blendRect);
const inUser = insideRect(wx, wy, userRect);
const invalidTransitionPort = key === "ports" && inUser && !inBlend && !portStillValid(world, p, userRect);
if (inBlend || invalidTransitionPort) {
removed++;
if (invalidTransitionPort) invalidPortsRemoved++;
} else {
kept.push(p);
}
}
sourceMap[key] = kept;
}
removed += hideAdminCentersInside(sourceMap, world, blendRect);
return { removedPoints: removed, invalidPortsRemoved };
}
function segmentTouchesRect(world, seg, rect) {
if (!Array.isArray(seg) || seg.length < 2) return false;
return insideRect(tupleWorldX(world, seg[0]), tupleWorldY(world, seg[0]), rect)
|| insideRect(tupleWorldX(world, seg[1]), tupleWorldY(world, seg[1]), rect);
}
function pathLayerPreservedCount(sourceMap, world, userRect) {
let preserved = 0;
for (const key of PATH_LAYER_KEYS) {
const arr = sourceMap[key];
if (!Array.isArray(arr)) continue;
for (const path of arr) {
if (!Array.isArray(path) || !path.length) continue;
if (path.every((tuple) => !insideRect(tupleWorldX(world, tuple), tupleWorldY(world, tuple), userRect))) preserved++;
}
}
return preserved;
}
function clipPathToOutsideAndAnchors(world, path, rects, mode, layerKey) {
const outsideParts = [];
const anchors = [];
let current = [];
let removedInside = 0;
let lastOutside = null;
let lastInside = null;
for (const tuple of path || []) {
const wx = tupleWorldX(world, tuple);
const wy = tupleWorldY(world, tuple);
const p = { x: Math.round(wx), y: Math.round(wy) };
const inUser = insideRect(p.x, p.y, rects.userRect);
if (!inUser) {
if (lastInside) anchors.push({ ...p, kind: mode, layerKey, source: "boundary" });
current.push(tuple);
lastOutside = p;
} else {
removedInside++;
if (lastOutside) anchors.push({ ...lastOutside, kind: mode, layerKey, source: "boundary" });
if (current.length >= 2) outsideParts.push(current);
current = [];
lastInside = p;
}
}
if (current.length >= 2) outsideParts.push(current);
const unique = [];
const seen = new Set();
for (const anchor of anchors) {
const land = nearestLand(world, anchor.x, anchor.y, rects.userRect, 10);
if (!land) continue;
const key = `${land.x},${land.y},${mode}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push({ ...anchor, x: land.x, y: land.y });
}
return { outsideParts, anchors: unique, removedInside };
}
function pruneLinearLayers(sourceMap, world, rects) {
let removed = 0;
const roadAnchors = [];
const railAnchors = [];
for (const key of PATH_LAYER_KEYS) {
const arr = sourceMap[key];
if (!Array.isArray(arr)) continue;
const kept = [];
for (const path of arr) {
const touchesUser = (path || []).some((tuple) => insideRect(tupleWorldX(world, tuple), tupleWorldY(world, tuple), rects.userRect));
if (!touchesUser) {
kept.push(path);
continue;
}
const mode = key.includes("Rail") || key.includes("rail") ? "rail" : "road";
const clipped = clipPathToOutsideAndAnchors(world, path, rects, mode, key);
kept.push(...clipped.outsideParts);
if (mode === "rail") railAnchors.push(...clipped.anchors);
else roadAnchors.push(...clipped.anchors);
removed++;
}
sourceMap[key] = kept;
}
for (const key of SEGMENT_LAYER_KEYS) {
const arr = sourceMap[key];
if (!Array.isArray(arr)) continue;
const kept = [];
for (const seg of arr) {
if (segmentTouchesRect(world, seg, rects.writeRect || rects.userRect)) removed++;
else kept.push(seg);
}
sourceMap[key] = kept;
}
return { removedLines: removed, roadAnchors, railAnchors };
}
function resetHumanFields(world, rect) {
for (const key of FLOAT_FIELD_KEYS) {
const field = ensureField(world, key, Float32Array, 0);
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const i = worldIndex(world, x, y);
if (i >= 0) field[i] = 0;
}
}
}
const landuse = ensureField(world, "landuse", Uint8Array, LANDUSE.RURAL);
const adminId = ensureField(world, "adminId", Int32Array, -1);
const municipalityId = ensureField(world, "municipalityId", Int32Array, -1);
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0) continue;
landuse[i] = world.fields.sea?.[i] ? LANDUSE.RURAL : ((world.fields.slope?.[i] || 0) > 0.34 ? LANDUSE.FOREST : LANDUSE.RURAL);
adminId[i] = -1;
municipalityId[i] = -1;
}
}
}
function snapshotFields(world, rect, keys) {
const width = rectWidth(rect);
const out = { rect: { ...rect }, width, fields: {} };
for (const key of keys) {
const field = world.fields[key];
if (!field) continue;
const Constructor = field.constructor;
const copy = new Constructor(width * rectHeight(rect));
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const wi = worldIndex(world, x, y);
const si = (y - rect.y0) * width + (x - rect.x0);
if (wi >= 0) copy[si] = field[wi];
}
}
out.fields[key] = copy;
}
return out;
}
function snapshotValue(snapshot, key, x, y) {
const field = snapshot?.fields?.[key];
const rect = snapshot?.rect;
if (!field || !insideRect(x, y, rect)) return undefined;
return field[(y - rect.y0) * snapshot.width + (x - rect.x0)];
}
function blendWeightFromRects(x, y, rects) {
if (insideRect(x, y, rects.blendRect)) return 1;
if (!insideRect(x, y, rects.repairRect)) return 0;
const band = Math.max(1, Math.min(
rects.blendRect.x0 - rects.repairRect.x0,
rects.blendRect.y0 - rects.repairRect.y0,
rects.repairRect.x1 - rects.blendRect.x1,
rects.repairRect.y1 - rects.blendRect.y1,
));
return clamp(distanceToRectEdge(x, y, rects.repairRect) / band);
}
function reconcileTransitionFields(world, rects, snapshot) {
let landUseCellsUpdated = 0;
const fields = ["settlementScore", "populationDensity", "stationInfluence", "roadInfluence", "railInfluence2", "villageInfluence"];
for (let y = rects.repairRect.y0; y < rects.repairRect.y1; y++) {
for (let x = rects.repairRect.x0; x < rects.repairRect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0) continue;
const w = blendWeightFromRects(x, y, rects);
for (const key of fields) {
const oldValue = snapshotValue(snapshot, key, x, y);
if (oldValue === undefined || !world.fields[key]) continue;
world.fields[key][i] = oldValue * (1 - w) + world.fields[key][i] * w;
}
const oldLanduse = snapshotValue(snapshot, "landuse", x, y);
if (oldLanduse !== undefined && world.fields.landuse && w < 0.48) world.fields.landuse[i] = oldLanduse;
if (world.fields.landuse) landUseCellsUpdated++;
const oldAdmin = snapshotValue(snapshot, "adminId", x, y);
const oldMunicipality = snapshotValue(snapshot, "municipalityId", x, y);
if (oldAdmin !== undefined && world.fields.adminId && w < 0.35) world.fields.adminId[i] = oldAdmin;
if (oldMunicipality !== undefined && world.fields.municipalityId && w < 0.35) world.fields.municipalityId[i] = oldMunicipality;
}
}
return landUseCellsUpdated;
}
function sourcePoint(world, p) {
const src = worldToSourcePoint(world, p.x, p.y);
return { ...p, x: src.x, y: src.y, worldX: p.x, worldY: p.y, insidePrefecture: true, patchGenerated: true };
}
function sourcePath(world, path) {
return path.map(([x, y]) => [x - world.originX, y - world.originY]);
}
function addInfluence(world, point, radius, amount, fields, clipRect = null) {
const r = Math.max(1, Math.floor(radius));
for (let y = Math.max(0, point.y - r); y <= Math.min(world.height - 1, point.y + r); y++) {
for (let x = Math.max(0, point.x - r); x <= Math.min(world.width - 1, point.x + r); x++) {
const d = Math.hypot(x - point.x, y - point.y);
if (d > r) continue;
if (clipRect && !insideRect(x, y, clipRect)) continue;
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
const w = (1 - d / r) ** 2 * amount;
for (const [key, mult] of fields) {
const field = ensureField(world, key, Float32Array, 0);
field[i] = clamp(field[i] + w * mult, 0, 1.8);
}
}
}
}
function setLanduseAround(world, point, radius, landuseCode, strength = 1, clipRect = null) {
const landuse = ensureField(world, "landuse", Uint8Array, LANDUSE.RURAL);
const r = Math.max(1, Math.floor(radius));
for (let y = Math.max(0, point.y - r); y <= Math.min(world.height - 1, point.y + r); y++) {
for (let x = Math.max(0, point.x - r); x <= Math.min(world.width - 1, point.x + r); x++) {
const d = Math.hypot(x - point.x, y - point.y);
if (d > r) continue;
if (clipRect && !insideRect(x, y, clipRect)) continue;
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
const p = 1 - d / r;
if (p * strength < 0.22) continue;
if (landuseCode > landuse[i] || p > 0.62) landuse[i] = landuseCode;
}
}
}
function assignFarmlandAndForest(world, rect) {
const landuse = ensureField(world, "landuse", Uint8Array, LANDUSE.RURAL);
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
if (landuse[i] >= LANDUSE.OLD_URBAN && landuse[i] <= LANDUSE.ROADSIDE) continue;
const slope = world.fields.slope?.[i] || 0;
const agriculture = world.fields.agriculture?.[i] || 0;
const plain = world.fields.plain?.[i] || 0;
if (slope > 0.42 || (world.fields.elevation?.[i] || 0) > 0.56) landuse[i] = LANDUSE.FOREST;
else if (agriculture > 0.28 || plain > 0.42) landuse[i] = LANDUSE.FARMLAND;
else landuse[i] = LANDUSE.RURAL;
}
}
}
function nextAdminId(sourceMap, world) {
let maxId = -1;
const field = world.fields.adminId;
if (field) {
for (let i = 0; i < field.length; i++) if (field[i] > maxId) maxId = field[i];
}
for (let i = 0; i < (sourceMap.adminCenters || []).length; i++) if (sourceMap.adminCenters[i]) maxId = Math.max(maxId, i);
return maxId + 1;
}
function assignLocalAdmin(world, sourceMap, rect, centers, seed, usedNames) {
const adminIdField = ensureField(world, "adminId", Int32Array, -1);
const municipalityField = ensureField(world, "municipalityId", Int32Array, -1);
const adminCenters = ensureSourceArray(sourceMap, "adminCenters");
let id = nextAdminId(sourceMap, world);
const centerRecords = [];
for (const center of centers) {
const population = center.population || 3500 + Math.round(seeded(seed, center.x, center.y, 221) * 21000 / 1000) * 1000;
const base = {
x: center.x,
y: center.y,
kind: "Municipal Center",
population,
municipalityPopulation: Math.max(population, Math.round(population * (1.8 + seeded(seed, center.x, center.y, 229) * 3.2))),
};
const name = center.name || makeName(seed, rect, center.x, center.y, { ...base, kind: "Municipal Center" }, usedNames, 230 + id);
const record = sourcePoint(world, { ...base, id, name, municipalityId: id, adminId: id, labelPriorityBase: 420 + Math.sqrt(population) });
adminCenters[id] = record;
centerRecords.push({ ...center, id, name, population: record.population, municipalityPopulation: record.municipalityPopulation });
id++;
}
if (!centerRecords.length) return centerRecords;
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
let best = centerRecords[0];
let bestD = Infinity;
for (const c of centerRecords) {
const d = Math.hypot(x - c.x, y - c.y) * (1 + (world.fields.slope?.[i] || 0) * 1.8) - seeded(seed, x, y, c.id) * 2.2;
if (d < bestD) {
bestD = d;
best = c;
}
}
adminIdField[i] = best.id;
municipalityField[i] = best.id;
}
}
return centerRecords;
}
function collectExistingAdminCenters(world, sourceMap, rect) {
const centers = [];
for (const p of sourceMap.adminCenters || []) {
if (!p || p.patchHidden) continue;
const x = Math.round(pointWorldX(world, p));
const y = Math.round(pointWorldY(world, p));
if (insideRect(x, y, rect)) continue;
const id = Number.isFinite(p.adminId) ? p.adminId : Number.isFinite(p.municipalityId) ? p.municipalityId : Number.isFinite(p.id) ? p.id : null;
if (id === null || id < 0) continue;
const distance = Math.max(rect.x0 - x, x - (rect.x1 - 1), rect.y0 - y, y - (rect.y1 - 1), 0);
if (distance <= 48) centers.push({ x, y, id, name: p.name, population: p.population || p.municipalityPopulation || 3000, external: true });
}
return centers;
}
function reassignAdminRepair(world, sourceMap, rects, localCenters, seed) {
const adminIdField = ensureField(world, "adminId", Int32Array, -1);
const municipalityField = ensureField(world, "municipalityId", Int32Array, -1);
const externalCenters = collectExistingAdminCenters(world, sourceMap, rects.userRect);
const centers = [...externalCenters, ...(localCenters || [])].filter((p) => Number.isFinite(p?.id));
if (!centers.length) return { adminCellsReassigned: 0, adminBoundarySmoothed: 0 };
let reassigned = 0;
for (let y = rects.repairRect.y0; y < rects.repairRect.y1; y++) {
for (let x = rects.repairRect.x0; x < rects.repairRect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
let best = null;
let bestScore = Infinity;
for (const c of centers) {
const d = Math.hypot(x - c.x, y - c.y);
const externalBias = c.external ? -4.5 * (1 - blendWeightFromRects(x, y, rects)) : 0;
const score = d * (1 + (world.fields.slope?.[i] || 0) * 1.25) + externalBias - seeded(seed, x, y, c.id + 1300) * 1.5;
if (score < bestScore) {
bestScore = score;
best = c;
}
}
if (best && adminIdField[i] !== best.id) {
adminIdField[i] = best.id;
municipalityField[i] = best.id;
reassigned++;
}
}
}
let smoothed = 0;
for (let pass = 0; pass < 2; pass++) {
const changes = [];
for (let y = rects.repairRect.y0 + 1; y < rects.repairRect.y1 - 1; y++) {
for (let x = rects.repairRect.x0 + 1; x < rects.repairRect.x1 - 1; x++) {
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i] || blendWeightFromRects(x, y, rects) < 0.2) continue;
const counts = new Map();
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const ni = worldIndex(world, x + dx, y + dy);
const id = ni >= 0 ? adminIdField[ni] : -1;
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1);
}
const current = adminIdField[i];
const best = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
if (best && best[0] !== current && best[1] >= 3) changes.push([i, best[0]]);
}
}
for (const [i, id] of changes) {
adminIdField[i] = id;
municipalityField[i] = id;
smoothed++;
}
}
return { adminCellsReassigned: reassigned, adminBoundarySmoothed: smoothed };
}
function buildAdminBorders(world, rect) {
const adminId = world.fields.adminId;
const sea = world.fields.sea;
if (!adminId) return [];
const segments = [];
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
const i = worldIndex(world, x, y);
if (i < 0 || sea?.[i]) continue;
const id = adminId[i];
if (id < 0) continue;
const east = worldIndex(world, x + 1, y);
if (x + 1 < rect.x1 && east >= 0 && !sea?.[east] && adminId[east] >= 0 && adminId[east] !== id) {
segments.push(sourcePath(world, [[x + 1, y], [x + 1, y + 1]]));
}
const south = worldIndex(world, x, y + 1);
if (y + 1 < rect.y1 && south >= 0 && !sea?.[south] && adminId[south] >= 0 && adminId[south] !== id) {
segments.push(sourcePath(world, [[x, y + 1], [x + 1, y + 1]]));
}
}
}
return segments;
}
function clampToRect(x, y, rect) {
return {
x: Math.max(rect.x0, Math.min(rect.x1 - 1, Math.round(x))),
y: Math.max(rect.y0, Math.min(rect.y1 - 1, Math.round(y))),
};
}
function nearestLand(world, x, y, rect, maxRadius = 10) {
const start = clampToRect(x, y, rect);
if (isLand(world, start.x, start.y)) return start;
for (let r = 1; r <= maxRadius; r++) {
let best = null;
let bestD = Infinity;
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
if (Math.abs(dx) !== r && Math.abs(dy) !== r) continue;
const p = clampToRect(start.x + dx, start.y + dy, rect);
if (!isLand(world, p.x, p.y)) continue;
const d = Math.hypot(p.x - x, p.y - y);
if (d < bestD) {
best = p;
bestD = d;
}
}
}
if (best) return best;
}
return null;
}
function findPath(world, startInput, endInput, rect, options = {}) {
const margin = options.margin ?? 8;
const searchRect = expandRect(rect, margin, world);
const start = nearestLand(world, startInput.x, startInput.y, searchRect, 12);
const end = nearestLand(world, endInput.x, endInput.y, searchRect, 12);
if (!start || !end) return [];
if (start.x === end.x && start.y === end.y) return [[start.x, start.y]];
const w = rectWidth(searchRect);
const h = rectHeight(searchRect);
const localIndex = (x, y) => (y - searchRect.y0) * w + (x - searchRect.x0);
const total = w * h;
const g = new Float32Array(total);
g.fill(Infinity);
const prev = new Int32Array(total);
prev.fill(-1);
const startIdx = localIndex(start.x, start.y);
const endIdx = localIndex(end.x, end.y);
const open = new MinHeap();
g[startIdx] = 0;
open.push({ i: startIdx, x: start.x, y: start.y, f: Math.hypot(end.x - start.x, end.y - start.y) });
const dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
let iterations = 0;
const maxIterations = Math.min(total * 8, 90000);
while (open.length && iterations++ < maxIterations) {
const current = open.pop();
if (!current) break;
if (current.i === endIdx) break;
if (current.f > g[current.i] + Math.hypot(end.x - current.x, end.y - current.y) * 1.45 + 100) continue;
for (const [dx, dy] of dirs) {
const nx = current.x + dx;
const ny = current.y + dy;
if (nx < searchRect.x0 || ny < searchRect.y0 || nx >= searchRect.x1 || ny >= searchRect.y1) continue;
const wi = worldIndex(world, nx, ny);
if (wi < 0 || world.fields.sea?.[wi]) continue;
const ni = localIndex(nx, ny);
const step = Math.hypot(dx, dy);
const slope = world.fields.slope?.[wi] || 0;
const river = world.fields.river?.[wi] || 0;
const plain = world.fields.plain?.[wi] || 0;
const roadEase = options.rail ? Math.max(0, slope - 0.10) * 9.5 : slope * 4.5;
const cost = step * (1 + roadEase - plain * 0.18 - river * 0.07);
const ng = g[current.i] + Math.max(0.2, cost);
if (ng >= g[ni]) continue;
g[ni] = ng;
prev[ni] = current.i;
const heuristic = Math.hypot(end.x - nx, end.y - ny) * (options.rail ? 1.20 : 1.05);
open.push({ i: ni, x: nx, y: ny, f: ng + heuristic });
}
}
if (!Number.isFinite(g[endIdx])) return straightFallback(world, start, end, searchRect);
const path = [];
let cursor = endIdx;
for (let guard = 0; cursor >= 0 && guard < total; guard++) {
const lx = cursor % w;
const ly = Math.floor(cursor / w);
path.push([searchRect.x0 + lx, searchRect.y0 + ly]);
if (cursor === startIdx) break;
cursor = prev[cursor];
}
path.reverse();
return simplifyPath(path);
}
function straightFallback(world, start, end, rect) {
const steps = Math.max(2, Math.ceil(Math.hypot(end.x - start.x, end.y - start.y)));
const path = [];
for (let k = 0; k <= steps; k++) {
const t = k / steps;
const p = nearestLand(world, start.x + (end.x - start.x) * t, start.y + (end.y - start.y) * t, rect, 5);
if (!p) continue;
if (!path.length || path[path.length - 1][0] !== p.x || path[path.length - 1][1] !== p.y) path.push([p.x, p.y]);
}
return simplifyPath(path);
}
function simplifyPath(path) {
if (!Array.isArray(path) || path.length <= 2) return path || [];
const out = [path[0]];
let lastDx = null;
let lastDy = null;
for (let i = 1; i < path.length - 1; i++) {
const prev = out[out.length - 1];
const cur = path[i];
const next = path[i + 1];
const dx1 = Math.sign(cur[0] - prev[0]);
const dy1 = Math.sign(cur[1] - prev[1]);
const dx2 = Math.sign(next[0] - cur[0]);
const dy2 = Math.sign(next[1] - cur[1]);
if (dx1 !== dx2 || dy1 !== dy2 || i % 8 === 0) out.push(cur);
lastDx = dx1;
lastDy = dy1;
}
out.push(path[path.length - 1]);
return out;
}
function writePathInfluence(world, path, key, radius, amount, clipRect = null) {
const field = ensureField(world, key, Float32Array, 0);
const r = Math.max(1, radius | 0);
for (const [px, py] of path || []) {
for (let y = Math.max(0, py - r); y <= Math.min(world.height - 1, py + r); y++) {
for (let x = Math.max(0, px - r); x <= Math.min(world.width - 1, px + r); x++) {
const d = Math.hypot(x - px, y - py);
if (d > r) continue;
if (clipRect && !insideRect(x, y, clipRect)) continue;
const i = worldIndex(world, x, y);
if (i < 0 || world.fields.sea?.[i]) continue;
field[i] = clamp(field[i] + (1 - d / r) * amount, 0, 1.6);
}
}
}
}
function chooseLocalCounts(rect, landCount, coastCount) {
const area = rectArea(rect);
const landArea = Math.max(0, landCount);
const scale = Math.sqrt(Math.max(1, area) / 3000);
const landScale = Math.sqrt(Math.max(1, landArea) / 3000);
return {
admin: Math.max(1, Math.min(10, Math.round(1 + landScale * 2.2))),
modern: Math.max(0, Math.min(7, Math.round(landScale * 1.35))),
markets: Math.max(1, Math.min(10, Math.round(landScale * 2.0))),
villages: Math.max(3, Math.min(22, Math.round(landScale * 5.2))),
ports: Math.max(0, Math.min(6, Math.round(Math.sqrt(Math.max(0, coastCount)) / 8))),
castles: Math.max(0, Math.min(4, Math.round(scale * 0.8))),
industrial: Math.max(0, Math.min(4, Math.round(landScale * 0.65))),
logistics: Math.max(0, Math.min(4, Math.round(landScale * 0.70))),
newTowns: Math.max(0, Math.min(4, Math.round(landScale * 0.55))),
};
}
function countLandAndCoast(world, rect) {
let land = 0;
let coast = 0;
for (let y = rect.y0; y < rect.y1; y++) {
for (let x = rect.x0; x < rect.x1; x++) {
if (!isLand(world, x, y)) continue;
land++;
if (seaNeighbors(world, x, y, 1) > 0) coast++;
}
}
return { land, coast };
}
function topN(points, n) {
return [...points].sort((a, b) => (b.score || 0) - (a.score || 0)).slice(0, Math.max(0, n));
}
function buildSettlementLayers(world, sourceMap, rect, seed, usedNames) {
const landStats = countLandAndCoast(world, rect);
if (landStats.land < 24) return { counts: {}, centers: [] };
const counts = chooseLocalCounts(rect, landStats.land, landStats.coast);
const landCandidates = collectLandCandidates(world, rect, seed, rectArea(rect) > 16000 ? 4 : 3);
const portCandidates = collectPortCandidates(world, rect, seed);
const ports = pickEntities(portCandidates, { max: counts.ports, minDistance: 18, threshold: 0.55, seed: seed + 110, jitter: 0.08 }).map((p, idx) => {
const portClass = idx === 0 && p.score > 1.05 ? "regional" : "fishing";
const pop = portClass === "regional" ? 9000 + Math.round(seeded(seed, p.x, p.y, 301) * 22000 / 1000) * 1000 : 1600 + Math.round(seeded(seed, p.x, p.y, 302) * 5200 / 100) * 100;
const point = { ...p, kind: portClass === "regional" ? "Regional Port" : "Fishing Port", portClass, population: pop, labelPriorityBase: portClass === "regional" ? 360 : 160 };
return sourcePoint(world, { ...point, name: makeName(seed, rect, p.x, p.y, point, usedNames, 300 + idx) });
});
const citySeeds = pickEntities(landCandidates, { max: counts.modern, minDistance: 24, threshold: 0.50, seed: seed + 120, jitter: 0.12 });
const modernCities = citySeeds.map((p, idx) => {
const rank = idx === 0 && rectArea(rect) > 9000 ? "Regional City" : "Local City";
const popBase = rank === "Regional City" ? 52000 : 18000;
const popSpan = rank === "Regional City" ? 140000 : 52000;
const population = popBase + Math.round(seeded(seed, p.x, p.y, 401) * popSpan / 1000) * 1000;
const point = { ...p, kind: rank, rank, population, labelPriorityBase: rank === "Regional City" ? 760 : 520, isRegionalCapital: rank === "Regional City" && idx === 0 };
return sourcePoint(world, { ...point, name: makeName(seed, rect, p.x, p.y, point, usedNames, 400 + idx) });
});
const marketSeeds = pickEntities(landCandidates, { max: counts.markets, minDistance: 14, threshold: 0.42, seed: seed + 130, jitter: 0.10 })
.filter((p) => citySeeds.every((c) => Math.hypot(p.x - c.x, p.y - c.y) >= 9));
const markets = marketSeeds.map((p, idx) => {
const population = 3000 + Math.round(seeded(seed, p.x, p.y, 501) * 13000 / 500) * 500;
const point = { ...p, kind: "Market Town", population, labelPriorityBase: 230 };
return sourcePoint(world, { ...point, name: makeName(seed, rect, p.x, p.y, point, usedNames, 500 + idx) });
});
const villageSeeds = pickEntities(landCandidates, { max: counts.villages, minDistance: 8, threshold: 0.31, seed: seed + 140, jitter: 0.12 })
.filter((p) => [...citySeeds, ...marketSeeds].every((c) => Math.hypot(p.x - c.x, p.y - c.y) >= 6));
const villages = villageSeeds.map((p, idx) => {
const population = 700 + Math.round(seeded(seed, p.x, p.y, 601) * 5200 / 100) * 100;
const point = { ...p, kind: "Village", population, labelPriorityBase: 80 };
return sourcePoint(world, { ...point, name: makeName(seed, rect, p.x, p.y, point, usedNames, 600 + idx) });
});
const castleSeeds = pickEntities(landCandidates.map((p) => ({ ...p, score: p.score + (world.fields.elevation?.[worldIndex(world, p.x, p.y)] || 0) * 0.4 })), { max: counts.castles, minDistance: 20, threshold: 0.52, seed: seed + 150, jitter: 0.15 });
const castles = castleSeeds.map((p, idx) => {
const point = { ...p, kind: "Castle", population: 0, labelPriorityBase: 190 };
return sourcePoint(world, { ...point, name: makeName(seed, rect, p.x, p.y, point, usedNames, 700 + idx) });
});
for (const p of ports) ensureSourceArray(sourceMap, "ports").push(p);
for (const p of modernCities) ensureSourceArray(sourceMap, "modernCities").push(p);
for (const p of markets) ensureSourceArray(sourceMap, "markets").push(p);
for (const p of villages) ensureSourceArray(sourceMap, "villages").push(p);
for (const p of castles) ensureSourceArray(sourceMap, "castles").push(p);
const centers = topN([
...modernCities.map((p) => ({ x: p.worldX, y: p.worldY, name: p.name, population: p.population, score: 2 + (p.population || 0) / 70000 })),
...markets.map((p) => ({ x: p.worldX, y: p.worldY, name: p.name, population: p.population, score: 1 + (p.population || 0) / 28000 })),
...ports.map((p) => ({ x: p.worldX, y: p.worldY, name: p.name, population: p.population, score: 0.8 + (p.population || 0) / 26000 })),
...villages.map((p) => ({ x: p.worldX, y: p.worldY, name: p.name, population: p.population, score: 0.4 + (p.population || 0) / 16000 })),
], counts.admin);
return {
counts: {
ports: ports.length,
modernCities: modernCities.length,
markets: markets.length,
villages: villages.length,
castles: castles.length,
},
centers,
localPoints: { ports, modernCities, markets, villages, castles },
};
}
function buildTransportLayers(world, sourceMap, rect, seed, localPoints, connectors = {}) {
const cities = (localPoints.modernCities || []).map((p) => ({ ...p, x: p.worldX, y: p.worldY }));
const ports = (localPoints.ports || []).map((p) => ({ ...p, x: p.worldX, y: p.worldY }));
const markets = (localPoints.markets || []).map((p) => ({ ...p, x: p.worldX, y: p.worldY }));
const villages = (localPoints.villages || []).map((p) => ({ ...p, x: p.worldX, y: p.worldY }));
const roadAnchors = (connectors.roadAnchors || []).map((p) => ({ ...p, score: 1.3 }));
const railAnchors = (connectors.railAnchors || []).map((p) => ({ ...p, score: 1.2 }));
const trunkNodes = topN([...cities, ...ports, ...markets], Math.min(7, Math.max(2, cities.length + ports.length + 1)));
let nationalRoads = 0;
let minorRoads = 0;
let railways = 0;
let stations = 0;
let roadConnectorsCreated = 0;
let railwayConnectorsCreated = 0;
for (let i = 1; i < trunkNodes.length; i++) {
const target = trunkNodes[i];
const previous = trunkNodes.slice(0, i).sort((a, b) => Math.hypot(a.x - target.x, a.y - target.y) - Math.hypot(b.x - target.x, b.y - target.y))[0];
const path = findPath(world, previous, target, rect, { margin: 10 });
if (path.length >= 3) {
ensureSourceArray(sourceMap, "nationalRoads").push(sourcePath(world, path));
writePathInfluence(world, path, "roadInfluence", 2, 0.36);
nationalRoads++;
}
}
const connectorTargets = trunkNodes.length ? trunkNodes : topN([...cities, ...ports, ...markets, ...villages], 4);
for (const anchor of roadAnchors.slice(0, 10)) {
if (!connectorTargets.length) break;
const nearest = connectorTargets
.filter((node) => Math.hypot(anchor.x - node.x, anchor.y - node.y) >= 4)
.sort((a, b) => Math.hypot(anchor.x - a.x, anchor.y - a.y) - Math.hypot(anchor.x - b.x, anchor.y - b.y))[0];
if (!nearest) continue;
const path = findPath(world, anchor, nearest, rect, { margin: 4 });
if (path.length >= 3) {
ensureSourceArray(sourceMap, "nationalRoads").push(sourcePath(world, path));
writePathInfluence(world, path, "roadInfluence", 2, 0.34);
nationalRoads++;
roadConnectorsCreated++;
}
}
for (const node of [...markets, ...villages]) {
const anchors = trunkNodes.length ? trunkNodes : cities;
if (!anchors.length) continue;
const nearest = anchors.sort((a, b) => Math.hypot(a.x - node.x, a.y - node.y) - Math.hypot(b.x - node.x, b.y - node.y))[0];
if (!nearest || Math.hypot(nearest.x - node.x, nearest.y - node.y) < 3) continue;
const path = findPath(world, node, nearest, rect, { margin: 8 });
if (path.length >= 3) {
ensureSourceArray(sourceMap, "minorRoads").push(sourcePath(world, path));
writePathInfluence(world, path, "roadInfluence", 1, 0.16);
minorRoads++;
}
}
const railNodes = topN([...cities, ...ports], Math.min(4, cities.length + ports.length));
for (let i = 1; i < railNodes.length; i++) {
const path = findPath(world, railNodes[i - 1], railNodes[i], rect, { margin: 12, rail: true });
if (path.length >= 6) {
ensureSourceArray(sourceMap, i === 1 ? "railways" : "branchRailways").push(sourcePath(world, path));
writePathInfluence(world, path, "railInfluence2", 2, 0.42);
railways++;
for (let k = 0; k < path.length; k += 14) {
const [x, y] = path[k];
if (!insideRect(x, y, rect) || !isLand(world, x, y)) continue;
const point = sourcePoint(world, { x, y, kind: k === 0 || k >= path.length - 14 ? "Major Station" : "Station", name: `Station ${Math.max(1, stations + 1)}`, population: 0, labelPriorityBase: 120 });
ensureSourceArray(sourceMap, "stations").push(point);
addInfluence(world, { x, y }, 5, 0.38, [["stationInfluence", 1.0], ["populationDensity", 0.25]]);
stations++;
}
}
}
const railTargets = railNodes.length ? railNodes : topN([...cities, ...ports], 3);
for (const anchor of railAnchors.slice(0, 6)) {
if (!railTargets.length) break;
const nearest = railTargets
.filter((node) => Math.hypot(anchor.x - node.x, anchor.y - node.y) >= 8)
.sort((a, b) => Math.hypot(anchor.x - a.x, anchor.y - a.y) - Math.hypot(anchor.x - b.x, anchor.y - b.y))[0];
if (!nearest) continue;
const path = findPath(world, anchor, nearest, rect, { margin: 6, rail: true });
if (path.length >= 6) {
ensureSourceArray(sourceMap, "branchRailways").push(sourcePath(world, path));
writePathInfluence(world, path, "railInfluence2", 2, 0.38);
railways++;
railwayConnectorsCreated++;
}
}
return { nationalRoads, minorRoads, railways, stations, roadConnectorsCreated, railwayConnectorsCreated };
}
function buildDevelopmentLayers(world, sourceMap, rect, seed, localPoints, usedNames) {
const bases = [
...(localPoints.modernCities || []).map((p) => ({ x: p.worldX, y: p.worldY, score: 1.4, name: p.name })),
...(localPoints.ports || []).map((p) => ({ x: p.worldX, y: p.worldY, score: 1.1, name: p.name })),
...(localPoints.markets || []).map((p) => ({ x: p.worldX, y: p.worldY, score: 0.8, name: p.name })),
];
const landCandidates = collectLandCandidates(world, rect, seed + 900, 4)
.map((p) => ({ ...p, score: p.score + (world.fields.roadInfluence?.[worldIndex(world, p.x, p.y)] || 0) * 0.9 + (world.fields.railInfluence2?.[worldIndex(world, p.x, p.y)] || 0) * 0.8 }));
const counts = chooseLocalCounts(rect, countLandAndCoast(world, rect).land, countLandAndCoast(world, rect).coast);
const industrialSeeds = pickEntities(landCandidates, { max: counts.industrial, minDistance: 18, threshold: 0.52, seed: seed + 910, jitter: 0.12 });
const logisticsSeeds = pickEntities(landCandidates, { max: counts.logistics, minDistance: 16, threshold: 0.50, seed: seed + 920, jitter: 0.12 });
const newTownSeeds = pickEntities(landCandidates, { max: counts.newTowns, minDistance: 18, threshold: 0.46, seed: seed + 930, jitter: 0.12 });
const industrialZones = industrialSeeds.map((p, idx) => {
const point = { ...p, kind: "Industrial Zone", population: 0, labelPriorityBase: 90 };
return sourcePoint(world, { ...point, name: `${makeName(seed, rect, p.x, p.y, point, usedNames, 910 + idx)} Industrial` });
});
const logisticsParks = logisticsSeeds.map((p, idx) => {
const point = { ...p, kind: "Logistics Park", population: 0, labelPriorityBase: 80 };
return sourcePoint(world, { ...point, name: null, facilityLabel: "Logistics Park", labelStyle: "facility", suppressSettlementLabel: true });
});
const newTowns = newTownSeeds.map((p, idx) => {
const population = 6000 + Math.round(seeded(seed, p.x, p.y, 931) * 26000 / 1000) * 1000;
const point = { ...p, kind: "New Town", population, labelPriorityBase: 180 };
return sourcePoint(world, { ...point, name: `${makeName(seed, rect, p.x, p.y, point, usedNames, 930 + idx)} New Town` });
});
for (const p of industrialZones) {
ensureSourceArray(sourceMap, "industrialZones").push(p);
setLanduseAround(world, { x: p.worldX, y: p.worldY }, 5, LANDUSE.INDUSTRIAL, 1.0);
addInfluence(world, { x: p.worldX, y: p.worldY }, 6, 0.25, [["populationDensity", 0.2]]);
}
for (const p of logisticsParks) {
ensureSourceArray(sourceMap, "logisticsParks").push(p);
setLanduseAround(world, { x: p.worldX, y: p.worldY }, 4, LANDUSE.LOGISTICS, 1.0);
}
for (const p of newTowns) {
ensureSourceArray(sourceMap, "newTowns").push(p);
setLanduseAround(world, { x: p.worldX, y: p.worldY }, 6, LANDUSE.NEW_TOWN, 1.0);
addInfluence(world, { x: p.worldX, y: p.worldY }, 8, 0.42, [["populationDensity", 1.0], ["settlementScore", 0.5]]);
}
return { industrialZones: industrialZones.length, logisticsParks: logisticsParks.length, newTowns: newTowns.length };
}
function applySettlementInfluence(world, localPoints) {
for (const p of localPoints.modernCities || []) {
const wp = { x: p.worldX, y: p.worldY };
setLanduseAround(world, wp, 5, LANDUSE.CBD, 1.0);
setLanduseAround(world, wp, 10, LANDUSE.SUBURB, 0.74);
addInfluence(world, wp, 13, 0.85, [["populationDensity", 1.0], ["settlementScore", 0.9]]);
}
for (const p of localPoints.markets || []) {
const wp = { x: p.worldX, y: p.worldY };
setLanduseAround(world, wp, 4, LANDUSE.OLD_URBAN, 0.86);
addInfluence(world, wp, 8, 0.52, [["populationDensity", 0.7], ["settlementScore", 0.8]]);
}
for (const p of localPoints.ports || []) {
const wp = { x: p.worldX, y: p.worldY };
setLanduseAround(world, wp, p.portClass === "regional" ? 5 : 3, LANDUSE.OLD_URBAN, 0.8);
addInfluence(world, wp, 7, 0.44, [["populationDensity", 0.55], ["settlementScore", 0.55]]);
}
for (const p of localPoints.villages || []) {
const wp = { x: p.worldX, y: p.worldY };
setLanduseAround(world, wp, 2, LANDUSE.FARMLAND, 0.72);
addInfluence(world, wp, 5, 0.28, [["populationDensity", 0.35], ["settlementScore", 0.45], ["villageInfluence", 1.0]]);
}
}
function applyPreservedInfluence(world, sourceMap, rect) {
const expanded = expandRect(rect, 20, world);
let pointsApplied = 0;
const pointConfigs = [
["modernCities", 13, 0.72, [["populationDensity", 1.0], ["settlementScore", 0.8]], LANDUSE.SUBURB],
["satelliteCities", 10, 0.55, [["populationDensity", 0.8], ["settlementScore", 0.65]], LANDUSE.SUBURB],
["markets", 8, 0.42, [["populationDensity", 0.65], ["settlementScore", 0.7]], LANDUSE.OLD_URBAN],
["ports", 7, 0.40, [["populationDensity", 0.55], ["settlementScore", 0.5]], LANDUSE.OLD_URBAN],
["villages", 5, 0.24, [["populationDensity", 0.32], ["settlementScore", 0.38], ["villageInfluence", 0.9]], LANDUSE.FARMLAND],
["stations", 5, 0.30, [["stationInfluence", 1.0], ["populationDensity", 0.20]], LANDUSE.ROADSIDE],
["industrialZones", 6, 0.22, [["populationDensity", 0.18]], LANDUSE.INDUSTRIAL],
["logisticsParks", 5, 0.18, [["roadInfluence", 0.25]], LANDUSE.LOGISTICS],
["newTowns", 8, 0.35, [["populationDensity", 0.85], ["settlementScore", 0.45]], LANDUSE.NEW_TOWN],
];
for (const [key, radius, amount, fields, landuseCode] of pointConfigs) {
for (const p of sourceMap[key] || []) {
if (!p || p.patchHidden) continue;
const point = { x: Math.round(pointWorldX(world, p)), y: Math.round(pointWorldY(world, p)) };
if (!insideRect(point.x, point.y, expanded)) continue;
addInfluence(world, point, radius, amount, fields, rect);
setLanduseAround(world, point, Math.max(2, Math.floor(radius * 0.45)), landuseCode, 0.45, rect);
pointsApplied++;
}
}
return pointsApplied;
}
function applyPreservedPathInfluence(world, sourceMap, rect) {
let roadPaths = 0;
let railPaths = 0;
for (const key of PATH_LAYER_KEYS) {
const isRail = key.includes("Rail") || key.includes("rail");
const influenceKey = isRail ? "railInfluence2" : "roadInfluence";
const radius = isRail ? 2 : 2;
const amount = isRail ? 0.30 : 0.22;
for (const path of sourceMap[key] || []) {
const worldPath = (path || []).map((tuple) => [Math.round(tupleWorldX(world, tuple)), Math.round(tupleWorldY(world, tuple))]);
if (!worldPath.some(([x, y]) => insideRect(x, y, expandRect(rect, 8, world)))) continue;
writePathInfluence(world, worldPath, influenceKey, radius, amount, rect);
if (isRail) railPaths++;
else roadPaths++;
}
}
return { roadPaths, railPaths };
}
function updatePopulationSummary(sourceMap) {
const popArrays = ["modernCities", "markets", "villages", "ports", "satelliteCities", "newTowns"];
let total = 0;
for (const key of popArrays) {
for (const p of sourceMap[key] || []) total += Number.isFinite(p?.population) ? p.population : 0;
}
sourceMap.totalPopulation = Math.max(0, Math.round(total));
}
export function regenerateHumanGeographyPatch(world, rects, options = {}) {
if (!world?.sourceMap || !rects?.repairRect || !rects?.blendRect) {
return { ok: false, reason: "World/source map/patch rects are missing." };
}
const sourceMap = world.sourceMap;
const seed = Number.isFinite(options.seed) ? options.seed >>> 0 : ((world.seed || 0) ^ 0xa511e9b3) >>> 0;
const userRect = rects.writeRect || rects.userRect;
const buildRect = rects.coreRect || rects.blendRect;
const repairRect = rects.repairRect;
const usedNames = usedNameSet(sourceMap);
const preservedExternalEntities = pointLayerPreservedCount(sourceMap, world, userRect) + pathLayerPreservedCount(sourceMap, world, userRect);
for (const [key, fallback] of INT_FIELD_DEFAULTS.entries()) ensureField(world, key, Int32Array, fallback);
ensureField(world, "landuse", Uint8Array, LANDUSE.RURAL);
for (const key of FLOAT_FIELD_KEYS) ensureField(world, key, Float32Array, 0);
const snapshot = snapshotFields(world, repairRect, [...FLOAT_FIELD_KEYS, "landuse", "adminId", "municipalityId"]);
const pointPrune = prunePointLayers(sourceMap, world, rects);
const linePrune = pruneLinearLayers(sourceMap, world, rects);
resetHumanFields(world, repairRect);
assignFarmlandAndForest(world, repairRect);
const preservedInfluencePoints = applyPreservedInfluence(world, sourceMap, repairRect);
const preservedInfluencePaths = applyPreservedPathInfluence(world, sourceMap, repairRect);
const settlements = buildSettlementLayers(world, sourceMap, buildRect, seed, usedNames);
const adminCenters = assignLocalAdmin(world, sourceMap, buildRect, settlements.centers || [], seed, usedNames);
const adminRepair = reassignAdminRepair(world, sourceMap, rects, adminCenters, seed);
const borders = buildAdminBorders(world, repairRect);
if (borders.length) ensureSourceArray(sourceMap, "adminBorders").push(...borders);
applySettlementInfluence(world, settlements.localPoints || {});
const transport = buildTransportLayers(world, sourceMap, repairRect, seed, settlements.localPoints || {}, linePrune);
const development = buildDevelopmentLayers(world, sourceMap, buildRect, seed, settlements.localPoints || {}, usedNames);
const landUseCellsUpdated = reconcileTransitionFields(world, rects, snapshot);
updatePopulationSummary(sourceMap);
const result = {
ok: true,
seed,
removedPoints: pointPrune.removedPoints,
removedLines: linePrune.removedLines,
removedLocalEntities: pointPrune.removedPoints + linePrune.removedLines,
preservedExternalEntities,
roadBoundaryAnchors: linePrune.roadAnchors.length,
railwayBoundaryAnchors: linePrune.railAnchors.length,
invalidPortsRemoved: pointPrune.invalidPortsRemoved,
logisticsLabelsFixed: true,
disconnectedRoadsRailsDetected: linePrune.removedLines,
preservedInfluencePoints,
preservedInfluenceRoads: preservedInfluencePaths.roadPaths,
preservedInfluenceRails: preservedInfluencePaths.railPaths,
landUseCellsUpdated,
...adminRepair,
adminCenters: adminCenters.length,
adminBorders: borders.length,
...settlements.counts,
...transport,
...development,
buildRect: { ...buildRect },
repairRect: { ...repairRect },
userRect: { ...userRect },
};
world.lastHumanPatchResult = result;
world.humanPatchHistory = [...(world.humanPatchHistory || []), { ...result, createdAt: Date.now() }];
return result;
}