308 lines
12 KiB
JavaScript
308 lines
12 KiB
JavaScript
import { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapUtils.js";
|
|
import { generateTerrainAndRivers } from "./mapTerrain.js";
|
|
import { generateMapFeatures } from "./mapFeatures.js";
|
|
import { finishMapOutput } from "./mapOutput.js";
|
|
import { generateAdminLayout } from "./mapAdminStage.js";
|
|
import { buildGeographicBasis, finalizeGeographicBasis } from "./mapGeography.js";
|
|
import { finalizeAdminAwareTransport } from "./mapPostAdminTransport.js";
|
|
|
|
export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapUtils.js";
|
|
|
|
function normalizeRectLike(rect) {
|
|
if (!rect) return null;
|
|
const x0 = Math.floor(Math.min(Number(rect.x0), Number(rect.x1)));
|
|
const y0 = Math.floor(Math.min(Number(rect.y0), Number(rect.y1)));
|
|
const x1 = Math.ceil(Math.max(Number(rect.x0), Number(rect.x1)));
|
|
const y1 = Math.ceil(Math.max(Number(rect.y0), Number(rect.y1)));
|
|
if (![x0, y0, x1, y1].every(Number.isFinite)) return null;
|
|
return { x0, y0, x1, y1 };
|
|
}
|
|
|
|
function normalizeGenerationContext(options = {}) {
|
|
const originX = Math.floor(Number.isFinite(options.originX) ? options.originX : 0);
|
|
const originY = Math.floor(Number.isFinite(options.originY) ? options.originY : 0);
|
|
const width = Math.max(1, Math.floor(Number.isFinite(options.width) ? options.width : MAP_W));
|
|
const height = Math.max(1, Math.floor(Number.isFinite(options.height) ? options.height : MAP_H));
|
|
const variant = Math.max(0, Math.floor(Number.isFinite(options.variant) ? options.variant : 0)) >>> 0;
|
|
const contextRect = normalizeRectLike(options.contextRect);
|
|
return {
|
|
worldNative: options.worldNative === true,
|
|
legacyTerrain: options.legacyTerrain !== false,
|
|
originX,
|
|
originY,
|
|
width,
|
|
height,
|
|
variant,
|
|
contextRect,
|
|
hasBoundaryWorld: !!options.boundaryWorld,
|
|
};
|
|
}
|
|
|
|
function mixUint(h, value) {
|
|
h = Math.imul((h ^ (value >>> 0)) >>> 0, 2246822519) >>> 0;
|
|
h ^= h >>> 13;
|
|
return Math.imul(h, 3266489917) >>> 0;
|
|
}
|
|
|
|
function mixString(h, value) {
|
|
const text = String(value ?? "");
|
|
for (let i = 0; i < text.length; i++) h = mixUint(h, text.charCodeAt(i));
|
|
return h >>> 0;
|
|
}
|
|
|
|
function contextualSeed(seed, context, options = {}) {
|
|
let h = seed >>> 0;
|
|
if (!context.worldNative && !context.variant && !context.originX && !context.originY) return h;
|
|
h = mixString(h, options.terrainType || options.generationType || "auto");
|
|
h = mixUint(h, context.variant);
|
|
h = mixUint(h, context.originX | 0);
|
|
h = mixUint(h, context.originY | 0);
|
|
h = mixUint(h, context.width);
|
|
h = mixUint(h, context.height);
|
|
if (context.contextRect) {
|
|
h = mixUint(h, context.contextRect.x0 | 0);
|
|
h = mixUint(h, context.contextRect.y0 | 0);
|
|
h = mixUint(h, context.contextRect.x1 | 0);
|
|
h = mixUint(h, context.contextRect.y1 | 0);
|
|
}
|
|
return h >>> 0;
|
|
}
|
|
|
|
function makeRuntimeOptions(options, baseSeed) {
|
|
const generationContext = normalizeGenerationContext(options);
|
|
const effectiveSeed = contextualSeed(baseSeed, generationContext, options);
|
|
return {
|
|
...options,
|
|
generationContext,
|
|
baseSeed,
|
|
effectiveSeed,
|
|
originX: generationContext.originX,
|
|
originY: generationContext.originY,
|
|
width: generationContext.width,
|
|
height: generationContext.height,
|
|
variant: generationContext.variant,
|
|
};
|
|
}
|
|
|
|
function generateInitialTerrain(seed, options = {}) {
|
|
// Production generation is intentionally pinned to the legacy high-detail
|
|
// terrain/natural-compartment pipeline. Rect-native terrain helpers may remain
|
|
// in the codebase for experiments, but they are not reachable from generateMap.
|
|
return generateTerrainAndRivers(seed, options);
|
|
}
|
|
|
|
function terrainStageLabel() {
|
|
return "Terrain, rivers, and natural compartments";
|
|
}
|
|
|
|
function nowMs() {
|
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
}
|
|
|
|
function timedStage(timings, options, key, label, fn) {
|
|
options?.onProgress?.({ status: "start", key, label, timings: timings.slice(), startedAt: nowMs() });
|
|
const t0 = nowMs();
|
|
const value = fn();
|
|
const ms = Math.round((nowMs() - t0) * 10) / 10;
|
|
const entry = { key, label, ms };
|
|
timings.push(entry);
|
|
options?.onProgress?.({ status: "done", key, label, ms, timings: timings.slice() });
|
|
return value;
|
|
}
|
|
|
|
function yieldToBrowser() {
|
|
if (typeof requestAnimationFrame === "function") {
|
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
}
|
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
async function timedStageAsync(timings, options, key, label, fn) {
|
|
options?.onProgress?.({ status: "start", key, label, timings: timings.slice(), startedAt: nowMs() });
|
|
await yieldToBrowser();
|
|
const t0 = nowMs();
|
|
const value = fn();
|
|
const ms = Math.round((nowMs() - t0) * 10) / 10;
|
|
const entry = { key, label, ms };
|
|
timings.push(entry);
|
|
options?.onProgress?.({ status: "done", key, label, ms, timings: timings.slice() });
|
|
await yieldToBrowser();
|
|
return value;
|
|
}
|
|
|
|
|
|
export function generateMap(seedInput = 114514, options = {}) {
|
|
const baseSeed = Number(seedInput) >>> 0;
|
|
options = makeRuntimeOptions(options, baseSeed);
|
|
const seed = options.effectiveSeed;
|
|
if (typeof options.onProgress !== "function") options = { ...options, onProgress: () => {} };
|
|
|
|
const generationTimings = [];
|
|
const stage = (key, label, fn) => timedStage(generationTimings, options, key, label, fn);
|
|
|
|
const terrain = stage("terrain", terrainStageLabel(options), () => generateInitialTerrain(seed, options));
|
|
const {
|
|
elevation,
|
|
slope,
|
|
sea,
|
|
river,
|
|
plain,
|
|
agriculture,
|
|
ridgeField,
|
|
valleyField,
|
|
basinField,
|
|
coastalLowland,
|
|
flowAccum,
|
|
naturalBarrierScore,
|
|
prefectureMask,
|
|
landMask,
|
|
naturalCompartmentId,
|
|
naturalCompartments,
|
|
} = terrain;
|
|
|
|
const geographyBasis = stage("geography", "Unified geographic basis", () => buildGeographicBasis(seed, terrain, options));
|
|
const terrainWithGeography = { ...terrain, geography: geographyBasis, generationContext: options.generationContext };
|
|
|
|
const features = stage("settlements", "Settlements, towns, ports, and land-use demand", () => generateMapFeatures(seed, terrainWithGeography, options));
|
|
const {
|
|
settlementScore,
|
|
villages,
|
|
markets,
|
|
modernCities,
|
|
populationDensity,
|
|
stations,
|
|
industrialZones,
|
|
logisticsParks,
|
|
satelliteCities,
|
|
newTowns,
|
|
ports,
|
|
landuse,
|
|
stationInfluence,
|
|
roadInfluence,
|
|
railInfluence2,
|
|
villageInfluence,
|
|
} = features;
|
|
|
|
const geography = stage("geographyFinal", "Unified accessibility and centrality fields", () => finalizeGeographicBasis(seed, terrain, features, geographyBasis, options));
|
|
|
|
const admin = stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
|
|
seed, generationContext: options.generationContext, prefectureMask: landMask || prefectureMask, focusedPrefectureMask: prefectureMask, naturalCompartmentId, naturalCompartments, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture,
|
|
geography, habitability: geography.habitability, accessibility: geography.accessibility, centrality: geography.centrality, geographicBarrier: geography.geographicBarrier, geographicBarrierCost: geography.geographicBarrierCost, adminBoundaryPreference: geography.adminBoundaryPreference, boundaryAvoidance: geography.boundaryAvoidance,
|
|
settlementScore, populationDensity, stationInfluence, roadInfluence, railInfluence2, villageInfluence, landuse, modernCities, satelliteCities, newTowns, markets, villages, ports, stations, industrialZones, logisticsParks,
|
|
adminProgress: (event) => options?.onProgress?.({
|
|
...event,
|
|
key: "admin",
|
|
label: event.status === "region-done"
|
|
? `Admin region ${event.regionId} done`
|
|
: event.status === "admin-step"
|
|
? `Admin region ${event.regionId}: ${event.step}`
|
|
: `Admin region ${event.regionId}`,
|
|
timings: generationTimings.slice(),
|
|
}),
|
|
}));
|
|
|
|
stage("transport", "Administrative-aware final transport network", () => finalizeAdminAwareTransport({ seed, terrain, features, admin, geography, generationContext: options.generationContext }));
|
|
|
|
const output = stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
|
|
seed,
|
|
options,
|
|
terrain,
|
|
features,
|
|
admin,
|
|
geography,
|
|
}));
|
|
output.generationTimings = generationTimings;
|
|
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
|
output.baseSeed = options.baseSeed;
|
|
output.effectiveSeed = seed;
|
|
output.generationContext = { ...options.generationContext };
|
|
return output;
|
|
}
|
|
|
|
export async function generateMapAsync(seedInput = 114514, options = {}) {
|
|
const baseSeed = Number(seedInput) >>> 0;
|
|
options = makeRuntimeOptions(options, baseSeed);
|
|
const seed = options.effectiveSeed;
|
|
if (typeof options.onProgress !== "function") options = { ...options, onProgress: () => {} };
|
|
|
|
const generationTimings = [];
|
|
const stage = (key, label, fn) => timedStageAsync(generationTimings, options, key, label, fn);
|
|
|
|
const terrain = await stage("terrain", terrainStageLabel(options), () => generateInitialTerrain(seed, options));
|
|
const {
|
|
elevation,
|
|
slope,
|
|
sea,
|
|
river,
|
|
plain,
|
|
agriculture,
|
|
ridgeField,
|
|
valleyField,
|
|
basinField,
|
|
coastalLowland,
|
|
flowAccum,
|
|
naturalBarrierScore,
|
|
prefectureMask,
|
|
landMask,
|
|
naturalCompartmentId,
|
|
naturalCompartments,
|
|
} = terrain;
|
|
|
|
const geographyBasis = await stage("geography", "Unified geographic basis", () => buildGeographicBasis(seed, terrain, options));
|
|
const terrainWithGeography = { ...terrain, geography: geographyBasis, generationContext: options.generationContext };
|
|
|
|
const features = await stage("settlements", "Settlements, towns, ports, and land-use demand", () => generateMapFeatures(seed, terrainWithGeography, options));
|
|
const {
|
|
settlementScore,
|
|
villages,
|
|
markets,
|
|
modernCities,
|
|
populationDensity,
|
|
stations,
|
|
industrialZones,
|
|
logisticsParks,
|
|
satelliteCities,
|
|
newTowns,
|
|
ports,
|
|
landuse,
|
|
stationInfluence,
|
|
roadInfluence,
|
|
railInfluence2,
|
|
villageInfluence,
|
|
} = features;
|
|
|
|
const geography = await stage("geographyFinal", "Unified accessibility and centrality fields", () => finalizeGeographicBasis(seed, terrain, features, geographyBasis, options));
|
|
|
|
const admin = await stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
|
|
seed, generationContext: options.generationContext, prefectureMask: landMask || prefectureMask, focusedPrefectureMask: prefectureMask, naturalCompartmentId, naturalCompartments, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture,
|
|
geography, habitability: geography.habitability, accessibility: geography.accessibility, centrality: geography.centrality, geographicBarrier: geography.geographicBarrier, geographicBarrierCost: geography.geographicBarrierCost, adminBoundaryPreference: geography.adminBoundaryPreference, boundaryAvoidance: geography.boundaryAvoidance,
|
|
settlementScore, populationDensity, stationInfluence, roadInfluence, railInfluence2, villageInfluence, landuse, modernCities, satelliteCities, newTowns, markets, villages, ports, stations, industrialZones, logisticsParks,
|
|
adminProgress: (event) => options?.onProgress?.({
|
|
...event,
|
|
key: "admin",
|
|
label: event.status === "region-done"
|
|
? `Admin region ${event.regionId} done`
|
|
: event.status === "admin-step"
|
|
? `Admin region ${event.regionId}: ${event.step}`
|
|
: `Admin region ${event.regionId}`,
|
|
timings: generationTimings.slice(),
|
|
}),
|
|
}));
|
|
|
|
await stage("transport", "Administrative-aware final transport network", () => finalizeAdminAwareTransport({ seed, terrain, features, admin, geography, generationContext: options.generationContext }));
|
|
|
|
const output = await stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
|
|
seed,
|
|
options,
|
|
terrain,
|
|
features,
|
|
admin,
|
|
geography,
|
|
}));
|
|
output.generationTimings = generationTimings;
|
|
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
|
output.baseSeed = options.baseSeed;
|
|
output.effectiveSeed = seed;
|
|
output.generationContext = { ...options.generationContext };
|
|
return output;
|
|
}
|