/
docNemo
/
hex-map-editor
Обзор
Документация
Войти
/
docNemo
/
hex-map-editor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
5
CI/CD
Аналитика
Безопасность
main
src/generation/margin.ts
128 строк
6 KB
docNemo
feat(atlas): реестр мира, переходы между картами и расчёт с запасом по краям
31 июл 2026, 18:06
31 июл 2026, 18:06
89b97a6
Код
Авторство
О чём код?
import { axialToOffset, HEX_DIRECTIONS, hexNeighbor } from '@core/hex'; import { HexMap } from '@core/map-model'; import type { BiomeTable } from '@presets/biome-table'; import { generateClimate, type ClimateOptions } from './climate'; import { fillDepressions, generateRivers, type RiverOptions } from './rivers'; /** * Расчёт климата и рек с запасом по краям карты. * * Без запаса края ячейки работают только на вытекание: приоритетное затопление * считает границу стоком, поэтому река может уйти с карты, но не может в неё войти. * Игроки увидят реки, начинающиеся ровно на кромке из ниоткуда. * * Решение — считать в области шире ячейки и обрезать результат. Река, берущая * начало за границей, приходит в краевой гекс уже с накопленным стоком и с верным * направлением. * * См. specs/world-atlas — «Расчёт рек с запасом по краям». */ /** * Ширина полосы запаса в гексах. * * Должна вмещать водосбор, достаточный для того, чтобы входящая река успела * набрать сток. Восемь гексов дают около 200 гексов площади с каждой стороны — * этого хватает при текущем пороге стока. Цена — примерно двукратный объём * расчёта: 56×56 вместо 40×40. */ export const DEFAULT_MARGIN = 8; export interface MarginResult { /** Обрезанная до размеров ячейки карта. */ map: HexMap; /** Сколько краевых гексов получили реку, приходящую извне. */ inflowHexes: number; } /** Копирует поля одного гекса между картами. */ function copyHex(from: HexMap, fromIndex: number, to: HexMap, toIndex: number): void { to.elevation[toIndex] = from.elevation[fromIndex] ?? 0; to.temperature[toIndex] = from.temperature[fromIndex] ?? 0; to.moisture[toIndex] = from.moisture[fromIndex] ?? 0; to.biome[toIndex] = from.biome[fromIndex] ?? 0; to.special[toIndex] = from.special[fromIndex] ?? 0; to.rivers[toIndex] = from.rivers[fromIndex] ?? 0; } /** * Считает климат и реки в расширенной области и возвращает обрезанную карту. * * @param map карта-приёмник размером с ячейку атласа; заполняется на месте */ export function generateWithMargin( map: HexMap, table: BiomeTable, climate: ClimateOptions, rivers: RiverOptions, margin: number = DEFAULT_MARGIN, ): MarginResult { if (margin <= 0) { generateClimate(map, table, climate); generateRivers(map, rivers, fillDepressions(map)); return { map, inflowHexes: 0 }; } // Полоса запаса добавляется с обеих сторон, поэтому чётность числа рядов // сохраняется — а вместе с ней и сдвиг строк на стыке ячеек атласа. const wide = new HexMap(map.columns + margin * 2, map.rows + margin * 2); const originCol = climate.originCol ?? climate.atlasX * map.columns; const originRow = climate.originRow ?? climate.atlasY * map.rows; generateClimate(wide, table, { ...climate, originCol: originCol - margin, originRow: originRow - margin, // Маска архетипа остаётся привязанной к ячейке: остров обязан быть островом // в границах карты, а не растянуться на поля. shapeExtent: { columns: map.columns, rows: map.rows, offsetCol: margin, offsetRow: margin, }, }); generateRivers(wide, rivers, fillDepressions(wide)); for (let row = 0; row < map.rows; row += 1) { for (let col = 0; col < map.columns; col += 1) { copyHex(wide, wide.index(col + margin, row + margin), map, map.index(col, row)); } } return { map, inflowHexes: countInflow(map, wide, margin) }; } /** * Сколько краевых гексов имеют речное ребро, ведущее за пределы карты. * * Такое ребро и есть след реки, приходящей извне или уходящей наружу: без запаса * по краям входящих среди них не было бы вовсе. */ function countInflow(map: HexMap, wide: HexMap, margin: number): number { let count = 0; for (const index of map.indices()) { if ((map.rivers[index] ?? 0) === 0) continue; const { col, row } = axialToOffset(map.coordAt(index)); const onEdge = col === 0 || row === 0 || col === map.columns - 1 || row === map.rows - 1; if (!onEdge) continue; const coord = map.coordAt(index); for (const direction of HEX_DIRECTIONS) { if ((map.rivers[index]! & (1 << direction)) === 0) continue; // Сосед за пределами ячейки, но внутри расширенной области. if (map.indexOf(hexNeighbor(coord, direction)) < 0) { count += 1; break; } } } void wide; void margin; return count; }