37 explicit HexRandom(std::uint32_t
seed) noexcept : state_(
seed == 0
u ? 0x9E3779B9u :
seed) {}
43 [[nodiscard]]
float unit() noexcept {
44 state_ ^= state_ << 13u;
45 state_ ^= state_ >> 17u;
46 state_ ^= state_ << 5u;
48 return static_cast<float>(state_ >> 8) * (1.f / 16777216.f);
56 [[nodiscard]] std::int32_t
index(std::int32_t
count)
noexcept {
57 if (
count <= 0)
return 0;
58 const auto span =
static_cast<std::uint32_t
>(
count);
59 return static_cast<std::int32_t
>(
static_cast<std::uint32_t
>(
unit() *
static_cast<float>(span))) %
count;
68 [[nodiscard]] std::int32_t
range(std::int32_t
minimum, std::int32_t maximumExclusive)
noexcept {
78 [[nodiscard]]
bool chance(
float probability)
noexcept {
return unit() < probability; }
90[[nodiscard]] std::uint32_t mix(std::uint32_t
x, std::uint32_t
y, std::uint32_t
z)
noexcept {
91 std::uint32_t
h =
x * 0x9E3779B1u;
93 h = (
h ^ (
h >> 15)) * 0xC2B2AE3Du;
95 h = (
h ^ (
h >> 13)) * 0x165667B1u;
100[[nodiscard]]
float lattice(std::int32_t ix, std::int32_t iz, std::uint32_t channel)
noexcept {
101 const std::uint32_t hashed = mix(
static_cast<std::uint32_t
>(ix),
static_cast<std::uint32_t
>(iz), channel);
102 return static_cast<float>(hashed >> 8) * (1.f / 16777216.f);
117[[nodiscard]]
float cellNoise(std::int32_t
x, std::int32_t
z, std::uint32_t channel)
noexcept {
118 constexpr float kScale = 0.1f;
119 const float fx =
static_cast<float>(
x) * kScale;
120 const float fz =
static_cast<float>(
z) * kScale;
121 const float bx = std::floor(fx);
122 const float bz = std::floor(fz);
123 const auto ix =
static_cast<std::int32_t
>(
bx);
124 const auto iz =
static_cast<std::int32_t
>(
bz);
125 const float tx = fx -
bx;
126 const float tz = fz -
bz;
127 const float sx = tx * tx * (3.f - 2.f * tx);
128 const float sz = tz * tz * (3.f - 2.f * tz);
130 const float c00 = lattice(ix, iz, channel);
131 const float c10 = lattice(ix + 1, iz, channel);
132 const float c01 = lattice(ix, iz + 1, channel);
133 const float c11 = lattice(ix + 1, iz + 1, channel);
134 const float top = c00 + (c10 - c00) *
sx;
135 const float bottom = c01 + (c11 - c01) *
sx;
142[[nodiscard]] std::int32_t roundToInt(
float value)
noexcept {
143 return static_cast<std::int32_t
>(std::floor(
value + 0.5f));
147[[nodiscard]] std::int32_t clampInt(std::int32_t
value, std::int32_t low, std::int32_t high)
noexcept {
158[[nodiscard]] std::int32_t clampSetting(std::int32_t
value, std::int32_t low, std::int32_t high,
159 std::int32_t fallback)
noexcept {
160 if (value < low || value > high)
return fallback;
169struct NormalizedSettings {
211[[nodiscard]] NormalizedSettings
normalize(
const HexMapGeneratorSettings&
settings)
noexcept {
212 NormalizedSettings out;
213 out.highRiseProbability = std::clamp(
settings.highRiseProbability, 0.f, 1.f);
214 out.sinkProbability = std::clamp(
settings.sinkProbability, 0.f, 1.f);
215 out.jitterProbability = std::clamp(
settings.jitterProbability, 0.f, 1.f);
216 out.chunkSizeMin = clampSetting(
settings.chunkSizeMin, 1, 100000, out.chunkSizeMin);
217 out.chunkSizeMax = clampSetting(
settings.chunkSizeMax, 2, 100000, out.chunkSizeMax);
218 out.landPercentage = clampSetting(
settings.landPercentage, 0, 100, out.landPercentage);
222 out.mapBorderX = clampSetting(
settings.mapBorderX, 0, 1000, out.mapBorderX);
223 out.mapBorderZ = clampSetting(
settings.mapBorderZ, 0, 1000, out.mapBorderZ);
224 out.regionBorder = clampSetting(
settings.regionBorder, 0, 1000, out.regionBorder);
225 out.regionCount = clampSetting(
settings.regionCount, 1, 4, out.regionCount);
226 out.erosionPercentage = clampSetting(
settings.erosionPercentage, 0, 100, out.erosionPercentage);
227 out.startingMoisture = std::clamp(
settings.startingMoisture, 0.f, 1.f);
228 out.evaporationFactor = std::clamp(
settings.evaporationFactor, 0.f, 1.f);
229 out.precipitationFactor = std::clamp(
settings.precipitationFactor, 0.f, 1.f);
230 out.runoffFactor = std::clamp(
settings.runoffFactor, 0.f, 1.f);
231 out.seepageFactor = std::clamp(
settings.seepageFactor, 0.f, 1.f);
232 out.windDirection =
settings.windDirection;
233 out.windStrength = std::clamp(
settings.windStrength, 0.f, 1000.f);
234 out.riverPercentage = clampSetting(
settings.riverPercentage, 0, 100, out.riverPercentage);
235 out.extraLakeProbability = std::clamp(
settings.extraLakeProbability, 0.f, 1.f);
236 out.lowTemperature = std::clamp(
settings.lowTemperature, -10.f, 10.f);
237 out.highTemperature = std::clamp(
settings.highTemperature, -10.f, 10.f);
238 out.temperatureJitter = std::clamp(
settings.temperatureJitter, 0.f, 10.f);
264void createRegions(std::int32_t
count, std::int32_t cellCountX, std::int32_t cellCountZ,
265 const NormalizedSettings&
settings, std::vector<MapRegion>& out) {
267 const std::int32_t borderX =
settings.mapBorderX;
268 const std::int32_t borderZ =
settings.mapBorderZ;
269 const std::int32_t midX = cellCountX / 2;
270 const std::int32_t midZ = cellCountZ / 2;
271 const std::int32_t thirdX = cellCountX / 3;
278 first.zMin = borderZ;
279 first.zMax = cellCountZ - borderZ;
281 second.zMax = cellCountZ - borderZ;
282 first.xMin = borderX;
285 second.xMax = cellCountX - borderX;
286 out.push_back(
first);
294 first.zMin = borderZ;
295 first.zMax = cellCountZ - borderZ;
297 second.zMax = cellCountZ - borderZ;
298 third.zMin = borderZ;
299 third.zMax = cellCountZ - borderZ;
300 first.xMin = borderX;
305 third.xMax = cellCountX - borderX;
306 out.push_back(
first);
308 out.push_back(
third);
316 first.xMin = borderX;
317 first.zMin = borderZ;
322 second.xMax = cellCountX - borderX;
326 third.xMax = cellCountX - borderX;
327 third.zMax = cellCountZ - borderZ;
328 fourth.xMin = borderX;
329 fourth.zMin = midZ +
border;
330 fourth.xMax = midX -
border;
331 fourth.zMax = cellCountZ - borderZ;
332 out.push_back(
first);
334 out.push_back(
third);
335 out.push_back(fourth);
341 only.xMax = cellCountX - borderX;
343 only.zMax = cellCountZ - borderZ;
356[[nodiscard]]
bool regionsUsable(
const std::vector<MapRegion>& regions, std::int32_t
regionCount) {
357 if (
static_cast<std::int32_t
>(regions.size()) !=
regionCount)
return false;
358 for (
const MapRegion& region : regions) {
359 if (!region.valid())
return false;
377constexpr float kTemperatureBands[3] = {0.1f, 0.3f, 0.6f};
379constexpr float kMoistureBands[3] = {0.12f, 0.28f, 0.85f};
381constexpr Biome kBiomes[16] = {{0, 0}, {4, 0}, {4, 0}, {4, 0}, {0, 0}, {2, 0}, {2, 1}, {2, 2},
382 {0, 0}, {1, 0}, {1, 1}, {1, 2}, {0, 0}, {1, 1}, {1, 2}, {1, 3}};
385struct GeneratorScratch {
418void writeElevation(HexMap& map, HexCoordinates coordinates, std::int32_t
elevation) {
419 map.setElevation(coordinates,
elevation).ignore(
"generator writes are clamped per cell");
428[[nodiscard]] std::int32_t waterLevelOf(
const HexMap& map, HexCoordinates coordinates)
noexcept {
429 return map.waterLevel(coordinates);
433void writeWaterLevel(HexMap& map, HexCoordinates coordinates, std::int32_t
waterLevel) {
434 map.setWaterLevel(coordinates,
waterLevel).ignore(
"generator writes are clamped per cell");
449[[nodiscard]] std::int32_t randomCellIndex(
const HexMap& map,
const MapRegion& region, HexRandom&
random) {
450 const std::int32_t
offsetX =
random.range(region.xMin, region.xMax);
451 const std::int32_t offsetZ =
random.range(region.zMin, region.zMax);
453 const std::int32_t
index = map.indexOf(
cell);
474std::int32_t raiseTerrain(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings,
475 const MapRegion& region, std::int32_t chunkSize, std::int32_t budget) {
476 const std::int32_t
phase = scratch.search.beginPhase();
477 const std::int32_t
first = randomCellIndex(map, region,
random);
478 scratch.search.data(
first).searchPhase =
phase;
479 scratch.search.enqueue(
first);
480 const HexCoordinates
center = map.coordinatesAt(
first);
482 const std::int32_t rise =
random.chance(
settings.highRiseProbability) ? 2 : 1;
483 std::int32_t
size = 0;
484 while (
size < chunkSize) {
485 std::int32_t
index = -1;
488 const std::int32_t originalElevation = map.elevation(map.coordinatesAt(
index));
489 const std::int32_t newElevation = originalElevation + rise;
490 if (newElevation >
settings.elevationMaximum)
continue;
492 const HexCoordinates coordinates = map.coordinatesAt(
index);
493 writeElevation(map, coordinates, newElevation);
494 if (originalElevation < settings.waterLevel && newElevation >=
settings.waterLevel) {
496 if (budget == 0)
break;
501 HexCoordinates neighbour{};
502 if (!map.getNeighbor(coordinates,
static_cast<HexDirection>(i), neighbour))
continue;
503 const std::int32_t neighbourIndex = map.indexOf(neighbour);
504 if (neighbourIndex < 0)
continue;
505 if (scratch.search.data(neighbourIndex).searchPhase >=
phase)
continue;
506 HexSearchData& record = scratch.search.data(neighbourIndex);
507 record.searchPhase =
phase;
508 record.distance = neighbour.distanceTo(
center);
509 record.heuristic =
random.chance(
settings.jitterProbability) ? 1 : 0;
510 scratch.search.enqueue(neighbourIndex);
532std::int32_t sinkTerrain(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings,
533 const MapRegion& region, std::int32_t chunkSize, std::int32_t budget) {
534 const std::int32_t
phase = scratch.search.beginPhase();
535 const std::int32_t
first = randomCellIndex(map, region,
random);
536 scratch.search.data(
first).searchPhase =
phase;
537 scratch.search.enqueue(
first);
538 const HexCoordinates
center = map.coordinatesAt(
first);
540 const std::int32_t sink =
random.chance(
settings.highRiseProbability) ? 2 : 1;
541 std::int32_t
size = 0;
542 while (
size < chunkSize) {
543 std::int32_t
index = -1;
546 const HexCoordinates coordinates = map.coordinatesAt(
index);
547 const std::int32_t originalElevation = map.elevation(coordinates);
548 const std::int32_t newElevation = originalElevation - sink;
549 if (newElevation <
settings.elevationMinimum)
continue;
551 writeElevation(map, coordinates, newElevation);
552 if (originalElevation >=
settings.waterLevel && newElevation <
settings.waterLevel) ++budget;
556 HexCoordinates neighbour{};
557 if (!map.getNeighbor(coordinates,
static_cast<HexDirection>(i), neighbour))
continue;
558 const std::int32_t neighbourIndex = map.indexOf(neighbour);
559 if (neighbourIndex < 0)
continue;
560 if (scratch.search.data(neighbourIndex).searchPhase >=
phase)
continue;
561 HexSearchData& record = scratch.search.data(neighbourIndex);
562 record.searchPhase =
phase;
563 record.distance = neighbour.distanceTo(
center);
564 record.heuristic =
random.chance(
settings.jitterProbability) ? 1 : 0;
565 scratch.search.enqueue(neighbourIndex);
585void createLand(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings,
586 const std::vector<MapRegion>& regions) {
587 const std::int32_t cellCount = map.cellCount();
588 const float cellShare =
static_cast<float>(cellCount) * 0.01f;
589 std::int32_t landBudget = roundToInt(cellShare *
static_cast<float>(
settings.landPercentage));
590 scratch.landCells = landBudget;
592 for (std::int32_t guard = 0; guard < 10000; ++guard) {
594 for (
const MapRegion& region : regions) {
597 landBudget = sinkTerrain(map, scratch,
random,
settings, region, chunkSize, landBudget);
599 landBudget = raiseTerrain(map, scratch,
random,
settings, region, chunkSize, landBudget);
600 if (landBudget == 0)
return;
604 if (landBudget > 0) {
607 scratch.landCells -= landBudget;
621[[nodiscard]]
bool isErodible(
const HexMap& map, HexCoordinates coordinates) {
622 const std::int32_t threshold = map.elevation(coordinates) - 2;
624 HexCoordinates neighbour{};
625 if (!map.getNeighbor(coordinates,
static_cast<HexDirection>(i), neighbour))
continue;
626 if (map.elevation(neighbour) <= threshold)
return true;
642[[nodiscard]] std::int32_t erosionTarget(
const HexMap& map, HexCoordinates coordinates) {
643 const std::int32_t threshold = map.elevation(coordinates) - 2;
645 HexCoordinates neighbour{};
646 if (!map.getNeighbor(coordinates,
static_cast<HexDirection>(i), neighbour))
continue;
647 if (map.elevation(neighbour) <= threshold)
return map.indexOf(neighbour);
664void addErodible(GeneratorScratch& scratch, std::int32_t
index, std::int32_t
elevation) {
665 if (
index < 0 ||
static_cast<std::size_t
>(
index) >= scratch.erodibleFlag.size())
return;
666 if (scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] != 0
u)
return;
667 scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] = 1u;
668 scratch.erodible.push_back(
index);
669 HexSearchData& record = scratch.search.data(
index);
671 record.heuristic = 0;
672 scratch.search.enqueue(
index);
680void removeErodible(GeneratorScratch& scratch, std::int32_t
index) {
681 if (
index < 0 ||
static_cast<std::size_t
>(
index) >= scratch.erodibleFlag.size())
return;
682 if (scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] == 0
u)
return;
683 scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] = 0
u;
684 if (!scratch.erodible.empty() && scratch.erodible.back() ==
index) {
685 scratch.erodible.pop_back();
688 const auto position = std::find(scratch.erodible.begin(), scratch.erodible.end(),
index);
689 if (
position == scratch.erodible.end())
return;
690 *
position = scratch.erodible.back();
691 scratch.erodible.pop_back();
704void requeueErodible(GeneratorScratch& scratch, std::int32_t
index, std::int32_t previousElevation) {
705 if (
index < 0 ||
static_cast<std::size_t
>(
index) >= scratch.erodibleFlag.size())
return;
706 if (scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] == 0
u)
return;
707 scratch.search.change(
index, previousElevation + 15);
722void erodeOnce(HexMap& map, GeneratorScratch& scratch, std::int32_t cellIndex) {
723 if (cellIndex < 0 || cellIndex >= map.cellCount())
return;
724 const HexCoordinates cellCoordinates = map.coordinatesAt(cellIndex);
725 const std::int32_t cellElevation = map.elevation(cellCoordinates);
726 const std::int32_t targetIndex = erosionTarget(map, cellCoordinates);
727 if (targetIndex < 0) {
728 removeErodible(scratch, cellIndex);
731 const HexCoordinates targetCoordinates = map.coordinatesAt(targetIndex);
732 const std::int32_t targetElevation = map.elevation(targetCoordinates);
737 writeElevation(map, cellCoordinates, cellElevation - 1);
738 writeElevation(map, targetCoordinates, targetElevation + 1);
739 requeueErodible(scratch, cellIndex, cellElevation);
740 requeueErodible(scratch, targetIndex, targetElevation);
742 if (!isErodible(map, cellCoordinates)) removeErodible(scratch, cellIndex);
745 HexCoordinates neighbour{};
746 if (!map.getNeighbor(cellCoordinates,
static_cast<HexDirection>(i), neighbour))
continue;
747 const std::int32_t neighbourIndex = map.indexOf(neighbour);
748 if (neighbourIndex < 0)
continue;
749 const std::int32_t neighbourElevation = map.elevation(neighbour);
750 if (neighbourElevation != cellElevation + 2)
continue;
751 addErodible(scratch, neighbourIndex, neighbourElevation);
754 if (isErodible(map, targetCoordinates)) addErodible(scratch, targetIndex, map.elevation(targetCoordinates));
757 HexCoordinates neighbour{};
758 if (!map.getNeighbor(targetCoordinates,
static_cast<HexDirection>(i), neighbour))
continue;
759 const std::int32_t neighbourIndex = map.indexOf(neighbour);
760 if (neighbourIndex < 0 || neighbourIndex == cellIndex)
continue;
761 if (map.elevation(neighbour) != targetElevation + 1)
continue;
762 if (!isErodible(map, neighbour)) removeErodible(scratch, neighbourIndex);
778void erodeLand(HexMap& map, GeneratorScratch& scratch,
const NormalizedSettings&
settings) {
779 const std::int32_t cellCount = map.cellCount();
780 scratch.erodible.clear();
781 scratch.erodibleFlag.assign(
static_cast<std::size_t
>(cellCount), 0
u);
784 (void)scratch.search.beginPhase();
787 const HexCoordinates coordinates = map.coordinatesAt(
index);
788 if (isErodible(map, coordinates)) addErodible(scratch,
index, map.elevation(coordinates));
791 const auto target =
static_cast<std::size_t
>(
static_cast<float>(scratch.erodible.size()) *
792 (100.f -
static_cast<float>(
settings.erosionPercentage)) * 0.01f);
793 while (scratch.erodible.size() >
target) {
794 std::int32_t
index = -1;
796 if (scratch.erodibleFlag[
static_cast<std::size_t
>(
index)] == 0
u)
continue;
797 erodeOnce(map, scratch,
index);
821[[nodiscard]]
float determineTemperature(
const HexMap& map, HexCoordinates coordinates,
822 const NormalizedSettings&
settings, std::int32_t jitterChannel)
noexcept {
823 const float latitude =
static_cast<float>(coordinates.z) /
static_cast<float>(map.cellCountZ());
826 const std::int32_t viewElevation = map.values(coordinates).viewElevation();
827 const float span =
static_cast<float>(
settings.elevationMaximum -
settings.waterLevel + 1);
828 const float elevationFactor =
829 1.f -
static_cast<float>(viewElevation -
settings.waterLevel) / (span == 0.f ? 1.f : span);
832 const HexVec3
position = map.cellGroundPosition(coordinates);
833 const auto channel =
static_cast<std::uint32_t
>(clampInt(jitterChannel, 0, 3)) * 0x9E3779B9u;
835 cellNoise(
static_cast<std::int32_t
>(
position.x),
static_cast<std::int32_t
>(
position.z), channel);
851void evolveClimate(
const HexMap& map, GeneratorScratch& scratch,
const NormalizedSettings&
settings,
852 std::int32_t cellIndex) {
853 const HexCoordinates coordinates = map.coordinatesAt(cellIndex);
854 const HexValues
values = map.values(coordinates);
855 ClimateData cellClimate = scratch.climate[
static_cast<std::size_t
>(cellIndex)];
857 if (
values.isUnderwater()) {
858 cellClimate.moisture = 1.f;
859 cellClimate.clouds +=
settings.evaporationFactor;
861 const float evaporation = cellClimate.moisture *
settings.evaporationFactor;
862 cellClimate.moisture -= evaporation;
863 cellClimate.clouds += evaporation;
866 const float precipitation = cellClimate.clouds *
settings.precipitationFactor;
867 cellClimate.clouds -= precipitation;
868 cellClimate.moisture += precipitation;
870 const float maximumElevation =
static_cast<float>(
settings.elevationMaximum);
871 const float cloudMaximum = 1.f -
static_cast<float>(
values.viewElevation()) / (maximumElevation + 1.f);
872 if (cellClimate.clouds > cloudMaximum) {
873 cellClimate.moisture += cellClimate.clouds - cloudMaximum;
874 cellClimate.clouds = cloudMaximum;
878 const float cloudDispersal = cellClimate.clouds * (1.f / (5.f +
settings.windStrength));
879 const float runoff = cellClimate.moisture *
settings.runoffFactor * (1.f / 6.f);
880 const float seepage = cellClimate.moisture *
settings.seepageFactor * (1.f / 6.f);
884 HexCoordinates neighbour{};
885 if (!map.getNeighbor(coordinates,
direction, neighbour))
continue;
886 const std::int32_t neighbourIndex = map.indexOf(neighbour);
887 if (neighbourIndex < 0)
continue;
889 ClimateData neighbourClimate = scratch.nextClimate[
static_cast<std::size_t
>(neighbourIndex)];
890 if (
direction == mainDispersalDirection) {
891 neighbourClimate.clouds += cloudDispersal *
settings.windStrength;
893 neighbourClimate.clouds += cloudDispersal;
896 const std::int32_t elevationDelta = map.values(neighbour).viewElevation() -
values.viewElevation();
897 if (elevationDelta < 0) {
898 cellClimate.moisture -= runoff;
899 neighbourClimate.moisture += runoff;
900 }
else if (elevationDelta == 0) {
901 cellClimate.moisture -= seepage;
902 neighbourClimate.moisture += seepage;
904 scratch.nextClimate[
static_cast<std::size_t
>(neighbourIndex)] = neighbourClimate;
907 ClimateData nextCell = scratch.nextClimate[
static_cast<std::size_t
>(cellIndex)];
908 nextCell.moisture += cellClimate.moisture;
909 if (nextCell.moisture > 1.f) nextCell.moisture = 1.f;
910 scratch.nextClimate[
static_cast<std::size_t
>(cellIndex)] = nextCell;
911 scratch.climate[
static_cast<std::size_t
>(cellIndex)] = ClimateData{};
924void createClimate(
const HexMap& map, GeneratorScratch& scratch,
const NormalizedSettings&
settings) {
925 const auto cellCount =
static_cast<std::size_t
>(map.cellCount());
927 initial.moisture =
settings.startingMoisture;
928 scratch.climate.assign(cellCount, initial);
929 scratch.nextClimate.assign(cellCount, ClimateData{});
931 for (std::int32_t cycle = 0; cycle < 40; ++cycle) {
933 std::swap(scratch.climate, scratch.nextClimate);
952void collectRiverOrigins(
const HexMap& map, GeneratorScratch& scratch,
const NormalizedSettings&
settings) {
953 scratch.riverOrigins.clear();
954 const float span =
static_cast<float>(
settings.elevationMaximum -
settings.waterLevel);
956 const HexCoordinates coordinates = map.coordinatesAt(
index);
957 const HexValues
values = map.values(coordinates);
958 if (
values.isUnderwater())
continue;
960 static_cast<float>(
values.elevation() -
settings.waterLevel) / (span == 0.f ? 1.f : span);
962 scratch.riverOrigins.push_back(
index);
963 scratch.riverOrigins.push_back(
index);
965 if (
weight > 0.5f) scratch.riverOrigins.push_back(
index);
966 if (
weight > 0.25f) scratch.riverOrigins.push_back(
index);
986std::int32_t createRiver(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings,
987 std::int32_t originIndex) {
989 HexCoordinates cellCoordinates = map.coordinatesAt(originIndex);
990 HexValues cellValues = map.values(cellCoordinates);
991 bool cellUnderwater = cellValues.isUnderwater();
996 while (!cellUnderwater) {
997 std::int32_t minNeighborElevation = std::numeric_limits<std::int32_t>::max();
998 scratch.flowDirections.clear();
1001 HexCoordinates neighbour{};
1002 if (!map.getNeighbor(cellCoordinates, candidate, neighbour))
continue;
1003 const std::int32_t neighbourIndex = map.indexOf(neighbour);
1004 if (neighbourIndex < 0)
continue;
1005 const HexCellData* neighbourCell = map.cellAt(neighbourIndex);
1006 if (neighbourCell ==
nullptr)
continue;
1008 const std::int32_t neighbourElevation = neighbourCell->values.elevation();
1009 if (neighbourElevation < minNeighborElevation) minNeighborElevation = neighbourElevation;
1011 if (neighbourIndex == originIndex || neighbourCell->flags.hasAnyRiverIn())
continue;
1013 const std::int32_t delta = neighbourElevation - cellValues.elevation();
1014 if (delta > 0)
continue;
1016 if (neighbourCell->flags.hasAnyRiverOut()) {
1017 map.setOutgoingRiver(cellCoordinates, candidate).ignore(
"river join is validated by the map");
1022 scratch.flowDirections.push_back(candidate);
1023 scratch.flowDirections.push_back(candidate);
1024 scratch.flowDirections.push_back(candidate);
1029 scratch.flowDirections.push_back(candidate);
1031 scratch.flowDirections.push_back(candidate);
1034 if (scratch.flowDirections.empty()) {
1035 if (
length == 1)
return 0;
1036 if (minNeighborElevation >= cellValues.elevation()) {
1037 writeWaterLevel(map, cellCoordinates, minNeighborElevation);
1038 if (minNeighborElevation == cellValues.elevation()) {
1039 writeElevation(map, cellCoordinates, minNeighborElevation - 1);
1045 const auto chosen = scratch.flowDirections[
static_cast<std::size_t
>(
1046 random.index(
static_cast<std::int32_t
>(scratch.flowDirections.size())))];
1047 const std::int32_t outgoingIndex = map.indexOf(cellCoordinates.step(chosen));
1048 if (outgoingIndex < 0)
break;
1050 map.setOutgoingRiver(cellCoordinates, chosen).ignore(
"flow direction was selected downhill");
1054 if (minNeighborElevation >= cellValues.elevation() &&
random.chance(
settings.extraLakeProbability)) {
1055 writeWaterLevel(map, cellCoordinates, cellValues.elevation());
1056 writeElevation(map, cellCoordinates, cellValues.elevation() - 1);
1059 cellCoordinates = map.coordinatesAt(outgoingIndex);
1060 cellValues = map.values(cellCoordinates);
1061 cellUnderwater = cellValues.isUnderwater();
1078void createRivers(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings) {
1079 collectRiverOrigins(map, scratch,
settings);
1080 std::int32_t riverBudget =
1081 roundToInt(
static_cast<float>(scratch.landCells) *
static_cast<float>(
settings.riverPercentage) * 0.01f);
1082 while (riverBudget > 0 && !scratch.riverOrigins.empty()) {
1084 static_cast<std::size_t
>(
random.index(
static_cast<std::int32_t
>(scratch.riverOrigins.size())));
1085 const std::int32_t originIndex = scratch.riverOrigins[
position];
1086 scratch.riverOrigins[
position] = scratch.riverOrigins.back();
1087 scratch.riverOrigins.pop_back();
1089 const HexCellData*
origin = map.cellAt(originIndex);
1090 if (
origin ==
nullptr ||
origin->flags.hasRiver())
continue;
1091 bool validOrigin =
true;
1093 HexCoordinates neighbour{};
1094 if (!map.getNeighbor(map.coordinatesAt(originIndex),
static_cast<HexDirection>(i), neighbour))
continue;
1095 const HexCellData* neighbourCell = map.cell(neighbour);
1096 if (neighbourCell ==
nullptr)
continue;
1097 if (neighbourCell->flags.hasRiver() || neighbourCell->values.isUnderwater()) validOrigin =
false;
1099 if (!validOrigin)
continue;
1100 riverBudget -= createRiver(map, scratch,
random,
settings, originIndex);
1120[[nodiscard]] std::int32_t underwaterTerrain(
const HexMap& map, HexCoordinates coordinates,
1121 const NormalizedSettings&
settings) {
1122 const std::int32_t
elevation = map.elevation(coordinates);
1124 const std::int32_t
water = waterLevelOf(map, coordinates);
1125 std::int32_t cliffs = 0;
1126 std::int32_t slopes = 0;
1128 HexCoordinates neighbour{};
1129 if (!map.getNeighbor(coordinates,
static_cast<HexDirection>(i), neighbour))
continue;
1130 const std::int32_t delta = map.elevation(neighbour) -
water;
1133 }
else if (delta > 0) {
1137 if (cliffs + slopes > 3)
return 1;
1138 if (cliffs > 0)
return 3;
1139 if (slopes > 0)
return 0;
1160void setTerrainType(HexMap& map, GeneratorScratch& scratch, HexRandom&
random,
const NormalizedSettings&
settings) {
1161 const std::int32_t jitterChannel =
random.index(4);
1162 const std::int32_t rockDesertElevation =
1166 const HexCoordinates coordinates = map.coordinatesAt(
index);
1167 HexValues
values = map.values(coordinates);
1168 const float temperature = determineTemperature(map, coordinates,
settings, jitterChannel);
1171 if (!
values.isUnderwater()) {
1172 std::int32_t temperatureBand = 0;
1173 for (; temperatureBand < 3; ++temperatureBand) {
1174 if (
temperature < kTemperatureBands[temperatureBand])
break;
1176 std::int32_t moistureBand = 0;
1177 for (; moistureBand < 3; ++moistureBand) {
1178 if (
moisture < kMoistureBands[moistureBand])
break;
1180 Biome
biome = kBiomes[
static_cast<std::size_t
>(temperatureBand * 4 + moistureBand)];
1182 if (
biome.terrain == 0) {
1183 if (
values.elevation() >= rockDesertElevation)
biome.terrain = 3;
1188 if (
biome.terrain == 4) {
1190 }
else if (
biome.plant < 3 && map.hasRiver(coordinates)) {
1199 map.setCellState(coordinates,
values, map.flags(coordinates))
1200 .ignore(
"terrain classification writes the values it just read back");
1217void finalizeGrid(HexMap& map) { map.markAllChunksDirty(); }
1224 const NormalizedSettings normalized = normalize(
settings);
1226 std::vector<MapRegion> regions;
1227 createRegions(normalized.regionCount, map.
cellCountX(), map.
cellCountZ(), normalized, regions);
1228 if (!regionsUsable(regions, normalized.regionCount))
1231 GeneratorScratch scratch;
1233 scratch.erodibleFlag.assign(
static_cast<std::size_t
>(map.
cellCount()), 0
u);
1234 scratch.climate.assign(
static_cast<std::size_t
>(map.
cellCount()), ClimateData{});
1235 scratch.nextClimate.assign(
static_cast<std::size_t
>(map.
cellCount()), ClimateData{});
1236 scratch.landCells = 0;
1242 writeElevation(map, coordinates, normalized.elevationMinimum);
1243 writeWaterLevel(map, coordinates, normalized.waterLevel);
1246 createLand(map, scratch,
random, normalized, regions);
1247 erodeLand(map, scratch, normalized);
1248 createClimate(map, scratch, normalized);
1249 createRivers(map, scratch,
random, normalized);
1250 setTerrainType(map, scratch,
random, normalized);
Stable, structured diagnostics shared by engine modules.
std::map< std::string, Var > values
std::int32_t riverPercentage
float precipitationFactor
std::int32_t regionBorder
std::int32_t landCells
Number of cells that count as land, driving the river budget.
std::vector< HexDirection > flowDirections
Candidate directions considered by one river step.
std::int32_t elevationMinimum
HexSearchContext search
Frontier reused by the land-growth and erosion searches.
std::int32_t erosionPercentage
std::vector< std::int32_t > riverOrigins
Weighted river-origin candidates.
std::vector< std::int32_t > erodible
Cells still eligible for erosion.
std::int32_t chunkSizeMax
float highRiseProbability
std::int32_t landPercentage
std::int32_t elevationMaximum
std::vector< ClimateData > climate
Current moisture and cloud state per cell.
HexDirection windDirection
std::int32_t chunkSizeMin
float extraLakeProbability
std::vector< std::uint8_t > erodibleFlag
Membership flags parallel to the erodible list.
std::vector< ClimateData > nextClimate
Per-cell accumulation target of the next climate cycle.
Deterministic procedural map generation for the hex grid.
Cell-graph search used by pathfinding and visibility.
std::array< float, 3 > position
RoadLaneDirection direction
TerrainThermalSettings settings
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
An editable, chunked, pointy-top hex map.
bool empty() const noexcept
Whether the map holds any cell.
HexCoordinates coordinatesAt(std::int32_t index) const noexcept
Coordinates of a linear cell index; out-of-range indices return (0, 0).
std::int32_t cellCountX() const noexcept
Number of columns.
std::int32_t cellCount() const noexcept
Total number of cells.
std::int32_t cellCountZ() const noexcept
Number of rows.
static constexpr int kMinElevation
Minimum editable elevation.
static constexpr int kMaxElevation
Maximum editable elevation.
@ Cell
A cell was removed; the out-parameter holds it.
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
constexpr HexDirection previous2(HexDirection d) noexcept
Two steps counter-clockwise.
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
constexpr HexDirection next2(HexDirection d) noexcept
Two steps clockwise.
HexDirection
Hex facing directions, counter-clockwise from north-east.
Result< void > generateHexMap(HexMap &map, const HexMapGeneratorSettings &settings)
Replaces the grid's contents with a procedurally generated map.
Vec2 normalize(const Vec2 &a)
Normalize.
Axial coordinates of one hex cell.
static constexpr HexCoordinates fromOffset(std::int32_t offsetX, std::int32_t offsetZ) noexcept
Converts an odd-row offset pair into axial coordinates.
Tunables of the reference project's map generator.