From 937a1324eb3516119b494dba9d27add4279e4046 Mon Sep 17 00:00:00 2001 From: szdytom Date: Sun, 3 Aug 2025 14:57:36 +0800 Subject: [PATCH] feat: add oil generation pass and update related configurations and documentation Signed-off-by: szdytom --- tilemap/CMakeLists.txt | 1 + tilemap/docs/api.md | 10 +- tilemap/docs/dev.md | 19 ++- tilemap/examples/biome_demo.cpp | 44 +++++-- tilemap/examples/bmp.h | 1 + tilemap/include/chunk.h | 65 ++++++++-- tilemap/include/generation.h | 76 ++++++++++++ tilemap/include/noise.h | 41 +++++-- tilemap/include/tile.h | 12 +- tilemap/include/tilemap.h | 7 ++ tilemap/src/chunk.cpp | 21 +++- tilemap/src/generation.cpp | 1 + tilemap/src/noise.cpp | 42 ++++++- tilemap/src/pass/oil.cpp | 209 ++++++++++++++++++++++++++++++++ tilemap/src/tilemap.cpp | 11 +- 15 files changed, 516 insertions(+), 44 deletions(-) create mode 100644 tilemap/src/pass/oil.cpp diff --git a/tilemap/CMakeLists.txt b/tilemap/CMakeLists.txt index 6388a00..cd2387c 100644 --- a/tilemap/CMakeLists.txt +++ b/tilemap/CMakeLists.txt @@ -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 diff --git a/tilemap/docs/api.md b/tilemap/docs/api.md index ea8bf8e..476cc61 100644 --- a/tilemap/docs/api.md +++ b/tilemap/docs/api.md @@ -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 }; ``` diff --git a/tilemap/docs/dev.md b/tilemap/docs/dev.md index 43534c4..ca5cd8c 100644 --- a/tilemap/docs/dev.md +++ b/tilemap/docs/dev.md @@ -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 diff --git a/tilemap/examples/biome_demo.cpp b/tilemap/examples/biome_demo.cpp index 486d77b..364cded 100644 --- a/tilemap/examples/biome_demo.cpp +++ b/tilemap/examples/biome_demo.cpp @@ -5,13 +5,18 @@ #include #include #include -#include #include #include -// 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(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 diff --git a/tilemap/examples/bmp.h b/tilemap/examples/bmp.h index f8c7cb1..56bba26 100644 --- a/tilemap/examples/bmp.h +++ b/tilemap/examples/bmp.h @@ -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 diff --git a/tilemap/include/chunk.h b/tilemap/include/chunk.h index ffef265..be61ac1 100644 --- a/tilemap/include/chunk.h +++ b/tilemap/include/chunk.h @@ -3,6 +3,8 @@ #include "tile.h" #include #include +#include +#include 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 subchunk_to_tile_start(SubChunkPos pos); } // namespace istd +template<> +struct std::hash { + ::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 \ No newline at end of file diff --git a/tilemap/include/generation.h b/tilemap/include/generation.h index 6fb95f1..d28f81d 100644 --- a/tilemap/include/generation.h +++ b/tilemap/include/generation.h @@ -5,6 +5,7 @@ #include "chunk.h" #include "noise.h" #include "tilemap.h" +#include "xoroshiro.h" #include #include @@ -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 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 diff --git a/tilemap/include/noise.h b/tilemap/include/noise.h index 856e500..652a73d 100644 --- a/tilemap/include/noise.h +++ b/tilemap/include/noise.h @@ -19,28 +19,55 @@ private: std::uint64_t mask; std::array 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::min(); + } + static constexpr result_type max() noexcept { + return std::numeric_limits::max(); + } + // Equivalent to next(), for STL compatibility + result_type operator()() noexcept; }; class PerlinNoise { diff --git a/tilemap/include/tile.h b/tilemap/include/tile.h index b6f0f65..e12b09e 100644 --- a/tilemap/include/tile.h +++ b/tilemap/include/tile.h @@ -2,7 +2,6 @@ #define ISTD_TILEMAP_TILE_H #include -#include // 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(BaseTileType::_count); +constexpr std::uint8_t base_tile_count = static_cast( + BaseTileType::_count +); -constexpr std::uint8_t surface_tile_count - = static_cast(SurfaceTileType::_count); +constexpr std::uint8_t surface_tile_count = static_cast( + 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"); diff --git a/tilemap/include/tilemap.h b/tilemap/include/tilemap.h index ef262dd..796ae67 100644 --- a/tilemap/include/tilemap.h +++ b/tilemap/include/tilemap.h @@ -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 diff --git a/tilemap/src/chunk.cpp b/tilemap/src/chunk.cpp index e5eb6c9..d3abae1 100644 --- a/tilemap/src/chunk.cpp +++ b/tilemap/src/chunk.cpp @@ -1,8 +1,9 @@ #include "chunk.h" +#include 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 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 TilePos::to_global() const { return {chunk_x * Chunk::size + local_x, chunk_y * Chunk::size + local_y}; } diff --git a/tilemap/src/generation.cpp b/tilemap/src/generation.cpp index 88af59f..82a4e1f 100644 --- a/tilemap/src/generation.cpp +++ b/tilemap/src/generation.cpp @@ -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) { diff --git a/tilemap/src/noise.cpp b/tilemap/src/noise.cpp index c7c6b63..6b29a79 100644 --- a/tilemap/src/noise.cpp +++ b/tilemap/src/noise.cpp @@ -1,5 +1,6 @@ #include "noise.h" #include +#include #include #include #include @@ -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(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); diff --git a/tilemap/src/pass/oil.cpp b/tilemap/src/pass/oil.cpp new file mode 100644 index 0000000..ba03cf5 --- /dev/null +++ b/tilemap/src/pass/oil.cpp @@ -0,0 +1,209 @@ +#include "biome.h" +#include "chunk.h" +#include "generation.h" +#include "noise.h" +#include "xoroshiro.h" +#include +#include +#include +#include + +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 ¢er : oil_centers) { + generate_oil_cluster(tilemap, center); + } +} + +std::vector OilGenerationPass::generate_oil_centers( + const TileMap &tilemap +) { + std::vector 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 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 cluster_tiles; + std::unordered_set visited; + + // Start with center if suitable + cluster_tiles.push_back(center); + visited.insert(center); + + // Grow cluster using random walk + std::queue 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 diff --git a/tilemap/src/tilemap.cpp b/tilemap/src/tilemap.cpp index 36307d0..20cc62c 100644 --- a/tilemap/src/tilemap.cpp +++ b/tilemap/src/tilemap.cpp @@ -1,4 +1,5 @@ #include "tilemap.h" +#include "chunk.h" #include 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 TileMap::get_neighbors(TilePos pos, bool chebyshiv) const {