mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 01:53:52 +00:00
feat(editor): add --random-populate-zone seeded creature/object spawner
Walks <zoneDir>/zone.json to compute the world AABB the zone occupies, then drops N random creatures and M random objects with positions inside that bbox. Seeded LCG so the same seed always produces the same population — useful for reproducible playtest scenarios and for CI fixtures that need deterministic content. Default flags: --seed 42 --creatures 20 --objects 10. Creatures draw from a small built-in bestiary (Wolf/Boar/Bear/Spider/Bandit/ Kobold/Murloc/Skeleton/Wisp/Goblin/Stag/Crab) with level jitter around a per-name baseline. Objects pick from a generic placeholder prop set (Tree/Boulder/Bush/Stump/Mushroom). Both lists are inline so users can extend them without dragging in the asset browser. Verified: 5-creature 3-object run produces a creatures.json with real names + level-9 wolves + behavior=2 (Wander) + an objects.json with real WMO paths, positions inside the zone bbox, randomized rotation + scale, and unique IDs. Brings command count to 232.
This commit is contained in:
parent
4e4102bf4a
commit
9dad8c2aa0
1 changed files with 155 additions and 0 deletions
|
|
@ -569,6 +569,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Combine two WOMs into one (vertex/index buffers concatenated, batches preserved)\n");
|
||||
std::printf(" --add-item <zoneDir> <name> [id] [quality] [displayId] [itemLevel]\n");
|
||||
std::printf(" Append one item entry to <zoneDir>/items.json (auto-creates the file)\n");
|
||||
std::printf(" --random-populate-zone <zoneDir> [--seed N] [--creatures N] [--objects N]\n");
|
||||
std::printf(" Add random creatures/objects to a zone (seeded for reproducibility)\n");
|
||||
std::printf(" --list-items <zoneDir> [--json]\n");
|
||||
std::printf(" Print every item in <zoneDir>/items.json with quality colors and key fields\n");
|
||||
std::printf(" --export-zone-items-md <zoneDir> [out.md]\n");
|
||||
|
|
@ -1015,6 +1017,7 @@ int main(int argc, char* argv[]) {
|
|||
"--check-project-content", "--check-project-refs",
|
||||
"--export-zone-deps-md", "--export-zone-spawn-png",
|
||||
"--add-creature", "--add-object", "--add-quest", "--add-item",
|
||||
"--random-populate-zone",
|
||||
"--list-items", "--info-item", "--set-item", "--export-zone-items-md",
|
||||
"--export-project-items-md", "--export-project-items-csv",
|
||||
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
|
||||
|
|
@ -13136,6 +13139,158 @@ int main(int argc, char* argv[]) {
|
|||
qualityNames[quality], itemLevel,
|
||||
path.c_str(), doc["items"].size());
|
||||
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<uint32_t>(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<int>(next01() * (b - a + 1));
|
||||
};
|
||||
|
||||
// Tiny bestiary so the random output reads as plausible
|
||||
// rather than "Creature1 / Creature2".
|
||||
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",
|
||||
};
|
||||
|
||||
// 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<int>(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<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);
|
||||
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<int>(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], "--list-items") == 0 && i + 1 < argc) {
|
||||
// Inspect <zoneDir>/items.json. Pretty-prints id / quality
|
||||
// / item level / display id / name as a table; also
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue