diff --git a/CMakeLists.txt b/CMakeLists.txt index e7f7a5ca..69061d2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1341,6 +1341,7 @@ add_executable(wowee_editor tools/editor/cli_clone.cpp tools/editor/cli_remove.cpp tools/editor/cli_add.cpp + tools/editor/cli_random.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_random.cpp b/tools/editor/cli_random.cpp new file mode 100644 index 00000000..808b38cd --- /dev/null +++ b/tools/editor/cli_random.cpp @@ -0,0 +1,499 @@ +#include "cli_random.hpp" + +#include "zone_manifest.hpp" +#include "npc_spawner.hpp" +#include "object_placer.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleRandomPopulateZone(int& i, int argc, char** argv) { + // Randomly add creatures and/or objects to a zone for + // playtest scenarios. Reads the zone manifest's tile + // bounds so spawn positions stay inside the actual + // playable area. Seeded LCG for reproducibility — same + // seed always produces the same population. + // + // Flags: + // --seed N (default 42) + // --creatures N (default 20) + // --objects N (default 10) + std::string zoneDir = argv[++i]; + uint32_t seed = 42; + int creatureCount = 20; + int objectCount = 10; + while (i + 2 < argc && argv[i + 1][0] == '-') { + std::string flag = argv[++i]; + if (flag == "--seed") { + try { seed = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } else if (flag == "--creatures") { + try { creatureCount = std::stoi(argv[++i]); } + catch (...) {} + } else if (flag == "--objects") { + try { objectCount = std::stoi(argv[++i]); } + catch (...) {} + } else { + std::fprintf(stderr, + "random-populate-zone: unknown flag '%s'\n", flag.c_str()); + return 1; + } + } + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "random-populate-zone: %s has no zone.json\n", + zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, + "random-populate-zone: failed to parse %s\n", + manifestPath.c_str()); + return 1; + } + if (zm.tiles.empty()) { + std::fprintf(stderr, + "random-populate-zone: zone has no tiles to populate\n"); + return 1; + } + // Compute the world AABB the zone occupies so spawns land + // inside it. Each tile is 533.33y; WoW grid centers tile + // (32, 32) at world origin. + constexpr float kTileSize = 533.33333f; + int tMinX = 64, tMaxX = -1, tMinY = 64, tMaxY = -1; + for (const auto& [tx, ty] : zm.tiles) { + tMinX = std::min(tMinX, tx); tMaxX = std::max(tMaxX, tx); + tMinY = std::min(tMinY, ty); tMaxY = std::max(tMaxY, ty); + } + float wMinX = (32.0f - tMaxY - 1) * kTileSize; + float wMaxX = (32.0f - tMinY) * kTileSize; + float wMinY = (32.0f - tMaxX - 1) * kTileSize; + float wMaxY = (32.0f - tMinX) * kTileSize; + float baseZ = zm.baseHeight; + + 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(next01() * (b - a + 1)); + }; + + // Tiny bestiary so the random output reads as plausible + // rather than "Creature1 / Creature2". + static const std::vector> 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 kRandomObjects = { + "World/Generic/Tree01.wmo", + "World/Generic/Boulder.wmo", + "World/Generic/Bush.wmo", + "World/Generic/Stump.wmo", + "World/Generic/Mushroom.wmo", + }; + + // Creatures. + wowee::editor::NpcSpawner spawner; + std::string cpath = zoneDir + "/creatures.json"; + if (fs::exists(cpath)) spawner.loadFromFile(cpath); + int placedCreatures = 0; + for (int n = 0; n < creatureCount; ++n) { + const auto& [name, baseLvl] = kRandomCreatures[ + rangeI(0, static_cast(kRandomCreatures.size()) - 1)]; + wowee::editor::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(baseLvl) + rangeI(-1, 2)); + s.level = static_cast(lvl); + s.health = 50 + s.level * 10; + s.orientation = rangeF(0.0f, 360.0f); + spawner.placeCreature(s); + placedCreatures++; + } + if (placedCreatures > 0) spawner.saveToFile(cpath); + // Objects. + wowee::editor::ObjectPlacer placer; + std::string opath = zoneDir + "/objects.json"; + if (fs::exists(opath)) placer.loadFromFile(opath); + int placedObjects = 0; + // Push PlacedObject directly into the placer's vector so + // we don't fight placeObject()'s early-return on empty + // activePath_. uniqueId starts after any existing objects + // to keep IDs collision-free. + auto& objs = placer.getObjects(); + uint32_t maxUid = 0; + for (const auto& o : objs) maxUid = std::max(maxUid, o.uniqueId); + for (int n = 0; n < objectCount; ++n) { + wowee::editor::PlacedObject o; + o.path = kRandomObjects[ + rangeI(0, static_cast(kRandomObjects.size()) - 1)]; + o.type = wowee::editor::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++; + } + if (placedObjects > 0) placer.saveToFile(opath); + std::printf("random-populate-zone: %s\n", zoneDir.c_str()); + std::printf(" seed : %u\n", seed); + std::printf(" zone bbox : (%.0f, %.0f) - (%.0f, %.0f)\n", + wMinX, wMinY, wMaxX, wMaxY); + std::printf(" creatures : %d added (%zu total)\n", + placedCreatures, spawner.spawnCount()); + std::printf(" objects : %d added (%zu total)\n", + placedObjects, placer.getObjects().size()); + return 0; +} + +int handleRandomPopulateItems(int& i, int argc, char** argv) { + // Seeded random items.json populator. Pulls a base name + // and a noun from inline word lists, picks a quality up + // to maxQuality, randomizes itemLevel and stack size + // around plausible defaults. Useful for playtest loot + // tables that need bulk content without hand-typing each + // entry. + // + // Flags: --seed N (default 7), --count N (default 30), + // --max-quality Q (default 4 = epic; 0..6 valid). + std::string zoneDir = argv[++i]; + uint32_t seed = 7; + int count = 30; + int maxQuality = 4; + while (i + 2 < argc && argv[i + 1][0] == '-') { + std::string flag = argv[++i]; + if (flag == "--seed") { + try { seed = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } else if (flag == "--count") { + try { count = std::stoi(argv[++i]); } catch (...) {} + } else if (flag == "--max-quality") { + try { maxQuality = std::stoi(argv[++i]); } catch (...) {} + } else { + std::fprintf(stderr, + "random-populate-items: unknown flag '%s'\n", flag.c_str()); + return 1; + } + } + if (maxQuality < 0 || maxQuality > 6) maxQuality = 4; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "random-populate-items: %s has no zone.json\n", + zoneDir.c_str()); + return 1; + } + uint32_t rng = seed ? seed : 1u; + auto next01 = [&]() { + rng = rng * 1664525u + 1013904223u; + return (rng >> 8) / float(1 << 24); + }; + auto rangeI = [&](int a, int b) { + return a + static_cast(next01() * (b - a + 1)); + }; + // Inline name lexicon. {prefix, noun} → "Glowing Sword". + // Quality ramps prefix selection; rare+ items get fancier + // adjectives. + static const std::vector kPrefixes[5] = { + {"Worn", "Tattered", "Cracked", "Dented", "Faded"}, // poor + {"Common", "Plain", "Basic", "Simple", "Standard"}, // common + {"Sharp", "Sturdy", "Polished", "Reinforced", "Fine"}, // uncommon + {"Glowing", "Runed", "Enchanted", "Storm", "Mystic"}, // rare + {"Ancient", "Eternal", "Heroic", "Vengeful", "Soul"}, // epic + }; + static const std::vector kNouns = { + "Sword", "Mace", "Axe", "Dagger", "Staff", + "Bow", "Helm", "Cuirass", "Greaves", "Gauntlets", + "Ring", "Amulet", "Cloak", "Belt", "Boots", + "Potion", "Scroll", "Tome", "Wand", "Shield", + }; + // Open the items doc. + std::string ipath = zoneDir + "/items.json"; + nlohmann::json doc = nlohmann::json::object({{"items", + nlohmann::json::array()}}); + if (fs::exists(ipath)) { + std::ifstream in(ipath); + try { in >> doc; } catch (...) {} + if (!doc.contains("items") || !doc["items"].is_array()) { + doc["items"] = nlohmann::json::array(); + } + } + std::set used; + for (const auto& it : doc["items"]) { + if (it.contains("id") && it["id"].is_number_unsigned()) + used.insert(it["id"].get()); + } + int added = 0; + for (int n = 0; n < count; ++n) { + int q = std::min(maxQuality, rangeI(0, maxQuality)); + int qBucket = std::min(q, 4); + const auto& prefixes = kPrefixes[qBucket]; + std::string name = prefixes[rangeI(0, + static_cast(prefixes.size()) - 1)]; + name += " "; + name += kNouns[rangeI(0, static_cast(kNouns.size()) - 1)]; + uint32_t id = 1; + while (used.count(id)) ++id; + used.insert(id); + int ilvl = std::max(1, + rangeI(1, 5) + q * 12 + rangeI(-3, 3)); + doc["items"].push_back({ + {"id", id}, + {"name", name}, + {"quality", q}, + {"displayId", rangeI(1000, 9999)}, + {"itemLevel", ilvl}, + {"stackable", q == 0 || q == 1 ? rangeI(1, 20) : 1}, + }); + added++; + } + std::ofstream out(ipath); + if (!out) { + std::fprintf(stderr, + "random-populate-items: failed to write %s\n", + ipath.c_str()); + return 1; + } + out << doc.dump(2); + out.close(); + std::printf("random-populate-items: %s\n", ipath.c_str()); + std::printf(" seed : %u\n", seed); + std::printf(" added : %d\n", added); + std::printf(" total items : %zu\n", doc["items"].size()); + std::printf(" max quality : %d\n", maxQuality); + return 0; +} + +int handleGenRandomZone(int& i, int argc, char** argv) { + // End-to-end random zone generator. Composes scaffold-zone + // + random-populate-zone + random-populate-items in one + // invocation. Useful for "I just want a complete test + // zone, don't make me chain three commands." + // + // Args: + // required (becomes the slug) + // [tx ty] optional (default 32 32) + // --seed N default 42 + // --creatures N default 20 + // --objects N default 10 + // --items N default 25 + // + // Honors --random-populate-zone's hard caps + the existing + // scaffold-zone validation. Sub-commands' output streams + // through. + std::string name = argv[++i]; + int tx = 32, ty = 32; + uint32_t seed = 42; + int creatures = 20, objects = 10, items = 25; + // Optional positional tx/ty (must be before any --flags). + if (i + 2 < argc && argv[i + 1][0] != '-' && argv[i + 2][0] != '-') { + try { tx = std::stoi(argv[++i]); ty = std::stoi(argv[++i]); } + catch (...) {} + } + while (i + 2 < argc && argv[i + 1][0] == '-') { + std::string flag = argv[++i]; + if (flag == "--seed") + try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} + else if (flag == "--creatures") + try { creatures = std::stoi(argv[++i]); } catch (...) {} + else if (flag == "--objects") + try { objects = std::stoi(argv[++i]); } catch (...) {} + else if (flag == "--items") + try { items = std::stoi(argv[++i]); } catch (...) {} + else { + std::fprintf(stderr, + "gen-random-zone: unknown flag '%s'\n", flag.c_str()); + return 1; + } + } + // Slug-clean the name to match scaffold-zone's expectations. + std::string slug; + for (char c : name) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + slug += c; + } else if (c == ' ') { + slug += '_'; + } + } + if (slug.empty()) { + std::fprintf(stderr, + "gen-random-zone: name '%s' has no valid characters\n", + name.c_str()); + return 1; + } + std::string self = argv[0]; + namespace fs = std::filesystem; + std::string zoneDir = "custom_zones/" + slug; + std::printf("gen-random-zone: %s (tile %d, %d)\n", + slug.c_str(), tx, ty); + std::fflush(stdout); + // 1. Scaffold. + std::string scaffoldCmd = "\"" + self + "\" --scaffold-zone \"" + + slug + "\" " + std::to_string(tx) + " " + + std::to_string(ty); + int rc = std::system(scaffoldCmd.c_str()); + if (rc != 0) { + std::fprintf(stderr, + "gen-random-zone: scaffold step failed (rc=%d)\n", rc); + return 1; + } + // 2. Random populate. + std::fflush(stdout); + std::string popCmd = "\"" + self + "\" --random-populate-zone \"" + + zoneDir + "\" --seed " + std::to_string(seed) + + " --creatures " + std::to_string(creatures) + + " --objects " + std::to_string(objects); + rc = std::system(popCmd.c_str()); + if (rc != 0) { + std::fprintf(stderr, + "gen-random-zone: populate step failed (rc=%d)\n", rc); + return 1; + } + // 3. Random items. + std::fflush(stdout); + std::string itemsCmd = "\"" + self + "\" --random-populate-items \"" + + zoneDir + "\" --seed " + std::to_string(seed + 1) + + " --count " + std::to_string(items); + rc = std::system(itemsCmd.c_str()); + if (rc != 0) { + std::fprintf(stderr, + "gen-random-zone: items step failed (rc=%d)\n", rc); + return 1; + } + std::printf("\ngen-random-zone: complete\n"); + std::printf(" zone dir : %s\n", zoneDir.c_str()); + std::printf(" creatures : %d\n", creatures); + std::printf(" objects : %d\n", objects); + std::printf(" items : %d\n", items); + return 0; +} + +int handleGenRandomProject(int& i, int argc, char** argv) { + // Project-wide companion: spawn N random zones in one + // pass. Names default to "Zone1, Zone2..."; tile + // coordinates step from (32, 32) outward in a simple + // raster so they don't overlap. Each zone gets a unique + // sub-seed so its random content differs. + int count = 0; + try { count = std::stoi(argv[++i]); } + catch (...) { + std::fprintf(stderr, + "gen-random-project: must be an integer\n"); + return 1; + } + if (count < 1 || count > 100) { + std::fprintf(stderr, + "gen-random-project: count %d out of range (1..100)\n", + count); + return 1; + } + std::string prefix = "Zone"; + uint32_t seed = 100; + int creatures = 20, objects = 10, items = 25; + while (i + 2 < argc && argv[i + 1][0] == '-') { + std::string flag = argv[++i]; + if (flag == "--prefix") prefix = argv[++i]; + else if (flag == "--seed") + try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} + else if (flag == "--creatures") + try { creatures = std::stoi(argv[++i]); } catch (...) {} + else if (flag == "--objects") + try { objects = std::stoi(argv[++i]); } catch (...) {} + else if (flag == "--items") + try { items = std::stoi(argv[++i]); } catch (...) {} + else { + std::fprintf(stderr, + "gen-random-project: unknown flag '%s'\n", flag.c_str()); + return 1; + } + } + std::string self = argv[0]; + int produced = 0, failed = 0; + std::printf("gen-random-project: %d zone(s) with prefix '%s'\n", + count, prefix.c_str()); + for (int n = 0; n < count; ++n) { + // Step outward from (32, 32) in a small raster so the + // tiles don't coincide. (-1,0,1,...) X (-1,0,1,...). + int side = 1; + while ((2 * side + 1) * (2 * side + 1) <= n) side++; + int idx = n; + int dx = idx % (2 * side + 1) - side; + int dy = (idx / (2 * side + 1)) - side; + int tx = std::max(0, std::min(63, 32 + dx)); + int ty = std::max(0, std::min(63, 32 + dy)); + std::string zoneName = prefix + std::to_string(n + 1); + std::printf("\n=== %s (tile %d, %d) ===\n", + zoneName.c_str(), tx, ty); + std::fflush(stdout); + std::string cmd = "\"" + self + "\" --gen-random-zone \"" + + zoneName + "\" " + + std::to_string(tx) + " " + std::to_string(ty) + + " --seed " + std::to_string(seed + n) + + " --creatures " + std::to_string(creatures) + + " --objects " + std::to_string(objects) + + " --items " + std::to_string(items); + int rc = std::system(cmd.c_str()); + if (rc == 0) produced++; + else failed++; + } + std::printf("\n--- summary ---\n"); + std::printf(" produced : %d\n", produced); + std::printf(" failed : %d\n", failed); + return failed == 0 ? 0 : 1; +} + + +} // namespace + +bool handleRandom(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--random-populate-zone") == 0 && i + 1 < argc) { + outRc = handleRandomPopulateZone(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--random-populate-items") == 0 && i + 1 < argc) { + outRc = handleRandomPopulateItems(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-random-zone") == 0 && i + 1 < argc) { + outRc = handleGenRandomZone(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-random-project") == 0 && i + 1 < argc) { + outRc = handleGenRandomProject(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_random.hpp b/tools/editor/cli_random.hpp new file mode 100644 index 00000000..18b0707f --- /dev/null +++ b/tools/editor/cli_random.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the random-* / gen-random-* zone-population handlers. +// All four use a seeded LCG so re-runs reproduce the same +// content; they're intended for playtest scenarios where you +// want bulk-populated zones without hand-typing every spawn. +// --random-populate-zone add N creatures + M objects to a zone +// --random-populate-items add N item records to items.json +// --gen-random-zone scaffold + populate + items in one shot +// --gen-random-project spawn N gen-random-zones in a raster +// +// Returns true if matched; outRc holds the exit code. +bool handleRandom(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index fa0c4e7e..0c814bb0 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -42,6 +42,7 @@ #include "cli_clone.hpp" #include "cli_remove.hpp" #include "cli_add.hpp" +#include "cli_random.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -504,6 +505,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleAdd(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleRandom(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1432,449 +1436,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--random-populate-zone") == 0 && i + 1 < argc) { - // Randomly add creatures and/or objects to a zone for - // playtest scenarios. Reads the zone manifest's tile - // bounds so spawn positions stay inside the actual - // playable area. Seeded LCG for reproducibility — same - // seed always produces the same population. - // - // Flags: - // --seed N (default 42) - // --creatures N (default 20) - // --objects N (default 10) - std::string zoneDir = argv[++i]; - uint32_t seed = 42; - int creatureCount = 20; - int objectCount = 10; - while (i + 2 < argc && argv[i + 1][0] == '-') { - std::string flag = argv[++i]; - if (flag == "--seed") { - try { seed = static_cast(std::stoul(argv[++i])); } - catch (...) {} - } else if (flag == "--creatures") { - try { creatureCount = std::stoi(argv[++i]); } - catch (...) {} - } else if (flag == "--objects") { - try { objectCount = std::stoi(argv[++i]); } - catch (...) {} - } else { - std::fprintf(stderr, - "random-populate-zone: unknown flag '%s'\n", flag.c_str()); - return 1; - } - } - namespace fs = std::filesystem; - std::string manifestPath = zoneDir + "/zone.json"; - if (!fs::exists(manifestPath)) { - std::fprintf(stderr, - "random-populate-zone: %s has no zone.json\n", - zoneDir.c_str()); - return 1; - } - wowee::editor::ZoneManifest zm; - if (!zm.load(manifestPath)) { - std::fprintf(stderr, - "random-populate-zone: failed to parse %s\n", - manifestPath.c_str()); - return 1; - } - if (zm.tiles.empty()) { - std::fprintf(stderr, - "random-populate-zone: zone has no tiles to populate\n"); - return 1; - } - // Compute the world AABB the zone occupies so spawns land - // inside it. Each tile is 533.33y; WoW grid centers tile - // (32, 32) at world origin. - constexpr float kTileSize = 533.33333f; - int tMinX = 64, tMaxX = -1, tMinY = 64, tMaxY = -1; - for (const auto& [tx, ty] : zm.tiles) { - tMinX = std::min(tMinX, tx); tMaxX = std::max(tMaxX, tx); - tMinY = std::min(tMinY, ty); tMaxY = std::max(tMaxY, ty); - } - float wMinX = (32.0f - tMaxY - 1) * kTileSize; - float wMaxX = (32.0f - tMinY) * kTileSize; - float wMinY = (32.0f - tMaxX - 1) * kTileSize; - float wMaxY = (32.0f - tMinX) * kTileSize; - float baseZ = zm.baseHeight; - - 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(next01() * (b - a + 1)); - }; - - // Tiny bestiary so the random output reads as plausible - // rather than "Creature1 / Creature2". - static const std::vector> 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 kRandomObjects = { - "World/Generic/Tree01.wmo", - "World/Generic/Boulder.wmo", - "World/Generic/Bush.wmo", - "World/Generic/Stump.wmo", - "World/Generic/Mushroom.wmo", - }; - - // Creatures. - wowee::editor::NpcSpawner spawner; - std::string cpath = zoneDir + "/creatures.json"; - if (fs::exists(cpath)) spawner.loadFromFile(cpath); - int placedCreatures = 0; - for (int n = 0; n < creatureCount; ++n) { - const auto& [name, baseLvl] = kRandomCreatures[ - rangeI(0, static_cast(kRandomCreatures.size()) - 1)]; - wowee::editor::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(baseLvl) + rangeI(-1, 2)); - s.level = static_cast(lvl); - s.health = 50 + s.level * 10; - s.orientation = rangeF(0.0f, 360.0f); - spawner.placeCreature(s); - placedCreatures++; - } - if (placedCreatures > 0) spawner.saveToFile(cpath); - // Objects. - wowee::editor::ObjectPlacer placer; - std::string opath = zoneDir + "/objects.json"; - if (fs::exists(opath)) placer.loadFromFile(opath); - int placedObjects = 0; - // Push PlacedObject directly into the placer's vector so - // we don't fight placeObject()'s early-return on empty - // activePath_. uniqueId starts after any existing objects - // to keep IDs collision-free. - auto& objs = placer.getObjects(); - uint32_t maxUid = 0; - for (const auto& o : objs) maxUid = std::max(maxUid, o.uniqueId); - for (int n = 0; n < objectCount; ++n) { - wowee::editor::PlacedObject o; - o.path = kRandomObjects[ - rangeI(0, static_cast(kRandomObjects.size()) - 1)]; - o.type = wowee::editor::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++; - } - if (placedObjects > 0) placer.saveToFile(opath); - std::printf("random-populate-zone: %s\n", zoneDir.c_str()); - std::printf(" seed : %u\n", seed); - std::printf(" zone bbox : (%.0f, %.0f) - (%.0f, %.0f)\n", - wMinX, wMinY, wMaxX, wMaxY); - std::printf(" creatures : %d added (%zu total)\n", - placedCreatures, spawner.spawnCount()); - std::printf(" objects : %d added (%zu total)\n", - placedObjects, placer.getObjects().size()); - return 0; - } else if (std::strcmp(argv[i], "--random-populate-items") == 0 && i + 1 < argc) { - // Seeded random items.json populator. Pulls a base name - // and a noun from inline word lists, picks a quality up - // to maxQuality, randomizes itemLevel and stack size - // around plausible defaults. Useful for playtest loot - // tables that need bulk content without hand-typing each - // entry. - // - // Flags: --seed N (default 7), --count N (default 30), - // --max-quality Q (default 4 = epic; 0..6 valid). - std::string zoneDir = argv[++i]; - uint32_t seed = 7; - int count = 30; - int maxQuality = 4; - while (i + 2 < argc && argv[i + 1][0] == '-') { - std::string flag = argv[++i]; - if (flag == "--seed") { - try { seed = static_cast(std::stoul(argv[++i])); } - catch (...) {} - } else if (flag == "--count") { - try { count = std::stoi(argv[++i]); } catch (...) {} - } else if (flag == "--max-quality") { - try { maxQuality = std::stoi(argv[++i]); } catch (...) {} - } else { - std::fprintf(stderr, - "random-populate-items: unknown flag '%s'\n", flag.c_str()); - return 1; - } - } - if (maxQuality < 0 || maxQuality > 6) maxQuality = 4; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "random-populate-items: %s has no zone.json\n", - zoneDir.c_str()); - return 1; - } - uint32_t rng = seed ? seed : 1u; - auto next01 = [&]() { - rng = rng * 1664525u + 1013904223u; - return (rng >> 8) / float(1 << 24); - }; - auto rangeI = [&](int a, int b) { - return a + static_cast(next01() * (b - a + 1)); - }; - // Inline name lexicon. {prefix, noun} → "Glowing Sword". - // Quality ramps prefix selection; rare+ items get fancier - // adjectives. - static const std::vector kPrefixes[5] = { - {"Worn", "Tattered", "Cracked", "Dented", "Faded"}, // poor - {"Common", "Plain", "Basic", "Simple", "Standard"}, // common - {"Sharp", "Sturdy", "Polished", "Reinforced", "Fine"}, // uncommon - {"Glowing", "Runed", "Enchanted", "Storm", "Mystic"}, // rare - {"Ancient", "Eternal", "Heroic", "Vengeful", "Soul"}, // epic - }; - static const std::vector kNouns = { - "Sword", "Mace", "Axe", "Dagger", "Staff", - "Bow", "Helm", "Cuirass", "Greaves", "Gauntlets", - "Ring", "Amulet", "Cloak", "Belt", "Boots", - "Potion", "Scroll", "Tome", "Wand", "Shield", - }; - // Open the items doc. - std::string ipath = zoneDir + "/items.json"; - nlohmann::json doc = nlohmann::json::object({{"items", - nlohmann::json::array()}}); - if (fs::exists(ipath)) { - std::ifstream in(ipath); - try { in >> doc; } catch (...) {} - if (!doc.contains("items") || !doc["items"].is_array()) { - doc["items"] = nlohmann::json::array(); - } - } - std::set used; - for (const auto& it : doc["items"]) { - if (it.contains("id") && it["id"].is_number_unsigned()) - used.insert(it["id"].get()); - } - int added = 0; - for (int n = 0; n < count; ++n) { - int q = std::min(maxQuality, rangeI(0, maxQuality)); - int qBucket = std::min(q, 4); - const auto& prefixes = kPrefixes[qBucket]; - std::string name = prefixes[rangeI(0, - static_cast(prefixes.size()) - 1)]; - name += " "; - name += kNouns[rangeI(0, static_cast(kNouns.size()) - 1)]; - uint32_t id = 1; - while (used.count(id)) ++id; - used.insert(id); - int ilvl = std::max(1, - rangeI(1, 5) + q * 12 + rangeI(-3, 3)); - doc["items"].push_back({ - {"id", id}, - {"name", name}, - {"quality", q}, - {"displayId", rangeI(1000, 9999)}, - {"itemLevel", ilvl}, - {"stackable", q == 0 || q == 1 ? rangeI(1, 20) : 1}, - }); - added++; - } - std::ofstream out(ipath); - if (!out) { - std::fprintf(stderr, - "random-populate-items: failed to write %s\n", - ipath.c_str()); - return 1; - } - out << doc.dump(2); - out.close(); - std::printf("random-populate-items: %s\n", ipath.c_str()); - std::printf(" seed : %u\n", seed); - std::printf(" added : %d\n", added); - std::printf(" total items : %zu\n", doc["items"].size()); - std::printf(" max quality : %d\n", maxQuality); - return 0; - } else if (std::strcmp(argv[i], "--gen-random-zone") == 0 && i + 1 < argc) { - // End-to-end random zone generator. Composes scaffold-zone - // + random-populate-zone + random-populate-items in one - // invocation. Useful for "I just want a complete test - // zone, don't make me chain three commands." - // - // Args: - // required (becomes the slug) - // [tx ty] optional (default 32 32) - // --seed N default 42 - // --creatures N default 20 - // --objects N default 10 - // --items N default 25 - // - // Honors --random-populate-zone's hard caps + the existing - // scaffold-zone validation. Sub-commands' output streams - // through. - std::string name = argv[++i]; - int tx = 32, ty = 32; - uint32_t seed = 42; - int creatures = 20, objects = 10, items = 25; - // Optional positional tx/ty (must be before any --flags). - if (i + 2 < argc && argv[i + 1][0] != '-' && argv[i + 2][0] != '-') { - try { tx = std::stoi(argv[++i]); ty = std::stoi(argv[++i]); } - catch (...) {} - } - while (i + 2 < argc && argv[i + 1][0] == '-') { - std::string flag = argv[++i]; - if (flag == "--seed") - try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} - else if (flag == "--creatures") - try { creatures = std::stoi(argv[++i]); } catch (...) {} - else if (flag == "--objects") - try { objects = std::stoi(argv[++i]); } catch (...) {} - else if (flag == "--items") - try { items = std::stoi(argv[++i]); } catch (...) {} - else { - std::fprintf(stderr, - "gen-random-zone: unknown flag '%s'\n", flag.c_str()); - return 1; - } - } - // Slug-clean the name to match scaffold-zone's expectations. - std::string slug; - for (char c : name) { - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || c == '_' || c == '-') { - slug += c; - } else if (c == ' ') { - slug += '_'; - } - } - if (slug.empty()) { - std::fprintf(stderr, - "gen-random-zone: name '%s' has no valid characters\n", - name.c_str()); - return 1; - } - std::string self = argv[0]; - namespace fs = std::filesystem; - std::string zoneDir = "custom_zones/" + slug; - std::printf("gen-random-zone: %s (tile %d, %d)\n", - slug.c_str(), tx, ty); - std::fflush(stdout); - // 1. Scaffold. - std::string scaffoldCmd = "\"" + self + "\" --scaffold-zone \"" + - slug + "\" " + std::to_string(tx) + " " + - std::to_string(ty); - int rc = std::system(scaffoldCmd.c_str()); - if (rc != 0) { - std::fprintf(stderr, - "gen-random-zone: scaffold step failed (rc=%d)\n", rc); - return 1; - } - // 2. Random populate. - std::fflush(stdout); - std::string popCmd = "\"" + self + "\" --random-populate-zone \"" + - zoneDir + "\" --seed " + std::to_string(seed) + - " --creatures " + std::to_string(creatures) + - " --objects " + std::to_string(objects); - rc = std::system(popCmd.c_str()); - if (rc != 0) { - std::fprintf(stderr, - "gen-random-zone: populate step failed (rc=%d)\n", rc); - return 1; - } - // 3. Random items. - std::fflush(stdout); - std::string itemsCmd = "\"" + self + "\" --random-populate-items \"" + - zoneDir + "\" --seed " + std::to_string(seed + 1) + - " --count " + std::to_string(items); - rc = std::system(itemsCmd.c_str()); - if (rc != 0) { - std::fprintf(stderr, - "gen-random-zone: items step failed (rc=%d)\n", rc); - return 1; - } - std::printf("\ngen-random-zone: complete\n"); - std::printf(" zone dir : %s\n", zoneDir.c_str()); - std::printf(" creatures : %d\n", creatures); - std::printf(" objects : %d\n", objects); - std::printf(" items : %d\n", items); - return 0; - } else if (std::strcmp(argv[i], "--gen-random-project") == 0 && i + 1 < argc) { - // Project-wide companion: spawn N random zones in one - // pass. Names default to "Zone1, Zone2..."; tile - // coordinates step from (32, 32) outward in a simple - // raster so they don't overlap. Each zone gets a unique - // sub-seed so its random content differs. - int count = 0; - try { count = std::stoi(argv[++i]); } - catch (...) { - std::fprintf(stderr, - "gen-random-project: must be an integer\n"); - return 1; - } - if (count < 1 || count > 100) { - std::fprintf(stderr, - "gen-random-project: count %d out of range (1..100)\n", - count); - return 1; - } - std::string prefix = "Zone"; - uint32_t seed = 100; - int creatures = 20, objects = 10, items = 25; - while (i + 2 < argc && argv[i + 1][0] == '-') { - std::string flag = argv[++i]; - if (flag == "--prefix") prefix = argv[++i]; - else if (flag == "--seed") - try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} - else if (flag == "--creatures") - try { creatures = std::stoi(argv[++i]); } catch (...) {} - else if (flag == "--objects") - try { objects = std::stoi(argv[++i]); } catch (...) {} - else if (flag == "--items") - try { items = std::stoi(argv[++i]); } catch (...) {} - else { - std::fprintf(stderr, - "gen-random-project: unknown flag '%s'\n", flag.c_str()); - return 1; - } - } - std::string self = argv[0]; - int produced = 0, failed = 0; - std::printf("gen-random-project: %d zone(s) with prefix '%s'\n", - count, prefix.c_str()); - for (int n = 0; n < count; ++n) { - // Step outward from (32, 32) in a small raster so the - // tiles don't coincide. (-1,0,1,...) X (-1,0,1,...). - int side = 1; - while ((2 * side + 1) * (2 * side + 1) <= n) side++; - int idx = n; - int dx = idx % (2 * side + 1) - side; - int dy = (idx / (2 * side + 1)) - side; - int tx = std::max(0, std::min(63, 32 + dx)); - int ty = std::max(0, std::min(63, 32 + dy)); - std::string zoneName = prefix + std::to_string(n + 1); - std::printf("\n=== %s (tile %d, %d) ===\n", - zoneName.c_str(), tx, ty); - std::fflush(stdout); - std::string cmd = "\"" + self + "\" --gen-random-zone \"" + - zoneName + "\" " + - std::to_string(tx) + " " + std::to_string(ty) + - " --seed " + std::to_string(seed + n) + - " --creatures " + std::to_string(creatures) + - " --objects " + std::to_string(objects) + - " --items " + std::to_string(items); - int rc = std::system(cmd.c_str()); - if (rc == 0) produced++; - else failed++; - } - std::printf("\n--- summary ---\n"); - std::printf(" produced : %d\n", produced); - std::printf(" failed : %d\n", failed); - return failed == 0 ? 0 : 1; } else if (std::strcmp(argv[i], "--set-item") == 0 && i + 2 < argc) { // Edit fields on an existing item in place. Lookup is by // id by default; '#N' for index lookup. Only specified