feat(editor): in-editor "Random Populate" menu mirrors CLI

Adds EditorApp::randomPopulateZone(creatureCount, objectCount, seed)
and a Generate-menu submenu with sliders for both counts plus a
seed input. Same logic and bestiary as the --random-populate-zone
CLI: 12 creature templates with level jitter, 5 placeholder WMO
prop types with randomized rotation/scale.

Spawns are placed within the loaded tile's world bbox, marked
dirty + auto-save pending so the populated content persists across
restarts.

The CLI command remains for CI / scripting / batch workflows; the
menu serves designers who want to populate from the GUI without
context-switching to a terminal.

Same-seed reproducibility: a hover hint reminds the user that the
seed determines the output deterministically.
This commit is contained in:
Kelsi 2026-05-07 12:14:13 -07:00
parent bdf2497f46
commit 89dacba666
3 changed files with 98 additions and 0 deletions

View file

@ -1702,6 +1702,84 @@ void EditorApp::generateCompleteZone() {
showToast("Zone generated!");
}
void EditorApp::randomPopulateZone(int creatureCount, int objectCount,
uint32_t seed) {
if (!terrain_.isLoaded() || loadedTileX_ < 0 || loadedTileY_ < 0) {
showToast("Load a tile first");
return;
}
// Loaded tile world bbox. Each tile is 533.33y; WoW grid centers
// tile (32, 32) at origin (+X = -wowY tile, +Y = -wowX tile).
constexpr float kTileSize = 533.33333f;
float wMinX = (32.0f - loadedTileY_ - 1) * kTileSize;
float wMaxX = (32.0f - loadedTileY_) * kTileSize;
float wMinY = (32.0f - loadedTileX_ - 1) * kTileSize;
float wMaxY = (32.0f - loadedTileX_) * kTileSize;
float baseZ = terrain_.chunks[0].position[2];
uint32_t rng = seed ? seed : 1u;
auto next01 = [&]() {
rng = rng * 1664525u + 1013904223u;
return (rng >> 8) / float(1 << 24);
};
auto rangeF = [&](float a, float b) { return a + next01() * (b - a); };
auto rangeI = [&](int a, int b) {
return a + static_cast<int>(next01() * (b - a + 1));
};
static const std::vector<std::pair<const char*, uint32_t>> kRandomCreatures = {
{"Wolf", 5}, {"Boar", 4}, {"Bear", 7}, {"Spider", 3},
{"Bandit", 6}, {"Kobold", 4}, {"Murloc", 5}, {"Skeleton", 5},
{"Wisp", 3}, {"Goblin", 5}, {"Stag", 4}, {"Crab", 3},
};
static const std::vector<const char*> kRandomObjects = {
"World/Generic/Tree01.wmo",
"World/Generic/Boulder.wmo",
"World/Generic/Bush.wmo",
"World/Generic/Stump.wmo",
"World/Generic/Mushroom.wmo",
};
int placedCreatures = 0, placedObjects = 0;
for (int n = 0; n < creatureCount; ++n) {
const auto& [name, baseLvl] = kRandomCreatures[
rangeI(0, static_cast<int>(kRandomCreatures.size()) - 1)];
CreatureSpawn s;
s.name = name;
s.position.x = rangeF(wMinX, wMaxX);
s.position.y = rangeF(wMinY, wMaxY);
s.position.z = baseZ;
int lvl = std::max(1, static_cast<int>(baseLvl) + rangeI(-1, 2));
s.level = static_cast<uint32_t>(lvl);
s.health = 50 + s.level * 10;
s.orientation = rangeF(0.0f, 360.0f);
npcSpawner_.placeCreature(s);
placedCreatures++;
}
auto& objs = objectPlacer_.getObjects();
uint32_t maxUid = 0;
for (const auto& o : objs) maxUid = std::max(maxUid, o.uniqueId);
for (int n = 0; n < objectCount; ++n) {
PlacedObject o;
o.path = kRandomObjects[
rangeI(0, static_cast<int>(kRandomObjects.size()) - 1)];
o.type = PlaceableType::WMO;
o.position.x = rangeF(wMinX, wMaxX);
o.position.y = rangeF(wMinY, wMaxY);
o.position.z = baseZ;
o.rotation = glm::vec3(0.0f, rangeF(0.0f, 6.28f), 0.0f);
o.scale = rangeF(0.8f, 1.4f);
o.uniqueId = ++maxUid;
o.nameId = 0;
o.selected = false;
objs.push_back(o);
placedObjects++;
}
objectsDirty_ = true;
autoSavePendingChanges_ = true;
viewport_.updateNpcMarkers(npcSpawner_.getSpawns());
showToast("Populated: " + std::to_string(placedCreatures) +
" creatures + " + std::to_string(placedObjects) + " objects");
}
void EditorApp::clearAllObjects() {
vkDeviceWaitIdle(window_->getVkContext()->getDevice());
objectPlacer_.clearAll();

View file

@ -98,6 +98,10 @@ public:
int batchConvertAssets(const std::string& dataDir);
void clearAllObjects();
void generateCompleteZone();
// Drop N random creatures + M random objects within the loaded
// tile's world bbox. Mirrors the --random-populate-zone CLI
// command so users can do bulk population from inside the editor.
void randomPopulateZone(int creatureCount, int objectCount, uint32_t seed);
void centerOnTerrain();
// Multi-tile support

View file

@ -379,6 +379,22 @@ void EditorUI::renderMenuBar(EditorApp& app) {
}
if (ImGui::MenuItem("Generate Complete Zone", nullptr, false, app.hasTerrainLoaded()))
app.generateCompleteZone();
if (ImGui::BeginMenu("Random Populate", app.hasTerrainLoaded())) {
static int rpCreatures = 20;
static int rpObjects = 10;
static int rpSeed = 42;
ImGui::SliderInt("Creatures##rp", &rpCreatures, 0, 200);
ImGui::SliderInt("Objects##rp", &rpObjects, 0, 200);
ImGui::InputInt("Seed##rp", &rpSeed);
if (rpSeed < 0) rpSeed = 0;
if (ImGui::Button("Populate##rp", ImVec2(-1, 0))) {
app.randomPopulateZone(rpCreatures, rpObjects,
static_cast<uint32_t>(rpSeed));
}
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
"Same seed → same population (reproducible)");
ImGui::EndMenu();
}
if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) {
if (app.getObjectPlacer().objectCount() > 0 || app.getNpcSpawner().spawnCount() > 0)
app.clearAllObjects();