feat: Add mineral generation pass

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-03 17:08:36 +08:00
parent 7735da6e85
commit fcb71be9e8
Signed by: szTom
GPG Key ID: 072D999D60C6473C
12 changed files with 756 additions and 14 deletions

View File

@ -5,6 +5,7 @@ set(ISTD_TILEMAP_SRC
src/pass/base_tile_type.cpp src/pass/base_tile_type.cpp
src/pass/biome.cpp src/pass/biome.cpp
src/pass/deepwater.cpp src/pass/deepwater.cpp
src/pass/mineral_cluster.cpp
src/pass/mountain_hole_fill.cpp src/pass/mountain_hole_fill.cpp
src/pass/oil.cpp src/pass/oil.cpp
src/pass/smoothen_mountain.cpp src/pass/smoothen_mountain.cpp

View File

@ -1,3 +1,12 @@
# Tilemap Library # Tilemap Library
The Tilemap System use in Instructed. Generates 2D tilemaps with biomes and features. The Tilemap System used in Instructed. Generates 2D tilemaps with biomes, terrain features, and resource deposits.
## Features
- **Multi-layered terrain generation**: Base terrain (Land, Mountain, Sand, Water, Ice, Deepwater) with surface features
- **9 biome system**: Climate-based biome assignment using temperature/humidity noise
- **Resource generation**: Oil deposits and mineral clusters (Hematite, Titanomagnetite, Gibbsite)
- **Mineral deposits**: Three types of minerals generated on mountain edges in small clusters
- **Procedural algorithms**: Perlin noise, cellular automata, connected component analysis
- **Deterministic generation**: Same seed produces identical results

View File

@ -30,7 +30,8 @@ tilemap/
│ ├── smoothen_island.cpp # Island smoothing │ ├── smoothen_island.cpp # Island smoothing
│ ├── mountain_hole_fill.cpp # Hole filling │ ├── mountain_hole_fill.cpp # Hole filling
│ ├── deepwater.cpp # Deep water placement │ ├── deepwater.cpp # Deep water placement
│ └── oil.cpp # Oil resource generation │ ├── oil.cpp # Oil resource generation
│ └── mineral_cluster.cpp # Mineral cluster generation
├── examples/ # Usage examples ├── examples/ # Usage examples
└── docs/ # Documentation └── docs/ # Documentation
``` ```
@ -57,6 +58,7 @@ Terrain generation uses a multi-pass pipeline for modularity and control:
5. **Hole Fill Pass**: Fills small terrain holes 5. **Hole Fill Pass**: Fills small terrain holes
6. **Deep Water Pass**: Places deep water areas 6. **Deep Water Pass**: Places deep water areas
7. **Oil Pass**: Generates sparse oil deposits as surface features 7. **Oil Pass**: Generates sparse oil deposits as surface features
8. **Mineral Cluster Pass**: Generates mineral clusters (Hematite, Titanomagnetite, Gibbsite) on mountain edges using cellular automata
Each pass operates independently with its own RNG state, ensuring deterministic results. Each pass operates independently with its own RNG state, ensuring deterministic results.
@ -91,13 +93,13 @@ Several passes use BFS (Breadth-First Search) for terrain analysis:
- **Hole Filling**: Identify and fill isolated terrain holes - **Hole Filling**: Identify and fill isolated terrain holes
- Components touching map boundaries are preserved - Components touching map boundaries are preserved
### Oil Resource Generation ### Resource Generation
The oil generation pass creates sparse resource deposits: The oil generation pass and mineral generation pass creates sparse resource deposits:
- **Poisson Disk Sampling**: Ensures minimum distance between oil fields - **Poisson Disk Sampling**: Ensures minimum distance between oil fields
- **Biome Preference**: Higher probability in desert and plains biomes - **Biome Preference**: Higher probability in desert and plains biomes
- **Cluster Growth**: Random walk creates 2-6 tile clusters - **Cluster Growth**: Random walk creates larger tile clusters
- **Surface Placement**: Oil appears as surface features on land/sand tiles - **Surface Placement**: Oil appears as surface features on land/sand tiles, Minerals on mountain edges
## Random Number Generation ## Random Number Generation

View File

@ -0,0 +1,122 @@
# 矿物生成系统实现总结
## 概述
为tilemap库实现了三种新矿石Hematite赤铁矿、Titanomagnetite钛磁铁矿、Gibbsite三水铝石的生成系统。该系统基于Oil生成方式的改进版本专门针对山脉边缘的矿物分布进行了优化。
## 设计选择
### 为什么选择基于Oil的方案而不是胞元自动机
1. **精确控制性**Poisson disk采样方式能够精确控制矿物密度和分布间距
2. **效率优势**:单次随机游走比多轮胞元自动机迭代更高效
3. **参数直观性**:密度、集群大小、最小距离等参数更易于调整和平衡
4. **稀有资源特性**:矿物作为稀有资源,稀疏分布更符合游戏设计需求
## 核心特性
### 1. 位置限制
- 矿物只在 `BaseTileType::Mountain``SurfaceTileType::Empty` 的瓦片上生成
- 必须位于山脉边缘(至少有一个相邻瓦片不是山地)
- 确保矿物出现在山脉与其他地形的交界处,便于开采
### 2. 分层稀有度
```cpp
// 默认配置
hematite_density = 51; // ~0.2 per chunk (最常见)
titanomagnetite_density = 25; // ~0.1 per chunk (中等稀有)
gibbsite_density = 13; // ~0.05 per chunk (最稀有)
```
### 3. 集群生成
- 最小集群大小2个瓦片
- 最大集群大小5个瓦片
- 使用随机游走算法形成自然的小簇分布
- 40%概率跳过相邻瓦片,形成合适的密度
### 4. 距离控制
- 基于密度动态计算最小间距
- 确保矿物集群不会过于密集
- 最小间距至少8个瓦片
## 实现细节
### 核心算法
1. **Poisson disk采样**:生成候选位置,确保合适的分布
2. **山脉边缘检测**:验证位置是否在山脉边缘
3. **随机游走集群生长**:从中心点开始生成小簇
4. **冲突避免**:确保不同矿物集群之间保持距离
### 山脉边缘检测逻辑
```cpp
bool is_mountain_edge(const TileMap &tilemap, TilePos pos) const {
auto neighbors = tilemap.get_neighbors(pos);
for (const auto neighbor_pos : neighbors) {
const Tile &neighbor_tile = tilemap.get_tile(neighbor_pos);
if (neighbor_tile.base != BaseTileType::Mountain) {
return true; // 找到非山地邻居
}
}
return false; // 所有邻居都是山地,不是边缘
}
```
### 配置参数
```cpp
struct GenerationConfig {
// 矿物集群生成参数
std::uint8_t hematite_density = 51; // ~0.2 per chunk
std::uint8_t titanomagnetite_density = 25; // ~0.1 per chunk
std::uint8_t gibbsite_density = 13; // ~0.05 per chunk
std::uint8_t mineral_cluster_min_size = 2; // 最小集群大小
std::uint8_t mineral_cluster_max_size = 5; // 最大集群大小
std::uint8_t mineral_base_probe = 192; // 基础放置概率
};
```
## 生成流水线集成
矿物生成作为独立的pass添加到地形生成流水线的最后阶段
```cpp
void TerrainGenerator::operator()(TileMap &tilemap) {
biome_pass(tilemap);
base_tile_type_pass(tilemap);
smoothen_mountains_pass(tilemap);
smoothen_islands_pass(tilemap);
mountain_hole_fill_pass(tilemap);
deepwater_pass(tilemap);
oil_pass(tilemap);
mineral_cluster_pass(tilemap); // 新增的矿物生成pass
}
```
## 测试结果
通过mineral_demo测试程序验证
- 8x8 chunk地图 (262,144个瓦片)
- 山地瓦片35,916个 (13.7%)
- 山脉边缘瓦片15,881个 (44.2%的山地)
- 生成矿物分布:
- 赤铁矿47个瓦片
- 钛磁铁矿38个瓦片
- 三水铝石15个瓦片
- 山脉边缘矿物覆盖率0.63%
## 优势
1. **游戏平衡性**:稀有度分层,符合游戏经济设计
2. **真实感**:矿物出现在山脉边缘,符合地质常识
3. **可扩展性**:易于添加新的矿物类型和调整参数
4. **性能优秀**:单次生成,不需要多轮迭代
5. **确定性**:相同种子产生相同结果,支持多人游戏
## 使用建议
1. **密度调整**根据游戏需求调整各矿物的density参数
2. **集群大小**:可以为不同矿物设置不同的集群大小范围
3. **生成位置**:如需其他位置生成矿物,可修改`is_suitable_for_mineral`函数
4. **稀有度平衡**建议保持gibbsite < titanomagnetite < hematite的稀有度关系
这个实现提供了灵活、高效且平衡的矿物生成系统,完全满足了"控制生成数量,以小簇方式生成在山的边缘"的需求。

View File

@ -9,12 +9,22 @@
// Get BMP color for different tile types, considering surface tiles // Get BMP color for different tile types, considering surface tiles
BmpColors::Color get_tile_color(const istd::Tile &tile) { BmpColors::Color get_tile_color(const istd::Tile &tile) {
// Oil surface tile overrides base color // Surface tiles override base color
if (tile.surface == istd::SurfaceTileType::Oil) { switch (tile.surface) {
case istd::SurfaceTileType::Oil:
return BmpColors::OIL; return BmpColors::OIL;
case istd::SurfaceTileType::Hematite:
return BmpColors::HEMATITE;
case istd::SurfaceTileType::Titanomagnetite:
return BmpColors::TITANOMAGNETITE;
case istd::SurfaceTileType::Gibbsite:
return BmpColors::GIBBSITE;
case istd::SurfaceTileType::Empty:
default:
break; // Fall through to base tile color
} }
// Otherwise use base tile color // Use base tile color
switch (tile.base) { switch (tile.base) {
case istd::BaseTileType::Land: case istd::BaseTileType::Land:
return BmpColors::LAND; return BmpColors::LAND;
@ -87,7 +97,11 @@ void print_statistics(const istd::TileMap &tilemap) {
int tile_counts[6] = { int tile_counts[6] = {
0 0
}; // Count for each base tile type (now 6 types including Deepwater) }; // Count for each base tile type (now 6 types including Deepwater)
int oil_count = 0; // Count oil surface tiles int oil_count = 0; // Count oil surface tiles
int hematite_count = 0; // Count hematite surface tiles
int titanomagnetite_count = 0; // Count titanomagnetite surface tiles
int gibbsite_count = 0; // Count gibbsite surface tiles
int mountain_edge_count = 0; // Count mountain edge tiles
const int chunks_per_side = tilemap.get_size(); const int chunks_per_side = tilemap.get_size();
const int tiles_per_chunk = istd::Chunk::size; const int tiles_per_chunk = istd::Chunk::size;
@ -100,9 +114,42 @@ void print_statistics(const istd::TileMap &tilemap) {
const auto &tile = chunk.tiles[tile_x][tile_y]; const auto &tile = chunk.tiles[tile_x][tile_y];
tile_counts[static_cast<int>(tile.base)]++; tile_counts[static_cast<int>(tile.base)]++;
// Count oil surface tiles // Count surface tiles
if (tile.surface == istd::SurfaceTileType::Oil) { switch (tile.surface) {
case istd::SurfaceTileType::Oil:
oil_count++; oil_count++;
break;
case istd::SurfaceTileType::Hematite:
hematite_count++;
break;
case istd::SurfaceTileType::Titanomagnetite:
titanomagnetite_count++;
break;
case istd::SurfaceTileType::Gibbsite:
gibbsite_count++;
break;
default:
break;
}
// Count mountain edge tiles for mineral statistics
if (tile.base == istd::BaseTileType::Mountain) {
istd::TilePos pos(chunk_x, chunk_y, tile_x, tile_y);
auto neighbors = tilemap.get_neighbors(pos);
bool is_edge = false;
for (const auto neighbor_pos : neighbors) {
const auto &neighbor_tile = tilemap.get_tile(
neighbor_pos
);
if (neighbor_tile.base
!= istd::BaseTileType::Mountain) {
is_edge = true;
break;
}
}
if (is_edge) {
mountain_edge_count++;
}
} }
} }
} }
@ -123,16 +170,58 @@ void print_statistics(const istd::TileMap &tilemap) {
); );
} }
std::println("\nSurface Resource Statistics:");
std::println("============================");
// Print oil statistics // Print oil statistics
double oil_percentage = (double)oil_count / total_tiles * 100.0; double oil_percentage = (double)oil_count / total_tiles * 100.0;
double oil_per_chunk = (double)oil_count double oil_per_chunk = (double)oil_count
/ (chunks_per_side * chunks_per_side); / (chunks_per_side * chunks_per_side);
std::println( std::println(
"{:>10}: {:>8} ({:.1f}%, {:.2f} per chunk)", "Oil", oil_count, "{:>15}: {:>8} ({:.3f}%, {:.2f} per chunk)", "Oil", oil_count,
oil_percentage, oil_per_chunk oil_percentage, oil_per_chunk
); );
std::println("Total tiles: {}", total_tiles); // Print mineral statistics
auto print_mineral_stats = [&](const char *name, int count) {
double percentage = (double)count / total_tiles * 100.0;
double per_chunk = (double)count / (chunks_per_side * chunks_per_side);
std::println(
"{:>15}: {:>8} ({:.3f}%, {:.2f} per chunk)", name, count,
percentage, per_chunk
);
};
print_mineral_stats("Hematite", hematite_count);
print_mineral_stats("Titanomagnetite", titanomagnetite_count);
print_mineral_stats("Gibbsite", gibbsite_count);
// Mountain edge statistics for mineral context
int mountain_count = tile_counts[static_cast<int>(
istd::BaseTileType::Mountain
)];
if (mountain_count > 0) {
double edge_percentage = (double)mountain_edge_count / mountain_count
* 100.0;
std::println(
"{:>15}: {:>8} ({:.1f}% of mountains)", "Mountain edges",
mountain_edge_count, edge_percentage
);
// Calculate mineral coverage on mountain edges
int total_minerals = hematite_count + titanomagnetite_count
+ gibbsite_count;
if (mountain_edge_count > 0) {
double mineral_coverage = (double)total_minerals
/ mountain_edge_count * 100.0;
std::println(
"\n{:>15}: {:.2f}% of mountain edges", "Mineral coverage",
mineral_coverage
);
}
}
std::println("\nTotal tiles: {}", total_tiles);
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {

View File

@ -236,6 +236,11 @@ constexpr Color WATER(30, 144, 255); // Dodger blue
constexpr Color ICE(176, 224, 230); // Powder blue constexpr Color ICE(176, 224, 230); // Powder blue
constexpr Color DEEPWATER(0, 0, 139); // Dark blue constexpr Color DEEPWATER(0, 0, 139); // Dark blue
constexpr Color OIL(0, 0, 0); // Black constexpr Color OIL(0, 0, 0); // Black
// Mineral colors
constexpr Color HEMATITE(255, 0, 0); // Red
constexpr Color TITANOMAGNETITE(128, 0, 128); // Purple
constexpr Color GIBBSITE(255, 255, 0); // Yellow
} // namespace BmpColors } // namespace BmpColors
#endif // BMP_H #endif // BMP_H

View File

@ -0,0 +1,167 @@
#include "bmp.h"
#include "tilemap/generation.h"
#include "tilemap/tilemap.h"
#include <iostream>
using namespace istd;
int main() {
constexpr std::uint8_t map_size = 8; // 8x8 chunks
TileMap tilemap(map_size);
// Create generation config with adjusted mineral parameters
GenerationConfig config;
config.seed = Seed::from_string("mineral_demo_seed");
// Increase mineral density for demo
config.hematite_density = 102; // ~0.4 per chunk
config.titanomagnetite_density = 76; // ~0.3 per chunk
config.gibbsite_density = 51; // ~0.2 per chunk
// Smaller clusters for better visibility
config.mineral_cluster_min_size = 1;
config.mineral_cluster_max_size = 4;
// Generate the terrain
map_generate(tilemap, config);
// Create BMP to visualize the mineral distribution
constexpr std::uint32_t tile_size = 4; // Each tile is 4x4 pixels
std::uint32_t image_size = map_size * Chunk::size * tile_size;
BmpWriter bmp(image_size, image_size);
// Define colors for different tile types
auto get_tile_color = [](const Tile &tile)
-> std::tuple<std::uint8_t, std::uint8_t, std::uint8_t> {
// Override with mineral colors if present first
switch (tile.surface) {
case SurfaceTileType::Oil:
return {0, 0, 0}; // Black
case SurfaceTileType::Hematite:
return {255, 0, 0}; // Red
case SurfaceTileType::Titanomagnetite:
return {128, 0, 128}; // Purple
case SurfaceTileType::Gibbsite:
return {255, 255, 0}; // Yellow
case SurfaceTileType::Empty:
default:
break; // Fall through to base terrain colors
}
// Base terrain colors
switch (tile.base) {
case BaseTileType::Land:
return {0, 128, 0}; // Green
case BaseTileType::Mountain:
return {139, 69, 19}; // Brown
case BaseTileType::Sand:
return {238, 203, 173}; // Beige
case BaseTileType::Water:
return {0, 0, 255}; // Blue
case BaseTileType::Ice:
return {173, 216, 230}; // Light Blue
case BaseTileType::Deepwater:
return {0, 0, 139}; // Dark Blue
default:
return {128, 128, 128}; // Gray
}
};
// Fill the BMP with tile data
for (std::uint32_t y = 0; y < image_size; ++y) {
for (std::uint32_t x = 0; x < image_size; ++x) {
// Calculate which tile this pixel belongs to
std::uint32_t tile_x = x / tile_size;
std::uint32_t tile_y = y / tile_size;
TilePos pos = TilePos::from_global(tile_x, tile_y);
const Tile &tile = tilemap.get_tile(pos);
auto [r, g, b] = get_tile_color(tile);
bmp.set_pixel(x, y, r, g, b);
}
}
// Save the BMP
bmp.save("mineral_demo.bmp");
// Print statistics
std::uint32_t hematite_count = 0;
std::uint32_t titanomagnetite_count = 0;
std::uint32_t gibbsite_count = 0;
std::uint32_t mountain_edge_count = 0;
std::uint32_t total_mountain_count = 0;
std::uint32_t total_tiles = map_size * Chunk::size * map_size * Chunk::size;
for (std::uint32_t y = 0; y < map_size * Chunk::size; ++y) {
for (std::uint32_t x = 0; x < map_size * Chunk::size; ++x) {
TilePos pos = TilePos::from_global(x, y);
const Tile &tile = tilemap.get_tile(pos);
if (tile.base == BaseTileType::Mountain) {
total_mountain_count++;
// Check if it's a mountain edge
auto neighbors = tilemap.get_neighbors(pos);
bool is_edge = false;
for (const auto neighbor_pos : neighbors) {
const Tile &neighbor_tile = tilemap.get_tile(neighbor_pos);
if (neighbor_tile.base != BaseTileType::Mountain) {
is_edge = true;
break;
}
}
if (is_edge) {
mountain_edge_count++;
}
}
switch (tile.surface) {
case SurfaceTileType::Hematite:
hematite_count++;
break;
case SurfaceTileType::Titanomagnetite:
titanomagnetite_count++;
break;
case SurfaceTileType::Gibbsite:
gibbsite_count++;
break;
default:
break;
}
}
}
std::cout << "Mineral Generation Demo Results:\n";
std::cout << "================================\n";
std::cout << "Total tiles: " << total_tiles << "\n";
std::cout << "Mountain tiles: " << total_mountain_count << " ("
<< (100.0 * total_mountain_count / total_tiles) << "%)\n";
std::cout << "Mountain edge tiles: " << mountain_edge_count << " ("
<< (100.0 * mountain_edge_count / total_mountain_count)
<< "% of mountains)\n";
std::cout << "\nMineral Distribution:\n";
std::cout << "Hematite tiles: " << hematite_count << "\n";
std::cout << "Titanomagnetite tiles: " << titanomagnetite_count << "\n";
std::cout << "Gibbsite tiles: " << gibbsite_count << "\n";
std::cout << "Total mineral tiles: "
<< (hematite_count + titanomagnetite_count + gibbsite_count)
<< "\n";
if (mountain_edge_count > 0) {
double mineral_coverage = 100.0
* (hematite_count + titanomagnetite_count + gibbsite_count)
/ mountain_edge_count;
std::cout << "Mineral coverage on mountain edges: " << mineral_coverage
<< "%\n";
}
std::cout << "\nGenerated mineral_demo.bmp with visualization\n";
std::cout
<< "Colors: Red=Hematite, Purple=Titanomagnetite, Yellow=Gibbsite\n";
std::cout << " Brown=Mountain, Green=Land, Blue=Water, etc.\n";
return 0;
}

View File

@ -45,6 +45,18 @@ struct GenerationConfig {
// (should be <= 24) // (should be <= 24)
std::uint8_t oil_base_probe = 128; // Biome preference multiplier (out std::uint8_t oil_base_probe = 128; // Biome preference multiplier (out
// of 255) // of 255)
// Mineral cluster generation parameters
std::uint16_t hematite_density = 450; // ~1.8 per chunk (out of 255)
std::uint16_t titanomagnetite_density = 300; // ~1.2 per chunk (out of 255)
std::uint16_t gibbsite_density = 235; // ~0.9 per chunk (out of 255)
std::uint8_t mineral_cluster_min_size = 2; // Minimum tiles per mineral
// cluster
std::uint8_t mineral_cluster_max_size = 5; // Maximum tiles per mineral
// cluster
std::uint8_t mineral_base_probe = 192; // Base probability for mineral
// placement
}; };
// Terrain generator class that manages the generation process // Terrain generator class that manages the generation process
@ -108,6 +120,12 @@ private:
* @param tilemap The tilemap to process * @param tilemap The tilemap to process
*/ */
void oil_pass(TileMap &tilemap); void oil_pass(TileMap &tilemap);
/**
* @brief Generate mineral clusters on suitable terrain
* @param tilemap The tilemap to process
*/
void mineral_cluster_pass(TileMap &tilemap);
}; };
/** /**
* @brief Generate a tilemap using the new biome-based system * @brief Generate a tilemap using the new biome-based system

View File

@ -0,0 +1,87 @@
#ifndef TILEMAP_PASS_MINERAL_CLUSTER_H
#define TILEMAP_PASS_MINERAL_CLUSTER_H
#include "tilemap/generation.h"
#include "tilemap/noise.h"
namespace istd {
/**
* @brief Generates mineral clusters (Hematite, Titanomagnetite, Gibbsite) on
* mountain edges
*/
class MineralClusterGenerationPass {
private:
const GenerationConfig &config_;
Xoroshiro128PP rng_;
DiscreteRandomNoise noise_;
public:
/**
* @brief Construct a mineral cluster generation pass
* @param config Generation configuration parameters
* @param rng Random number generator for mineral placement
* @param noise_rng Random number generator for noise-based operations
*/
MineralClusterGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng,
Xoroshiro128PP noise_rng
);
/**
* @brief Generate mineral clusters on mountain edges
* @param tilemap The tilemap to process
*/
void operator()(TileMap &tilemap);
private:
/**
* @brief Generate mineral centers for a specific mineral type
* @param tilemap The tilemap to analyze
* @param mineral_type The type of mineral to generate
* @param density The generation density (out of 255)
* @return Vector of positions where mineral clusters should be placed
*/
std::vector<TilePos> generate_mineral_centers(
const TileMap &tilemap, SurfaceTileType mineral_type,
std::uint16_t density
);
/**
* @brief Generate a mineral cluster around a center position
* @param tilemap The tilemap to modify
* @param center Center position for the mineral cluster
* @param mineral_type The type of mineral to place
*/
void generate_mineral_cluster(
TileMap &tilemap, TilePos center, SurfaceTileType mineral_type
);
/**
* @brief Check if a tile is suitable for mineral placement
* @param tilemap The tilemap to check
* @param pos Position to check
* @return True if minerals can be placed at this position
*/
bool is_suitable_for_mineral(const TileMap &tilemap, TilePos pos) const;
/**
* @brief Check if a mountain tile is on the edge (adjacent to non-mountain)
* @param tilemap The tilemap to check
* @param pos Position to check
* @return True if this mountain tile is on the edge
*/
bool is_mountain_edge(const TileMap &tilemap, TilePos pos) const;
/**
* @brief Calculate minimum distance between mineral clusters based on map
* size
* @param density The mineral density setting
* @return Minimum distance in tiles
*/
std::uint32_t calculate_min_mineral_distance(std::uint16_t density) const;
};
} // namespace istd
#endif // TILEMAP_PASS_MINERAL_CLUSTER_H

