map/mapPipeline.js
2026-05-26 15:32:27 +09:00

191 lines
6.4 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";
export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapUtils.js";
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 seed = Number(seedInput) >>> 0;
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", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed));
const {
elevation,
slope,
sea,
river,
plain,
agriculture,
ridgeField,
valleyField,
basinField,
coastalLowland,
flowAccum,
naturalBarrierScore,
prefectureMask,
landMask,
naturalCompartmentId,
naturalCompartments,
} = terrain;
const features = stage("human", "Human geography and transport", () => generateMapFeatures(seed, terrain));
const {
settlementScore,
villages,
markets,
modernCities,
populationDensity,
stations,
industrialZones,
logisticsParks,
satelliteCities,
newTowns,
ports,
landuse,
stationInfluence,
roadInfluence,
railInfluence2,
villageInfluence,
} = features;
const admin = stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
seed, prefectureMask: landMask || prefectureMask, focusedPrefectureMask: prefectureMask, naturalCompartmentId, naturalCompartments, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture,
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(),
}),
}));
const output = stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
seed,
options,
terrain,
features,
admin,
}));
output.generationTimings = generationTimings;
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
return output;
}
export async function generateMapAsync(seedInput = 114514, options = {}) {
const seed = Number(seedInput) >>> 0;
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", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed));
const {
elevation,
slope,
sea,
river,
plain,
agriculture,
ridgeField,
valleyField,
basinField,
coastalLowland,
flowAccum,
naturalBarrierScore,
prefectureMask,
landMask,
naturalCompartmentId,
naturalCompartments,
} = terrain;
const features = await stage("human", "Human geography and transport", () => generateMapFeatures(seed, terrain));
const {
settlementScore,
villages,
markets,
modernCities,
populationDensity,
stations,
industrialZones,
logisticsParks,
satelliteCities,
newTowns,
ports,
landuse,
stationInfluence,
roadInfluence,
railInfluence2,
villageInfluence,
} = features;
const admin = await stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
seed, prefectureMask: landMask || prefectureMask, focusedPrefectureMask: prefectureMask, naturalCompartmentId, naturalCompartments, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture,
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(),
}),
}));
const output = await stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
seed,
options,
terrain,
features,
admin,
}));
output.generationTimings = generationTimings;
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
return output;
}