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