View File

@ -18,6 +18,9 @@ enum class BaseTileType : std::uint8_t {
enum class SurfaceTileType : std::uint8_t { enum class SurfaceTileType : std::uint8_t {
Empty, Empty,
Oil, Oil,
Hematite,
Titanomagnetite,
Gibbsite,
_count _count
}; };

View File

@ -12,6 +12,7 @@ void TerrainGenerator::operator()(TileMap &tilemap) {
mountain_hole_fill_pass(tilemap); mountain_hole_fill_pass(tilemap);
deepwater_pass(tilemap); deepwater_pass(tilemap);
oil_pass(tilemap); oil_pass(tilemap);
mineral_cluster_pass(tilemap);
} }
void map_generate(TileMap &tilemap, const GenerationConfig &config) { void map_generate(TileMap &tilemap, const GenerationConfig &config) {

View File

@ -0,0 +1,238 @@
#include "tilemap/pass/mineral_cluster.h"
#include "tilemap/chunk.h"
#include "tilemap/generation.h"
#include "tilemap/noise.h"
#include "tilemap/xoroshiro.h"
#include <algorithm>
#include <queue>
#include <random>
#include <unordered_set>
namespace istd {
MineralClusterGenerationPass::MineralClusterGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng, Xoroshiro128PP noise_rng
)
: config_(config), rng_(rng), noise_(noise_rng) {}
void MineralClusterGenerationPass::operator()(TileMap &tilemap) {
// Generate each mineral type with different densities
const std::vector<std::pair<SurfaceTileType, std::uint16_t>> minerals = {
{SurfaceTileType::Hematite, config_.hematite_density },
{SurfaceTileType::Titanomagnetite, config_.titanomagnetite_density},
{SurfaceTileType::Gibbsite, config_.gibbsite_density }
};
for (const auto [mineral_type, density] : minerals) {
// Generate mineral centers using Poisson disk sampling approach
auto mineral_centers = generate_mineral_centers(
tilemap, mineral_type, density
);
// Generate mineral clusters around each center
for (const auto &center : mineral_centers) {
generate_mineral_cluster(tilemap, center, mineral_type);
}
}
}
std::vector<TilePos> MineralClusterGenerationPass::generate_mineral_centers(
const TileMap &tilemap, SurfaceTileType mineral_type, std::uint16_t density
) {
std::vector<TilePos> centers;
std::uint8_t map_size = tilemap.get_size();
std::uint32_t total_chunks = map_size * map_size;
// Calculate expected number of mineral clusters based on density
std::uint32_t expected_clusters = (total_chunks * density) / 255;
// Minimum distance between mineral clusters to ensure spacing
std::uint32_t min_distance = calculate_min_mineral_distance(density);
const std::uint32_t max_coord = map_size * Chunk::size - 1;
// Generate candidates with rejection sampling
std::uint32_t attempts = 0;
const std::uint32_t max_attempts = expected_clusters
* 64; // More attempts for sparse minerals
std::uniform_int_distribution<std::uint16_t> dist(0, max_coord);
while (centers.size() < expected_clusters && attempts < max_attempts) {
++attempts;
// Generate random position
const auto global_x = dist(rng_);
const auto global_y = dist(rng_);
TilePos candidate = TilePos::from_global(global_x, global_y);
// Check if position is suitable for minerals (mountain edge)
if (!is_suitable_for_mineral(tilemap, candidate)) {
continue;
}
// Check distance to existing mineral centers
auto distance_checker = [candidate, min_distance](TilePos existing) {
return candidate.sqr_distance_to(existing)
< (min_distance * min_distance);
};
if (std::ranges::any_of(centers, distance_checker)) {
continue;
}
// Use base probability for mineral placement
std::uint8_t sample = noise_.noise(
global_x, global_y,
static_cast<std::uint32_t>(
mineral_type
) // Use mineral type as seed variation
);
if (sample < config_.mineral_base_probe) {
centers.push_back(candidate);
}
}
return centers;
}
void MineralClusterGenerationPass::generate_mineral_cluster(
TileMap &tilemap, TilePos center, SurfaceTileType mineral_type
) {
auto [global_x, global_y] = center.to_global();
// Calculate cluster size using similar approach to oil
auto span = config_.mineral_cluster_max_size
- config_.mineral_cluster_min_size;
auto cluster_size = config_.mineral_cluster_min_size;
// Use binomial distribution for cluster size
for (int i = 1; i <= span; ++i) {
auto sample = noise_.noise(
global_x, global_y,
i + static_cast<std::uint32_t>(mineral_type) * 16
)
& 1;
cluster_size += sample;
}
DiscreteRandomNoiseStream rng(
noise_, global_x, global_y,
64 + static_cast<std::uint32_t>(mineral_type) * 16
);
std::vector<TilePos> cluster_tiles;
std::unordered_set<TilePos> visited;
// Start with center if suitable
cluster_tiles.push_back(center);
visited.insert(center);
// Grow cluster using random walk
std::queue<TilePos> candidates;
candidates.push(center);
while (!candidates.empty() && cluster_tiles.size() < cluster_size) {
TilePos current = candidates.front();
candidates.pop();
auto neighbors = tilemap.get_neighbors(current);
std::shuffle(neighbors.begin(), neighbors.end(), rng);
for (const auto neighbor : neighbors) {
// 40% chance to skip this neighbor (slightly less dense than oil)
auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global();
auto sample = noise_.noise(
neighbor_global_x, neighbor_global_y,
0x3c73dde4
+ static_cast<std::uint32_t>(
mineral_type
) // random seed per mineral
);
if ((sample % 5) < 2) { // 40% chance to skip
continue;
}
if (visited.count(neighbor) > 0) {
continue; // Already visited
}
if (!is_suitable_for_mineral(tilemap, neighbor)) {
continue; // Not suitable for minerals
}
// Add to cluster
cluster_tiles.push_back(neighbor);
visited.insert(neighbor);
// Stop if we reached the desired cluster size
if (cluster_tiles.size() >= cluster_size) {
break;
}
candidates.push(neighbor);
}
}
// Place minerals on all cluster tiles
for (const auto &pos : cluster_tiles) {
Tile &tile = tilemap.get_tile(pos);
tile.surface = mineral_type;
}
}
bool MineralClusterGenerationPass::is_suitable_for_mineral(
const TileMap &tilemap, TilePos pos
) const {
const Tile &tile = tilemap.get_tile(pos);
// Minerals can only be placed on mountains with empty surface
if (tile.base != BaseTileType::Mountain
|| tile.surface != SurfaceTileType::Empty) {
return false;
}
// Must be on mountain edge (adjacent to non-mountain tiles)
return is_mountain_edge(tilemap, pos);
}
bool MineralClusterGenerationPass::is_mountain_edge(
const TileMap &tilemap, TilePos pos
) const {
// Check if this mountain tile has at least one non-mountain neighbor
auto neighbors = tilemap.get_neighbors(pos);
for (const auto neighbor_pos : neighbors) {
const Tile &neighbor_tile = tilemap.get_tile(neighbor_pos);
if (neighbor_tile.base != BaseTileType::Mountain) {
return true; // Found a non-mountain neighbor
}
}
return false; // All neighbors are mountains, not an edge
}
std::uint32_t MineralClusterGenerationPass::calculate_min_mineral_distance(
std::uint16_t density
) const {
// Base distance on chunk size, but allow closer spacing than oil
// since minerals are rarer and smaller clusters
std::uint32_t base_distance = Chunk::size / 2;
// Scale inversely with density, but with a minimum distance
std::uint32_t scaled_distance = base_distance * 128
/ std::max<std::uint16_t>(density, 1);
// Ensure minimum distance of at least 8 tiles
return std::max(scaled_distance, 8u);
}
void TerrainGenerator::mineral_cluster_pass(TileMap &tilemap) {
auto rng = master_rng_;
master_rng_ = master_rng_.jump_96();
auto noise_rng = master_rng_;
master_rng_ = master_rng_.jump_96();
MineralClusterGenerationPass pass(config_, rng, noise_rng);
pass(tilemap);
}
} // namespace istd