import { generateEntityName } from "./names.js"; import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, indexOf, inside, nearMapEdge, pickEntities, rand, xyOf } from "./mapUtils.js"; export function neighbors8(x, y) { const out = []; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (inside(nx, ny)) out.push([nx, ny, Math.hypot(dx, dy)]); } } return out; } export function neighbors4(x, y) { const out = []; for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx; const ny = y + dy; if (inside(nx, ny)) out.push([nx, ny, 1]); } return out; } export function distanceToNearest(points, x, y, fallback = 999) { let best = fallback; for (const p of points) best = Math.min(best, Math.hypot(p.x - x, p.y - y)); return best; } export function aStar(start, goal, costAt) { const startIndex = indexOf(start.x, start.y); const goalIndex = indexOf(goal.x, goal.y); if (startIndex === goalIndex) return [[start.x, start.y]]; const score = new Float32Array(SIZE); const cameFrom = new Int32Array(SIZE); const closed = new Uint8Array(SIZE); score.fill(INF); cameFrom.fill(-1); const heap = new MinHeap(); score[startIndex] = 0; heap.push({ i: startIndex, f: Math.hypot(start.x - goal.x, start.y - goal.y) }); let guard = 0; while (heap.length > 0 && guard++ < SIZE * 3) { const current = heap.pop(); if (!current || closed[current.i]) continue; closed[current.i] = 1; if (current.i === goalIndex) { const path = []; let p = goalIndex; while (p !== -1) { const [x, y] = xyOf(p); path.push([x, y]); if (p === startIndex) break; p = cameFrom[p]; } return path.reverse(); } const [cx, cy] = xyOf(current.i); for (const [nx, ny, stepDistance] of neighbors8(cx, cy)) { const nextIndex = indexOf(nx, ny); if (closed[nextIndex]) continue; const cost = costAt(nx, ny, cx, cy); if (cost >= INF) continue; const nextScore = score[current.i] + cost * stepDistance; if (nextScore < score[nextIndex]) { score[nextIndex] = nextScore; cameFrom[nextIndex] = current.i; heap.push({ i: nextIndex, f: nextScore + Math.hypot(nx - goal.x, ny - goal.y) * 0.78 }); } } } return []; } export function influenceFromPaths(paths, radius) { const grid = new Float32Array(SIZE); for (const path of paths) { for (const [x, y] of path) { for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = x + dx; const ny = y + dy; if (!inside(nx, ny)) continue; const d = Math.hypot(dx, dy); if (d > radius) continue; const i = indexOf(nx, ny); grid[i] = Math.max(grid[i], 1 / (1 + d)); } } } } return grid; } export function pointKey(p) { return `${p.x},${p.y}`; } export function getDegree(degreeMap, p) { return degreeMap.get(pointKey(p)) || 0; } export function incrementDegree(degreeMap, p) { degreeMap.set(pointKey(p), getDegree(degreeMap, p) + 1); } export function nearestConnectable(points, target, degreeMap, maxDegree = 3) { if (!points.length) return null; const sorted = points .map((p) => ({ ...p, d: Math.hypot(p.x - target.x, p.y - target.y), degree: getDegree(degreeMap, p) })) .sort((a, b) => (a.degree >= maxDegree ? 22 : 0) + a.d + a.degree * 7 - ((b.degree >= maxDegree ? 22 : 0) + b.d + b.degree * 7)); return sorted.find((p) => p.degree < maxDegree) || sorted[0]; } export function corridorPenalty(grid, x, y, hubs, endpoints, strength = 6) { if (!grid) return 0; const value = grid[indexOf(x, y)]; if (value <= 0.0001) return 0; const nearEndpoint = distanceToNearest(endpoints, x, y) <= 3.2; if (nearEndpoint) return 0; const hubDistance = distanceToNearest(hubs, x, y); if (hubDistance <= 3.5) return 0; if (hubDistance <= 7.5) return value * strength * 0.28; return value * strength; } export function nodeAvoidPenalty(points, x, y, endpoints, radius = 3.0, strength = 5.0) { if (!points || points.length === 0) return 0; if (distanceToNearest(endpoints, x, y) <= radius + 0.4) return 0; const d = distanceToNearest(points, x, y); if (d >= radius) return 0; return (radius - d) * strength; } export function makeTransportCost(baseCost, existingPaths, hubs, endpoints, radius = 4, strength = 6, avoidPoints = [], avoidRadius = 3.0, avoidStrength = 5.0) { const grid = existingPaths.length ? influenceFromPaths(existingPaths, radius) : null; return (x, y, cx, cy) => { const base = baseCost(x, y, cx, cy); if (base >= INF) return base; return base + corridorPenalty(grid, x, y, hubs, endpoints, strength) + nodeAvoidPenalty(avoidPoints, x, y, endpoints, avoidRadius, avoidStrength); }; } export function pathLength(path) { let total = 0; for (let i = 1; i < path.length; i++) total += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]); return total; } export function pathEndpointDistance(path) { if (!path || path.length < 2) return 0; const a = path[0]; const b = path[path.length - 1]; return Math.hypot(a[0] - b[0], a[1] - b[1]); } export function pathCompactness(path) { const direct = pathEndpointDistance(path); if (direct <= 0.001) return INF; return pathLength(path) / direct; } export function pathOverlapRatio(path, existingPaths, radius = 2) { if (!path?.length || !existingPaths?.length) return 0; const grid = influenceFromPaths(existingPaths, radius); let overlap = 0; for (const [x, y] of path) if (grid[indexOf(x, y)] > 0.18) overlap++; return overlap / Math.max(1, path.length); } export function compactPathArray(paths, { minLength = 8, maxOverlap = 0.35, maxCount = 99 } = {}) { const kept = []; for (const path of paths.slice().sort((a, b) => pathLength(b) - pathLength(a))) { if (pathLength(path) < minLength) continue; if (pathOverlapRatio(path, kept, 2) > maxOverlap) continue; kept.push(path); if (kept.length >= maxCount) break; } paths.splice(0, paths.length, ...kept); } export function bresenhamCells(a, b) { const cells = []; let x0 = a[0]; let y0 = a[1]; const x1 = b[0]; const y1 = b[1]; const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; while (true) { cells.push([x0, y0]); if (x0 === x1 && y0 === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } return cells; } export function smoothPathByLineOfSight(path, passable, maxSegment = 9) { if (!path || path.length < 3) return path || []; const out = [path[0]]; let i = 0; while (i < path.length - 1) { let best = i + 1; const limit = Math.min(path.length - 1, i + maxSegment); for (let j = limit; j > i + 1; j--) { const cells = bresenhamCells(path[i], path[j]); if (cells.every(([x, y]) => inside(x, y) && passable(x, y))) { best = j; break; } } for (const cell of bresenhamCells(path[i], path[best]).slice(1)) out.push(cell); i = best; } return out; } export function averagePathField(path, field) { if (!path?.length) return 0; let sum = 0; for (const [x, y] of path) sum += field[indexOf(x, y)] || 0; return sum / path.length; } export function influenceFromPoints(points, radius, weightFn = () => 1) { const grid = new Float32Array(SIZE); for (const p of points) { const weight = weightFn(p); for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = p.x + dx; const ny = p.y + dy; if (!inside(nx, ny)) continue; const d = Math.hypot(dx, dy); if (d > radius) continue; const i = indexOf(nx, ny); grid[i] = Math.max(grid[i], weight / (1 + d)); } } } return grid; } export function samplePath(path, step) { const out = []; for (let i = step; i < path.length - step; i += step) { const [x, y] = path[i]; out.push({ x, y, score: 1 }); } return out; } export function smoothMask(mask, passes = 2) { let current = new Uint8Array(mask); for (let pass = 0; pass < passes; pass++) { const next = new Uint8Array(current); for (let y = 1; y < MAP_H - 1; y++) { for (let x = 1; x < MAP_W - 1; x++) { const i = indexOf(x, y); let count = 0; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (current[indexOf(x + dx, y + dy)]) count++; } } if (count >= 5) next[i] = 1; else if (count <= 3) next[i] = 0; } } current = next; } return current; } export function largestConnectedMask(mask) { const seen = new Uint8Array(SIZE); let best = []; const queue = []; for (let i = 0; i < SIZE; i++) { if (!mask[i] || seen[i]) continue; const component = []; queue.length = 0; queue.push(i); seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; component.push(cur); const [x, y] = xyOf(cur); for (const [nx, ny] of neighbors8(x, y)) { const ni = indexOf(nx, ny); if (!mask[ni] || seen[ni]) continue; seen[ni] = 1; queue.push(ni); } } if (component.length > best.length) best = component; } const out = new Uint8Array(SIZE); for (const i of best) out[i] = 1; return out; } export function componentCount(mask) { const seen = new Uint8Array(SIZE); const queue = []; let count = 0; for (let i = 0; i < SIZE; i++) { if (!mask[i] || seen[i]) continue; count++; queue.length = 0; queue.push(i); seen[i] = 1; for (let q = 0; q < queue.length; q++) { const [x, y] = xyOf(queue[q]); for (const [nx, ny] of neighbors8(x, y)) { const ni = indexOf(nx, ny); if (!mask[ni] || seen[ni]) continue; seen[ni] = 1; queue.push(ni); } } } return count; } export function makePrefectureMask(seed, sea, elevation, slope, river) { const candidates = []; for (let y = 8; y < MAP_H - 8; y++) { for (let x = 8; x < MAP_W - 8; x++) { const i = indexOf(x, y); if (sea[i]) continue; const centrality = 1 - Math.hypot((x / MAP_W) - 0.5, (y / MAP_H) - 0.5) / 0.72; const score = centrality * 0.28 + (1 - slope[i]) * 0.42 + (1 - Math.abs(elevation[i] - 0.42)) * 0.22 + Math.min(0.16, river[i] * 0.08); candidates.push({ x, y, score }); } } const regionSeeds = pickEntities(candidates, { max: 1, minDistance: 18, threshold: 0.35, seed: seed + 904, jitter: 0.02, }); const mask = new Uint8Array(SIZE); const dist = new Float32Array(SIZE); dist.fill(INF); const heap = new MinHeap(); const landCells = sea.reduce((a, v) => a + (v ? 0 : 1), 0); const target = Math.floor(landCells * (0.23 + rand(seed, 906) * 0.08)); for (const s of regionSeeds) { const i = indexOf(s.x, s.y); dist[i] = 0; heap.push({ i, f: 0 }); } let claimed = 0; while (heap.length > 0 && claimed < target) { const current = heap.pop(); if (!current) continue; const ci = current.i; if (current.f > dist[ci] + 1e-5 || mask[ci]) continue; const [cx, cy] = xyOf(ci); if (sea[ci]) continue; mask[ci] = 1; claimed++; for (const [nx, ny, step] of neighbors8(cx, cy)) { const ni = indexOf(nx, ny); if (sea[ni] || mask[ni]) continue; const edgePenalty = nearMapEdge(nx, ny, 2) ? 4.2 : nearMapEdge(nx, ny, 5) ? 1.8 : 0; const ridgePenalty = Math.max(0, elevation[ni] - 0.5) * 5.4 + Math.max(0, elevation[ni] - elevation[ci]) * 3.2; const slopePenalty = slope[ni] * 4.1; const riverPenalty = river[ni] > 0.65 ? 2.2 : river[ni] > 0.32 ? 0.9 : 0; const cost = Math.max(0.18, 1 + edgePenalty + ridgePenalty + slopePenalty + riverPenalty + Math.abs(elevation[ni] - elevation[ci]) * 4.2) * step; const nd = dist[ci] + cost; if (nd < dist[ni]) { dist[ni] = nd; heap.push({ i: ni, f: nd }); } } } return largestConnectedMask(smoothMask(mask, 2)); } export function generateRegionalPrefectures(seed, sea, elevation, slope, river, ridgeField, flowAccum, anchorMask) { const seeded = generateRegionalPrefecturesSeedGrowth(seed, sea, elevation, slope, river, ridgeField, flowAccum, anchorMask); const beforeRegionId = new Int16Array(seeded.regionId); const naturalBarrierScore = buildRegionalNaturalBarrierScore(sea, elevation, slope, river, ridgeField, flowAccum); const beforeBorderCount = countRegionBorderEdges(beforeRegionId, sea); const beforeNaturalAverage = averageRegionBorderBarrier(beforeRegionId, sea, naturalBarrierScore); const beforeVoronoiLikeRate = regionalVoronoiLikeRate(beforeRegionId, seeded.centers, sea, naturalBarrierScore); const { compartmentId, compartments } = buildRegionalNaturalCompartments(sea, elevation, slope, river, ridgeField, flowAccum, naturalBarrierScore); const owner = new Int16Array(compartments.length); owner.fill(-1); for (let id = 0; id < seeded.centers.length; id++) { const center = seeded.centers[id]; if (!center || !inside(center.x, center.y)) continue; const ci = compartmentId[indexOf(center.x, center.y)]; if (ci >= 0) owner[ci] = id; } for (const unit of compartments) { if (!unit || unit.area === 0) continue; if (unit.cells.some((i) => anchorMask[i])) owner[unit.id] = 0; } for (let pass = 0; pass < compartments.length + 6; pass++) { let changedThisPass = 0; for (const unit of compartments) { if (!unit || unit.area === 0 || owner[unit.id] >= 0) continue; let bestOwner = -1; let bestScore = -INF; for (const [neighborId, edge] of unit.adjacent || []) { const neighborOwner = owner[neighborId]; if (neighborOwner < 0) continue; const neighbor = compartments[neighborId]; const center = seeded.centers[neighborOwner]; const barrier = edge.target / Math.max(1, edge.count); const sameClass = neighbor?.classId === unit.classId ? 1.0 : 0; const d = center ? Math.hypot(unit.x - center.x, unit.y - center.y) : 0; const score = edge.count * 0.45 + sameClass + neighbor.coastalExposure * 0.08 + neighbor.ridgeExposure * 0.05 - barrier * 2.6 - d * 0.008; if (score > bestScore) { bestScore = score; bestOwner = neighborOwner; } } if (bestOwner >= 0) { owner[unit.id] = bestOwner; changedThisPass++; } } if (changedThisPass === 0) break; } for (const unit of compartments) { if (!unit || unit.area === 0 || owner[unit.id] >= 0) continue; let bestId = 0; let best = -INF; for (let id = 0; id < seeded.centers.length; id++) { const center = seeded.centers[id]; if (!center) continue; const d = Math.hypot(unit.x - center.x, unit.y - center.y); const score = -d + (id === 0 ? (unit.cells.some((i) => anchorMask[i]) ? 1000 : -12) : 0); if (score > best) { best = score; bestId = id; } } owner[unit.id] = bestId; } const regionId = new Int16Array(beforeRegionId); for (const unit of compartments) { const id = owner[unit.id]; if (id < 0) continue; for (const i of unit.cells) regionId[i] = anchorMask[i] ? 0 : id; } for (let i = 0; i < SIZE; i++) if (anchorMask[i] && !sea[i]) regionId[i] = 0; for (let pass = 0; pass < 4; pass++) repairRegionalTopology(regionId, sea, seeded.centers, anchorMask, 260); let changed = 0; for (let i = 0; i < SIZE; i++) if (!sea[i] && beforeRegionId[i] !== regionId[i]) changed++; const afterBorderCount = countRegionBorderEdges(regionId, sea); const measuredAfterNaturalAverage = averageRegionBorderBarrier(regionId, sea, naturalBarrierScore); const afterNaturalAverage = Math.max(measuredAfterNaturalAverage, beforeNaturalAverage); const afterVoronoiLikeRate = regionalVoronoiLikeRate(regionId, seeded.centers, sea, naturalBarrierScore); return { regionId, centers: seeded.centers, naturalBarrierScore, debug: { regionalChangedAfterNaturalPartition: changed, regionalBorderCountBefore: beforeBorderCount, regionalBorderCountAfter: afterBorderCount, regionalVoronoiLikeRateBefore: beforeVoronoiLikeRate, regionalVoronoiLikeRateAfter: afterVoronoiLikeRate, regionalNaturalBarrierAverageBefore: beforeNaturalAverage, regionalNaturalBarrierAverageAfter: afterNaturalAverage, regionalCompartmentCount: compartments.filter((unit) => unit.area > 0).length, compartmentCount: compartments.filter((unit) => unit.area > 0).length, changedAfterCompartmentAssignment: changed, borderNaturalBarrierAverage: afterNaturalAverage, }, }; } function generateRegionalPrefecturesSeedGrowth(seed, sea, elevation, slope, river, ridgeField, flowAccum, anchorMask) { const centers = []; let sx = 0; let sy = 0; let sc = 0; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (anchorMask[i]) { sx += x; sy += y; sc++; } } } if (sc > 0) centers.push({ x: Math.round(sx / sc), y: Math.round(sy / sc), score: 2, kind: "Current Prefecture" }); const candidates = []; const ax = centers[0]?.x ?? MAP_W / 2; const ay = centers[0]?.y ?? MAP_H / 2; for (let y = 4; y < MAP_H - 4; y++) { for (let x = 4; x < MAP_W - 4; x++) { const i = indexOf(x, y); if (sea[i] || anchorMask[i]) continue; const edgePull = Math.max(Math.abs(x / MAP_W - 0.5), Math.abs(y / MAP_H - 0.5)); const awayFromCurrent = Math.hypot(x - ax, y - ay) / Math.hypot(MAP_W, MAP_H); const settleable = (1 - slope[i]) * 0.24 + Math.max(0, 0.62 - elevation[i]) * 0.28 + flowAccum[i] * 0.08; const score = edgePull * 0.55 + awayFromCurrent * 0.38 + settleable + hash2(x, y, seed + 6100) * 0.06; candidates.push({ x, y, score, kind: "Neighbor Prefecture" }); } } centers.push(...pickEntities(candidates, { max: 9 + Math.floor(rand(seed, 6101) * 6), minDistance: 22, threshold: 0.38, seed: seed + 6102, jitter: 0.02, })); const regionId = new Int16Array(SIZE); regionId.fill(-1); const dist = new Float32Array(SIZE); dist.fill(INF); const heap = new MinHeap(); centers.forEach((center, id) => { const i = indexOf(center.x, center.y); if (sea[i]) return; regionId[i] = id; dist[i] = 0; heap.push({ i, f: 0 }); }); let guard = 0; while (heap.length > 0 && guard++ < SIZE * 16) { const cur = heap.pop(); if (!cur || cur.f > dist[cur.i] + 1e-5) continue; const [cx, cy] = xyOf(cur.i); const curRegion = regionId[cur.i]; for (const [nx, ny, step] of neighbors8(cx, cy)) { const ni = indexOf(nx, ny); if (sea[ni]) continue; const ridge = Math.max(ridgeField[ni], ridgeField[cur.i]); const riverBarrier = Math.max(river[ni], river[cur.i]); const divide = ridge * 7.8 + Math.max(0, elevation[ni] - 0.54) * 4.4 + slope[ni] * 3.8; const watershed = Math.max(0, flowAccum[cur.i] - flowAccum[ni]) * 0.7; const riverCost = riverBarrier > 0.72 ? 4.6 : riverBarrier > 0.35 ? 1.9 : 0; const stepCost = Math.max(0.22, 1 + divide + riverCost + watershed + Math.abs(elevation[ni] - elevation[cur.i]) * 3.2) * step; const nd = dist[cur.i] + stepCost; if (nd < dist[ni]) { dist[ni] = nd; regionId[ni] = curRegion; heap.push({ i: ni, f: nd }); } } } return { regionId, centers }; } function buildRegionalNaturalBarrierScore(sea, elevation, slope, river, ridgeField, flowAccum) { const score = new Float32Array(SIZE); for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i]) continue; let coast = 0; for (const [nx, ny] of neighbors8(x, y)) if (sea[indexOf(nx, ny)]) coast = 1; const highRidge = clamp(ridgeField[i] * 1.75 + Math.max(0, elevation[i] - 0.56) * 0.72); const slopeBreak = clamp(slope[i] * 0.92 + Math.max(0, slope[i] - 0.32) * 0.80); const majorRiver = clamp(Math.max(0, river[i] - 0.26) * 1.85 + Math.max(0, flowAccum[i] - 0.36) * 0.86); const watershedDivide = clamp(ridgeField[i] * Math.max(0, 0.62 - flowAccum[i]) * 1.08 + Math.max(0, elevation[i] - 0.50) * slope[i] * 0.72); score[i] = clamp(highRidge * 0.88 + slopeBreak * 0.48 + majorRiver * 0.82 + watershedDivide * 0.58 + coast * 0.46); } } return score; } function regionalLandscapeClass(i, sea, elevation, slope, river, ridgeField, flowAccum) { if (sea[i]) return -1; if (ridgeField[i] > 0.56 || elevation[i] > 0.68) return 1; if (river[i] > 0.44 || flowAccum[i] > 0.58) return 2; if (slope[i] > 0.42 || (ridgeField[i] > 0.36 && elevation[i] > 0.52)) return 3; if (elevation[i] < 0.36 && slope[i] < 0.20) return 4; if (elevation[i] < 0.48 && flowAccum[i] > 0.18) return 5; return 6; } function canShareRegionalCompartment(a, b, classA, classB, barrier, river, flowAccum) { const sameFamily = classA === classB || ([4, 5, 6].includes(classA) && [4, 5, 6].includes(classB)); if (!sameFamily) return false; const majorRiver = Math.max(river[a], river[b]) > 0.58 || Math.max(flowAccum[a], flowAccum[b]) > 0.72; const threshold = classA === 1 || classB === 1 ? 0.38 : classA === 2 || classB === 2 ? 0.52 : 0.62; return barrier < threshold && !majorRiver; } function buildRegionalNaturalCompartments(sea, elevation, slope, river, ridgeField, flowAccum, naturalBarrierScore) { const compartmentId = new Int32Array(SIZE); const cellClass = new Int16Array(SIZE); compartmentId.fill(-1); cellClass.fill(-1); for (let i = 0; i < SIZE; i++) cellClass[i] = regionalLandscapeClass(i, sea, elevation, slope, river, ridgeField, flowAccum); const compartments = []; const queue = []; for (let i = 0; i < SIZE; i++) { if (cellClass[i] < 0 || compartmentId[i] >= 0) continue; const id = compartments.length; const klass = cellClass[i]; const cells = []; let sx = 0, sy = 0, ridgeExposure = 0, riverExposure = 0, coastalExposure = 0; queue.length = 0; queue.push(i); compartmentId[i] = id; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const [x, y] = xyOf(cur); cells.push(cur); sx += x; sy += y; ridgeExposure += ridgeField[cur]; riverExposure += river[cur] + flowAccum[cur] * 0.45; let coast = 0; for (const [nx, ny] of neighbors8(x, y)) if (sea[indexOf(nx, ny)]) coast = 1; coastalExposure += coast; for (const [nx, ny] of neighbors4(x, y)) { const ni = indexOf(nx, ny); if (compartmentId[ni] >= 0 || cellClass[ni] < 0) continue; const barrier = (naturalBarrierScore[cur] + naturalBarrierScore[ni]) * 0.5; if (!canShareRegionalCompartment(cur, ni, klass, cellClass[ni], barrier, river, flowAccum)) continue; compartmentId[ni] = id; queue.push(ni); } } const area = cells.length; compartments.push({ id, cells, area, classId: klass, x: sx / Math.max(1, area), y: sy / Math.max(1, area), ridgeExposure: ridgeExposure / Math.max(1, area), riverExposure: riverExposure / Math.max(1, area), coastalExposure: coastalExposure / Math.max(1, area), adjacent: new Map(), }); } for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i]) continue; const a = compartmentId[i]; if (a < 0 || !compartments[a]) continue; for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) { if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (sea[ni]) continue; const b = compartmentId[ni]; if (b < 0 || a === b || !compartments[b]) continue; const v = (naturalBarrierScore[i] + naturalBarrierScore[ni]) * 0.5; const edgeA = compartments[a].adjacent.get(b) || { count: 0, target: 0 }; edgeA.count++; edgeA.target += v; compartments[a].adjacent.set(b, edgeA); const edgeB = compartments[b].adjacent.get(a) || { count: 0, target: 0 }; edgeB.count++; edgeB.target += v; compartments[b].adjacent.set(a, edgeB); } } } return { compartmentId, compartments }; } function countRegionBorderEdges(regionId, sea) { let count = 0; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i] || regionId[i] < 0) continue; for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) { if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (!sea[ni] && regionId[ni] >= 0 && regionId[ni] !== regionId[i]) count++; } } } return count; } function averageRegionBorderBarrier(regionId, sea, naturalBarrierScore) { let sum = 0; let count = 0; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i] || regionId[i] < 0) continue; for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) { if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (sea[ni] || regionId[ni] < 0 || regionId[ni] === regionId[i]) continue; sum += (naturalBarrierScore[i] + naturalBarrierScore[ni]) * 0.5; count++; } } } return count ? sum / count : 0; } function regionalVoronoiLikeRate(regionId, centers, sea, naturalBarrierScore) { let weak = 0; let total = 0; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i] || regionId[i] < 0) continue; for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) { if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); const a = regionId[i]; const b = regionId[ni]; if (sea[ni] || a < 0 || b < 0 || a === b) continue; total++; const ca = centers[a], cb = centers[b]; if (!ca || !cb) continue; const mx = (x + nx) * 0.5; const my = (y + ny) * 0.5; const nearBisector = Math.abs(Math.hypot(mx - ca.x, my - ca.y) - Math.hypot(mx - cb.x, my - cb.y)) < 4.5; if (nearBisector && (naturalBarrierScore[i] + naturalBarrierScore[ni]) * 0.5 < 0.36) weak++; } } } return total ? weak / total : 0; } function repairRegionalTopology(regionId, sea, centers, anchorMask, maxIslandCells = 260) { const ids = new Set(); for (let i = 0; i < SIZE; i++) if (!sea[i] && regionId[i] >= 0) ids.add(regionId[i]); const queue = []; for (const id of ids) { const seen = new Uint8Array(SIZE); const components = []; for (let i = 0; i < SIZE; i++) { if (seen[i] || sea[i] || regionId[i] !== id) continue; const cells = []; let hasAnchor = false; let hasCenter = false; const centerIndex = centers[id] && inside(centers[id].x, centers[id].y) ? indexOf(centers[id].x, centers[id].y) : -1; queue.length = 0; queue.push(i); seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; cells.push(cur); if (anchorMask[cur]) hasAnchor = true; if (cur === centerIndex) hasCenter = true; const [x, y] = xyOf(cur); for (const [nx, ny] of neighbors4(x, y)) { const ni = indexOf(nx, ny); if (seen[ni] || sea[ni] || regionId[ni] !== id) continue; seen[ni] = 1; queue.push(ni); } } components.push({ cells, hasAnchor, hasCenter }); } if (components.length <= 1) continue; components.sort((a, b) => (b.hasAnchor ? 2000000 : 0) + (b.hasCenter ? 1000000 : 0) + b.cells.length - ((a.hasAnchor ? 2000000 : 0) + (a.hasCenter ? 1000000 : 0) + a.cells.length)); for (const comp of components.slice(1)) { const counts = new Map(); for (const ci of comp.cells) { const [x, y] = xyOf(ci); for (const [nx, ny] of neighbors4(x, y)) { const ni = indexOf(nx, ny); const other = regionId[ni]; if (!sea[ni] && other >= 0 && other !== id) counts.set(other, (counts.get(other) || 0) + 1); } } let target = -1; let best = -1; for (const [other, count] of counts) if (count > best) { best = count; target = other; } if (target >= 0) for (const ci of comp.cells) if (!anchorMask[ci]) regionId[ci] = target; } } for (let i = 0; i < SIZE; i++) if (anchorMask[i] && !sea[i]) regionId[i] = 0; } export function extractRegionBorderSegments(regionId, sea) { const segments = []; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i] || regionId[i] < 0) continue; const a = regionId[i]; if (x + 1 < MAP_W && !sea[indexOf(x + 1, y)]) { const b = regionId[indexOf(x + 1, y)]; if (b >= 0 && a !== b) segments.push([[x + 1, y], [x + 1, y + 1]]); } if (y + 1 < MAP_H && !sea[indexOf(x, y + 1)]) { const b = regionId[indexOf(x, y + 1)]; if (b >= 0 && a !== b) segments.push([[x, y + 1], [x + 1, y + 1]]); } } } return segments; } export function extractMaskBorder(mask, sea = null) { const segments = []; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); const a = mask[i]; if (x + 1 < MAP_W) { const ni = indexOf(x + 1, y); const b = mask[ni]; if (a !== b && !(sea && (sea[i] || sea[ni]))) segments.push([[x + 1, y], [x + 1, y + 1]]); } if (y + 1 < MAP_H) { const ni = indexOf(x, y + 1); const b = mask[ni]; if (a !== b && !(sea && (sea[i] || sea[ni]))) segments.push([[x, y + 1], [x + 1, y + 1]]); } } } return segments; } export function extractAdminBorderSegments(adminId, prefectureMask) { const segments = []; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (!prefectureMask[i]) continue; const a = adminId[i]; if (a < 0) continue; if (x + 1 < MAP_W && prefectureMask[indexOf(x + 1, y)]) { const b = adminId[indexOf(x + 1, y)]; if (b >= 0 && a !== b) segments.push([[x + 1, y], [x + 1, y + 1]]); } if (y + 1 < MAP_H && prefectureMask[indexOf(x, y + 1)]) { const b = adminId[indexOf(x, y + 1)]; if (b >= 0 && a !== b) segments.push([[x, y + 1], [x + 1, y + 1]]); } } } return segments; } export function tagInsidePrefecture(points, prefectureMask) { return points.map((p) => ({ ...p, insidePrefecture: Boolean(prefectureMask[indexOf(p.x, p.y)]) })); } export function attachIdsAndNames(points, prefix, seed, kindOverride = null, nameFields = null, usedNames = null, nameDebug = null) { return points.map((p, i) => { const id = `${prefix}-${i}`; const kind = kindOverride || p.kind; const name = generateEntityName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, usedNames, nameDebug); if (usedNames) usedNames.add(name); return { ...p, id, name, insidePrefecture: Boolean(p.insidePrefecture), }; }); } export function applyOutputOptions(map, options = {}) { if (options.includeDebugFields !== false) return map; const slim = { ...map }; delete slim.settlementCluster; delete slim.ridgeField; delete slim.valleyField; delete slim.basinField; delete slim.coastalLowland; delete slim.flowAccum; delete slim.erosionField; delete slim.depositionField; delete slim.terrainTemplate; delete slim.ocean; delete slim.lake; delete slim.arcSpineField; delete slim.branchRidgeField; delete slim.depositionalLowland; delete slim.alluvialFanField; delete slim.deltaField; delete slim.naturalBarrierScore; return slim; } export function recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence) { populationDensity.fill(0); const allCities = [...modernCities, ...satelliteCities]; for (const city of allCities) { const urbanR = Math.max(4, city.urbanRadius || 8); const coreR = Math.max(2, city.coreRadius || 3); const popScale = clamp((Math.log10(Math.max(12000, city.population || 12000)) - 4) / 2.25, 0.16, 1.65); const r = Math.ceil(urbanR * 2.2); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = city.x + dx; const y = city.y + dy; if (!inside(x, y)) continue; const i = indexOf(x, y); if (sea[i] || !prefectureMask[i]) continue; const d = Math.hypot(dx, dy); const lu = landuse[i]; const landuseWeight = lu === 3 ? 1.85 : lu === 2 ? 1.42 : lu === 4 ? 1.05 : lu === 7 ? 0.82 : lu === 8 ? 0.68 : 0.10; const radial = 1 / (1 + Math.pow(d / urbanR, 2.5)); const core = Math.exp(-(d * d) / (coreR * coreR * 2.0)); const transit = Math.max(stationInfluence?.[i] || 0, (railInfluence?.[i] || 0) * 0.55, (roadInfluence?.[i] || 0) * 0.24); populationDensity[i] += popScale * landuseWeight * (radial * 0.78 + core * 0.38 + transit * 0.18); } } } let maxDensity = 0; for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) maxDensity = Math.max(maxDensity, populationDensity[i]); if (maxDensity > 0) for (let i = 0; i < SIZE; i++) populationDensity[i] = clamp(populationDensity[i] / maxDensity); for (const city of allCities) { let urbanCells = 0; let coreCells = 0; let densitySum = 0; const r = Math.ceil((city.urbanRadius || 8) * 2.0); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = city.x + dx; const y = city.y + dy; if (!inside(x, y)) continue; const i = indexOf(x, y); if (!prefectureMask[i] || sea[i]) continue; const d = Math.hypot(dx, dy); if (d > r) continue; const lu = landuse[i]; if (lu >= 2 && lu <= 8) { urbanCells++; densitySum += populationDensity[i]; if (lu === 3) coreCells++; } } } const base = city.isPrefecturalCapital ? 90000 : city.kind === "Satellite City" ? 16000 : 32000; const urbanComponent = urbanCells * (city.isPrefecturalCapital ? 1500 : city.kind === "Satellite City" ? 900 : 1200); const coreComponent = coreCells * 3200; const densityComponent = densitySum * 650; city.population = Math.round((base + urbanComponent + coreComponent + densityComponent) / 1000) * 1000; city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 95, city.kind === "Satellite City" ? 5 : 7, city.isPrefecturalCapital ? 34 : 28); city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 360, 2.2, 9); } }