Browse Source

Add ground variations system with 5 ground types

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 3 weeks ago
parent
commit
3c52397765

+ 119 - 0
assets/maps/GROUND_TYPES.md

@@ -0,0 +1,119 @@
+# Ground Type System
+
+The ground type system allows designers to quickly select from predefined ground variations when creating maps. Each ground type provides a different visual appearance for the terrain.
+
+## Available Ground Types
+
+| Ground Type ID  | Description                        | Visual Characteristics                                 |
+|-----------------|------------------------------------|---------------------------------------------------------|
+| `forest_mud`    | Deep Green + Mud Ground (Default)  | Lush green grass with dark brown muddy soil            |
+| `grass_dry`     | Dry Mediterranean Grass            | Yellowish-green grass with light brown dusty soil      |
+| `soil_rocky`    | Light-Brown Rocky Soil             | Sparse grass with prominent light brown rocks          |
+| `alpine_mix`    | Alpine Rock + Snow Mix             | Cool-toned grass with light gray/white rocky terrain   |
+| `soil_fertile`  | Dark Fertile Farmland Soil         | Rich green grass with very dark brown fertile soil     |
+
+## Usage
+
+Add the `groundType` property to the `biome` section of your map JSON file:
+
+```json
+{
+  "name": "My Alpine Map",
+  "biome": {
+    "groundType": "alpine_mix",
+    "seed": 12345
+  }
+}
+```
+
+## Default Behavior
+
+If no `groundType` is specified, the system defaults to `forest_mud`, which is the original ground style.
+
+## Overriding Ground Type Defaults
+
+Ground type presets can be overridden with explicit values. The ground type defaults are applied first, then any explicit values in the JSON are applied on top:
+
+```json
+{
+  "biome": {
+    "groundType": "alpine_mix",
+    "grassPrimary": [0.2, 0.5, 0.2],
+    "seed": 54321
+  }
+}
+```
+
+In this example, the map will use all alpine_mix defaults except for `grassPrimary`, which is explicitly set.
+
+## Backward Compatibility
+
+Existing maps without the `groundType` property will continue to work exactly as before, using the default `forest_mud` ground type settings.
+
+## Example: Complete Map with Ground Type
+
+```json
+{
+  "name": "Mediterranean Battlefield",
+  "coordSystem": "grid",
+  "grid": {
+    "width": 100,
+    "height": 100,
+    "tileSize": 1.0
+  },
+  "biome": {
+    "groundType": "grass_dry",
+    "seed": 98765,
+    "patchDensity": 3.0,
+    "plantDensity": 0.4
+  },
+  "camera": {
+    "center": [50, 0, 50],
+    "distance": 30.0,
+    "tiltDeg": 45.0,
+    "yaw": 225.0
+  }
+}
+```
+
+## Ground Type Color Presets
+
+### forest_mud (Default)
+- **Grass Primary:** Deep green (0.30, 0.60, 0.28)
+- **Grass Secondary:** Medium green (0.44, 0.70, 0.32)
+- **Grass Dry:** Olive (0.72, 0.66, 0.48)
+- **Soil Color:** Dark brown (0.28, 0.24, 0.18)
+- **Rock Low:** Gray (0.48, 0.46, 0.44)
+- **Rock High:** Light gray (0.68, 0.69, 0.73)
+
+### grass_dry
+- **Grass Primary:** Dry yellow-green (0.55, 0.52, 0.30)
+- **Grass Secondary:** Tan-green (0.62, 0.58, 0.35)
+- **Grass Dry:** Golden (0.75, 0.68, 0.42)
+- **Soil Color:** Sandy brown (0.45, 0.38, 0.28)
+- **Rock Low:** Warm gray (0.58, 0.55, 0.50)
+- **Rock High:** Light warm gray (0.72, 0.70, 0.65)
+
+### soil_rocky
+- **Grass Primary:** Muted green (0.42, 0.48, 0.30)
+- **Grass Secondary:** Sage (0.50, 0.54, 0.35)
+- **Grass Dry:** Tan (0.60, 0.55, 0.40)
+- **Soil Color:** Medium brown (0.50, 0.42, 0.32)
+- **Rock Low:** Brownish gray (0.55, 0.52, 0.48)
+- **Rock High:** Warm light gray (0.70, 0.68, 0.65)
+
+### alpine_mix
+- **Grass Primary:** Cool green (0.35, 0.42, 0.32)
+- **Grass Secondary:** Teal-green (0.40, 0.48, 0.38)
+- **Grass Dry:** Cool gray-green (0.55, 0.52, 0.45)
+- **Soil Color:** Cool gray (0.38, 0.35, 0.32)
+- **Rock Low:** Bluish gray (0.60, 0.62, 0.65)
+- **Rock High:** Near white/snow (0.85, 0.88, 0.92)
+
+### soil_fertile
+- **Grass Primary:** Rich green (0.28, 0.52, 0.25)
+- **Grass Secondary:** Vibrant green (0.38, 0.62, 0.32)
+- **Grass Dry:** Olive-brown (0.55, 0.50, 0.35)
+- **Soil Color:** Very dark brown (0.22, 0.18, 0.14)
+- **Rock Low:** Dark gray (0.42, 0.40, 0.38)
+- **Rock High:** Medium gray (0.55, 0.54, 0.52)

