names
This commit is contained in:
parent
e1bb10ff8a
commit
4bf36b1f59
6 changed files with 157 additions and 43 deletions
|
|
@ -25,7 +25,7 @@ function neighbors4(x, y) {
|
|||
return out;
|
||||
}
|
||||
|
||||
export function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse) {
|
||||
export function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, crestCrossingScore, populationDensity, landuse) {
|
||||
const adminId = new Int16Array(SIZE);
|
||||
adminId.fill(-1);
|
||||
const dist = new Float32Array(SIZE);
|
||||
|
|
@ -55,14 +55,16 @@ export function generateAdminRegions(centers, prefectureMask, sea, elevation, sl
|
|||
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
|
||||
const riverBarrier = Math.max(river[ni], river[curIndex]);
|
||||
const highDivide = Math.max(elevation[ni], elevation[curIndex]);
|
||||
const crest = Math.max(crestCrossingScore?.[ni] || 0, crestCrossingScore?.[curIndex] || 0);
|
||||
const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0);
|
||||
const crestPenalty = crest * (18.0 + Math.max(0, highDivide - 0.48) * 38.0) * (1 + Math.max(0, highDivide - 0.56) * 1.9);
|
||||
const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier;
|
||||
const slopePenalty = slope[ni] * 10.6;
|
||||
const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0;
|
||||
const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0;
|
||||
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0;
|
||||
const valleyLocalityBonus = valleyField[ni] * 0.16;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + crestPenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
const nextDist = dist[curIndex] + stepCost;
|
||||
|
||||
if (nextDist < dist[ni]) {
|
||||
|
|
@ -266,15 +268,16 @@ export function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCente
|
|||
}
|
||||
}
|
||||
|
||||
export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse) {
|
||||
export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse) {
|
||||
const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse);
|
||||
const crest = crestCrossingScore?.[i] || 0;
|
||||
const majorRiver = clamp(Math.max(river[i] - 0.32, 0) * 1.9 + Math.max(flowAccum[i] - 0.38, 0) * 0.75);
|
||||
const minorStream = clamp(river[i] * 0.34 + flowAccum[i] * 0.18);
|
||||
const ridgeDivide = clamp(ridgeField[i] * 1.55 + Math.max(0, elevation[i] - 0.54) * ridgeField[i] * 0.95);
|
||||
const slopeBreak = clamp(slope[i] * 0.58 + Math.max(0, slope[i] - 0.32) * 0.68);
|
||||
const highGround = Math.max(0, elevation[i] - 0.56) * 0.22;
|
||||
const valleyFloorPenalty = valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62);
|
||||
return clamp(ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72);
|
||||
return clamp(ridgeDivide + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72);
|
||||
}
|
||||
|
||||
function urbanBoundaryPenalty(i, populationDensity, landuse) {
|
||||
|
|
@ -344,7 +347,7 @@ function buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters = []
|
|||
function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, populationDensity, landuse, river, valleyField) {
|
||||
const [x, y] = xyOf(i);
|
||||
const oldId = labels[i];
|
||||
let energy = centerDist[candidateId]?.[i] ?? 0;
|
||||
let energy = (centerDist[candidateId]?.[i] ?? 0) * 0.035;
|
||||
let same4 = 0;
|
||||
let diff4 = 0;
|
||||
let diagDiff = 0;
|
||||
|
|
@ -375,7 +378,7 @@ function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, po
|
|||
if (diagDiff >= 3 && diff4 >= 2 && targetScore[i] < 0.50) energy += 0.42;
|
||||
if (candidateId !== oldId && centerDist[candidateId] && centerDist[oldId]) {
|
||||
const drift = centerDist[candidateId][i] - centerDist[oldId][i];
|
||||
if (drift > 0) energy += Math.min(0.9, drift * 0.012);
|
||||
if (drift > 0) energy += Math.min(0.55, drift * 0.006);
|
||||
}
|
||||
return energy;
|
||||
}
|
||||
|
|
@ -467,7 +470,7 @@ function classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyFie
|
|||
return 11;
|
||||
}
|
||||
|
||||
function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse) {
|
||||
function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse) {
|
||||
const unitId = new Int32Array(SIZE);
|
||||
unitId.fill(-1);
|
||||
const cellClass = new Int16Array(SIZE);
|
||||
|
|
@ -504,7 +507,7 @@ function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridge
|
|||
}
|
||||
|
||||
const targetScore = new Float32Array(SIZE);
|
||||
for (let i = 0; i < SIZE; i++) if (cellClass[i] >= 0) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse);
|
||||
for (let i = 0; i < SIZE; i++) if (cellClass[i] >= 0) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse);
|
||||
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
|
||||
mergeTinyLandscapeUnits(unitId, units, 10);
|
||||
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
|
||||
|
|
@ -569,8 +572,8 @@ function landscapeTransitionCost(a, b, edge) {
|
|||
return Math.max(0.18, 0.70 + boundaryTarget * 4.2 + (a.classId === b.classId ? 0 : 0.75) + ((a.classId === 8 || b.classId === 8) ? 1.2 : 0) - urbanContinuity - (bothCorridor ? 0.55 : 0));
|
||||
}
|
||||
|
||||
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = []) {
|
||||
const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
|
||||
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse, adminCenters = []) {
|
||||
const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse);
|
||||
if (units.length === 0) return;
|
||||
for (let id = 0; id < adminCenters.length; id++) {
|
||||
const center = adminCenters[id];
|
||||
|
|
@ -621,11 +624,11 @@ export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, e
|
|||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
|
||||
}
|
||||
|
||||
export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) {
|
||||
export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) {
|
||||
const targetScore = new Float32Array(SIZE);
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse);
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse);
|
||||
const protectedMask = buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters, protectedPoints, populationDensity, landuse);
|
||||
const band = buildBoundaryBand(adminId, prefectureMask, sea, 5);
|
||||
const band = buildBoundaryBand(adminId, prefectureMask, sea, 8);
|
||||
const adminIds = [...new Set([...adminId].filter((id) => id >= 0))];
|
||||
const centerDist = buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea);
|
||||
let current = new Int16Array(adminId);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function createMapFields() {
|
|||
basinField: new Float32Array(SIZE),
|
||||
coastalLowland: new Float32Array(SIZE),
|
||||
flowAccum: new Float32Array(SIZE),
|
||||
crestCrossingScore: new Float32Array(SIZE),
|
||||
erosionField: new Float32Array(SIZE),
|
||||
depositionField: new Float32Array(SIZE),
|
||||
flowTo,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { CUSTOM_NAMES, NAME_PARTS } from "./names.js";
|
||||
import { NAME_PROBABILITIES, contextualDefaultName } from "./placeNameContext.js";
|
||||
import {
|
||||
applyLandscapeUnitAdminPartition,
|
||||
generateAdminRegions,
|
||||
|
|
@ -12,7 +13,6 @@ import { pickEntities } from "./entitySelection.js";
|
|||
import { createMapFields } from "./fields.js";
|
||||
import { MinHeap } from "./graph.js";
|
||||
import { CELL_SIZE, INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside, nearMapEdge, xyOf } from "./grid.js";
|
||||
import { contextualDefaultName } from "./placeNameContext.js";
|
||||
import { fbm, hash2, lerp, rand, smoothstep, valueNoise } from "./random.js";
|
||||
|
||||
export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./grid.js";
|
||||
|
|
@ -570,14 +570,25 @@ function attachIdsAndNames(points, prefix, seed, kindOverride = null, nameFields
|
|||
return points.map((p, i) => {
|
||||
const id = `${prefix}-${i}`;
|
||||
const kind = kindOverride || p.kind;
|
||||
let name = CUSTOM_NAMES[id];
|
||||
|
||||
const customName = CUSTOM_NAMES[id];
|
||||
const customProbability = NAME_PROBABILITIES?.customName ?? 1.0;
|
||||
|
||||
const useCustomName =
|
||||
Boolean(customName) &&
|
||||
hash2(i + prefix.length * 101, seed + i * 37, seed + 99991) < customProbability;
|
||||
|
||||
let name = useCustomName ? customName : null;
|
||||
|
||||
if (!name) {
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
name = defaultName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, attempt);
|
||||
if (!usedNames || !usedNames.has(name)) break;
|
||||
}
|
||||
}
|
||||
if (usedNames) usedNames.add(name);
|
||||
|
||||
if (usedNames && name) usedNames.add(name);
|
||||
|
||||
return {
|
||||
...p,
|
||||
id,
|
||||
|
|
@ -596,6 +607,7 @@ function applyOutputOptions(map, options = {}) {
|
|||
delete slim.basinField;
|
||||
delete slim.coastalLowland;
|
||||
delete slim.flowAccum;
|
||||
delete slim.crestCrossingScore;
|
||||
delete slim.erosionField;
|
||||
delete slim.depositionField;
|
||||
return slim;
|
||||
|
|
@ -662,6 +674,43 @@ function recalculatePopulationAfterLanduse(modernCities, satelliteCities, popula
|
|||
}
|
||||
}
|
||||
|
||||
function computeCrestCrossingScore(elevation, sea, ridgeField, valleyField, coastalLowland, flowAccum, out) {
|
||||
out.fill(0);
|
||||
const pairs = [
|
||||
[[1, 0], [-1, 0]],
|
||||
[[0, 1], [0, -1]],
|
||||
[[1, 1], [-1, -1]],
|
||||
[[1, -1], [-1, 1]],
|
||||
];
|
||||
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
let bestProminence = 0;
|
||||
for (const [[ax, ay], [bx, by]] of pairs) {
|
||||
const ai = indexOf(x + ax, y + ay);
|
||||
const bi = indexOf(x + bx, y + by);
|
||||
if (sea[ai] || sea[bi]) continue;
|
||||
if (elevation[i] <= elevation[ai] || elevation[i] <= elevation[bi]) continue;
|
||||
bestProminence = Math.max(bestProminence, elevation[i] - Math.max(elevation[ai], elevation[bi]));
|
||||
}
|
||||
|
||||
if (bestProminence < 0.018) continue;
|
||||
const ridge = ridgeField[i];
|
||||
if (elevation[i] < 0.38 && ridge < 0.34) continue;
|
||||
|
||||
const elevationBoost = Math.exp(Math.max(0, elevation[i] - 0.42) * 3.2);
|
||||
const ridgeBoost = 0.55 + ridge * 1.9;
|
||||
const divideSupport = clamp(ridge * 1.25 + Math.max(0, elevation[i] - 0.56) * 0.85);
|
||||
const valleyPenalty = valleyField[i] * (divideSupport > 0.58 ? 0.22 : 0.72);
|
||||
const coastPenalty = coastalLowland[i] * (ridge > 0.50 ? 0.20 : 0.82);
|
||||
const flowPenalty = flowAccum[i] * (ridge > 0.52 ? 0.18 : 0.88);
|
||||
out[i] = clamp(bestProminence * 18.0 * elevationBoost * ridgeBoost - valleyPenalty - coastPenalty - flowPenalty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMap(seedInput = 114514, options = {}) {
|
||||
const seed = Number(seedInput) >>> 0;
|
||||
|
||||
|
|
@ -682,6 +731,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
crestCrossingScore,
|
||||
erosionField,
|
||||
depositionField,
|
||||
flowTo,
|
||||
|
|
@ -1428,6 +1478,8 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
const tributaryRivers = riverPaths.filter((path) => path.some(([x, y]) => !mainRiverCells.has(`${x},${y}`)) && !mainRivers.includes(path));
|
||||
const smallStreams = streamPaths.filter((path) => path.length >= 5);
|
||||
|
||||
computeCrestCrossingScore(elevation, sea, ridgeField, valleyField, coastalLowland, flowAccum, crestCrossingScore);
|
||||
|
||||
prefectureMask = makePrefectureMask(seed, sea, elevation, slope, river);
|
||||
prefectureBorder = extractMaskBorder(prefectureMask, sea);
|
||||
const regionalPrefectures = generateRegionalPrefectures(seed, sea, elevation, slope, river, ridgeField, flowAccum, prefectureMask);
|
||||
|
|
@ -2805,7 +2857,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
const extra = pickEntities(municipalityCandidates, { max: 10 - adminCentersRaw.length, minDistance: 8, threshold: 0.32, seed: seed + 1304 });
|
||||
adminCentersRaw.push(...extra.filter((p) => adminCentersRaw.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 6)));
|
||||
}
|
||||
const adminId = generateAdminRegions(adminCentersRaw, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse);
|
||||
const adminId = generateAdminRegions(adminCentersRaw, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, crestCrossingScore, populationDensity, landuse);
|
||||
smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, 7);
|
||||
|
||||
function lockUrbanClusterToMunicipality(city, radius, allowSuburban = true) {
|
||||
|
|
@ -2865,8 +2917,8 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 620);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 260);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 180);
|
||||
applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 5);
|
||||
applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse, adminCentersRaw);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 5);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 360);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 220);
|
||||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
|
@ -2972,6 +3024,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
crestCrossingScore,
|
||||
erosionField,
|
||||
depositionField,
|
||||
villages,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { MAP_H, MAP_W, indexOf, inside } from "./grid.js";
|
||||
import { hash2 } from "./random.js";
|
||||
|
||||
export const NAME_PROBABILITIES = {
|
||||
contextSuffix: 0.42,
|
||||
secondInfix: 0.22,
|
||||
prefixOnly: 0.38,
|
||||
customName: 1.0,
|
||||
};
|
||||
|
||||
const CONTEXT_SUFFIXES = {
|
||||
coastal: ["\u6d5c", "\u6e4a", "\u6e2f", "\u6d66", "\u6d25", "\u5d0e", "\u6e7e"],
|
||||
river: ["\u5ddd", "\u702c", "\u6a4b", "\u6e21", "\u6cbc", "\u6ca2"],
|
||||
|
|
@ -22,38 +29,79 @@ function isNearSea(x, y, sea, radius = 3) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function chooseNameSuffixPool(entity, fields) {
|
||||
function boundedIndex(entity) {
|
||||
return indexOf(Math.max(0, Math.min(MAP_W - 1, entity.x)), Math.max(0, Math.min(MAP_H - 1, entity.y)));
|
||||
}
|
||||
|
||||
function chooseNameContext(entity, fields) {
|
||||
const kind = String(entity.kind || "").toLowerCase();
|
||||
const i = indexOf(Math.max(0, Math.min(MAP_W - 1, entity.x)), Math.max(0, Math.min(MAP_H - 1, entity.y)));
|
||||
const i = boundedIndex(entity);
|
||||
const coastal = (fields?.coastalLowland?.[i] || 0) > 0.35 || isNearSea(entity.x, entity.y, fields?.sea);
|
||||
const river = (fields?.river?.[i] || 0) > 0.35 || (fields?.valleyField?.[i] || 0) > 0.42;
|
||||
const plain = (fields?.plain?.[i] || 0) > 0.48 || (fields?.agriculture?.[i] || 0) > 0.46 || (fields?.basinField?.[i] || 0) > 0.35;
|
||||
const mountain = (fields?.elevation?.[i] || 0) > 0.52 || (fields?.slope?.[i] || 0) > 0.35 || (fields?.ridgeField?.[i] || 0) > 0.42;
|
||||
|
||||
if (kind.includes("port") || kind.includes("harbor") || entity.portClass) return CONTEXT_SUFFIXES.coastal;
|
||||
if (kind.includes("crossing") || kind.includes("bridge")) return CONTEXT_SUFFIXES.river;
|
||||
if (kind.includes("pass") || kind.includes("mountain")) return CONTEXT_SUFFIXES.mountain;
|
||||
if (kind.includes("castle") || kind.includes("market")) return CONTEXT_SUFFIXES.historic;
|
||||
if (kind.includes("new town") || kind.includes("satellite")) return CONTEXT_SUFFIXES.suburban;
|
||||
if (coastal && !mountain) return CONTEXT_SUFFIXES.coastal;
|
||||
if (river) return CONTEXT_SUFFIXES.river;
|
||||
if (mountain && !plain) return CONTEXT_SUFFIXES.mountain;
|
||||
if (plain) return CONTEXT_SUFFIXES.plain;
|
||||
return CONTEXT_SUFFIXES.plain;
|
||||
if (kind.includes("port") || kind.includes("harbor") || entity.portClass) return "coastal";
|
||||
if (kind.includes("crossing") || kind.includes("bridge")) return "river";
|
||||
if (kind.includes("pass") || kind.includes("mountain")) return "mountain";
|
||||
if (kind.includes("castle") || kind.includes("market")) return "historic";
|
||||
if (kind.includes("new town") || kind.includes("satellite")) return "suburban";
|
||||
if (coastal && !mountain) return "coastal";
|
||||
if (river) return "river";
|
||||
if (mountain && !plain) return "mountain";
|
||||
if (plain) return "plain";
|
||||
return "plain";
|
||||
}
|
||||
|
||||
export function chooseNameSuffixPool(entity, fields) {
|
||||
return CONTEXT_SUFFIXES[chooseNameContext(entity, fields)] || CONTEXT_SUFFIXES.plain;
|
||||
}
|
||||
|
||||
function chooseContextFreePool(entity, fields, nameParts) {
|
||||
const context = chooseNameContext(entity, fields);
|
||||
return nameParts.contextFree?.[context] || nameParts.contextFree?.plain || nameParts.infixes || nameParts.free || [""];
|
||||
}
|
||||
|
||||
function pick(pool, seed, n, salt) {
|
||||
if (!pool?.length) return "";
|
||||
const index = Math.floor(hash2(n + salt * 101, seed + salt * 1009, seed + n * 37 + salt) * pool.length) % pool.length;
|
||||
return pool[index];
|
||||
}
|
||||
|
||||
function chance(seed, n, salt, probability) {
|
||||
return hash2(n + salt * 173, seed + salt * 811, seed + n * 43 + salt) < probability;
|
||||
}
|
||||
|
||||
function appendPart(parts, part) {
|
||||
if (!part) return;
|
||||
const previous = parts[parts.length - 1] || "";
|
||||
if (previous && (previous === part || previous.endsWith(part) || part.startsWith(previous))) return;
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
export function contextualDefaultName(seed, id, entity, nameParts, fields, attempt = 0) {
|
||||
const prefixKey = id.split("-")[0];
|
||||
const n = Number(id.split("-")[1] || 0) + attempt * 997;
|
||||
const prefixes = nameParts.prefixes || [""];
|
||||
const infixes = nameParts.infixes || [""];
|
||||
const suffixPool = chooseNameSuffixPool(entity, fields);
|
||||
const fallbackSuffixes = nameParts.suffixes || [""];
|
||||
const prefix = prefixes[(seed + n * 7) % prefixes.length];
|
||||
const useInfix = hash2(n + prefixKey.length * 17, seed + n * 31, seed + 2777) >= 0.78;
|
||||
const infix = useInfix ? infixes[(seed * 3 + n * 11) % infixes.length] : "";
|
||||
const contextualSuffix = suffixPool[(seed * 5 + n * 13 + prefixKey.length) % suffixPool.length];
|
||||
const fallbackSuffix = fallbackSuffixes[(seed * 7 + n * 17 + prefixKey.length) % fallbackSuffixes.length];
|
||||
const suffix = attempt % 4 === 3 ? fallbackSuffix : contextualSuffix;
|
||||
return `${prefix}${infix}${suffix}`;
|
||||
const infixes = nameParts.infixes || nameParts.free || [""];
|
||||
const suffixes = nameParts.suffixes || [""];
|
||||
const contextFree = chooseContextFreePool(entity, fields, nameParts);
|
||||
const contextSuffixes = chooseNameSuffixPool(entity, fields);
|
||||
const parts = [];
|
||||
|
||||
if (chance(seed, n, 11 + prefixKey.length, NAME_PROBABILITIES.prefixOnly)) {
|
||||
appendPart(parts, pick(prefixes, seed, n, 101));
|
||||
}
|
||||
|
||||
const useContextMain = chance(seed, n, 13 + prefixKey.length, 0.42);
|
||||
appendPart(parts, pick(useContextMain ? contextFree : infixes, seed, n, useContextMain ? 211 : 223));
|
||||
|
||||
if (chance(seed, n, 19 + prefixKey.length, NAME_PROBABILITIES.secondInfix)) {
|
||||
appendPart(parts, pick(infixes, seed, n, 401));
|
||||
}
|
||||
|
||||
const useContextSuffix = chance(seed, n, 23 + prefixKey.length, NAME_PROBABILITIES.contextSuffix);
|
||||
appendPart(parts, pick(useContextSuffix ? contextSuffixes : suffixes, seed, n, useContextSuffix ? 503 : 509));
|
||||
|
||||
return parts.slice(-3).join("") || pick(suffixes, seed, n, 601) || "\u539f";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@ import { MAP_H, MAP_W, indexOf } from "./grid.js";
|
|||
export function terrainBoundaryTargetForMetrics(map, i) {
|
||||
const lu = map.landuse[i];
|
||||
const urbanPenalty = Math.min(1, (lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0) + map.populationDensity[i] * 1.35);
|
||||
const crest = map.crestCrossingScore?.[i] || 0;
|
||||
const majorRiver = Math.min(1, Math.max(map.river[i] - 0.32, 0) * 1.9 + Math.max(map.flowAccum[i] - 0.38, 0) * 0.75);
|
||||
const minorStream = Math.min(1, map.river[i] * 0.34 + map.flowAccum[i] * 0.18);
|
||||
const ridgeDivide = Math.min(1, map.ridgeField[i] * 1.55 + Math.max(0, map.elevation[i] - 0.54) * map.ridgeField[i] * 0.95);
|
||||
const slopeBreak = Math.min(1, map.slope[i] * 0.58 + Math.max(0, map.slope[i] - 0.32) * 0.68);
|
||||
const highGround = Math.max(0, map.elevation[i] - 0.56) * 0.22;
|
||||
const valleyFloorPenalty = map.valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62);
|
||||
return Math.max(0, Math.min(1, ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72));
|
||||
return Math.max(0, Math.min(1, ridgeDivide + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72));
|
||||
}
|
||||
|
||||
export function adminBoundaryMetrics(map) {
|
||||
let borderEdges = 0;
|
||||
let targetSum = 0;
|
||||
let crestSum = 0;
|
||||
let denseUrbanEdges = 0;
|
||||
let rightAngleRuns = 0;
|
||||
let voronoiLikeEdges = 0;
|
||||
|
|
@ -28,7 +30,9 @@ export function adminBoundaryMetrics(map) {
|
|||
if (!map.prefectureMask[ni] || map.sea[ni] || map.adminId[ni] < 0 || map.adminId[ni] === map.adminId[i]) continue;
|
||||
borderEdges++;
|
||||
const edgeTarget = (terrainBoundaryTargetForMetrics(map, i) + terrainBoundaryTargetForMetrics(map, ni)) * 0.5;
|
||||
const crestSupport = ((map.crestCrossingScore?.[i] || 0) + (map.crestCrossingScore?.[ni] || 0)) * 0.5;
|
||||
targetSum += edgeTarget;
|
||||
crestSum += crestSupport;
|
||||
const urban = Math.max(map.populationDensity[i], map.populationDensity[ni]) > 0.58 || [2, 3, 4, 7, 8].includes(map.landuse[i]) || [2, 3, 4, 7, 8].includes(map.landuse[ni]);
|
||||
if (urban) denseUrbanEdges++;
|
||||
const ca = map.adminCenters[map.adminId[i]];
|
||||
|
|
@ -38,7 +42,8 @@ export function adminBoundaryMetrics(map) {
|
|||
const my = y + dy * 0.5;
|
||||
const dA = Math.hypot(mx - ca.x, my - ca.y);
|
||||
const dB = Math.hypot(mx - cb.x, my - cb.y);
|
||||
if (Math.abs(dA - dB) < 4.2 && edgeTarget < 0.40) voronoiLikeEdges++;
|
||||
const naturalSupport = Math.max(edgeTarget, crestSupport, map.ridgeField[i], map.ridgeField[ni], map.river[i], map.river[ni]);
|
||||
if (Math.abs(dA - dB) < 4.2 && naturalSupport < 0.42) voronoiLikeEdges++;
|
||||
}
|
||||
if (edgeTarget < 0.16 && Math.max(map.slope[i], map.slope[ni]) < 0.24 && Math.max(map.ridgeField[i], map.ridgeField[ni]) < 0.28 && Math.max(map.river[i], map.river[ni]) < 0.26) {
|
||||
lowScoreFlatEdges++;
|
||||
|
|
@ -100,6 +105,7 @@ export function adminBoundaryMetrics(map) {
|
|||
return {
|
||||
borderEdges,
|
||||
avgTarget: borderEdges ? targetSum / borderEdges : 0,
|
||||
avgCrest: borderEdges ? crestSum / borderEdges : 0,
|
||||
denseUrbanRate: borderEdges ? denseUrbanEdges / borderEdges : 0,
|
||||
rightAngleRate: borderEdges ? rightAngleRuns / borderEdges : 0,
|
||||
voronoiLikeRate: borderEdges ? voronoiLikeEdges / borderEdges : 0,
|
||||
|
|
|
|||
3
test.js
3
test.js
|
|
@ -127,6 +127,7 @@ try {
|
|||
assert(map.prefectureMask.length === size, "prefecture mask length matches map size");
|
||||
assert(map.populationDensity.length === size, "population density length matches map size");
|
||||
assert(map.ridgeField.length === size && map.valleyField.length === size && map.flowAccum.length === size, "causal terrain fields match map size");
|
||||
assert(map.crestCrossingScore.length === size, "crest crossing score field matches map size");
|
||||
assert(map.erosionField.length === size && map.depositionField.length === size, "erosion and deposition fields match map size");
|
||||
assert(map.settlementCluster.length === size, "settlement cluster field matches map size");
|
||||
assert(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist");
|
||||
|
|
@ -182,6 +183,7 @@ try {
|
|||
assert(adminMetrics.maxComponents <= 4, "municipal topology repair prevents excessive disconnected fragments");
|
||||
assert(adminMetrics.disconnectedMunicipalities <= Math.max(2, Math.ceil(adminMetrics.municipalityCount * 0.20)), "most municipalities remain connected after terrain snapping");
|
||||
assert(adminMetrics.avgTarget > 0.18, "admin borders align with terrain target features often enough");
|
||||
assert(adminMetrics.avgCrest > 0.006, "admin borders pick up crest crossings");
|
||||
assert(adminMetrics.denseUrbanRate < 0.42, "admin borders avoid excessive dense urban crossings");
|
||||
assert(adminMetrics.rightAngleRate < 0.46, "admin borders avoid excessive unsupported stair-step artifacts");
|
||||
assert(adminMetrics.voronoiLikeRate < 0.58, "admin borders are not dominated by weak-terrain center bisectors");
|
||||
|
|
@ -240,6 +242,7 @@ try {
|
|||
assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`);
|
||||
assert(metrics.maxComponents <= 5, `seed ${seed}: topology repair limits disconnected fragments`);
|
||||
assert(metrics.avgTarget > 0.14, `seed ${seed}: borders retain terrain-boundary affinity`);
|
||||
assert(metrics.avgCrest > 0.004, `seed ${seed}: borders retain crest-crossing affinity`);
|
||||
assert(metrics.denseUrbanRate < 0.50, `seed ${seed}: borders avoid excessive dense urban cuts`);
|
||||
assert(metrics.voronoiLikeRate < 0.66, `seed ${seed}: weak-terrain Voronoi-like border ratio stays bounded`);
|
||||
assert(metrics.lowScoreFlatRate < 0.50, `seed ${seed}: low-score flat border ratio stays bounded`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue