diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index e866d59f..d7989338 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -1668,6 +1668,34 @@ void EditorUI::renderObjectPanel(EditorApp& app) { } } + if (ImGui::CollapsingHeader("Auto-Populate Biome")) { + static int popBiome = 0; + static uint32_t popSeed = 42; + const char* biomeNames[] = {"Grassland", "Forest", "Jungle", "Desert", + "Barrens", "Snow", "Swamp", "Rocky", "Beach", "Volcanic"}; + ImGui::Combo("Biome##pop", &popBiome, biomeNames, 10); + int seed = static_cast(popSeed); + if (ImGui::InputInt("Seed##pop", &seed)) popSeed = static_cast(seed); + + auto veg = getBiomeVegetation(static_cast(popBiome)); + ImGui::TextColored(ImVec4(0.6f,0.6f,0.6f,1), "%zu asset types, density-based", + veg.assets.size()); + + if (ImGui::Button("Populate Zone", ImVec2(-1, 0)) && app.hasTerrainLoaded()) { + auto* t = app.getTerrainEditor().getTerrain(); + float tileSize = 533.33333f; + glm::vec3 origin( + (32.0f - t->coord.y) * tileSize, + (32.0f - t->coord.x) * tileSize, 0); + int n = placer.populateBiome(veg, tileSize, origin, popSeed); + app.markObjectsDirty(); + app.showToast("Populated " + std::string(biomeNames[popBiome]) + + ": " + std::to_string(n) + " objects"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Auto-place trees, rocks, bushes based on biome rules"); + } + ImGui::Separator(); // Bulk operations if (ImGui::CollapsingHeader("Bulk Operations")) { diff --git a/tools/editor/object_placer.cpp b/tools/editor/object_placer.cpp index 64b057ef..b7a25da5 100644 --- a/tools/editor/object_placer.cpp +++ b/tools/editor/object_placer.cpp @@ -1,4 +1,5 @@ #include "object_placer.hpp" +#include "terrain_biomes.hpp" #include "core/logger.hpp" #include #include @@ -199,6 +200,45 @@ void ObjectPlacer::scatter(const glm::vec3& center, float radius, int count, LOG_INFO("Scattered ", count, " objects in radius ", radius); } +int ObjectPlacer::populateBiome(const BiomeVegetation& vegetation, + float tileSize, const glm::vec3& tileOrigin, + uint32_t seed) { + int placed = 0; + std::mt19937 rng(seed); + std::uniform_real_distribution distPos(0.0f, 1.0f); + std::uniform_real_distribution distRot(0.0f, 360.0f); + + for (const auto& asset : vegetation.assets) { + // Calculate object count from density (per 100x100 area) + float areaFactor = (tileSize * tileSize) / 10000.0f; + int count = static_cast(asset.density * areaFactor); + + std::uniform_real_distribution distScale(asset.minScale, asset.maxScale); + + for (int i = 0; i < count; i++) { + float u = distPos(rng); + float v = distPos(rng); + glm::vec3 pos = tileOrigin + glm::vec3( + -u * tileSize, -v * tileSize, 0.0f); + + PlacedObject obj; + obj.type = PlaceableType::M2; + obj.path = asset.path; + obj.nameId = 0; + obj.uniqueId = nextUniqueId(); + obj.position = pos; + obj.rotation = glm::vec3(0.0f, distRot(rng), 0.0f); + obj.scale = distScale(rng); + obj.selected = false; + objects_.push_back(obj); + placed++; + } + } + + LOG_INFO("Biome populated: ", vegetation.name, " — ", placed, " objects placed"); + return placed; +} + void ObjectPlacer::undoLastPlace() { if (undoStack_.empty()) return; int idx = undoStack_.back(); diff --git a/tools/editor/object_placer.hpp b/tools/editor/object_placer.hpp index fc2ed7a9..565f7bbc 100644 --- a/tools/editor/object_placer.hpp +++ b/tools/editor/object_placer.hpp @@ -82,6 +82,11 @@ public: void scatter(const glm::vec3& center, float radius, int count, float minScale, float maxScale); + // Procedural biome population: auto-place vegetation based on rules + int populateBiome(const struct BiomeVegetation& vegetation, + float tileSize, const glm::vec3& tileOrigin, + uint32_t seed = 42); + private: uint32_t nextUniqueId(); diff --git a/tools/editor/terrain_biomes.hpp b/tools/editor/terrain_biomes.hpp index 0269482f..6bd2055c 100644 --- a/tools/editor/terrain_biomes.hpp +++ b/tools/editor/terrain_biomes.hpp @@ -109,5 +109,75 @@ inline const char* getBiomeName(Biome b) { return getBiomeTextures(b).name; } +// Vegetation rule: which M2 models to scatter per biome +struct VegetationAsset { + const char* path; + float density; // objects per 100x100 unit area + float minScale; + float maxScale; + float maxSlope; // max terrain slope (0-1, 0=flat only, 1=any) + float minHeight; // relative to base (-999=any) + float maxHeight; // relative to base (999=any) +}; + +struct BiomeVegetation { + const char* name; + std::vector assets; +}; + +inline BiomeVegetation getBiomeVegetation(Biome biome) { + switch (biome) { + case Biome::Grassland: return {"Grassland", { + {"World\\Doodad\\Azeroth\\Elwynn\\PineTree\\ElwynnPineTree01.m2", 3.0f, 0.8f, 1.4f, 0.6f, -999, 999}, + {"World\\Doodad\\Azeroth\\Elwynn\\ElwynnBush01.m2", 5.0f, 0.6f, 1.2f, 0.8f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\Rock01.m2", 1.0f, 0.5f, 1.5f, 1.0f, -999, 999}, + }}; + case Biome::Forest: return {"Forest", { + {"World\\Doodad\\Azeroth\\Ashenvale\\AshenvaleTree01.m2", 6.0f, 0.7f, 1.5f, 0.5f, -999, 999}, + {"World\\Doodad\\Azeroth\\Ashenvale\\AshenvaleTree02.m2", 4.0f, 0.8f, 1.3f, 0.5f, -999, 999}, + {"World\\Doodad\\Azeroth\\Ashenvale\\AshenvaleFern01.m2", 8.0f, 0.4f, 0.9f, 0.7f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\ForestRock01.m2", 1.5f, 0.6f, 1.8f, 1.0f, -999, 999}, + }}; + case Biome::Jungle: return {"Jungle", { + {"World\\Doodad\\Azeroth\\Stranglethorn\\StranglethornPalmTree01.m2", 5.0f, 0.8f, 1.4f, 0.5f, -999, 999}, + {"World\\Doodad\\Azeroth\\Stranglethorn\\StranglethornFern01.m2", 10.0f, 0.3f, 0.8f, 0.8f, -999, 999}, + {"World\\Doodad\\Azeroth\\Stranglethorn\\StranglethornVines01.m2", 3.0f, 0.7f, 1.2f, 0.6f, -999, 999}, + }}; + case Biome::Desert: return {"Desert", { + {"World\\Doodad\\Azeroth\\Tanaris\\TanarisCactus01.m2", 2.0f, 0.6f, 1.3f, 0.7f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\DesertRock01.m2", 1.5f, 0.5f, 2.0f, 1.0f, -999, 999}, + {"World\\Doodad\\Azeroth\\Tanaris\\TanarisBones01.m2", 0.5f, 0.8f, 1.2f, 0.5f, -999, 999}, + }}; + case Biome::Barrens: return {"Barrens", { + {"World\\Doodad\\Azeroth\\Barrens\\BarrensTree01.m2", 1.5f, 0.7f, 1.3f, 0.6f, -999, 999}, + {"World\\Doodad\\Azeroth\\Barrens\\BarrensBush01.m2", 3.0f, 0.5f, 1.0f, 0.8f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\BarrensRock01.m2", 1.0f, 0.6f, 1.5f, 1.0f, -999, 999}, + }}; + case Biome::Snow: return {"Snow", { + {"World\\Doodad\\Azeroth\\Winterspring\\WinterspringPine01.m2", 4.0f, 0.8f, 1.5f, 0.5f, -999, 999}, + {"World\\Doodad\\Azeroth\\Winterspring\\WinterspringSnowDrift01.m2", 2.0f, 0.5f, 1.2f, 0.4f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\SnowRock01.m2", 1.0f, 0.6f, 1.8f, 1.0f, -999, 999}, + }}; + case Biome::Swamp: return {"Swamp", { + {"World\\Doodad\\Azeroth\\Wetlands\\WetlandsTree01.m2", 4.0f, 0.7f, 1.3f, 0.5f, -999, 999}, + {"World\\Doodad\\Azeroth\\Wetlands\\WetlandsMushroom01.m2", 6.0f, 0.3f, 0.7f, 0.8f, -999, 999}, + {"World\\Doodad\\Azeroth\\Wetlands\\WetlandsLog01.m2", 1.5f, 0.8f, 1.2f, 0.4f, -999, 999}, + }}; + case Biome::Rocky: return {"Rocky", { + {"World\\Doodad\\Azeroth\\Rock\\Rock01.m2", 3.0f, 0.5f, 2.5f, 1.0f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\RockPile01.m2", 2.0f, 0.6f, 1.5f, 1.0f, -999, 999}, + }}; + case Biome::Beach: return {"Beach", { + {"World\\Doodad\\Azeroth\\Stranglethorn\\StranglethornPalmTree01.m2", 2.0f, 0.7f, 1.3f, 0.4f, -999, 999}, + {"World\\Doodad\\Azeroth\\Rock\\BeachRock01.m2", 1.5f, 0.5f, 1.5f, 0.6f, -999, 999}, + }}; + case Biome::Volcanic: return {"Volcanic", { + {"World\\Doodad\\Azeroth\\Rock\\LavaRock01.m2", 2.5f, 0.6f, 2.0f, 1.0f, -999, 999}, + {"World\\Doodad\\Azeroth\\Burning Steppes\\BurningSteppesCharredTree01.m2", 1.0f, 0.8f, 1.2f, 0.5f, -999, 999}, + }}; + default: return {"Unknown", {}}; + } +} + } // namespace editor } // namespace wowee