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