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

191 lines
7.1 KiB
JavaScript

import { MAP_H, MAP_W, SIZE } from "./mapUtils.js";
export const DEFAULT_WORLD_PADDING_X = MAP_W;
export const DEFAULT_WORLD_PADDING_Y = MAP_H;
const NEGATIVE_ONE_FIELDS = new Set([
"adminId",
"prefectureRegionId",
"regionId",
"municipalityId",
"naturalCompartmentId",
"watershedId",
]);
function isCellField(value) {
return ArrayBuffer.isView(value) && typeof value.length === "number" && value.length === SIZE;
}
function defaultForField(name, Constructor) {
if (name === "sea") return 1;
if (name === "elevation") return 0.08;
if (name === "seaLevel") return undefined;
if (NEGATIVE_ONE_FIELDS.has(name)) return -1;
if (Constructor === Float32Array || Constructor === Float64Array) return 0;
return 0;
}
function makeWorldField(name, source, worldWidth, worldHeight, originX, originY) {
const Constructor = NEGATIVE_ONE_FIELDS.has(name) ? Int32Array : source.constructor;
const out = new Constructor(worldWidth * worldHeight);
const fallback = defaultForField(name, Constructor);
if (fallback !== 0) out.fill(fallback);
for (let y = 0; y < MAP_H; y++) {
const srcRow = y * MAP_W;
const dstRow = (originY + y) * worldWidth + originX;
for (let x = 0; x < MAP_W; x++) out[dstRow + x] = source[srcRow + x];
}
return out;
}
function sanitizeInitialWorldFields(fields, width, height) {
const sea = fields.sea;
if (!sea) return;
const adminFields = ["adminId", "municipalityId", "prefectureRegionId"];
for (let i = 0; i < width * height; i++) {
const isSea = Boolean(sea[i]);
if (fields.landMask) fields.landMask[i] = isSea ? 0 : (fields.prefectureMask?.[i] ? 1 : fields.landMask[i]);
if (fields.humanRegionMask && isSea) fields.humanRegionMask[i] = 0;
if (isSea) {
for (const key of adminFields) if (fields[key]) fields[key][i] = -1;
if (fields.landuse) fields.landuse[i] = 0;
} else {
if (fields.municipalityId && fields.adminId && fields.municipalityId[i] < 0 && fields.adminId[i] >= 0) fields.municipalityId[i] = fields.adminId[i];
}
}
}
export function createWorldMap(initialMap, options = {}) {
const paddingX = Number.isFinite(options.paddingX) ? Math.max(0, Math.floor(options.paddingX)) : DEFAULT_WORLD_PADDING_X;
const paddingY = Number.isFinite(options.paddingY) ? Math.max(0, Math.floor(options.paddingY)) : DEFAULT_WORLD_PADDING_Y;
const worldWidth = MAP_W + paddingX * 2;
const worldHeight = MAP_H + paddingY * 2;
const originX = paddingX;
const originY = paddingY;
const fields = {};
for (const [name, value] of Object.entries(initialMap || {})) {
if (isCellField(value)) fields[name] = makeWorldField(name, value, worldWidth, worldHeight, originX, originY);
}
if (!fields.sea) {
fields.sea = new Uint8Array(worldWidth * worldHeight);
fields.sea.fill(1);
}
if (!fields.elevation) {
fields.elevation = new Float32Array(worldWidth * worldHeight);
fields.elevation.fill(0.08);
}
sanitizeInitialWorldFields(fields, worldWidth, worldHeight);
return {
seed: initialMap?.seed ?? 0,
width: worldWidth,
height: worldHeight,
originX,
originY,
sourceWidth: initialMap?.width || MAP_W,
sourceHeight: initialMap?.height || MAP_H,
sourceMap: initialMap,
fields,
invalidatedRects: [],
humanPatchHistory: [],
generatedRects: [{
x0: originX,
y0: originY,
x1: originX + MAP_W,
y1: originY + MAP_H,
terrainType: initialMap?.terrainTemplate?.terrainType || initialMap?.terrainDebug?.terrainType || "auto",
label: initialMap?.terrainTemplate?.terrainTypeLabel || initialMap?.terrainDebug?.terrainTypeLabel || "Initial generation",
}],
};
}
export function createInitialCamera(world) {
return {
x: world?.originX || 0,
y: world?.originY || 0,
};
}
export function clampCameraToWorld(camera, world, viewWidth = MAP_W, viewHeight = MAP_H) {
if (!camera || !world) return { x: 0, y: 0 };
const maxX = Math.max(0, world.width - viewWidth);
const maxY = Math.max(0, world.height - viewHeight);
return {
x: Math.min(Math.max(Math.round(camera.x || 0), 0), maxX),
y: Math.min(Math.max(Math.round(camera.y || 0), 0), maxY),
};
}
function expandRectByOffset(rect, dx, dy) {
if (!rect) return rect;
return { ...rect, x0: rect.x0 + dx, y0: rect.y0 + dy, x1: rect.x1 + dx, y1: rect.y1 + dy };
}
function shiftRectCollections(world, dx, dy) {
if (!dx && !dy) return;
for (const key of ["generatedRects", "invalidatedRects", "humanPatchHistory"]) {
if (!Array.isArray(world[key])) continue;
world[key] = world[key].map((item) => {
const out = expandRectByOffset(item, dx, dy);
for (const sub of ["coreRect", "writeRect", "repairRect", "contextRect", "blendRect", "transportReachRect"]) {
if (out?.[sub]) out[sub] = expandRectByOffset(out[sub], dx, dy);
}
return out;
});
}
if (world.lastPatchResult) {
world.lastPatchResult = { ...world.lastPatchResult };
for (const sub of ["coreRect", "writeRect", "repairRect", "contextRect", "blendRect", "transportReachRect"]) {
if (world.lastPatchResult[sub]) world.lastPatchResult[sub] = expandRectByOffset(world.lastPatchResult[sub], dx, dy);
}
}
}
export function expandWorldMap(world, margins = {}) {
if (!world) return { world, dx: 0, dy: 0, expanded: false };
const left = Math.max(0, Math.floor(margins.left || 0));
const right = Math.max(0, Math.floor(margins.right || 0));
const top = Math.max(0, Math.floor(margins.top || 0));
const bottom = Math.max(0, Math.floor(margins.bottom || 0));
if (!left && !right && !top && !bottom) return { world, dx: 0, dy: 0, expanded: false };
const oldWidth = world.width;
const oldHeight = world.height;
const newWidth = oldWidth + left + right;
const newHeight = oldHeight + top + bottom;
const newFields = {};
for (const [name, field] of Object.entries(world.fields || {})) {
if (!ArrayBuffer.isView(field)) continue;
const Constructor = NEGATIVE_ONE_FIELDS.has(name) ? Int32Array : field.constructor;
const out = new Constructor(newWidth * newHeight);
const fallback = defaultForField(name, Constructor);
if (fallback !== 0) out.fill(fallback);
for (let y = 0; y < oldHeight; y++) {
const srcRow = y * oldWidth;
const dstRow = (y + top) * newWidth + left;
for (let x = 0; x < oldWidth; x++) out[dstRow + x] = field[srcRow + x];
}
newFields[name] = out;
}
world.width = newWidth;
world.height = newHeight;
world.originX += left;
world.originY += top;
world.fields = newFields;
shiftRectCollections(world, left, top);
return { world, dx: left, dy: top, expanded: true };
}
export function ensureWorldPaddingForCamera(world, camera, viewWidth = MAP_W, viewHeight = MAP_H, padding = Math.floor(Math.min(MAP_W, MAP_H) * 0.45)) {
if (!world || !camera) return { dx: 0, dy: 0, expanded: false };
const grow = Math.max(64, Math.floor(padding));
const margins = { left: 0, right: 0, top: 0, bottom: 0 };
if (camera.x < grow) margins.left = grow;
if (camera.y < grow) margins.top = grow;
if (camera.x + viewWidth > world.width - grow) margins.right = grow;
if (camera.y + viewHeight > world.height - grow) margins.bottom = grow;
return expandWorldMap(world, margins);
}