/
docNemo
/
hex-map-editor
Обзор
Документация
Войти
/
docNemo
/
hex-map-editor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
5
CI/CD
Аналитика
Безопасность
main
src/generation/settlements.ts
116 строк
4 KB
docNemo
feat(generation): реки, поселения, дороги и конвейер генерации
31 июл 2026, 15:42
31 июл 2026, 15:42
19a855e
Код
Авторство
О чём код?
import { hexDistance, hexNeighbors } from '@core/hex'; import { Biome, FeatureType, type HexMap } from '@core/map-model'; import { RIVER_SUITABILITY_BONUS, terrainOf } from '@presets/terrain'; import { createRandom, type RandomFn } from '@core/rng'; import { hasRiver } from './rivers'; /** * Размещение поселений по пригодности местности. * * Пригодность складывается из свойств биома, наличия реки и близости к побережью — * то есть из тех же причин, по которым города возникают на самом деле. Минимальная * дистанция не даёт им слипнуться в агломерацию. */ export interface SettlementOptions { /** Сколько поселений разместить, если хватит пригодных мест. */ count: number; /** Минимальное расстояние между поселениями в гексах. */ minDistance: number; /** Ниже этой пригодности гекс не рассматривается. */ minSuitability: number; } export const DEFAULT_SETTLEMENT_OPTIONS: SettlementOptions = { count: 12, minDistance: 4, minSuitability: 0.15, }; /** * Иерархия поселений от крупнейшего к мелкому и их доля от общего числа. * Крупные города достаются самым пригодным местам. */ const HIERARCHY: { type: FeatureType; share: number }[] = [ { type: FeatureType.City, share: 0.08 }, { type: FeatureType.Town, share: 0.25 }, { type: FeatureType.Village, share: 0.4 }, { type: FeatureType.Hamlet, share: 1 }, ]; function isWater(map: HexMap, index: number): boolean { const biome = map.getBiome(index); return biome === Biome.DeepWater || biome === Biome.ShallowWater; } /** Граничит ли гекс с водой — побережья и берега озёр привлекательны для поселений. */ function isCoastal(map: HexMap, index: number): boolean { for (const neighbour of hexNeighbors(map.coordAt(index))) { const neighbourIndex = map.indexOf(neighbour); if (neighbourIndex >= 0 && isWater(map, neighbourIndex)) return true; } return false; } function suitabilityOf(map: HexMap, index: number): number { if (isWater(map, index)) return 0; const { suitability } = terrainOf(map.getBiome(index), map.getSpecial(index)); if (suitability <= 0) return 0; let value = suitability; if (hasRiver(map, index)) value *= RIVER_SUITABILITY_BONUS; if (isCoastal(map, index)) value *= 1.35; return value; } function assignType(rank: number, total: number): FeatureType { const fraction = total <= 1 ? 0 : rank / (total - 1); for (const level of HIERARCHY) { if (fraction <= level.share) return level.type; } return FeatureType.Hamlet; } /** * Размещает поселения и возвращает их индексы в порядке убывания значимости. * Порядок важен: по нему дороги соединяют сначала крупные центры. */ export function generateSettlements( map: HexMap, seed: string, options: SettlementOptions = DEFAULT_SETTLEMENT_OPTIONS, ): number[] { const random: RandomFn = createRandom(`${seed}:settlements`); const candidates: { index: number; score: number }[] = []; for (const index of map.indices()) { const suitability = suitabilityOf(map, index); if (suitability < options.minSuitability) continue; // Небольшой случайный разброс не даёт всем поселениям сесть в один самый // пригодный биом, оставляя выбор осмысленным. candidates.push({ index, score: suitability * (0.75 + random() * 0.5) }); } candidates.sort((a, b) => b.score - a.score); const placed: number[] = []; for (const candidate of candidates) { if (placed.length >= options.count) break; const coord = map.coordAt(candidate.index); const tooClose = placed.some( (index) => hexDistance(coord, map.coordAt(index)) < options.minDistance, ); if (tooClose) continue; placed.push(candidate.index); } placed.forEach((index, rank) => { map.setFeature(index, assignType(rank, placed.length)); }); return placed; }