map/mapPipeline.js
2026-05-29 14:31:42 +09:00

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