mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 09:33:51 +00:00
feat(editor): add --info-quests-by-level + --info-quests-by-xp analytics
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Quest-side analytics paralleling --info-creatures-by-faction/-level.
Two distribution views for difficulty-curve and reward-pacing analysis:
wowee_editor --info-quests-by-level $Z/quests.json
Quests by required level: ... (47 total)
range : 1 to 60 (avg 22.4)
level count bar
1 8 ████████████████████████████████████████
5 6 ██████████████████████████████
...
60 1 █████
wowee_editor --info-quests-by-xp $Z/quests.json
Quests by XP reward: ... (47 total)
range : 100 to 5000 (avg 1462, 0 with 0 XP)
bucket (≥XP) count bar
0 8 ████████████████████████████████████████
250 6 ██████████████████████████████
500 4 ████████████████████
5000 1 █████
(bucket size: 250 XP)
--by-level: catches difficulty-curve gaps (every quest level 1 → no
mid-game; cluster at 60 → no early game) and outliers (level-30
quest dropped into a starter zone).
--by-xp: bucket size auto-grows with the max XP value so the
histogram stays readable for both starter zones (10-100 XP per bin)
and endgame (5000+ XP per bin). Surfaces no-reward quests
explicitly so designers spot ones they forgot to fill in.
JSON modes emit per-bucket records for dashboards. Verified on a
4-quest seed (xp 100/250/500/5000): bucket-size correctly auto-
selected as 250 XP, range and avg match.
This commit is contained in:
parent
d12ea8e23e
commit
3e260453a5
1 changed files with 132 additions and 0 deletions
|
|
@ -731,6 +731,10 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Print objects.json summary (counts, types, scale range) and exit\n");
|
||||
std::printf(" --info-quests <p> [--json]\n");
|
||||
std::printf(" Print quests.json summary (counts, rewards, chain errors) and exit\n");
|
||||
std::printf(" --info-quests-by-level <p> [--json]\n");
|
||||
std::printf(" Distribution of required levels across quests (min/max/avg + bar chart)\n");
|
||||
std::printf(" --info-quests-by-xp <p> [--json]\n");
|
||||
std::printf(" Distribution of XP rewards (min/max/avg + per-bucket histogram)\n");
|
||||
std::printf(" --list-creatures <p> [--json]\n");
|
||||
std::printf(" List every creature with index, name, position, level (for --remove-creature)\n");
|
||||
std::printf(" --list-objects <p> [--json]\n");
|
||||
|
|
@ -813,6 +817,7 @@ int main(int argc, char* argv[]) {
|
|||
"--info-quest-graph-stats",
|
||||
"--info-creatures-by-faction", "--info-creatures-by-level",
|
||||
"--info-objects-by-path", "--info-objects-by-type",
|
||||
"--info-quests-by-level", "--info-quests-by-xp",
|
||||
"--unpack-wcp", "--pack-wcp",
|
||||
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
|
||||
"--validate-whm", "--validate-all", "--validate-project",
|
||||
|
|
@ -2746,6 +2751,133 @@ int main(int argc, char* argv[]) {
|
|||
std::printf(" WMO : %d (scale %.2f-%.2f, avg %.2f)\n",
|
||||
wmoCount, wmoMin, wmoMax, wmoAvg);
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--info-quests-by-level") == 0 && i + 1 < argc) {
|
||||
// Required-level distribution. Catches difficulty-curve
|
||||
// issues where every quest is requiredLevel=1 (player skips
|
||||
// the chain) or every quest is requiredLevel=60 (no early
|
||||
// game), and outliers (a level-30 quest dropped into a
|
||||
// starter zone).
|
||||
std::string path = argv[++i];
|
||||
bool jsonOut = (i + 1 < argc &&
|
||||
std::strcmp(argv[i + 1], "--json") == 0);
|
||||
if (jsonOut) i++;
|
||||
wowee::editor::QuestEditor qe;
|
||||
if (!qe.loadFromFile(path)) {
|
||||
std::fprintf(stderr,
|
||||
"info-quests-by-level: failed to load %s\n", path.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::map<uint32_t, int> hist;
|
||||
uint32_t minL = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t maxL = 0;
|
||||
uint64_t sumL = 0;
|
||||
for (const auto& q : qe.getQuests()) {
|
||||
hist[q.requiredLevel]++;
|
||||
if (q.requiredLevel < minL) minL = q.requiredLevel;
|
||||
if (q.requiredLevel > maxL) maxL = q.requiredLevel;
|
||||
sumL += q.requiredLevel;
|
||||
}
|
||||
double avgL = qe.questCount() > 0 ?
|
||||
double(sumL) / qe.questCount() : 0.0;
|
||||
if (qe.questCount() == 0) minL = 0;
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["file"] = path;
|
||||
j["totalQuests"] = qe.questCount();
|
||||
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("Quests by required level: %s (%zu total)\n",
|
||||
path.c_str(), qe.questCount());
|
||||
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], "--info-quests-by-xp") == 0 && i + 1 < argc) {
|
||||
// XP reward distribution. Bucket into 100-XP groups so a
|
||||
// 10000-XP quest doesn't make the histogram unreadable.
|
||||
// Catches no-reward quests + cluster analysis (mostly
|
||||
// 100-XP smalls vs mostly 5000-XP boss kills).
|
||||
std::string path = argv[++i];
|
||||
bool jsonOut = (i + 1 < argc &&
|
||||
std::strcmp(argv[i + 1], "--json") == 0);
|
||||
if (jsonOut) i++;
|
||||
wowee::editor::QuestEditor qe;
|
||||
if (!qe.loadFromFile(path)) {
|
||||
std::fprintf(stderr,
|
||||
"info-quests-by-xp: failed to load %s\n", path.c_str());
|
||||
return 1;
|
||||
}
|
||||
uint32_t minXp = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t maxXp = 0;
|
||||
uint64_t sumXp = 0;
|
||||
int zeroXp = 0;
|
||||
// Bucket size grows with max — keeps the histogram readable
|
||||
// for both starter zones (10-100 XP) and endgame (5000+).
|
||||
std::map<uint32_t, int> buckets;
|
||||
for (const auto& q : qe.getQuests()) {
|
||||
if (q.reward.xp < minXp) minXp = q.reward.xp;
|
||||
if (q.reward.xp > maxXp) maxXp = q.reward.xp;
|
||||
sumXp += q.reward.xp;
|
||||
if (q.reward.xp == 0) zeroXp++;
|
||||
}
|
||||
uint32_t bucketSize = 100;
|
||||
if (maxXp > 1000) bucketSize = 250;
|
||||
if (maxXp > 5000) bucketSize = 500;
|
||||
if (maxXp > 20000) bucketSize = 1000;
|
||||
for (const auto& q : qe.getQuests()) {
|
||||
buckets[(q.reward.xp / bucketSize) * bucketSize]++;
|
||||
}
|
||||
double avgXp = qe.questCount() > 0 ?
|
||||
double(sumXp) / qe.questCount() : 0.0;
|
||||
if (qe.questCount() == 0) minXp = 0;
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["file"] = path;
|
||||
j["totalQuests"] = qe.questCount();
|
||||
j["minXp"] = minXp;
|
||||
j["maxXp"] = maxXp;
|
||||
j["avgXp"] = avgXp;
|
||||
j["zeroXpQuests"] = zeroXp;
|
||||
j["bucketSize"] = bucketSize;
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& [b, c] : buckets) {
|
||||
arr.push_back({{"bucket", b}, {"count", c}});
|
||||
}
|
||||
j["buckets"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("Quests by XP reward: %s (%zu total)\n",
|
||||
path.c_str(), qe.questCount());
|
||||
std::printf(" range : %u to %u (avg %.0f, %d with 0 XP)\n",
|
||||
minXp, maxXp, avgXp, zeroXp);
|
||||
std::printf("\n bucket (≥XP) count bar\n");
|
||||
int maxBarCount = 0;
|
||||
for (const auto& [_, c] : buckets) maxBarCount = std::max(maxBarCount, c);
|
||||
for (const auto& [b, c] : buckets) {
|
||||
int barLen = maxBarCount > 0 ? (40 * c) / maxBarCount : 0;
|
||||
std::printf(" %12u %5d ", b, c);
|
||||
for (int x = 0; x < barLen; ++x) std::printf("█");
|
||||
std::printf("\n");
|
||||
}
|
||||
std::printf(" (bucket size: %u XP)\n", bucketSize);
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue