feat: add oil generation pass and update related configurations and documentation

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2025-08-03 14:57:36 +08:00
parent 6a3f23f989
commit 937a1324eb
Signed by: szTom
GPG Key ID: 072D999D60C6473C
15 changed files with 516 additions and 44 deletions

View File

@ -6,6 +6,7 @@ set(ISTD_TILEMAP_SRC
src/pass/biome.cpp
src/pass/deepwater.cpp
src/pass/mountain_hole_fill.cpp
src/pass/oil.cpp
src/pass/smoothen_mountain.cpp
src/pass/smoothen_island.cpp
src/generation.cpp

View File

@ -51,10 +51,10 @@ struct Tile {
```
**Base Tile Types:**
- `Land`, `Mountain`, `Sand`, `Water`, `Ice`
- `Land`, `Mountain`, `Sand`, `Water`, `Ice`, `Deepwater`
**Surface Tile Types:**
- `Empty`, `Wood`, `Structure`
- `Empty`, `Oil`
### Position Types
@ -90,6 +90,12 @@ struct GenerationConfig {
int temperature_octaves = 3;
int humidity_octaves = 3;
int base_octaves = 3;
// Oil generation parameters
double oil_density = 0.8; // Average oil fields per chunk
std::uint32_t oil_cluster_min_size = 2; // Minimum tiles per cluster
std::uint32_t oil_cluster_max_size = 6; // Maximum tiles per cluster
double oil_biome_preference = 2.0; // Multiplier for preferred biomes
};
```

View File

@ -27,8 +27,10 @@ tilemap/
│ ├── biome.cpp # Climate-based biome generation
│ ├── base_tile_type.cpp # Base terrain generation
│ ├── smoothen_mountain.cpp # Mountain smoothing
│ ├── smoothen_island.cpp # Island smoothing
│ ├── mountain_hole_fill.cpp # Hole filling
│ └── deepwater.cpp # Deep water placement
│ ├── deepwater.cpp # Deep water placement
│ └── oil.cpp # Oil resource generation
├── examples/ # Usage examples
└── docs/ # Documentation
```
@ -51,8 +53,10 @@ Terrain generation uses a multi-pass pipeline for modularity and control:
1. **Biome Pass**: Generates climate data and assigns biomes to sub-chunks
2. **Base Tile Type Pass**: Creates base terrain based on biomes
3. **Mountain Smoothing Pass**: Removes isolated mountain clusters
4. **Hole Fill Pass**: Fills small terrain holes
5. **Deep Water Pass**: Places deep water areas
4. **Island Smoothing Pass**: Smooths island coastlines using cellular automata
5. **Hole Fill Pass**: Fills small terrain holes
6. **Deep Water Pass**: Places deep water areas
7. **Oil Pass**: Generates sparse oil deposits as surface features
Each pass operates independently with its own RNG state, ensuring deterministic results.
@ -83,9 +87,18 @@ The library addresses Perlin noise distribution issues:
Several passes use BFS (Breadth-First Search) for terrain analysis:
- **Mountain Smoothing**: Find and remove small mountain components
- **Island Smoothing**: Apply cellular automata for natural coastlines
- **Hole Filling**: Identify and fill isolated terrain holes
- Components touching map boundaries are preserved
### Oil Resource Generation
The oil generation pass creates sparse resource deposits:
- **Poisson Disk Sampling**: Ensures minimum distance between oil fields
- **Biome Preference**: Higher probability in desert and plains biomes
- **Cluster Growth**: Random walk creates 2-6 tile clusters
- **Surface Placement**: Oil appears as surface features on land/sand tiles
## Random Number Generation
### Xoroshiro128++ Implementation

View File

@ -5,13 +5,18 @@
#include <chrono>
#include <cstdlib>
#include <format>
#include <iostream>
#include <print>
#include <string>
// Get BMP color for different base tile types
BmpColors::Color get_tile_color(istd::BaseTileType type) {
switch (type) {
// Get BMP color for different tile types, considering surface tiles
BmpColors::Color get_tile_color(const istd::Tile &tile) {
// Oil surface tile overrides base color
if (tile.surface == istd::SurfaceTileType::Oil) {
return BmpColors::OIL;
}
// Otherwise use base tile color
switch (tile.base) {
case istd::BaseTileType::Land:
return BmpColors::LAND;
case istd::BaseTileType::Mountain:
@ -50,7 +55,7 @@ void generate_bmp(const istd::TileMap &tilemap, const std::string &filename) {
int global_x = chunk_x * tiles_per_chunk + tile_x;
int global_y = chunk_y * tiles_per_chunk + tile_y;
auto color = get_tile_color(tile.base);
auto color = get_tile_color(tile);
// Draw a tile_size x tile_size block
for (int dy = 0; dy < tile_size; ++dy) {
@ -83,6 +88,7 @@ void print_statistics(const istd::TileMap &tilemap) {
int tile_counts[6] = {
0
}; // Count for each base tile type (now 6 types including Deepwater)
int oil_count = 0; // Count oil surface tiles
const int chunks_per_side = tilemap.get_size();
const int tiles_per_chunk = istd::Chunk::size;
@ -94,15 +100,20 @@ void print_statistics(const istd::TileMap &tilemap) {
for (int tile_y = 0; tile_y < tiles_per_chunk; ++tile_y) {
const auto &tile = chunk.tiles[tile_x][tile_y];
tile_counts[static_cast<int>(tile.base)]++;
// Count oil surface tiles
if (tile.surface == istd::SurfaceTileType::Oil) {
oil_count++;
}
}
}
}
}
const char *tile_names[]
= {"Land", "Mountain", "Sand", "Water", "Ice", "Deepwater"};
int total_tiles
= chunks_per_side * chunks_per_side * tiles_per_chunk * tiles_per_chunk;
const char *tile_names[] = {"Land", "Mountain", "Sand",
"Water", "Ice", "Deepwater"};
int total_tiles = chunks_per_side * chunks_per_side * tiles_per_chunk
* tiles_per_chunk;
std::println("\nTile Statistics:");
std::println("================");
@ -112,6 +123,16 @@ void print_statistics(const istd::TileMap &tilemap) {
"{:>10}: {:>8} ({:.1f}%)", tile_names[i], tile_counts[i], percentage
);
}
// Print oil statistics
double oil_percentage = (double)oil_count / total_tiles * 100.0;
double oil_per_chunk = (double)oil_count
/ (chunks_per_side * chunks_per_side);
std::println(
"{:>10}: {:>8} ({:.1f}%, {:.2f} per chunk)", "Oil", oil_count,
oil_percentage, oil_per_chunk
);
std::println("Total tiles: {}", total_tiles);
}
@ -131,8 +152,9 @@ int main(int argc, char *argv[]) {
return 1;
}
istd::Seed seed
= istd::Seed::from_string(argc >= 2 ? argv[1] : "hello_world");
istd::Seed seed = istd::Seed::from_string(
argc >= 2 ? argv[1] : "hello_world"
);
std::string output_filename = argc >= 3 ? argv[2] : "output.bmp";
int chunks_per_side = 8; // Default value

View File

@ -235,6 +235,7 @@ constexpr Color SAND(244, 164, 96); // Sandy brown
constexpr Color WATER(30, 144, 255); // Dodger blue
constexpr Color ICE(176, 224, 230); // Powder blue
constexpr Color DEEPWATER(0, 0, 139); // Dark blue
constexpr Color OIL(0, 0, 0); // Black
} // namespace BmpColors
#endif // BMP_H

View File

@ -3,6 +3,8 @@
#include "tile.h"
#include <compare>
#include <cstdint>
#include <functional>
#include <utility>
namespace istd {
@ -27,6 +29,13 @@ struct TilePos {
uint8_t local_x;
uint8_t local_y;
/**
* @brief Calculate squared distance to another TilePos
* @param other Other TilePos to compare with
* @return Squared distance between the two positions
*/
std::uint32_t sqr_distance_to(TilePos other) const;
/**
* @brief Convert TilePos to global coordinates
* @return Pair of global X and Y coordinates
@ -40,15 +49,23 @@ struct TilePos {
* @return TilePos corresponding to the global coordinates
*/
static TilePos from_global(std::uint16_t global_x, std::uint16_t global_y);
};
/**
* @brief Three-way comparison operator for TilePos
* @param lhs Left-hand side TilePos
* @param rhs Right-hand side TilePos
* @return Strong ordering comparison result
*/
std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs);
/**
* @brief Three-way comparison operator for TilePos
* @param lhs Left-hand side TilePos
* @param rhs Right-hand side TilePos
* @return Strong ordering comparison result
*/
friend std::strong_ordering operator<=>(TilePos lhs, TilePos rhs);
/**
* @brief Equality comparison operator for TilePos
* @param lhs Left-hand side TilePos
* @param rhs Right-hand side TilePos
* @return True if positions are equal
*/
friend bool operator==(TilePos lhs, TilePos rhs) = default;
};
struct Chunk {
// Size of a chunk in tiles (64 x 64)
@ -83,6 +100,28 @@ struct Chunk {
const BiomeType &get_biome(SubChunkPos pos) const {
return biome[pos.sub_x][pos.sub_y];
}
/**
* @brief Get biome for a specific local tile position
* @param local_x Local X coordinate within the chunk
* @param local_y Local Y coordinate within the chunk
* @return Reference to biome type
*/
const BiomeType &get_biome(
std::uint8_t local_x, std::uint8_t local_y
) const {
SubChunkPos sub_pos(local_x / subchunk_size, local_y / subchunk_size);
return get_biome(sub_pos);
}
/**
* @brief Get biome for a specific TilePos
* @param pos TilePos to get the biome for
* @return Reference to biome type
*/
const BiomeType &get_biome(TilePos pos) const {
return get_biome(pos.local_x, pos.local_y);
}
};
/**
@ -94,4 +133,14 @@ std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos);
} // namespace istd
template<>
struct std::hash<istd::TilePos> {
::std::size_t operator()(istd::TilePos pos) const {
return (static_cast<::std::size_t>(pos.chunk_x) << 24)
| (static_cast<::std::size_t>(pos.chunk_y) << 16)
| (static_cast<::std::size_t>(pos.local_x) << 8)
| static_cast<::std::size_t>(pos.local_y);
}
};
#endif

View File

@ -5,6 +5,7 @@
#include "chunk.h"
#include "noise.h"
#include "tilemap.h"
#include "xoroshiro.h"
#include <cstdint>
#include <vector>
@ -40,6 +41,14 @@ struct GenerationConfig {
std::uint32_t fill_threshold = 10; // Fill holes smaller than this size
std::uint32_t deepwater_radius = 2; // Radius for deepwater generation
// Oil generation parameters
std::uint8_t oil_density = 204; // Average oil fields per 255 chunk (~0.8)
std::uint8_t oil_cluster_min_size = 1; // Minimum tiles per oil cluster
std::uint8_t oil_cluster_max_size = 7; // Maximum tiles per oil cluster
// (should be <= 24)
std::uint8_t oil_base_probe = 128; // Biome preference multiplier (out
// of 255)
};
class BiomeGenerationPass {
@ -351,6 +360,12 @@ private:
* @param tilemap The tilemap to process
*/
void deepwater_pass(TileMap &tilemap);
/**
* @brief Generate oil clusters on suitable terrain
* @param tilemap The tilemap to process
*/
void oil_pass(TileMap &tilemap);
};
class DeepwaterGenerationPass {
@ -407,6 +422,67 @@ private:
) const;
};
class OilGenerationPass {
private:
const GenerationConfig &config_;
Xoroshiro128PP rng_;
DiscreteRandomNoise noise_;
public:
/**
* @brief Construct an oil generation pass
* @param config Generation configuration parameters
* @param rng Random number generator for oil placement
* @param noise_rng Random number generator for noise-based operations
*/
OilGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng,
Xoroshiro128PP noise_rng
);
/**
* @brief Generate oil clusters on the tilemap
* @param tilemap The tilemap to process
*/
void operator()(TileMap &tilemap);
private:
/**
* @brief Generate oil center positions using Poisson disk sampling
* @param tilemap The tilemap to analyze
* @return Vector of positions where oil clusters should be placed
*/
std::vector<TilePos> generate_oil_centers(const TileMap &tilemap);
/**
* @brief Generate an oil cluster around a center position
* @param tilemap The tilemap to modify
* @param center Center position for the oil cluster
*/
void generate_oil_cluster(TileMap &tilemap, TilePos center);
/**
* @brief Check if a tile is suitable for oil placement
* @param tilemap The tilemap to check
* @param pos Position to check
* @return True if oil can be placed at this position
*/
bool is_suitable_for_oil(const TileMap &tilemap, TilePos pos) const;
/**
* @brief Get biome preference multiplier for oil generation (out of 255)
* @param biome The biome type to check
* @return Preference value (0-255)
*/
std::uint8_t get_biome_oil_preference(BiomeType biome) const;
/**
* @brief Calculate minimum distance between oil fields based on map size
* @return Minimum distance in tiles
*/
std::uint32_t calculate_min_oil_distance() const;
};
/**
* @brief Generate a tilemap using the new biome-based system
* @param tilemap The tilemap to generate into

View File

@ -19,28 +19,55 @@ private:
std::uint64_t mask;
std::array<std::uint8_t, 256> permutation_;
std::uint8_t perm(int x) const;
std::uint32_t map(std::uint32_t x) const;
std::uint8_t perm(int x) const noexcept;
std::uint32_t rot8(std::uint32_t x) const noexcept;
std::uint32_t map_once(std::uint32_t x) const noexcept;
std::uint32_t map(std::uint32_t x) const noexcept;
public:
/**
* @brief Construct a DiscreteRandomNoise generator with the given seed
* @param rng Random number generator for noise
*/
explicit DiscreteRandomNoise(Xoroshiro128PP rng);
DiscreteRandomNoise() = default;
explicit DiscreteRandomNoise(Xoroshiro128PP rng) noexcept;
/**
* @brief Generate a discrete random value at the given coordinates
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate (optional)
* @return Discrete random value between 0 and 255
* @return Discrete random value between 0 and 2^64-1
*/
std::uint64_t noise(
std::uint32_t x, std::uint32_t y, std::uint32_t z = 0
) const;
) const noexcept;
};
class DiscreteRandomNoiseStream {
private:
const DiscreteRandomNoise &noise_;
std::uint32_t x_;
std::uint32_t y_;
std::uint32_t idx_;
public:
DiscreteRandomNoiseStream(
const DiscreteRandomNoise &noise, std::uint32_t x, std::uint32_t y,
std::uint32_t idx = 0
);
std::uint64_t next() noexcept;
// Adaption for STL RandomEngine named requirements
using result_type = std::uint64_t;
static constexpr result_type min() noexcept {
return std::numeric_limits<result_type>::min();
}
static constexpr result_type max() noexcept {
return std::numeric_limits<result_type>::max();
}
// Equivalent to next(), for STL compatibility
result_type operator()() noexcept;
};
class PerlinNoise {

View File

@ -2,7 +2,6 @@
#define ISTD_TILEMAP_TILE_H
#include <cstdint>
#include <stdexcept> // For std::invalid_argument
namespace istd {
@ -18,14 +17,17 @@ enum class BaseTileType : std::uint8_t {
enum class SurfaceTileType : std::uint8_t {
Empty,
Oil,
_count
};
constexpr std::uint8_t base_tile_count
= static_cast<std::uint8_t>(BaseTileType::_count);
constexpr std::uint8_t base_tile_count = static_cast<std::uint8_t>(
BaseTileType::_count
);
constexpr std::uint8_t surface_tile_count
= static_cast<std::uint8_t>(SurfaceTileType::_count);
constexpr std::uint8_t surface_tile_count = static_cast<std::uint8_t>(
SurfaceTileType::_count
);
static_assert(base_tile_count <= 16, "Base tile don't fit in 4 bits");
static_assert(surface_tile_count <= 16, "Surface tile don't fit in 4 bits");

View File

@ -34,6 +34,13 @@ public:
Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y);
const Chunk &get_chunk(std::uint8_t chunk_x, std::uint8_t chunk_y) const;
/**
* @brief Get a reference to the chunk containing the given TilePos
* @param pos The TilePos to get the chunk for
*/
Chunk &get_chunk_of(TilePos pos);
const Chunk &get_chunk_of(TilePos pos) const;
/**
* @brief Get a tile at the given position
* @param pos The position of the tile

View File

@ -1,8 +1,9 @@
#include "chunk.h"
#include <cstdint>
namespace istd {
std::strong_ordering operator<=>(const TilePos &lhs, const TilePos &rhs) {
std::strong_ordering operator<=>(TilePos lhs, TilePos rhs) {
if (lhs.chunk_x != rhs.chunk_x) {
return lhs.chunk_x <=> rhs.chunk_x;
}
@ -23,6 +24,24 @@ std::pair<std::uint8_t, std::uint8_t> subchunk_to_tile_start(SubChunkPos pos) {
return {pos.sub_x * Chunk::subchunk_size, pos.sub_y * Chunk::subchunk_size};
}
std::uint32_t TilePos::sqr_distance_to(TilePos other) const {
auto [this_global_x, this_global_y] = to_global();
auto [other_global_x, other_global_y] = other.to_global();
using std::swap;
if (this_global_x < other_global_x) {
swap(this_global_x, other_global_x);
}
if (this_global_y < other_global_y) {
swap(this_global_y, other_global_y);
}
std::uint32_t dx = this_global_x - other_global_x;
std::uint32_t dy = this_global_y - other_global_y;
return dx * dx + dy * dy;
}
std::pair<std::uint16_t, std::uint16_t> TilePos::to_global() const {
return {chunk_x * Chunk::size + local_x, chunk_y * Chunk::size + local_y};
}

View File

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

View File

@ -1,5 +1,6 @@
#include "noise.h"
#include <algorithm>
#include <bit>
#include <cmath>
#include <numeric>
#include <random>
@ -7,19 +8,23 @@
namespace istd {
DiscreteRandomNoise::DiscreteRandomNoise(Xoroshiro128PP rng) {
DiscreteRandomNoise::DiscreteRandomNoise(Xoroshiro128PP rng) noexcept {
mask = rng.next();
std::iota(permutation_.begin(), permutation_.end(), 0);
std::shuffle(permutation_.begin(), permutation_.end(), rng);
}
std::uint8_t DiscreteRandomNoise::perm(int x) const {
std::uint8_t DiscreteRandomNoise::perm(int x) const noexcept {
// Map x to [0, 255] range
x &= 0xFF;
return permutation_[x];
}
std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const {
std::uint32_t DiscreteRandomNoise::rot8(std::uint32_t x) const noexcept {
return std::rotl(x, 8);
}
std::uint32_t DiscreteRandomNoise::map_once(std::uint32_t x) const noexcept {
std::uint8_t a = x & 0xFF;
std::uint8_t b = (x >> 8) & 0xFF;
std::uint8_t c = (x >> 16) & 0xFF;
@ -31,9 +36,17 @@ std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const {
return (d << 24U) | (c << 16U) | (b << 8U) | a;
}
std::uint32_t DiscreteRandomNoise::map(std::uint32_t x) const noexcept {
for (int i = 0; i < 3; ++i) {
x = map_once(x);
x = rot8(x);
}
return x;
}
std::uint64_t DiscreteRandomNoise::noise(
std::uint32_t x, std::uint32_t y, std::uint32_t z
) const {
) const noexcept {
auto A = map(x);
auto B = map(y ^ A);
auto C = map(z ^ B);
@ -43,6 +56,22 @@ std::uint64_t DiscreteRandomNoise::noise(
return ((static_cast<std::uint64_t>(C) << 32) | F) ^ mask;
}
DiscreteRandomNoiseStream::DiscreteRandomNoiseStream(
const DiscreteRandomNoise &noise, std::uint32_t x, std::uint32_t y,
std::uint32_t idx
)
: noise_(noise), x_(x), y_(y), idx_(idx) {}
std::uint64_t DiscreteRandomNoiseStream::next() noexcept {
std::uint64_t value = noise_.noise(x_, y_, idx_);
++idx_;
return value;
}
std::uint64_t DiscreteRandomNoiseStream::operator()() noexcept {
return next();
}
PerlinNoise::PerlinNoise(Xoroshiro128PP rng) {
// Initialize permutation array with values 0-255
permutation_.resize(256);
@ -191,8 +220,9 @@ double UniformPerlinNoise::uniform_noise(double x, double y) const {
double UniformPerlinNoise::map_to_uniform(double raw_value) const {
// Find position in CDF using binary search
auto it
= std::lower_bound(cdf_values_.begin(), cdf_values_.end(), raw_value);
auto it = std::lower_bound(
cdf_values_.begin(), cdf_values_.end(), raw_value
);
// Calculate quantile (position in CDF as fraction)
size_t position = std::distance(cdf_values_.begin(), it);

209
tilemap/src/pass/oil.cpp Normal file
View File

@ -0,0 +1,209 @@
#include "biome.h"
#include "chunk.h"
#include "generation.h"
#include "noise.h"
#include "xoroshiro.h"
#include <algorithm>
#include <queue>
#include <random>
#include <unordered_set>
namespace istd {
OilGenerationPass::OilGenerationPass(
const GenerationConfig &config, Xoroshiro128PP rng, Xoroshiro128PP noise_rng
)
: config_(config), rng_(rng), noise_(noise_rng) {}
void OilGenerationPass::operator()(TileMap &tilemap) {
// Generate oil center positions using Poisson disk sampling
auto oil_centers = generate_oil_centers(tilemap);
// Generate oil clusters around each center
for (const auto &center : oil_centers) {
generate_oil_cluster(tilemap, center);
}
}
std::vector<TilePos> OilGenerationPass::generate_oil_centers(
const TileMap &tilemap
) {
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 oil fields based on density (out of 255)
std::uint32_t expected_oil_fields = (total_chunks * config_.oil_density)
/ 255;
// Minimum distance between oil fields to ensure spacing
std::uint32_t min_distance = calculate_min_oil_distance();
// Use Poisson disk sampling approach
const std::uint32_t max_coord = map_size * Chunk::size - 1;
// Generate candidates with rejection sampling
// Avoid infinite loops
std::uint32_t attempts = 0;
const std::uint32_t max_attempts = expected_oil_fields * 32;
std::uniform_int_distribution<std::uint16_t> dist(0, max_coord);
while (centers.size() < expected_oil_fields && attempts < max_attempts) {
++attempts;
// Generate random position using xoroshiro
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 oil
if (!is_suitable_for_oil(tilemap, candidate)) {
continue;
}
// Check distance to existing oil 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;
}
// Apply biome preference
const Chunk &chunk = tilemap.get_chunk_of(candidate);
BiomeType biome = chunk.get_biome(candidate);
std::uint8_t biome_preference = get_biome_oil_preference(biome);
// Use integer probability check (0-255)
std::uint8_t sample = noise_.noise(global_x, global_y);
if (sample < biome_preference) {
centers.push_back(candidate);
}
}
return centers;
}
void OilGenerationPass::generate_oil_cluster(TileMap &tilemap, TilePos center) {
auto [global_x, global_y] = center.to_global();
auto span = config_.oil_cluster_max_size - config_.oil_cluster_min_size;
auto cluster_size = config_.oil_cluster_min_size;
// Binomial distribution for cluster size
for (int i = 1; i <= span; ++i) {
auto sample = noise_.noise(global_x, global_y, i) & 1;
cluster_size += sample; // Increase cluster size based on noise
}
DiscreteRandomNoiseStream rng(noise_, global_x, global_y, 48);
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) {
// 50% chance to skip this neighbor
auto [neighbor_global_x, neighbor_global_y] = neighbor.to_global();
auto sample = noise_.noise(
neighbor_global_x, neighbor_global_y,
0x2b52aaed // random seed
);
if ((sample & 1) == 0) {
continue;
}
if (visited.count(neighbor) > 0) {
continue; // Already visited
}
if (!is_suitable_for_oil(tilemap, neighbor)) {
continue; // Not suitable for oil
}
// 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 oil on all cluster tiles
for (const auto &pos : cluster_tiles) {
Tile &tile = tilemap.get_tile(pos);
tile.surface = SurfaceTileType::Oil;
}
}
bool OilGenerationPass::is_suitable_for_oil(
const TileMap &tilemap, TilePos pos
) const {
const Tile &tile = tilemap.get_tile(pos);
// Oil can only be placed on land or sand, and surface must be empty
return (tile.base == BaseTileType::Land || tile.base == BaseTileType::Sand)
&& tile.surface == SurfaceTileType::Empty;
}
std::uint8_t OilGenerationPass::get_biome_oil_preference(
BiomeType biome
) const {
std::uint8_t base_preference = config_.oil_base_probe;
switch (biome) {
case BiomeType::Desert:
case BiomeType::Plains:
return base_preference; // Full preference for desert/plains
case BiomeType::Savanna:
case BiomeType::SnowyPlains:
return (base_preference * 204) / 255; // ~80% preference
case BiomeType::Forest:
return (base_preference * 128) / 255; // ~50% preference
case BiomeType::SnowyPeeks:
return (base_preference * 77) / 255; // ~30% preference
case BiomeType::FrozenOcean:
case BiomeType::Ocean:
case BiomeType::LukeOcean:
default:
return 0; // No oil in oceans
}
}
std::uint32_t OilGenerationPass::calculate_min_oil_distance() const {
// Base minimum distance on chunk size and oil density
// Lower density = higher minimum distance
std::uint32_t base_distance = Chunk::size * 4 / 5;
return base_distance * 255 / config_.oil_density;
}
void TerrainGenerator::oil_pass(TileMap &tilemap) {
auto rng = master_rng_;
master_rng_ = master_rng_.jump_96();
auto noise_rng = master_rng_;
master_rng_ = master_rng_.jump_96();
OilGenerationPass pass(config_, rng, noise_rng);
pass(tilemap);
}
} // namespace istd

View File

@ -1,4 +1,5 @@
#include "tilemap.h"
#include "chunk.h"
#include <stdexcept>
namespace istd {
@ -31,6 +32,14 @@ const Chunk &TileMap::get_chunk(
return chunks_[chunk_x][chunk_y];
}
Chunk &TileMap::get_chunk_of(TilePos pos) {
return get_chunk(pos.chunk_x, pos.chunk_y);
}
const Chunk &TileMap::get_chunk_of(TilePos pos) const {
return get_chunk(pos.chunk_x, pos.chunk_y);
}
Tile &TileMap::get_tile(TilePos pos) {
if (pos.chunk_x >= size_ || pos.chunk_y >= size_) {
throw std::out_of_range("Chunk coordinates out of bounds");
@ -68,7 +77,7 @@ bool TileMap::is_at_boundary(TilePos pos) const {
std::uint32_t max_global = map_size * Chunk::size - 1;
return global_x == 0 || global_x == max_global || global_y == 0
|| global_y == max_global;
|| global_y == max_global;
}
std::vector<TilePos> TileMap::get_neighbors(TilePos pos, bool chebyshiv) const {