From e0ed2ab58e30ec7c0fbbce35c38618f5876c1bf2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 17:13:44 -0700 Subject: [PATCH] feat(editor): add --info-creatures-by-faction + --info-creatures-by-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two analytics commands for combat-balance work. Where --info-creatures gives totals + behavior counts, these give the distributions: wowee_editor --info-creatures-by-faction $Z/creatures.json Creatures by faction: ... (47 total) faction count share 7 12 25.5% 14 29 61.7% 35 6 12.8% (factions: 7=human, 14=monster, 35=neutral, etc.) wowee_editor --info-creatures-by-level $Z/creatures.json Creatures by level: ... (47 total) range : 5 to 32 (avg 14.2) level count bar 5 4 ████████████████████████████████████████ 6 3 ██████████████████████████████ ... 30 1 ██████████ Faction histogram catches single-faction zones (one giant melee) and mixed-faction tuning issues. Level histogram catches difficulty-curve problems (cluster at 5, gap, cluster at 30) and outlier spawns (level-60 boss accidentally placed in starter area). ASCII bar chart for level distribution since gameplay tuning is visual — '60% of mobs are levels 8-12 with a long tail' is more intuitive as a bar than as numbers. Bars scale to longest bin so small zones still get usable visualization. JSON mode emits per-faction / per-level records for dashboards. Verified on a 4-creature seed (3×faction-14 + 1×faction-35; levels 7/8/12/30): faction percentages and level range/avg both correct. --- tools/editor/main.cpp | 99 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 95f56101..af7324f5 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -708,6 +708,10 @@ static void printUsage(const char* argv0) { std::printf(" Print zone.json fields (manifest, tiles, audio, flags) and exit\n"); std::printf(" --info-creatures

[--json]\n"); std::printf(" Print creatures.json summary (counts, behaviors) and exit\n"); + std::printf(" --info-creatures-by-faction

[--json]\n"); + std::printf(" Histogram of creature counts grouped by faction id\n"); + std::printf(" --info-creatures-by-level

[--json]\n"); + std::printf(" Distribution of creature levels (min/max/avg + per-level counts)\n"); std::printf(" --info-objects

[--json]\n"); std::printf(" Print objects.json summary (counts, types, scale range) and exit\n"); std::printf(" --info-quests

[--json]\n"); @@ -790,6 +794,7 @@ int main(int argc, char* argv[]) { "--list-quest-objectives", "--list-quest-rewards", "--info-creature", "--info-quest", "--info-object", "--info-quest-graph-stats", + "--info-creatures-by-faction", "--info-creatures-by-level", "--unpack-wcp", "--pack-wcp", "--validate", "--validate-wom", "--validate-wob", "--validate-woc", "--validate-whm", "--validate-all", "--validate-glb", "--info-glb", @@ -2529,6 +2534,100 @@ int main(int argc, char* argv[]) { stationary, wander, patrol); std::printf(" unique displayIds: %zu\n", displayIdHist.size()); return 0; + } else if (std::strcmp(argv[i], "--info-creatures-by-faction") == 0 && i + 1 < argc) { + // Faction histogram for combat balance analysis. AzerothCore + // factions: 7=human, 14=monster, 16=alliance-friendly, 35=neutral, + // etc. A zone with all faction=14 is going to be one giant + // free-for-all; a mixed-faction zone needs combat-tuning. + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + wowee::editor::NpcSpawner sp; + if (!sp.loadFromFile(path)) { + std::fprintf(stderr, + "info-creatures-by-faction: failed to load %s\n", path.c_str()); + return 1; + } + std::map hist; + for (const auto& s : sp.getSpawns()) hist[s.faction]++; + if (jsonOut) { + nlohmann::json j; + j["file"] = path; + j["totalCreatures"] = sp.spawnCount(); + j["uniqueFactions"] = hist.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& [f, c] : hist) { + arr.push_back({{"faction", f}, {"count", c}}); + } + j["factions"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Creatures by faction: %s (%zu total)\n", + path.c_str(), sp.spawnCount()); + std::printf(" faction count share\n"); + for (const auto& [f, c] : hist) { + double pct = sp.spawnCount() > 0 ? 100.0 * c / sp.spawnCount() : 0.0; + std::printf(" %7u %5d %5.1f%%\n", f, c, pct); + } + std::printf(" (factions: 7=human, 14=monster, 35=neutral, etc.)\n"); + return 0; + } else if (std::strcmp(argv[i], "--info-creatures-by-level") == 0 && i + 1 < argc) { + // Level distribution for difficulty-curve analysis. Min/max/ + // avg + per-level histogram. A zone with all level-1 spawns + // is a starter area; one with all 60s is endgame; spikes in + // the middle suggest content-tuning issues. + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + wowee::editor::NpcSpawner sp; + if (!sp.loadFromFile(path)) { + std::fprintf(stderr, + "info-creatures-by-level: failed to load %s\n", path.c_str()); + return 1; + } + std::map hist; + uint32_t minL = std::numeric_limits::max(); + uint32_t maxL = 0; + uint64_t sumL = 0; + for (const auto& s : sp.getSpawns()) { + hist[s.level]++; + if (s.level < minL) minL = s.level; + if (s.level > maxL) maxL = s.level; + sumL += s.level; + } + double avgL = sp.spawnCount() > 0 ? double(sumL) / sp.spawnCount() : 0.0; + if (sp.spawnCount() == 0) minL = 0; + if (jsonOut) { + nlohmann::json j; + j["file"] = path; + j["totalCreatures"] = sp.spawnCount(); + j["minLevel"] = minL; + j["maxLevel"] = maxL; + j["avgLevel"] = avgL; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& [l, c] : hist) { + arr.push_back({{"level", l}, {"count", c}}); + } + j["levels"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Creatures by level: %s (%zu total)\n", + path.c_str(), sp.spawnCount()); + std::printf(" range : %u to %u (avg %.1f)\n", minL, maxL, avgL); + std::printf("\n level count bar\n"); + int maxBarCount = 0; + for (const auto& [_, c] : hist) maxBarCount = std::max(maxBarCount, c); + for (const auto& [l, c] : hist) { + int barLen = maxBarCount > 0 ? (40 * c) / maxBarCount : 0; + std::printf(" %5u %5d ", l, c); + for (int b = 0; b < barLen; ++b) std::printf("█"); + std::printf("\n"); + } + return 0; } else if (std::strcmp(argv[i], "--list-creatures") == 0 && i + 1 < argc) { // Verbose enumeration of every spawn — needed because // --remove-creature takes a 0-based index but --info-creatures