Browse Source

Add ground irregularities feature for flat terrain

djeada 3 weeks ago
parent
commit
f1b024f6a4

+ 4 - 1
assets/maps/map_forest.json

@@ -56,7 +56,10 @@
       0.69,
       0.69,
       0.73
       0.73
     ],
     ],
-    "plantDensity": 0.65
+    "plantDensity": 0.65,
+    "groundIrregularityEnabled": true,
+    "irregularityScale": 0.12,
+    "irregularityAmplitude": 0.06
   },
   },
   "camera": {
   "camera": {
     "center": [
     "center": [

+ 4 - 1
assets/maps/map_mountain.json

@@ -56,7 +56,10 @@
       0.69,
       0.69,
       0.73
       0.73
     ],
     ],
-    "plantDensity": 0.5
+    "plantDensity": 0.5,
+    "groundIrregularityEnabled": true,
+    "irregularityScale": 0.10,
+    "irregularityAmplitude": 0.05
   },
   },
   "camera": {
   "camera": {
     "center": [
     "center": [

+ 4 - 1
assets/maps/map_rivers.json

@@ -56,7 +56,10 @@
       0.69,
       0.69,
       0.73
       0.73
     ],
     ],
-    "plantDensity": 0.5
+    "plantDensity": 0.5,
+    "groundIrregularityEnabled": true,
+    "irregularityScale": 0.15,
+    "irregularityAmplitude": 0.07
   },
   },
   "camera": {
   "camera": {
     "center": [
     "center": [

+ 10 - 1
assets/shaders/ground_plane.frag

@@ -2,6 +2,7 @@
 in vec3 v_worldPos;
 in vec3 v_worldPos;
 in vec3 v_normal;
 in vec3 v_normal;
 in vec2 v_uv;
 in vec2 v_uv;
+in float v_disp;
 layout(location = 0) out vec4 FragColor;
 layout(location = 0) out vec4 FragColor;
 uniform vec3 u_grassPrimary;
 uniform vec3 u_grassPrimary;
 uniform vec3 u_grassSecondary;
 uniform vec3 u_grassSecondary;
@@ -39,6 +40,7 @@ float fbm(vec2 p) {
 
 
 void main() {
 void main() {
   vec3 n = normalize(v_normal);
   vec3 n = normalize(v_normal);
+  float heightTint = clamp((v_worldPos.y + 0.6) * 0.6, -0.25, 0.35);
   float ts = max(u_tileSize, 1e-4);
   float ts = max(u_tileSize, 1e-4);
   vec2 wuv = (v_worldPos.xz / ts) + u_noiseOffset;
   vec2 wuv = (v_worldPos.xz / ts) + u_noiseOffset;
   float macro = fbm(wuv * u_macroNoiseScale);
   float macro = fbm(wuv * u_macroNoiseScale);
@@ -81,6 +83,13 @@ void main() {
   float roughnessVar = mix(0.65, 0.95, 1.0 - moistureVar);
   float roughnessVar = mix(0.65, 0.95, 1.0 - moistureVar);
   float specContrib = fres * 0.08 * roughnessVar;
   float specContrib = fres * 0.08 * roughnessVar;
   float shade = ambient + ndl * 0.65 + specContrib;
   float shade = ambient + ndl * 0.65 + specContrib;
-  vec3 lit = col * shade * u_ambientBoost;
+  vec3 lit = col * shade * (u_ambientBoost + heightTint);
+
+  // Debug tint to make displacement obvious: blue for negative, red for
+  // positive
+  vec3 dispTint = mix(vec3(0.3, 0.5, 1.0), vec3(1.0, 0.4, 0.3),
+                      smoothstep(-1.0, 1.0, v_disp));
+  lit = mix(lit, lit * dispTint, 0.25);
+
   FragColor = vec4(clamp(lit, 0.0, 1.0), 1.0);
   FragColor = vec4(clamp(lit, 0.0, 1.0), 1.0);
 }
 }

+ 43 - 3
assets/shaders/ground_plane.vert

@@ -10,6 +10,7 @@ uniform float u_heightNoiseFrequency;
 out vec3 v_worldPos;
 out vec3 v_worldPos;
 out vec3 v_normal;
 out vec3 v_normal;
 out vec2 v_uv;
 out vec2 v_uv;
+out float v_disp;
 
 
 float hash21(vec2 p) {
 float hash21(vec2 p) {
   return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
   return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
@@ -37,17 +38,56 @@ mat2 rot2(float a) {
 
 
 void main() {
 void main() {
   vec3 wp = (u_model * vec4(a_position, 1.0)).xyz;
   vec3 wp = (u_model * vec4(a_position, 1.0)).xyz;
-  vec3 wn = normalize(mat3(u_model) * a_normal);
+  vec3 worldNormal = normalize(mat3(u_model) * a_normal);
   float ang =
   float ang =
       fract(sin(dot(u_noiseOffset, vec2(12.9898, 78.233))) * 43758.5453) *
       fract(sin(dot(u_noiseOffset, vec2(12.9898, 78.233))) * 43758.5453) *
       6.2831853;
       6.2831853;
   vec2 uv = rot2(ang) * (wp.xz + u_noiseOffset);
   vec2 uv = rot2(ang) * (wp.xz + u_noiseOffset);
-  float h = fbm2(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
-  float amp = clamp(u_heightNoiseStrength, 0.0, 0.20);
+
+  // Gentle domain warp to break straight lines and angular patterns
+  float warpBase = fbm2(uv * 0.35);
+  float warpOff = fbm2((uv + vec2(11.3, -7.7)) * 0.35);
+  vec2 uvWarp = uv + (vec2(warpBase, warpOff) - 0.5) * 0.7;
+
+  // Tighten irregularities so bumps are closer together
+  float freq = max(u_heightNoiseFrequency * 3.0, 1.1);
+  float base = fbm2(uvWarp * freq * 0.65);
+  float detail = fbm2(uvWarp * freq * 1.6);
+  float fine = noise21(uvWarp * freq * 3.2);
+  float h = (base * 0.50 + detail * 0.35 + fine * 0.15) * 2.0 - 1.0;
+  // Soften extremes to avoid blocky plateaus
+  h = h - 0.2 * h * h * h;
+
+  // Ensure a clearly visible base amount of warping but keep it grounded
+  float strength = max(u_heightNoiseStrength, 0.35);
+  float amp = clamp(strength * 1.8, 0.10, 0.65);
   float disp = h * amp;
   float disp = h * amp;
+
+  // Subtle sine warp just to guarantee non-flatness, but keep it small
+  disp += sin(uvWarp.x * 1.8) * 0.05 + sin(uvWarp.y * 2.1) * 0.05;
+
   wp.y += disp;
   wp.y += disp;
+
+  // Estimate slope to bend normals with the displaced surface
+  float gradStep = max(0.15, 0.35 / max(freq * 1.1, 0.05));
+  float h_base_x = fbm2((uvWarp + vec2(gradStep, 0.0)) * freq * 0.65);
+  float h_det_x = fbm2((uvWarp + vec2(gradStep, 0.0)) * freq * 1.6);
+  float h_fin_x = noise21((uvWarp + vec2(gradStep, 0.0)) * freq * 3.2);
+  float hx = (h_base_x * 0.50 + h_det_x * 0.35 + h_fin_x * 0.15) * 2.0 - 1.0;
+
+  float h_base_z = fbm2((uvWarp + vec2(0.0, gradStep)) * freq * 0.65);
+  float h_det_z = fbm2((uvWarp + vec2(0.0, gradStep)) * freq * 1.6);
+  float h_fin_z = noise21((uvWarp + vec2(0.0, gradStep)) * freq * 3.2);
+  float hz = (h_base_z * 0.50 + h_det_z * 0.35 + h_fin_z * 0.15) * 2.0 - 1.0;
+
+  vec3 dx = vec3(gradStep, (hx - h) * amp, 0.0);
+  vec3 dz = vec3(0.0, (hz - h) * amp, gradStep);
+  vec3 warpedNormal = normalize(cross(dz, dx));
+  vec3 wn = normalize(mix(worldNormal, warpedNormal, 0.85));
+
   v_worldPos = wp;
   v_worldPos = wp;
   v_normal = wn;
   v_normal = wn;
   v_uv = a_uv;
   v_uv = a_uv;
+  v_disp = disp;
   gl_Position = u_mvp * vec4(wp, 1.0);
   gl_Position = u_mvp * vec4(wp, 1.0);
 }
 }

+ 4 - 0
game/map/json_keys.h

@@ -61,6 +61,10 @@ inline constexpr const char *BACKGROUND_SWAY_VARIANCE =
     "backgroundSwayVariance";
     "backgroundSwayVariance";
 inline constexpr const char *BACKGROUND_SCATTER_RADIUS =
 inline constexpr const char *BACKGROUND_SCATTER_RADIUS =
     "backgroundScatterRadius";
     "backgroundScatterRadius";
+inline constexpr const char *GROUND_IRREGULARITY_ENABLED =
+    "groundIrregularityEnabled";
+inline constexpr const char *IRREGULARITY_SCALE = "irregularityScale";
+inline constexpr const char *IRREGULARITY_AMPLITUDE = "irregularityAmplitude";
 
 
 inline constexpr const char *TYPE = "type";
 inline constexpr const char *TYPE = "type";
 inline constexpr const char *X = "x";
 inline constexpr const char *X = "x";

+ 12 - 0
game/map/map_loader.cpp

@@ -200,6 +200,18 @@ void readBiome(const QJsonObject &obj, BiomeSettings &out) {
     out.plant_density =
     out.plant_density =
         float(obj.value(PLANT_DENSITY).toDouble(out.plant_density));
         float(obj.value(PLANT_DENSITY).toDouble(out.plant_density));
   }
   }
+  if (obj.contains(GROUND_IRREGULARITY_ENABLED)) {
+    out.groundIrregularityEnabled = obj.value(GROUND_IRREGULARITY_ENABLED)
+                                        .toBool(out.groundIrregularityEnabled);
+  }
+  if (obj.contains(IRREGULARITY_SCALE)) {
+    out.irregularityScale =
+        float(obj.value(IRREGULARITY_SCALE).toDouble(out.irregularityScale));
+  }
+  if (obj.contains(IRREGULARITY_AMPLITUDE)) {
+    out.irregularityAmplitude = float(
+        obj.value(IRREGULARITY_AMPLITUDE).toDouble(out.irregularityAmplitude));
+  }
 }
 }
 
 
 void readVictoryConfig(const QJsonObject &obj, VictoryConfig &out) {
 void readVictoryConfig(const QJsonObject &obj, VictoryConfig &out) {

+ 71 - 26
game/map/terrain.cpp

@@ -470,40 +470,85 @@ void TerrainHeightMap::applyBiomeVariation(const BiomeSettings &settings) {
     return;
     return;
   }
   }
 
 
-  const float amplitude = std::max(0.0F, settings.heightNoiseAmplitude);
-  if (amplitude <= 0.0001F) {
-    return;
-  }
+  if (settings.groundIrregularityEnabled) {
+    const float amplitude = std::max(0.0F, settings.irregularityAmplitude);
+    if (amplitude > 0.0001F) {
+      const float frequency = std::max(0.0001F, settings.irregularityScale);
+      const float half_width = m_width * 0.5F - 0.5F;
+      const float half_height = m_height * 0.5F - 0.5F;
+
+      for (int z = 0; z < m_height; ++z) {
+        for (int x = 0; x < m_width; ++x) {
+          int const idx = indexAt(x, z);
+          TerrainType const type = m_terrain_types[idx];
+
+          if (type != TerrainType::Flat) {
+            continue;
+          }
 
 
-  const float frequency = std::max(0.0001F, settings.heightNoiseFrequency);
-  const float half_width = m_width * 0.5F - 0.5F;
-  const float half_height = m_height * 0.5F - 0.5F;
+          if (isRiverOrNearby(x, z, 2)) {
+            continue;
+          }
 
 
-  for (int z = 0; z < m_height; ++z) {
-    for (int x = 0; x < m_width; ++x) {
-      int const idx = indexAt(x, z);
-      TerrainType const type = m_terrain_types[idx];
-      if (type == TerrainType::Mountain) {
-        continue;
+          float const world_x =
+              (static_cast<float>(x) - half_width) * m_tile_size;
+          float const world_z =
+              (static_cast<float>(z) - half_height) * m_tile_size;
+          float const sample_x = world_x * frequency;
+          float const sample_z = world_z * frequency;
+
+          float const base_noise =
+              valueNoise2D(sample_x, sample_z, settings.seed);
+          float const detail_noise = valueNoise2D(
+              sample_x * 2.5F, sample_z * 2.5F, settings.seed ^ 0xA21C9E37U);
+          float const fine_noise = valueNoise2D(
+              sample_x * 5.0F, sample_z * 5.0F, settings.seed ^ 0x7E4B92F1U);
+
+          float const blended =
+              0.5F * base_noise + 0.35F * detail_noise + 0.15F * fine_noise;
+          float const perturb = (blended - 0.5F) * 2.0F * amplitude;
+
+          m_heights[idx] = std::max(0.0F, m_heights[idx] + perturb);
+        }
       }
       }
+    }
+  }
 
 
-      float const world_x = (static_cast<float>(x) - half_width) * m_tile_size;
-      float const world_z = (static_cast<float>(z) - half_height) * m_tile_size;
-      float const sample_x = world_x * frequency;
-      float const sample_z = world_z * frequency;
+  const float legacy_amplitude = std::max(0.0F, settings.heightNoiseAmplitude);
+  if (legacy_amplitude > 0.0001F) {
+    const float frequency = std::max(0.0001F, settings.heightNoiseFrequency);
+    const float half_width = m_width * 0.5F - 0.5F;
+    const float half_height = m_height * 0.5F - 0.5F;
 
 
-      float const base_noise = valueNoise2D(sample_x, sample_z, settings.seed);
-      float const detail_noise = valueNoise2D(sample_x * 2.0F, sample_z * 2.0F,
-                                              settings.seed ^ 0xA21C9E37U);
+    for (int z = 0; z < m_height; ++z) {
+      for (int x = 0; x < m_width; ++x) {
+        int const idx = indexAt(x, z);
+        TerrainType const type = m_terrain_types[idx];
+        if (type == TerrainType::Mountain) {
+          continue;
+        }
 
 
-      float const blended = 0.65F * base_noise + 0.35F * detail_noise;
-      float perturb = (blended - 0.5F) * 2.0F * amplitude;
+        float const world_x =
+            (static_cast<float>(x) - half_width) * m_tile_size;
+        float const world_z =
+            (static_cast<float>(z) - half_height) * m_tile_size;
+        float const sample_x = world_x * frequency;
+        float const sample_z = world_z * frequency;
 
 
-      if (type == TerrainType::Hill) {
-        perturb *= 0.6F;
-      }
+        float const base_noise =
+            valueNoise2D(sample_x, sample_z, settings.seed);
+        float const detail_noise = valueNoise2D(
+            sample_x * 2.0F, sample_z * 2.0F, settings.seed ^ 0xA21C9E37U);
 
 
-      m_heights[idx] = std::max(0.0F, m_heights[idx] + perturb);
+        float const blended = 0.65F * base_noise + 0.35F * detail_noise;
+        float perturb = (blended - 0.5F) * 2.0F * legacy_amplitude;
+
+        if (type == TerrainType::Hill) {
+          perturb *= 0.6F;
+        }
+
+        m_heights[idx] = std::max(0.0F, m_heights[idx] + perturb);
+      }
     }
     }
   }
   }
 }
 }

+ 3 - 0
game/map/terrain.h

@@ -92,6 +92,9 @@ struct BiomeSettings {
   float plant_density = 0.5F;
   float plant_density = 0.5F;
   float spawnEdgePadding = 0.08F;
   float spawnEdgePadding = 0.08F;
   std::uint32_t seed = 1337U;
   std::uint32_t seed = 1337U;
+  bool groundIrregularityEnabled = true;
+  float irregularityScale = 0.15F;
+  float irregularityAmplitude = 0.08F;
 };
 };
 
 
 struct TerrainFeature {
 struct TerrainFeature {

+ 12 - 0
render/gl/backend.cpp

@@ -802,6 +802,18 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
               m_terrainPipeline->m_groundUniforms.soilBlendSharpness,
               m_terrainPipeline->m_groundUniforms.soilBlendSharpness,
               terrain.params.soilBlendSharpness);
               terrain.params.soilBlendSharpness);
         }
         }
+        if (m_terrainPipeline->m_groundUniforms.heightNoiseStrength !=
+            Shader::InvalidUniform) {
+          active_shader->setUniform(
+              m_terrainPipeline->m_groundUniforms.heightNoiseStrength,
+              terrain.params.heightNoiseStrength);
+        }
+        if (m_terrainPipeline->m_groundUniforms.heightNoiseFrequency !=
+            Shader::InvalidUniform) {
+          active_shader->setUniform(
+              m_terrainPipeline->m_groundUniforms.heightNoiseFrequency,
+              terrain.params.heightNoiseFrequency);
+        }
         if (m_terrainPipeline->m_groundUniforms.ambientBoost !=
         if (m_terrainPipeline->m_groundUniforms.ambientBoost !=
             Shader::InvalidUniform) {
             Shader::InvalidUniform) {
           active_shader->setUniform(
           active_shader->setUniform(

+ 4 - 0
render/gl/backend/terrain_pipeline.cpp

@@ -100,6 +100,10 @@ void TerrainPipeline::cacheGroundUniforms() {
       m_groundShader->uniformHandle("u_soilBlendHeight");
       m_groundShader->uniformHandle("u_soilBlendHeight");
   m_groundUniforms.soilBlendSharpness =
   m_groundUniforms.soilBlendSharpness =
       m_groundShader->uniformHandle("u_soilBlendSharpness");
       m_groundShader->uniformHandle("u_soilBlendSharpness");
+  m_groundUniforms.heightNoiseStrength =
+      m_groundShader->uniformHandle("u_heightNoiseStrength");
+  m_groundUniforms.heightNoiseFrequency =
+      m_groundShader->uniformHandle("u_heightNoiseFrequency");
   m_groundUniforms.ambientBoost =
   m_groundUniforms.ambientBoost =
       m_groundShader->uniformHandle("u_ambientBoost");
       m_groundShader->uniformHandle("u_ambientBoost");
   m_groundUniforms.light_dir = m_groundShader->uniformHandle("u_lightDir");
   m_groundUniforms.light_dir = m_groundShader->uniformHandle("u_lightDir");

+ 2 - 0
render/gl/backend/terrain_pipeline.h

@@ -46,6 +46,8 @@ public:
     GL::Shader::UniformHandle detail_noiseScale{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle detail_noiseScale{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle soilBlendHeight{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle soilBlendHeight{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle soilBlendSharpness{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle soilBlendSharpness{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle heightNoiseStrength{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle heightNoiseFrequency{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle ambientBoost{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle ambientBoost{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle light_dir{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle light_dir{GL::Shader::InvalidUniform};
   };
   };

+ 51 - 5
render/ground/ground_renderer.cpp

@@ -94,12 +94,21 @@ auto GroundRenderer::buildParams() const -> TerrainChunkParams {
   params.noiseOffset = m_noiseOffset;
   params.noiseOffset = m_noiseOffset;
   params.noiseAngle = m_noiseAngle;
   params.noiseAngle = m_noiseAngle;
 
 
-  const float target_amp =
-      std::clamp(m_biomeSettings.heightNoiseAmplitude * 0.22F, 0.10F, 0.20F);
-  params.heightNoiseStrength = target_amp;
+  float target_amp;
+  float target_freq;
+  if (m_biomeSettings.groundIrregularityEnabled) {
 
 
-  params.heightNoiseFrequency =
-      std::max(0.6F, m_biomeSettings.heightNoiseFrequency * 1.05F);
+    target_amp =
+        std::clamp(m_biomeSettings.irregularityAmplitude * 0.85F, 0.15F, 0.70F);
+    target_freq = std::max(0.45F, m_biomeSettings.irregularityScale * 2.5F);
+  } else {
+
+    target_amp =
+        std::clamp(m_biomeSettings.heightNoiseAmplitude * 0.22F, 0.10F, 0.20F);
+    target_freq = std::max(0.6F, m_biomeSettings.heightNoiseFrequency * 1.05F);
+  }
+  params.heightNoiseStrength = target_amp;
+  params.heightNoiseFrequency = target_freq;
 
 
   params.microBumpAmp = 0.07F;
   params.microBumpAmp = 0.07F;
   params.microBumpFreq = 2.2F;
   params.microBumpFreq = 2.2F;
@@ -122,6 +131,8 @@ auto GroundRenderer::buildParams() const -> TerrainChunkParams {
 }
 }
 
 
 void GroundRenderer::submit(Renderer &renderer, ResourceManager *resources) {
 void GroundRenderer::submit(Renderer &renderer, ResourceManager *resources) {
+  syncBiomeFromService();
+
   if (resources == nullptr) {
   if (resources == nullptr) {
     return;
     return;
   }
   }
@@ -154,4 +165,39 @@ void GroundRenderer::submit(Renderer &renderer, ResourceManager *resources) {
   renderer.grid(m_model, m_color, cell, 0.06F, extent);
   renderer.grid(m_model, m_color, cell, 0.06F, extent);
 }
 }
 
 
+void GroundRenderer::syncBiomeFromService() {
+  auto &service = Game::Map::TerrainService::instance();
+  if (!service.isInitialized()) {
+    return;
+  }
+  const auto &current = service.biomeSettings();
+  if (!m_hasBiome || !biomeEquals(current, m_biomeSettings)) {
+    m_biomeSettings = current;
+    m_hasBiome = true;
+    updateNoiseOffset();
+    invalidateParamsCache();
+  }
+}
+
+auto GroundRenderer::biomeEquals(const Game::Map::BiomeSettings &a,
+                                 const Game::Map::BiomeSettings &b) -> bool {
+  return a.grassPrimary == b.grassPrimary &&
+         a.grassSecondary == b.grassSecondary && a.grassDry == b.grassDry &&
+         a.soilColor == b.soilColor && a.rockLow == b.rockLow &&
+         a.rockHigh == b.rockHigh &&
+         a.terrainMacroNoiseScale == b.terrainMacroNoiseScale &&
+         a.terrainDetailNoiseScale == b.terrainDetailNoiseScale &&
+         a.terrainSoilHeight == b.terrainSoilHeight &&
+         a.terrainSoilSharpness == b.terrainSoilSharpness &&
+         a.terrainRockThreshold == b.terrainRockThreshold &&
+         a.terrainRockSharpness == b.terrainRockSharpness &&
+         a.terrainAmbientBoost == b.terrainAmbientBoost &&
+         a.terrainRockDetailStrength == b.terrainRockDetailStrength &&
+         a.heightNoiseAmplitude == b.heightNoiseAmplitude &&
+         a.heightNoiseFrequency == b.heightNoiseFrequency &&
+         a.groundIrregularityEnabled == b.groundIrregularityEnabled &&
+         a.irregularityScale == b.irregularityScale &&
+         a.irregularityAmplitude == b.irregularityAmplitude && a.seed == b.seed;
+}
+
 } // namespace Render::GL
 } // namespace Render::GL

+ 4 - 0
render/ground/ground_renderer.h

@@ -5,6 +5,7 @@
 #include <cstdint>
 #include <cstdint>
 
 
 #include "../../game/map/terrain.h"
 #include "../../game/map/terrain.h"
+#include "../../game/map/terrain_service.h"
 #include "../i_render_pass.h"
 #include "../i_render_pass.h"
 #include "terrain_gpu.h"
 #include "terrain_gpu.h"
 
 
@@ -50,6 +51,9 @@ private:
   void recomputeModel();
   void recomputeModel();
   void updateNoiseOffset();
   void updateNoiseOffset();
   auto buildParams() const -> Render::GL::TerrainChunkParams;
   auto buildParams() const -> Render::GL::TerrainChunkParams;
+  void syncBiomeFromService();
+  static auto biomeEquals(const Game::Map::BiomeSettings &a,
+                          const Game::Map::BiomeSettings &b) -> bool;
 
 
   float m_tile_size = 1.0F;
   float m_tile_size = 1.0F;
   int m_width = 50;
   int m_width = 50;