+ 1 - 0
game/map/json_keys.h

@@ -8,6 +8,7 @@ inline constexpr const char *COORD_SYSTEM = "coordSystem";
 inline constexpr const char *MAX_TROOPS_PER_PLAYER = "maxTroopsPerPlayer";
 inline constexpr const char *GRID = "grid";
 inline constexpr const char *BIOME = "biome";
+inline constexpr const char *GROUND_TYPE = "groundType";
 inline constexpr const char *CAMERA = "camera";
 inline constexpr const char *SPAWNS = "spawns";
 inline constexpr const char *FIRECAMPS = "firecamps";

+ 13 - 0
game/map/map_loader.cpp

@@ -85,6 +85,19 @@ auto readVector3(const QJsonValue &value,
 }
 
 void readBiome(const QJsonObject &obj, BiomeSettings &out) {
+  // First, check for ground_type and apply defaults if specified
+  if (obj.contains(GROUND_TYPE)) {
+    const QString ground_type_str = obj.value(GROUND_TYPE).toString();
+    GroundType parsed_ground_type;
+    if (tryParseGroundType(ground_type_str, parsed_ground_type)) {
+      applyGroundTypeDefaults(out, parsed_ground_type);
+    } else {
+      qWarning() << "MapLoader: unknown ground type" << ground_type_str
+                 << "- using default (forest_mud)";
+      applyGroundTypeDefaults(out, GroundType::ForestMud);
+    }
+  }
+  // Then apply any explicit overrides from JSON
   if (obj.contains(SEED)) {
     out.seed = static_cast<std::uint32_t>(
         std::max(0.0, obj.value(SEED).toDouble(out.seed)));

+ 125 - 0
game/map/terrain.h

@@ -12,6 +12,68 @@ namespace Game::Map {
 
 enum class TerrainType { Flat, Hill, Mountain, River };
 
+enum class GroundType {
+  ForestMud,    // Default: Deep green + mud ground (current style)
+  GrassDry,     // Dry Mediterranean Grass
+  SoilRocky,    // Light-Brown Rocky Soil
+  AlpineMix,    // Alpine Rock + Snow Mix
+  SoilFertile   // Dark Fertile Farmland Soil
+};
+
+inline auto groundTypeToQString(GroundType type) -> QString {
+  switch (type) {
+  case GroundType::ForestMud:
+    return QStringLiteral("forest_mud");
+  case GroundType::GrassDry:
+    return QStringLiteral("grass_dry");
+  case GroundType::SoilRocky:
+    return QStringLiteral("soil_rocky");
+  case GroundType::AlpineMix:
+    return QStringLiteral("alpine_mix");
+  case GroundType::SoilFertile:
+    return QStringLiteral("soil_fertile");
+  }
+  return QStringLiteral("forest_mud");
+}
+
+inline auto groundTypeToString(GroundType type) -> std::string {
+  return groundTypeToQString(type).toStdString();
+}
+
+inline auto tryParseGroundType(const QString &value, GroundType &out) -> bool {
+  const QString lowered = value.trimmed().toLower();
+  if (lowered == QStringLiteral("forest_mud")) {
+    out = GroundType::ForestMud;
+    return true;
+  }
+  if (lowered == QStringLiteral("grass_dry")) {
+    out = GroundType::GrassDry;
+    return true;
+  }
+  if (lowered == QStringLiteral("soil_rocky")) {
+    out = GroundType::SoilRocky;
+    return true;
+  }
+  if (lowered == QStringLiteral("alpine_mix")) {
+    out = GroundType::AlpineMix;
+    return true;
+  }
+  if (lowered == QStringLiteral("soil_fertile")) {
+    out = GroundType::SoilFertile;
+    return true;
+  }
+  return false;
+}
+
+inline auto
+groundTypeFromString(const std::string &str) -> std::optional<GroundType> {
+  GroundType result;
+  if (tryParseGroundType(QString::fromStdString(str), result)) {
+    return result;
+  }
+  return std::nullopt;
+}
+
 inline auto terrainTypeToQString(TerrainType type) -> QString {
   switch (type) {
   case TerrainType::Flat:
@@ -62,6 +124,7 @@ terrainTypeFromString(const std::string &str) -> std::optional<TerrainType> {
 }
 
 struct BiomeSettings {
+  GroundType groundType = GroundType::ForestMud;
   QVector3D grassPrimary{0.30F, 0.60F, 0.28F};
   QVector3D grassSecondary{0.44F, 0.70F, 0.32F};
   QVector3D grassDry{0.72F, 0.66F, 0.48F};
@@ -97,6 +160,68 @@ struct BiomeSettings {
   float irregularityAmplitude = 0.08F;
 };
 
+inline void applyGroundTypeDefaults(BiomeSettings &settings,
+                                    GroundType groundType) {
+  settings.groundType = groundType;
+  switch (groundType) {
+  case GroundType::ForestMud:
+    // Default: Deep green + mud ground (current style)
+    settings.grassPrimary = QVector3D(0.30F, 0.60F, 0.28F);
+    settings.grassSecondary = QVector3D(0.44F, 0.70F, 0.32F);
+    settings.grassDry = QVector3D(0.72F, 0.66F, 0.48F);
+    settings.soilColor = QVector3D(0.28F, 0.24F, 0.18F);
+    settings.rockLow = QVector3D(0.48F, 0.46F, 0.44F);
+    settings.rockHigh = QVector3D(0.68F, 0.69F, 0.73F);
+    settings.terrainAmbientBoost = 1.08F;
+    settings.terrainRockDetailStrength = 0.35F;
+    break;
+  case GroundType::GrassDry:
+    // Dry Mediterranean Grass
+    settings.grassPrimary = QVector3D(0.55F, 0.52F, 0.30F);
+    settings.grassSecondary = QVector3D(0.62F, 0.58F, 0.35F);
+    settings.grassDry = QVector3D(0.75F, 0.68F, 0.42F);
+    settings.soilColor = QVector3D(0.45F, 0.38F, 0.28F);
+    settings.rockLow = QVector3D(0.58F, 0.55F, 0.50F);
+    settings.rockHigh = QVector3D(0.72F, 0.70F, 0.65F);
+    settings.terrainAmbientBoost = 1.15F;
+    settings.terrainRockDetailStrength = 0.30F;
+    break;
+  case GroundType::SoilRocky:
+    // Light-Brown Rocky Soil
+    settings.grassPrimary = QVector3D(0.42F, 0.48F, 0.30F);
+    settings.grassSecondary = QVector3D(0.50F, 0.54F, 0.35F);
+    settings.grassDry = QVector3D(0.60F, 0.55F, 0.40F);
+    settings.soilColor = QVector3D(0.50F, 0.42F, 0.32F);
+    settings.rockLow = QVector3D(0.55F, 0.52F, 0.48F);
+    settings.rockHigh = QVector3D(0.70F, 0.68F, 0.65F);
+    settings.terrainAmbientBoost = 1.05F;
+    settings.terrainRockDetailStrength = 0.55F;
+    break;
+  case GroundType::AlpineMix:
+    // Alpine Rock + Snow Mix
+    settings.grassPrimary = QVector3D(0.35F, 0.42F, 0.32F);
+    settings.grassSecondary = QVector3D(0.40F, 0.48F, 0.38F);
+    settings.grassDry = QVector3D(0.55F, 0.52F, 0.45F);
+    settings.soilColor = QVector3D(0.38F, 0.35F, 0.32F);
+    settings.rockLow = QVector3D(0.60F, 0.62F, 0.65F);
+    settings.rockHigh = QVector3D(0.85F, 0.88F, 0.92F);
+    settings.terrainAmbientBoost = 1.20F;
+    settings.terrainRockDetailStrength = 0.45F;
+    break;
+  case GroundType::SoilFertile:
+    // Dark Fertile Farmland Soil
+    settings.grassPrimary = QVector3D(0.28F, 0.52F, 0.25F);
+    settings.grassSecondary = QVector3D(0.38F, 0.62F, 0.32F);
+    settings.grassDry = QVector3D(0.55F, 0.50F, 0.35F);
+    settings.soilColor = QVector3D(0.22F, 0.18F, 0.14F);
+    settings.rockLow = QVector3D(0.42F, 0.40F, 0.38F);
+    settings.rockHigh = QVector3D(0.55F, 0.54F, 0.52F);
+    settings.terrainAmbientBoost = 1.02F;
+    settings.terrainRockDetailStrength = 0.25F;
+    break;
+  }
+}
+
 struct TerrainFeature {
   TerrainType type;
   float center_x{};

+ 1 - 1
render/ground/ground_renderer.cpp

@@ -181,7 +181,7 @@ void GroundRenderer::syncBiomeFromService() {
 
 auto GroundRenderer::biomeEquals(const Game::Map::BiomeSettings &a,
                                  const Game::Map::BiomeSettings &b) -> bool {
-  return a.grassPrimary == b.grassPrimary &&
+  return a.groundType == b.groundType && a.grassPrimary == b.grassPrimary &&
          a.grassSecondary == b.grassSecondary && a.grassDry == b.grassDry &&
          a.soilColor == b.soilColor && a.rockLow == b.rockLow &&
          a.rockHigh == b.rockHigh &&

+ 1 - 0
tests/CMakeLists.txt

@@ -4,6 +4,7 @@
 # Test executable
 add_executable(standard_of_iron_tests
     core/serialization_test.cpp
+    core/ground_type_test.cpp
     db/save_storage_test.cpp
     render/pose_controller_test.cpp
     render/pose_controller_compatibility_test.cpp

+ 251 - 0
tests/core/ground_type_test.cpp

@@ -0,0 +1,251 @@
+#include "map/map_loader.h"
+#include "map/terrain.h"
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QTemporaryFile>
+#include <gtest/gtest.h>
+
+using namespace Game::Map;
+
+class GroundTypeTest : public ::testing::Test {
+protected:
+  void SetUp() override {}
+  void TearDown() override {}
+};
+
+TEST_F(GroundTypeTest, GroundTypeEnumToString) {
+  EXPECT_EQ(groundTypeToQString(GroundType::ForestMud),
+            QStringLiteral("forest_mud"));
+  EXPECT_EQ(groundTypeToQString(GroundType::GrassDry),
+            QStringLiteral("grass_dry"));
+  EXPECT_EQ(groundTypeToQString(GroundType::SoilRocky),
+            QStringLiteral("soil_rocky"));
+  EXPECT_EQ(groundTypeToQString(GroundType::AlpineMix),
+            QStringLiteral("alpine_mix"));
+  EXPECT_EQ(groundTypeToQString(GroundType::SoilFertile),
+            QStringLiteral("soil_fertile"));
+}
+
+TEST_F(GroundTypeTest, GroundTypeStringToEnum) {
+  GroundType result;
+
+  EXPECT_TRUE(tryParseGroundType("forest_mud", result));
+  EXPECT_EQ(result, GroundType::ForestMud);
+
+  EXPECT_TRUE(tryParseGroundType("grass_dry", result));
+  EXPECT_EQ(result, GroundType::GrassDry);
+
+  EXPECT_TRUE(tryParseGroundType("soil_rocky", result));
+  EXPECT_EQ(result, GroundType::SoilRocky);
+
+  EXPECT_TRUE(tryParseGroundType("alpine_mix", result));
+  EXPECT_EQ(result, GroundType::AlpineMix);
+
+  EXPECT_TRUE(tryParseGroundType("soil_fertile", result));
+  EXPECT_EQ(result, GroundType::SoilFertile);
+}
+
+TEST_F(GroundTypeTest, GroundTypeParsingCaseInsensitive) {
+  GroundType result;
+
+  EXPECT_TRUE(tryParseGroundType("FOREST_MUD", result));
+  EXPECT_EQ(result, GroundType::ForestMud);
+
+  EXPECT_TRUE(tryParseGroundType("Forest_Mud", result));
+  EXPECT_EQ(result, GroundType::ForestMud);
+
+  EXPECT_TRUE(tryParseGroundType("  grass_dry  ", result));
+  EXPECT_EQ(result, GroundType::GrassDry);
+}
+
+TEST_F(GroundTypeTest, GroundTypeParsingInvalidReturnsDefault) {
+  GroundType result = GroundType::ForestMud;
+
+  EXPECT_FALSE(tryParseGroundType("invalid_type", result));
+  EXPECT_FALSE(tryParseGroundType("", result));
+  EXPECT_FALSE(tryParseGroundType("unknown", result));
+}
+
+TEST_F(GroundTypeTest, ApplyGroundTypeDefaultsForestMud) {
+  BiomeSettings settings;
+  applyGroundTypeDefaults(settings, GroundType::ForestMud);
+
+  EXPECT_EQ(settings.groundType, GroundType::ForestMud);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.x(), 0.30F);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.y(), 0.60F);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.z(), 0.28F);
+  EXPECT_FLOAT_EQ(settings.soilColor.x(), 0.28F);
+  EXPECT_FLOAT_EQ(settings.soilColor.y(), 0.24F);
+  EXPECT_FLOAT_EQ(settings.soilColor.z(), 0.18F);
+}
+
+TEST_F(GroundTypeTest, ApplyGroundTypeDefaultsGrassDry) {
+  BiomeSettings settings;
+  applyGroundTypeDefaults(settings, GroundType::GrassDry);
+
+  EXPECT_EQ(settings.groundType, GroundType::GrassDry);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.x(), 0.55F);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.y(), 0.52F);
+  EXPECT_FLOAT_EQ(settings.grassPrimary.z(), 0.30F);
+  EXPECT_FLOAT_EQ(settings.terrainAmbientBoost, 1.15F);
+}
+
+TEST_F(GroundTypeTest, ApplyGroundTypeDefaultsSoilRocky) {
+  BiomeSettings settings;
+  applyGroundTypeDefaults(settings, GroundType::SoilRocky);
+
+  EXPECT_EQ(settings.groundType, GroundType::SoilRocky);
+  EXPECT_FLOAT_EQ(settings.soilColor.x(), 0.50F);
+  EXPECT_FLOAT_EQ(settings.soilColor.y(), 0.42F);
+  EXPECT_FLOAT_EQ(settings.soilColor.z(), 0.32F);
+  EXPECT_FLOAT_EQ(settings.terrainRockDetailStrength, 0.55F);
+}
+
+TEST_F(GroundTypeTest, ApplyGroundTypeDefaultsAlpineMix) {
+  BiomeSettings settings;
+  applyGroundTypeDefaults(settings, GroundType::AlpineMix);
+
+  EXPECT_EQ(settings.groundType, GroundType::AlpineMix);
+  EXPECT_FLOAT_EQ(settings.rockHigh.x(), 0.85F);
+  EXPECT_FLOAT_EQ(settings.rockHigh.y(), 0.88F);
+  EXPECT_FLOAT_EQ(settings.rockHigh.z(), 0.92F);
+  EXPECT_FLOAT_EQ(settings.terrainAmbientBoost, 1.20F);
+}
+
+TEST_F(GroundTypeTest, ApplyGroundTypeDefaultsSoilFertile) {
+  BiomeSettings settings;
+  applyGroundTypeDefaults(settings, GroundType::SoilFertile);
+
+  EXPECT_EQ(settings.groundType, GroundType::SoilFertile);
+  EXPECT_FLOAT_EQ(settings.soilColor.x(), 0.22F);
+  EXPECT_FLOAT_EQ(settings.soilColor.y(), 0.18F);
+  EXPECT_FLOAT_EQ(settings.soilColor.z(), 0.14F);
+  EXPECT_FLOAT_EQ(settings.terrainRockDetailStrength, 0.25F);
+}
+
+TEST_F(GroundTypeTest, MapLoaderWithGroundType) {
+  QTemporaryFile temp_file;
+  ASSERT_TRUE(temp_file.open());
+
+  QJsonObject biome;
+  biome["groundType"] = "grass_dry";
+  biome["seed"] = 12345;
+
+  QJsonObject grid;
+  grid["width"] = 50;
+  grid["height"] = 50;
+  grid["tileSize"] = 1.0;
+
+  QJsonObject root;
+  root["name"] = "Test Map";
+  root["grid"] = grid;
+  root["biome"] = biome;
+
+  QJsonDocument doc(root);
+  temp_file.write(doc.toJson());
+  temp_file.close();
+
+  MapDefinition map_def;
+  QString error;
+  bool success = MapLoader::loadFromJsonFile(temp_file.fileName(), map_def, &error);
+
+  ASSERT_TRUE(success) << "Failed to load map: " << error.toStdString();
+  EXPECT_EQ(map_def.biome.groundType, GroundType::GrassDry);
+  EXPECT_EQ(map_def.biome.seed, 12345U);
+}
+
+TEST_F(GroundTypeTest, MapLoaderWithoutGroundTypeUsesDefault) {
+  QTemporaryFile temp_file;
+  ASSERT_TRUE(temp_file.open());
+
+  QJsonObject biome;
+  biome["seed"] = 54321;
+
+  QJsonObject grid;
+  grid["width"] = 50;
+  grid["height"] = 50;
+  grid["tileSize"] = 1.0;
+
+  QJsonObject root;
+  root["name"] = "Test Map Without Ground Type";
+  root["grid"] = grid;
+  root["biome"] = biome;
+
+  QJsonDocument doc(root);
+  temp_file.write(doc.toJson());
+  temp_file.close();
+
+  MapDefinition map_def;
+  QString error;
+  bool success = MapLoader::loadFromJsonFile(temp_file.fileName(), map_def, &error);
+
+  ASSERT_TRUE(success) << "Failed to load map: " << error.toStdString();
+  EXPECT_EQ(map_def.biome.groundType, GroundType::ForestMud);
+  EXPECT_EQ(map_def.biome.seed, 54321U);
+}
+
+TEST_F(GroundTypeTest, MapLoaderGroundTypeOverriddenByExplicitValues) {
+  QTemporaryFile temp_file;
+  ASSERT_TRUE(temp_file.open());
+
+  QJsonObject biome;
+  biome["groundType"] = "alpine_mix";
+  biome["seed"] = 99999;
+  // Override the grass primary color that would be set by alpine_mix defaults
+  QJsonArray grass_primary;
+  grass_primary.append(0.10);
+  grass_primary.append(0.20);
+  grass_primary.append(0.30);
+  biome["grassPrimary"] = grass_primary;
+
+  QJsonObject grid;
+  grid["width"] = 50;
+  grid["height"] = 50;
+  grid["tileSize"] = 1.0;
+
+  QJsonObject root;
+  root["name"] = "Test Map With Override";
+  root["grid"] = grid;
+  root["biome"] = biome;
+
+  QJsonDocument doc(root);
+  temp_file.write(doc.toJson());
+  temp_file.close();
+
+  MapDefinition map_def;
+  QString error;
+  bool success = MapLoader::loadFromJsonFile(temp_file.fileName(), map_def, &error);
+
+  ASSERT_TRUE(success) << "Failed to load map: " << error.toStdString();
+  EXPECT_EQ(map_def.biome.groundType, GroundType::AlpineMix);
+  EXPECT_EQ(map_def.biome.seed, 99999U);
+  // Grass primary should be the overridden values, not the alpine_mix defaults
+  EXPECT_NEAR(map_def.biome.grassPrimary.x(), 0.10F, 0.001F);
+  EXPECT_NEAR(map_def.biome.grassPrimary.y(), 0.20F, 0.001F);
+  EXPECT_NEAR(map_def.biome.grassPrimary.z(), 0.30F, 0.001F);
+}
+
+TEST_F(GroundTypeTest, AllGroundTypesFromString) {
+  auto forest_mud = groundTypeFromString("forest_mud");
+  ASSERT_TRUE(forest_mud.has_value());
+  EXPECT_EQ(forest_mud.value(), GroundType::ForestMud);
+
+  auto grass_dry = groundTypeFromString("grass_dry");
+  ASSERT_TRUE(grass_dry.has_value());
+  EXPECT_EQ(grass_dry.value(), GroundType::GrassDry);
+
+  auto soil_rocky = groundTypeFromString("soil_rocky");
+  ASSERT_TRUE(soil_rocky.has_value());
+  EXPECT_EQ(soil_rocky.value(), GroundType::SoilRocky);
+
+  auto alpine_mix = groundTypeFromString("alpine_mix");
+  ASSERT_TRUE(alpine_mix.has_value());
+  EXPECT_EQ(alpine_mix.value(), GroundType::AlpineMix);
+
+  auto soil_fertile = groundTypeFromString("soil_fertile");
+  ASSERT_TRUE(soil_fertile.has_value());
+  EXPECT_EQ(soil_fertile.value(), GroundType::SoilFertile);
+
+  auto invalid = groundTypeFromString("invalid");
+  EXPECT_FALSE(invalid.has_value());
+}