2026-05-21 13:20:19 +09:00
import { createNameDebug } from "./names.js" ;
import { CELL _SIZE , INF , MAP _H , MAP _W , clamp , indexOf , inside , rand } from "./mapUtils.js" ;
2026-05-22 19:28:39 +09:00
import { aStar , applyOutputOptions , attachIdsAndNames , recalculatePopulationAfterLanduse , tagInsidePrefecture } from "./mapGeneratorHelpers.js" ;
2026-05-21 13:20:19 +09:00
export function finishMapOutput ( {
seed ,
options ,
2026-05-21 22:03:14 +09:00
terrainTemplate ,
2026-05-22 14:13:35 +09:00
seaLevel ,
2026-05-21 13:20:19 +09:00
cityPopulationCap ,
stationInfluence ,
roadInfluence ,
railInfluence2 ,
elevation ,
moisture ,
slope ,
sea ,
2026-05-21 22:03:14 +09:00
ocean ,
lake ,
2026-05-21 13:20:19 +09:00
river ,
floodplain ,
plain ,
agriculture ,
settlementCluster ,
ridgeField ,
valleyField ,
basinField ,
coastalLowland ,
flowAccum ,
erosionField ,
depositionField ,
2026-05-21 22:03:14 +09:00
arcSpineField ,
branchRidgeField ,
depositionalLowland ,
alluvialFanField ,
deltaField ,
naturalBarrierScore ,
2026-05-21 13:20:19 +09:00
villages ,
ports ,
crossings ,
passes ,
markets ,
castles ,
castleTowns ,
premodernRoads ,
minorRoads ,
modernCities ,
populationDensity ,
railways ,
branchRailways ,
ringRailways ,
externalRailways ,
stations ,
industrialZones ,
nationalRoads ,
ringRoads ,
expressways ,
ringExpressways ,
icAccessRoads ,
externalRoads ,
externalExpressways ,
interchanges ,
logisticsParks ,
satelliteCities ,
newTowns ,
landuse ,
adminCentersRaw ,
adminId ,
adminBorders ,
adminDebug ,
riverPaths ,
mainRivers ,
tributaryRivers ,
smallStreams ,
externalGateways ,
2026-05-22 02:11:18 +09:00
transportDebug ,
2026-05-21 13:20:19 +09:00
prefectureMask ,
prefectureBorder ,
prefectureRegionId ,
regionalDebug ,
2026-05-22 02:11:18 +09:00
terrainDebug ,
2026-05-21 13:20:19 +09:00
regionalPrefectureBorders ,
} ) {
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
// This keeps population figures proportional to the actually rendered urbanized area.
recalculatePopulationAfterLanduse ( modernCities , satelliteCities , populationDensity , landuse , prefectureMask , sea , stationInfluence , roadInfluence , railInfluence2 ) ;
for ( const city of modernCities ) {
if ( city . isPrefecturalCapital ) continue ;
const cap = cityPopulationCap ( city ) ;
if ( cap < INF && ( city . population || 0 ) > cap ) {
city . population = Math . round ( cap / 1000 ) * 1000 ;
city . urbanRadius = clamp ( 5.0 + Math . sqrt ( city . population ) / 100 , 6 , 16 ) ;
city . coreRadius = clamp ( 1.8 + Math . sqrt ( city . population ) / 400 , 2.2 , 5.2 ) ;
city . urbanWeight = clamp ( 0.72 + Math . log10 ( Math . max ( 10000 , city . population ) ) * 0.30 , 1.0 , 2.0 ) ;
}
}
function makeHarborWorks ( ports ) {
const out = [ ] ;
for ( const port of ports ) {
const parts = [ ] ;
const limit = port . portClass === "major" ? 5 : port . portClass === "regional" ? 3 : 1 ;
for ( const [ dx , dy ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] , [ 1 , 1 ] , [ - 1 , 1 ] , [ 1 , - 1 ] , [ - 1 , - 1 ] ] ) {
const sx = port . x + dx ;
const sy = port . y + dy ;
if ( ! inside ( sx , sy ) || ! sea [ indexOf ( sx , sy ) ] ) continue ;
parts . push ( [ [ port . x , port . y ] , [ sx , sy ] ] ) ;
const wx = sx + dx ;
const wy = sy + dy ;
if ( port . portClass === "major" && inside ( wx , wy ) && sea [ indexOf ( wx , wy ) ] && rand ( seed , sx * 101 + sy * 103 ) > 0.22 ) parts . push ( [ [ sx , sy ] , [ wx , wy ] ] ) ;
if ( parts . length >= limit ) break ;
}
if ( parts . length ) out . push ( { port , segments : parts , kind : port . portClass === "major" ? "Major Harbor Works" : "Harbor Works" } ) ;
}
return out ;
}
2026-05-22 19:28:39 +09:00
function generatedNameStem ( fullName ) {
return String ( fullName || "" ) . replace ( /[都道府県市町村区]$/u , "" ) ;
}
function terrainSettlementScore ( x , y , sideBias = null ) {
const i = indexOf ( x , y ) ;
if ( ! inside ( x , y ) || sea [ i ] ) return - INF ;
const edgeBias = sideBias === "north" ? ( MAP _H - y ) / MAP _H
: sideBias === "south" ? y / MAP _H
: sideBias === "west" ? ( MAP _W - x ) / MAP _W
: sideBias === "east" ? x / MAP _W
: 0 ;
return settlementCluster [ i ] * 0.55 + plain [ i ] * 0.42 + agriculture [ i ] * 0.22 + basinField [ i ] * 0.20 + coastalLowland [ i ] * 0.20 + valleyField [ i ] * 0.16 + edgeBias * 0.08 - slope [ i ] * 0.54 - ridgeField [ i ] * 0.22 - Math . max ( 0 , elevation [ i ] - 0.62 ) * 1.25 ;
}
function chooseSpacedPoints ( candidates , count , minDistance , seedOffset = 0 ) {
const selected = [ ] ;
const ordered = candidates
. map ( ( p , n ) => ( { ... p , score : ( p . score || 0 ) + rand ( seed , seedOffset + n * 17 + p . x * 5 + p . y * 7 ) * 0.06 } ) )
. sort ( ( a , b ) => b . score - a . score ) ;
for ( const p of ordered ) {
if ( selected . some ( ( q ) => Math . hypot ( q . x - p . x , q . y - p . y ) < minDistance ) ) continue ;
selected . push ( p ) ;
if ( selected . length >= count ) break ;
}
return selected ;
}
function labelCollisionScore ( x , y , avoidPoints ) {
let nearest = 99 ;
for ( const p of avoidPoints ) {
if ( ! p ) continue ;
const d = Math . hypot ( x - p . x , y - p . y ) ;
nearest = Math . min ( nearest , d ) ;
}
return nearest ;
}
const MAX _BRIDGE _CELLS = 7 ;
function pointPair ( p ) {
if ( Array . isArray ( p ) ) return [ p [ 0 ] , p [ 1 ] ] ;
return [ p . x , p . y ] ;
}
function sampledWaterRunBetween ( a , b ) {
if ( ! a || ! b ) return 0 ;
const [ ax , ay ] = pointPair ( a ) ;
const [ bx , by ] = pointPair ( b ) ;
const steps = Math . max ( 1 , Math . ceil ( Math . hypot ( bx - ax , by - ay ) * 1.6 ) ) ;
let run = 0 ;
let maxRun = 0 ;
for ( let k = 0 ; k <= steps ; k ++ ) {
const t = k / steps ;
const x = clamp ( Math . round ( ax + ( bx - ax ) * t ) , 0 , MAP _W - 1 ) ;
const y = clamp ( Math . round ( ay + ( by - ay ) * t ) , 0 , MAP _H - 1 ) ;
const isWater = sea [ indexOf ( x , y ) ] ;
if ( isWater ) {
run ++ ;
maxRun = Math . max ( maxRun , run ) ;
} else {
run = 0 ;
}
}
return maxRun ;
}
function pathMaxWaterRun ( path ) {
if ( ! path || path . length < 2 ) return 0 ;
let maxRun = 0 ;
for ( let k = 1 ; k < path . length ; k ++ ) {
maxRun = Math . max ( maxRun , sampledWaterRunBetween ( path [ k - 1 ] , path [ k ] ) ) ;
}
return maxRun ;
}
function landDetourCost ( x , y ) {
if ( ! inside ( x , y ) ) return INF ;
const i = indexOf ( x , y ) ;
if ( sea [ i ] ) return INF ;
return Math . max (
0.35 ,
1 +
slope [ i ] * 7.6 +
Math . max ( 0 , elevation [ i ] - 0.58 ) * 10.5 +
ridgeField [ i ] * 2.2 -
plain [ i ] * 0.45 -
valleyField [ i ] * 0.54 -
coastalLowland [ i ] * 0.38
) ;
}
function compactOutputPath ( path ) {
const out = [ ] ;
let last = "" ;
for ( const p of path || [ ] ) {
const [ x , y ] = pointPair ( p ) ;
const key = ` ${ x } , ${ y } ` ;
if ( key === last ) continue ;
last = key ;
out . push ( [ x , y ] ) ;
}
return out ;
}
function repairLongBridgeSegments ( path ) {
const compact = compactOutputPath ( path ) ;
if ( compact . length < 2 ) return compact . length >= 3 ? compact : [ ] ;
const out = [ compact [ 0 ] ] ;
for ( let k = 1 ; k < compact . length ; k ++ ) {
const from = out [ out . length - 1 ] ;
const to = compact [ k ] ;
if ( sampledWaterRunBetween ( from , to ) <= MAX _BRIDGE _CELLS ) {
out . push ( to ) ;
continue ;
}
const detour = compactOutputPath ( aStar ( { x : from [ 0 ] , y : from [ 1 ] } , { x : to [ 0 ] , y : to [ 1 ] } , landDetourCost ) ) ;
if ( detour . length >= 2 && pathMaxWaterRun ( detour ) <= MAX _BRIDGE _CELLS ) {
out . push ( ... detour . slice ( 1 ) ) ;
} else {
return [ ] ;
}
}
const repaired = compactOutputPath ( out ) ;
return repaired . length >= 3 ? repaired : [ ] ;
}
function enforceBridgeLimitList ( paths ) {
return ( paths || [ ] )
. map ( ( path ) => pathMaxWaterRun ( path ) > MAX _BRIDGE _CELLS ? repairLongBridgeSegments ( path ) : compactOutputPath ( path ) )
. filter ( ( path ) => path && path . length >= 3 && pathMaxWaterRun ( path ) <= MAX _BRIDGE _CELLS ) ;
}
function shoreContactScore ( x , y ) {
let contacts = 0 ;
for ( let dy = - 2 ; dy <= 2 ; dy ++ ) {
for ( let dx = - 2 ; dx <= 2 ; dx ++ ) {
if ( ! dx && ! dy ) continue ;
const nx = x + dx ;
const ny = y + dy ;
if ( ! inside ( nx , ny ) ) continue ;
if ( ! sea [ indexOf ( nx , ny ) ] ) contacts ++ ;
}
}
return contacts ;
}
function labelCandidateScore ( x , y , avoidPoints , fallback , capital , seedOffset , preferSea = false ) {
const i = indexOf ( x , y ) ;
const nearest = labelCollisionScore ( x , y , avoidPoints ) ;
const capD = capital ? Math . hypot ( x - capital . x , y - capital . y ) : 24 ;
const fallbackD = fallback ? Math . hypot ( x - fallback . x , y - fallback . y ) : 0 ;
const terrainBias = sea [ i ]
? 8 + Math . min ( 18 , shoreContactScore ( x , y ) ) * 0.45
: plain [ i ] * 0.18 + basinField [ i ] * 0.10 + coastalLowland [ i ] * 0.10 - slope [ i ] * 0.08 ;
const clearance = Math . min ( nearest , 24 ) * 1.55 - Math . max ( 0 , 8 - nearest ) * 5.0 ;
const capitalBias = capital ? - Math . max ( 0 , capD - ( preferSea ? 26 : 45 ) ) * 0.09 : 0 ;
return clearance + terrainBias + capitalBias - fallbackD * 0.012 + rand ( seed , seedOffset + x * 17 + y * 19 ) * 0.42 ;
}
function pickPrefectureLabelPosition ( avoidPoints , fallback , options = { } ) {
const {
capital = null ,
landPredicate = ( x , y , i ) => prefectureMask [ i ] && ! sea [ i ] ,
areaPredicate = null ,
seedOffset = 7461 ,
} = options ;
let best = null ;
let bestScore = - INF ;
// Prefer open water near the prefectural capital when it reads like a coastal prefecture label.
if ( capital ) {
const rMax = 30 ;
for ( let dy = - rMax ; dy <= rMax ; dy += 2 ) {
for ( let dx = - rMax ; dx <= rMax ; dx += 2 ) {
const x = clamp ( Math . round ( capital . x + dx ) , 0 , MAP _W - 1 ) ;
const y = clamp ( Math . round ( capital . y + dy ) , 0 , MAP _H - 1 ) ;
if ( x < 6 || y < 6 || x > MAP _W - 7 || y > MAP _H - 7 ) continue ;
const i = indexOf ( x , y ) ;
if ( ! sea [ i ] ) continue ;
if ( areaPredicate && ! areaPredicate ( x , y ) ) continue ;
const capD = Math . hypot ( x - capital . x , y - capital . y ) ;
if ( capD < 5 || capD > rMax ) continue ;
const coastTouch = shoreContactScore ( x , y ) ;
if ( coastTouch < 3 || coastTouch > 20 ) continue ;
const score = labelCandidateScore ( x , y , avoidPoints , fallback , capital , seedOffset + 3000 , true ) + coastTouch * 0.35 ;
if ( score > bestScore ) { bestScore = score ; best = { x , y , placement : "sea" } ; }
}
}
if ( best && bestScore >= 18 ) return best ;
}
best = null ;
bestScore = - INF ;
for ( let y = 6 ; y < MAP _H - 6 ; y += 3 ) {
for ( let x = 6 ; x < MAP _W - 6 ; x += 3 ) {
const i = indexOf ( x , y ) ;
if ( ! landPredicate ( x , y , i ) ) continue ;
const score = labelCandidateScore ( x , y , avoidPoints , fallback , capital , seedOffset , false ) ;
if ( score > bestScore ) { bestScore = score ; best = { x , y , placement : "land" } ; }
}
}
if ( ! best || bestScore < 10 ) return fallback ;
return best ;
}
function looseOutsidePath ( a , b , salt , keepOutside = true ) {
if ( ! a || ! b ) return [ ] ;
const steps = Math . max ( 4 , Math . ceil ( Math . max ( Math . abs ( a . x - b . x ) , Math . abs ( a . y - b . y ) ) ) ) ;
const path = [ ] ;
let lastKey = "" ;
const bend = ( rand ( seed , 7600 + salt ) - 0.5 ) * 5.5 ;
const sideways = Math . abs ( a . x - b . x ) > Math . abs ( a . y - b . y ) ? "y" : "x" ;
for ( let k = 0 ; k <= steps ; k ++ ) {
const t = k / steps ;
let x = Math . round ( a . x + ( b . x - a . x ) * t ) ;
let y = Math . round ( a . y + ( b . y - a . y ) * t ) ;
const wave = Math . sin ( t * Math . PI ) * bend ;
if ( sideways === "y" ) y = Math . round ( y + wave ) ;
else x = Math . round ( x + wave ) ;
x = clamp ( x , 0 , MAP _W - 1 ) ;
y = clamp ( y , 0 , MAP _H - 1 ) ;
const i = indexOf ( x , y ) ;
if ( sea [ i ] ) continue ;
if ( keepOutside && prefectureMask [ i ] ) continue ;
const key = ` ${ x } , ${ y } ` ;
if ( key === lastKey ) continue ;
lastKey = key ;
path . push ( [ x , y ] ) ;
}
return path . length >= 3 ? repairLongBridgeSegments ( path ) : [ ] ;
}
function generatePrefectureIdentity ( usedNamesForIdentity , avoidPoints ) {
const stemsA = [ "青" , "白" , "黒" , "高" , "奥" , "新" , "東" , "西" , "南" , "北" , "中" , "美" , "豊" , "若" , "真" , "清" , "瑞" , "長" , "久" , "安" , "阿" , "葛" , "榛" , "碓" , "那" , "鹿" , "宇" , "志" , "遠" , "羽" ] ;
const stemsB = [ "森" , "川" , "野" , "原" , "沢" , "島" , "浦" , "浜" , "海" , "山" , "岳" , "谷" , "津" , "崎" , "里" , "畑" , "橋" , "瀬" , "井" , "沼" , "丘" , "郷" , "城" , "坂" , "泊" , "戸" , "湊" , "庄" ] ;
const suffixes = [ "県" , "県" , "県" , "県" , "県" , "府" ] ;
const localUsed = new Set ( usedNamesForIdentity || [ ] ) ;
function pickGeneratedName ( offset ) {
const a = stemsA [ Math . floor ( rand ( seed , 7410 + offset * 11 ) * stemsA . length ) % stemsA . length ] ;
const b = stemsB [ Math . floor ( rand ( seed , 7420 + offset * 13 ) * stemsB . length ) % stemsB . length ] ;
const c = suffixes [ Math . floor ( rand ( seed , 7430 + offset * 17 ) * suffixes . length ) % suffixes . length ] ;
return ` ${ a } ${ b } ${ c } ` ;
}
function pickUniqueName ( offset , preferred = null ) {
let candidate = preferred || pickGeneratedName ( offset ) ;
let guard = 0 ;
while ( localUsed . has ( candidate ) && guard ++ < 24 ) candidate = pickGeneratedName ( offset + guard + 3 ) ;
localUsed . add ( candidate ) ;
if ( usedNamesForIdentity ) usedNamesForIdentity . add ( candidate ) ;
return candidate ;
}
const capital = modernCities . find ( ( city ) => city . isPrefecturalCapital && prefectureMask [ indexOf ( city . x , city . y ) ] ) ;
const capStem = generatedNameStem ( capital ? . name ) ;
const prefectureSuffix = suffixes [ Math . floor ( rand ( seed , 7433 ) * suffixes . length ) % suffixes . length ] ;
const capitalBased = capStem && rand ( seed , 7440 ) < 0.80 ? ` ${ capStem } ${ prefectureSuffix } ` : null ;
const name = pickUniqueName ( 0 , capitalBased ) ;
let sx = 0 , sy = 0 , n = 0 ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( ! prefectureMask [ i ] || sea [ i ] ) continue ;
sx += x ; sy += y ; n ++ ;
}
}
const fallback = { x : n ? sx / n : MAP _W / 2 , y : n ? sy / n : MAP _H / 2 } ;
const labelPoint = pickPrefectureLabelPosition ( avoidPoints , fallback , {
capital ,
landPredicate : ( x , y , i ) => prefectureMask [ i ] && ! sea [ i ] ,
seedOffset : 7461 ,
} ) ;
const label = { name , x : labelPoint . x , y : labelPoint . y , kind : "Prefecture Label" , placement : labelPoint . placement || "land" } ;
const sideSamples = { north : [ ] , south : [ ] , west : [ ] , east : [ ] } ;
for ( let x = 2 ; x < MAP _W - 2 ; x += 4 ) {
for ( let y = 0 ; y < Math . min ( 13 , MAP _H ) ; y ++ ) if ( inside ( x , y ) && ! prefectureMask [ indexOf ( x , y ) ] && ! sea [ indexOf ( x , y ) ] ) { sideSamples . north . push ( { x , y : 3 } ) ; break ; }
for ( let y = MAP _H - 1 ; y >= Math . max ( 0 , MAP _H - 13 ) ; y -- ) if ( inside ( x , y ) && ! prefectureMask [ indexOf ( x , y ) ] && ! sea [ indexOf ( x , y ) ] ) { sideSamples . south . push ( { x , y : MAP _H - 4 } ) ; break ; }
}
for ( let y = 2 ; y < MAP _H - 2 ; y += 4 ) {
for ( let x = 0 ; x < Math . min ( 13 , MAP _W ) ; x ++ ) if ( inside ( x , y ) && ! prefectureMask [ indexOf ( x , y ) ] && ! sea [ indexOf ( x , y ) ] ) { sideSamples . west . push ( { x : 3 , y } ) ; break ; }
for ( let x = MAP _W - 1 ; x >= Math . max ( 0 , MAP _W - 13 ) ; x -- ) if ( inside ( x , y ) && ! prefectureMask [ indexOf ( x , y ) ] && ! sea [ indexOf ( x , y ) ] ) { sideSamples . east . push ( { x : MAP _W - 4 , y } ) ; break ; }
}
const neighbors = [ ] ;
const sideOrder = [ "north" , "east" , "south" , "west" ] ;
for ( let si = 0 ; si < sideOrder . length ; si ++ ) {
const side = sideOrder [ si ] ;
const samples = sideSamples [ side ] ;
if ( ! samples . length && rand ( seed , 7500 + si ) < 0.45 ) continue ;
const nm = pickUniqueName ( si + 1 ) ;
const p = samples . length ? samples [ Math . floor ( rand ( seed , 7520 + si ) * samples . length ) % samples . length ] : (
side === "north" ? { x : MAP _W * 0.5 , y : 3 } : side === "south" ? { x : MAP _W * 0.5 , y : MAP _H - 4 } : side === "west" ? { x : 3 , y : MAP _H * 0.5 } : { x : MAP _W - 4 , y : MAP _H * 0.5 }
) ;
neighbors . push ( { name : nm , side , x : p . x , y : p . y , kind : "Neighbor Prefecture Label" } ) ;
}
return { name , label , neighbors } ;
}
function generateNeighborPrefectureDetails ( neighborLabels , usedNamesForNeighbor , nameDebugForNeighbor ) {
const allCities = [ ] ;
const allAdmins = [ ] ;
const allCbds = [ ] ;
const allRoads = [ ] ;
const allRailways = [ ] ;
const details = [ ] ;
const sideBand = {
north : ( x , y ) => y <= Math . floor ( MAP _H * 0.30 ) ,
south : ( x , y ) => y >= Math . ceil ( MAP _H * 0.70 ) ,
west : ( x , y ) => x <= Math . floor ( MAP _W * 0.34 ) ,
east : ( x , y ) => x >= Math . ceil ( MAP _W * 0.66 ) ,
} ;
for ( let ni = 0 ; ni < neighborLabels . length ; ni ++ ) {
const label = neighborLabels [ ni ] ;
const predicate = sideBand [ label . side ] || ( ( ) => true ) ;
const candidates = [ ] ;
for ( let y = 2 ; y < MAP _H - 2 ; y += 2 ) {
for ( let x = 2 ; x < MAP _W - 2 ; x += 2 ) {
const i = indexOf ( x , y ) ;
if ( prefectureMask [ i ] || sea [ i ] || ! predicate ( x , y ) ) continue ;
const dToLabel = Math . hypot ( x - label . x , y - label . y ) ;
const borderAway = label . side === "north" ? y : label . side === "south" ? MAP _H - 1 - y : label . side === "west" ? x : MAP _W - 1 - x ;
const score = terrainSettlementScore ( x , y , label . side ) - dToLabel * 0.006 + Math . min ( 16 , borderAway ) * 0.006 ;
if ( score > 0.06 ) candidates . push ( { x , y , score , neighborIndex : ni , neighborName : label . name , side : label . side } ) ;
}
}
const rawCities = chooseSpacedPoints ( candidates , 2 + Math . floor ( rand ( seed , 7700 + ni ) * 3 ) , 12 , 7710 + ni * 100 )
. map ( ( p , n ) => {
const rank = n === 0 ? "Neighbor Prefectural Capital" : n === 1 ? "Neighbor Regional Center" : "Neighbor City" ;
const popBase = n === 0 ? 240000 : n === 1 ? 90000 : 36000 ;
const popSpread = n === 0 ? 620000 : n === 1 ? 220000 : 90000 ;
const population = Math . round ( ( popBase + popSpread * Math . pow ( clamp ( p . score + rand ( seed , 7730 + ni * 31 + n ) , 0 , 1 ) , 1.8 ) ) / 1000 ) * 1000 ;
return { ... p , kind : rank , rank , population , urbanRadius : clamp ( 7 + Math . sqrt ( population ) / 105 , 7 , 21 ) , coreRadius : clamp ( 2.4 + Math . sqrt ( population ) / 420 , 2.4 , 6.2 ) , urbanWeight : 1.0 + Math . log10 ( Math . max ( 10000 , population ) ) * 0.23 , insidePrefecture : false } ;
} ) ;
const namedCities = attachIdsAndNames ( rawCities , ` neighborCity ${ ni } ` , seed + ni * 100 , null , nameFields , usedNamesForNeighbor , nameDebugForNeighbor ) ;
const adminCandidates = chooseSpacedPoints ( candidates . filter ( ( p ) => ! namedCities . some ( ( c ) => Math . hypot ( c . x - p . x , c . y - p . y ) < 7 ) ) , 4 + Math . floor ( rand ( seed , 7760 + ni ) * 5 ) , 8 , 7770 + ni * 100 )
. map ( ( p ) => ( { ... p , kind : "Neighbor Municipal Center" , insidePrefecture : false } ) ) ;
const namedAdmins = attachIdsAndNames ( adminCandidates , ` neighborAdmin ${ ni } ` , seed + ni * 131 , "Neighbor Municipal Center" , nameFields , usedNamesForNeighbor , nameDebugForNeighbor ) ;
const cbds = namedCities . map ( ( city , ci ) => ( { x : city . x , y : city . y , parentId : city . id , parentName : city . name , name : ` ${ city . name } CBD ` , kind : ci === 0 ? "Neighbor Central Business District" : "Neighbor Urban Center" , neighborIndex : ni , neighborName : label . name , insidePrefecture : false } ) ) ;
const nodes = [ ... namedCities , ... namedAdmins ] . sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) || ( b . score || 0 ) - ( a . score || 0 ) ) ;
const roads = [ ] ;
const rails = [ ] ;
for ( let i = 1 ; i < nodes . length ; i ++ ) {
const target = nodes [ i ] ;
const anchor = nodes . 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 road = looseOutsidePath ( anchor , target , ni * 400 + i * 17 , true ) ;
if ( road . length >= 3 ) roads . push ( road ) ;
if ( i <= 2 && rand ( seed , 7790 + ni * 19 + i ) > 0.30 ) {
const rail = looseOutsidePath ( anchor , target , ni * 500 + i * 23 , true ) ;
if ( rail . length >= 4 ) rails . push ( rail ) ;
}
}
if ( nodes [ 0 ] ) {
const gate = { x : label . x , y : label . y } ;
const gatewayRoad = looseOutsidePath ( nodes [ 0 ] , gate , ni * 600 + 7 , true ) ;
if ( gatewayRoad . length >= 3 ) roads . push ( gatewayRoad ) ;
}
const labelAvoid = [ ... namedCities , ... namedAdmins , ... allCities , ... allAdmins ] ;
const labelCandidates = candidates . length ? candidates : [ { x : label . x , y : label . y , score : 0.1 } ] ;
const fallbackLabel = chooseSpacedPoints ( labelCandidates , 1 , 1 , 7810 + ni ) [ 0 ] || label ;
const capitalPoint = namedCities [ 0 ] || fallbackLabel ;
const betterLabel = pickPrefectureLabelPosition ( labelAvoid , fallbackLabel , {
capital : capitalPoint ,
landPredicate : ( x , y , i ) => ! prefectureMask [ i ] && ! sea [ i ] && predicate ( x , y ) ,
areaPredicate : predicate ,
seedOffset : 7810 + ni * 97 ,
} ) || fallbackLabel ;
label . x = betterLabel . x ;
label . y = betterLabel . y ;
label . placement = betterLabel . placement || "land" ;
allCities . push ( ... namedCities ) ;
allAdmins . push ( ... namedAdmins ) ;
allCbds . push ( ... cbds ) ;
allRoads . push ( ... roads ) ;
allRailways . push ( ... rails ) ;
details . push ( { ... label , cities : namedCities , adminCenters : namedAdmins , centralBusinessDistricts : cbds , roads , railways : rails } ) ;
}
return { prefectures : details , cities : allCities , adminCenters : allAdmins , centralBusinessDistricts : allCbds , roads : allRoads , railways : allRailways } ;
}
2026-05-21 13:20:19 +09:00
// Bridge and tunnel icon systems were removed from the visual model.
// Arrays remain empty for backward-compatible tests and downstream code.
const bridges = [ ] ;
const tunnels = [ ] ;
const harborWorks = makeHarborWorks ( ports ) ;
2026-05-22 19:28:39 +09:00
let abandonedRailways = branchRailways . filter ( ( _ , i ) => i % 3 === 0 ) ;
2026-05-21 13:20:19 +09:00
let castleRuins = castles . filter ( ( _ , i ) => i % 2 === 1 ) . map ( ( c ) => ( { ... c , kind : "Castle Ruins" } ) ) ;
2026-05-22 19:28:39 +09:00
let preservedOldRoads = premodernRoads . filter ( ( _ , i ) => i % 2 === 0 ) ;
2026-05-21 13:20:19 +09:00
const nameFields = { elevation , slope , sea , river , plain , agriculture , ridgeField , valleyField , basinField , coastalLowland , flowAccum , landuse , populationDensity } ;
const usedNames = new Set ( ) ;
const nameDebug = createNameDebug ( ) ;
villages = attachIdsAndNames ( tagInsidePrefecture ( villages , prefectureMask ) , "village" , seed , null , nameFields , usedNames , nameDebug ) ;
ports = attachIdsAndNames ( tagInsidePrefecture ( ports , prefectureMask ) , "port" , seed , null , nameFields , usedNames , nameDebug ) ;
crossings = attachIdsAndNames ( tagInsidePrefecture ( crossings , prefectureMask ) , "crossing" , seed , null , nameFields , usedNames , nameDebug ) ;
passes = attachIdsAndNames ( tagInsidePrefecture ( passes , prefectureMask ) , "pass" , seed , null , nameFields , usedNames , nameDebug ) ;
markets = attachIdsAndNames ( tagInsidePrefecture ( markets , prefectureMask ) , "market" , seed , null , nameFields , usedNames , nameDebug ) ;
castles = attachIdsAndNames ( tagInsidePrefecture ( castles , prefectureMask ) , "castle" , seed , null , nameFields , usedNames , nameDebug ) ;
castleTowns = attachIdsAndNames ( tagInsidePrefecture ( castleTowns , prefectureMask ) , "castleTown" , seed , null , nameFields , usedNames , nameDebug ) ;
modernCities = attachIdsAndNames ( tagInsidePrefecture ( modernCities , prefectureMask ) , "city" , seed , null , nameFields , usedNames , nameDebug ) ;
stations = attachIdsAndNames ( tagInsidePrefecture ( stations , prefectureMask ) , "station" , seed , null , nameFields , usedNames , nameDebug ) ;
industrialZones = attachIdsAndNames ( tagInsidePrefecture ( industrialZones , prefectureMask ) , "industrial" , seed , null , nameFields , usedNames , nameDebug ) ;
interchanges = attachIdsAndNames ( tagInsidePrefecture ( interchanges , prefectureMask ) , "interchange" , seed , null , nameFields , usedNames , nameDebug ) ;
logisticsParks = attachIdsAndNames ( tagInsidePrefecture ( logisticsParks , prefectureMask ) , "logistics" , seed , null , nameFields , usedNames , nameDebug ) ;
satelliteCities = attachIdsAndNames ( tagInsidePrefecture ( satelliteCities , prefectureMask ) , "satellite" , seed , null , nameFields , usedNames , nameDebug ) ;
newTowns = attachIdsAndNames ( tagInsidePrefecture ( newTowns , prefectureMask ) , "newtown" , seed , null , nameFields , usedNames , nameDebug ) ;
castleRuins = attachIdsAndNames ( tagInsidePrefecture ( castleRuins , prefectureMask ) , "castleRuin" , seed , null , nameFields , usedNames , nameDebug ) ;
externalGateways = attachIdsAndNames ( tagInsidePrefecture ( externalGateways , prefectureMask ) , "gateway" , seed , "External Gateway" , nameFields , usedNames , nameDebug ) ;
const adminCenters = attachIdsAndNames ( tagInsidePrefecture ( adminCentersRaw , prefectureMask ) , "admin" , seed , "Municipal Center" , nameFields , usedNames , nameDebug ) ;
2026-05-21 22:03:14 +09:00
const representativeFeatures = [
... modernCities . map ( ( p ) => ( { ... p , representativeWeight : 5.0 + ( p . population || 0 ) / 180000 } ) ) ,
... markets . map ( ( p ) => ( { ... p , representativeWeight : 3.2 } ) ) ,
... ports . map ( ( p ) => ( { ... p , representativeWeight : p . portClass === "major" ? 3.8 : 2.4 } ) ) ,
... villages . map ( ( p ) => ( { ... p , representativeWeight : 1.6 } ) ) ,
] . filter ( ( p ) => p . insidePrefecture && p . name ) ;
for ( const center of adminCenters ) {
const centerAdmin = adminId ? . [ indexOf ( center . x , center . y ) ] ;
let best = null ;
let bestScore = - INF ;
for ( const sameAdminOnly of [ true , false ] ) {
for ( const feature of representativeFeatures ) {
const featureAdmin = adminId ? . [ indexOf ( feature . x , feature . y ) ] ;
if ( sameAdminOnly && centerAdmin >= 0 && featureAdmin >= 0 && featureAdmin !== centerAdmin ) continue ;
const d = Math . hypot ( center . x - feature . x , center . y - feature . y ) ;
const score = feature . representativeWeight - d * 0.11 - ( sameAdminOnly ? 0 : 1.2 ) ;
if ( score > bestScore ) {
bestScore = score ;
best = feature ;
}
}
if ( best ) break ;
}
if ( best ) {
center . representativeFeatureId = best . id ;
center . representativeFeatureName = best . name ;
center . generatedMunicipalityName = center . name ;
center . name = best . name ;
}
}
const usedAdminNames = new Set ( ) ;
for ( const center of adminCenters ) {
let candidate = center . name ;
2026-05-22 02:11:18 +09:00
const generated = String ( center . generatedMunicipalityName || "" ) ;
if ( usedAdminNames . has ( candidate ) && Array . from ( generated ) . length >= 2 && ! usedAdminNames . has ( generated ) ) {
candidate = generated ;
2026-05-21 22:03:14 +09:00
}
center . name = candidate ;
usedAdminNames . add ( center . name ) ;
}
2026-05-22 13:57:52 +09:00
nameDebug . maxDerivedPerBase = 0 ;
2026-05-21 13:20:19 +09:00
const entitiesForNames = [
... modernCities ,
... ports ,
... markets ,
... castles ,
... stations ,
... industrialZones ,
... interchanges ,
... logisticsParks ,
... satelliteCities ,
... newTowns ,
... passes ,
... crossings ,
2026-05-21 22:03:14 +09:00
... adminCenters ,
2026-05-21 13:20:19 +09:00
... externalGateways ,
] . filter ( ( p ) => p . insidePrefecture || p . kind === "External Gateway" ) ;
2026-05-22 19:28:39 +09:00
const avoidForPrefectureLabel = [
... modernCities ,
... ports ,
... markets ,
... castles ,
... castleTowns ,
... adminCenters ,
... stations ,
... satelliteCities ,
... newTowns ,
] . filter ( ( p ) => p . insidePrefecture && p . name ) ;
const prefectureIdentity = generatePrefectureIdentity ( usedNames , avoidForPrefectureLabel ) ;
const neighborPrefectureDetails = generateNeighborPrefectureDetails ( prefectureIdentity . neighbors , usedNames , nameDebug ) ;
const bridgeLimitedPathGroups = [
"premodernRoads" ,
"minorRoads" ,
"nationalRoads" ,
"ringRoads" ,
"expressways" ,
"ringExpressways" ,
"icAccessRoads" ,
"externalRoads" ,
"externalExpressways" ,
"railways" ,
"branchRailways" ,
"ringRailways" ,
"externalRailways" ,
"abandonedRailways" ,
"preservedOldRoads" ,
] ;
premodernRoads = enforceBridgeLimitList ( premodernRoads ) ;
minorRoads = enforceBridgeLimitList ( minorRoads ) ;
nationalRoads = enforceBridgeLimitList ( nationalRoads ) ;
ringRoads = enforceBridgeLimitList ( ringRoads ) ;
expressways = enforceBridgeLimitList ( expressways ) ;
ringExpressways = enforceBridgeLimitList ( ringExpressways ) ;
icAccessRoads = enforceBridgeLimitList ( icAccessRoads ) ;
externalRoads = enforceBridgeLimitList ( externalRoads ) ;
externalExpressways = enforceBridgeLimitList ( externalExpressways ) ;
railways = enforceBridgeLimitList ( railways ) ;
branchRailways = enforceBridgeLimitList ( branchRailways ) ;
ringRailways = enforceBridgeLimitList ( ringRailways ) ;
externalRailways = enforceBridgeLimitList ( externalRailways ) ;
abandonedRailways = enforceBridgeLimitList ( abandonedRailways ) ;
preservedOldRoads = enforceBridgeLimitList ( preservedOldRoads ) ;
for ( const pref of neighborPrefectureDetails . prefectures || [ ] ) {
pref . roads = enforceBridgeLimitList ( pref . roads ) ;
pref . railways = enforceBridgeLimitList ( pref . railways ) ;
}
neighborPrefectureDetails . roads = enforceBridgeLimitList ( neighborPrefectureDetails . roads ) ;
neighborPrefectureDetails . railways = enforceBridgeLimitList ( neighborPrefectureDetails . railways ) ;
const bridgeLimitDebug = {
maxBridgeCells : MAX _BRIDGE _CELLS ,
maxWaterRun : Math . max (
0 ,
... [
... premodernRoads ,
... minorRoads ,
... nationalRoads ,
... ringRoads ,
... expressways ,
... ringExpressways ,
... icAccessRoads ,
... externalRoads ,
... externalExpressways ,
... railways ,
... branchRailways ,
... ringRailways ,
... externalRailways ,
... abandonedRailways ,
... preservedOldRoads ,
... ( neighborPrefectureDetails . roads || [ ] ) ,
... ( neighborPrefectureDetails . railways || [ ] ) ,
] . map ( pathMaxWaterRun )
) ,
enforcedGroups : bridgeLimitedPathGroups ,
} ;
2026-05-21 13:20:19 +09:00
return applyOutputOptions ( {
width : MAP _W ,
height : MAP _H ,
cellSize : CELL _SIZE ,
2026-05-22 19:28:39 +09:00
// Keep the apparent map scale close to the original 172-cell-wide version
// after increasing logical terrain resolution.
scaleKmPerCell : 172 / MAP _W ,
prefectureName : prefectureIdentity . name ,
prefectureLabel : prefectureIdentity . label ,
neighborPrefectures : prefectureIdentity . neighbors ,
neighborPrefectureDetails ,
2026-05-21 22:03:14 +09:00
terrainTemplate ,
2026-05-22 14:13:35 +09:00
seaLevel ,
2026-05-21 13:20:19 +09:00
prefectureMask ,
prefectureBorder ,
prefectureRegionId ,
regionalDebug ,
2026-05-22 02:11:18 +09:00
terrainDebug ,
2026-05-21 13:20:19 +09:00
regionalPrefectureBorders ,
elevation ,
moisture ,
slope ,
sea ,
2026-05-21 22:03:14 +09:00
ocean ,
lake ,
2026-05-21 13:20:19 +09:00
river ,
floodplain ,
plain ,
agriculture ,
settlementCluster ,
ridgeField ,
valleyField ,
basinField ,
coastalLowland ,
flowAccum ,
erosionField ,
depositionField ,
2026-05-21 22:03:14 +09:00
arcSpineField ,
branchRidgeField ,
depositionalLowland ,
alluvialFanField ,
deltaField ,
naturalBarrierScore ,
2026-05-21 13:20:19 +09:00
villages ,
ports ,
crossings ,
passes ,
markets ,
castles ,
castleTowns ,
premodernRoads ,
minorRoads ,
modernCities ,
prefecturalCapital : modernCities . find ( ( city ) => city . isPrefecturalCapital && prefectureMask [ indexOf ( city . x , city . y ) ] ) || null ,
totalPopulation : [ ... modernCities , ... satelliteCities ] . reduce ( ( sum , city ) => sum + ( city . population || 0 ) , 0 ) ,
populationDensity ,
railways ,
branchRailways ,
ringRailways ,
externalRailways ,
stations ,
industrialZones ,
nationalRoads ,
ringRoads ,
expressways ,
ringExpressways ,
icAccessRoads ,
externalRoads ,
externalExpressways ,
interchanges ,
logisticsParks ,
satelliteCities ,
newTowns ,
bridges ,
tunnels ,
harborWorks ,
landuse ,
adminCenters ,
adminId ,
adminBorders ,
adminDebug ,
abandonedRailways ,
castleRuins ,
preservedOldRoads ,
riverPaths ,
mainRivers ,
tributaryRivers ,
smallStreams ,
externalGateways ,
2026-05-22 02:11:18 +09:00
transportDebug ,
2026-05-22 19:28:39 +09:00
bridgeLimitDebug ,
2026-05-21 13:20:19 +09:00
entitiesForNames ,
nameDebug ,
} , options ) ;
}