201 lines
7.5 KiB
JavaScript
201 lines
7.5 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 shiftSelectionShape(shape, dx, dy) {
|
|
if (!shape?.polygon) return shape;
|
|
return {
|
|
...shape,
|
|
x0: Number.isFinite(shape.x0) ? shape.x0 + dx : shape.x0,
|
|
y0: Number.isFinite(shape.y0) ? shape.y0 + dy : shape.y0,
|
|
x1: Number.isFinite(shape.x1) ? shape.x1 + dx : shape.x1,
|
|
y1: Number.isFinite(shape.y1) ? shape.y1 + dy : shape.y1,
|
|
polygon: shape.polygon.map((p) => ({ ...p, x: p.x + dx, y: p.y + dy })),
|
|
};
|
|
}
|
|
|
|
function shiftPatchMetadata(item, dx, dy) {
|
|
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);
|
|
}
|
|
if (out?.selectionShape) out.selectionShape = shiftSelectionShape(out.selectionShape, dx, dy);
|
|
return out;
|
|
}
|
|
|
|
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) => shiftPatchMetadata(item, dx, dy));
|
|
}
|
|
if (world.lastPatchResult) world.lastPatchResult = shiftPatchMetadata(world.lastPatchResult, 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);
|
|
}
|