mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 09:33:51 +00:00
feat(editor): add --export-quest-graph for Graphviz quest-chain visualization
Visualizing quest chains in plain text rapidly becomes unreadable past ~10 quests. This emits a DOT file that renders to a labeled DAG you can paste into a wiki or PR description: wowee_editor --export-quest-graph custom_zones/MyZone dot -Tpng custom_zones/MyZone/quests.dot -o quests.png Node coloring conveys completion-readiness at a glance: - lightgreen: has objectives + reward (will complete in-game) - lightyellow: has objectives but no reward (uncommon) - lightgray: no objectives (won't complete — common authoring bug) - mistyrose dashed: synthetic node for a quest ID referenced by nextQuestId but missing from quests.json (broken chain) Edges: - solid black: valid chain link - dashed red labeled 'missing': dangling nextQuestId Labels include quest ID, title (DOT-escaped for safety), required level, and XP reward — enough context that the graph stands alone without needing to cross-reference quests.json. Verified on a 3-quest zone with chain 1->2->3->999 (last broken): DOT output has 3 colored nodes (lightgreen for the 2 quests with objectives, lightgray for the third), 2 solid edges, 1 dashed-red edge to a synthetic mistyrose <missing> node.
This commit is contained in:
parent
a183143002
commit
c1c1f1b2a8
1 changed files with 98 additions and 1 deletions
|
|
@ -500,6 +500,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" One-shot validate + creature/object/quest counts and exit\n");
|
||||
std::printf(" --export-zone-summary-md <zoneDir> [out.md]\n");
|
||||
std::printf(" Render a markdown documentation page for a zone (manifest + content)\n");
|
||||
std::printf(" --export-quest-graph <zoneDir> [out.dot]\n");
|
||||
std::printf(" Render quest-chain DAG as Graphviz DOT (pipe to `dot -Tpng -o quests.png`)\n");
|
||||
std::printf(" --info <wom-base> [--json]\n");
|
||||
std::printf(" Print WOM file metadata (version, counts) and exit\n");
|
||||
std::printf(" --info-batches <wom-base> [--json]\n");
|
||||
|
|
@ -582,7 +584,7 @@ int main(int argc, char* argv[]) {
|
|||
"--unpack-wcp", "--pack-wcp",
|
||||
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
|
||||
"--validate-whm", "--validate-all", "--zone-summary",
|
||||
"--export-zone-summary-md",
|
||||
"--export-zone-summary-md", "--export-quest-graph",
|
||||
"--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles",
|
||||
"--for-each-zone", "--zone-stats", "--list-zone-deps",
|
||||
"--check-zone-refs",
|
||||
|
|
@ -2762,6 +2764,101 @@ int main(int argc, char* argv[]) {
|
|||
zm.mapName.c_str(), zm.tiles.size(), sp.spawnCount(),
|
||||
op.getObjects().size(), qe.questCount());
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--export-quest-graph") == 0 && i + 1 < argc) {
|
||||
// Render quest chains as a Graphviz DOT graph. Visualizing
|
||||
// quest dependencies in plain text rapidly becomes unreadable
|
||||
// past ~10 quests; piping this through 'dot -Tpng -o q.png'
|
||||
// makes complex chains immediately legible.
|
||||
//
|
||||
// wowee_editor --export-quest-graph custom_zones/MyZone
|
||||
// dot -Tpng custom_zones/MyZone/quests.dot -o quests.png
|
||||
std::string zoneDir = argv[++i];
|
||||
std::string outPath;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
outPath = argv[++i];
|
||||
}
|
||||
std::string path = zoneDir + "/quests.json";
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::fprintf(stderr,
|
||||
"export-quest-graph: %s not found\n", path.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (outPath.empty()) outPath = zoneDir + "/quests.dot";
|
||||
wowee::editor::QuestEditor qe;
|
||||
if (!qe.loadFromFile(path)) {
|
||||
std::fprintf(stderr,
|
||||
"export-quest-graph: failed to parse %s\n", path.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::ofstream out(outPath);
|
||||
if (!out) {
|
||||
std::fprintf(stderr,
|
||||
"export-quest-graph: cannot write %s\n", outPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
// DOT-escape strings (just quotes and backslashes) — quest
|
||||
// titles can include arbitrary punctuation that breaks DOT
|
||||
// parsing if not escaped.
|
||||
auto dotEsc = [](const std::string& s) {
|
||||
std::string out;
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') out += '\\';
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const auto& quests = qe.getQuests();
|
||||
// Build an index of valid quest IDs so dangling chain
|
||||
// pointers can be styled differently (red, dashed).
|
||||
std::unordered_set<uint32_t> validIds;
|
||||
for (const auto& q : quests) validIds.insert(q.id);
|
||||
out << "digraph QuestChains {\n";
|
||||
out << " // Generated by wowee_editor --export-quest-graph\n";
|
||||
out << " rankdir=LR;\n";
|
||||
out << " node [shape=box, style=filled, fontname=\"sans-serif\"];\n";
|
||||
// Nodes: one per quest, colored by completion-readiness:
|
||||
// green = has objectives + reward + valid NPCs
|
||||
// yellow = missing some non-fatal field (description, etc.)
|
||||
// gray = no objectives (won't actually complete in-game)
|
||||
for (const auto& q : quests) {
|
||||
bool hasObjs = !q.objectives.empty();
|
||||
bool hasReward = (q.reward.xp > 0 || !q.reward.itemRewards.empty());
|
||||
std::string color = hasObjs ? (hasReward ? "lightgreen" : "lightyellow")
|
||||
: "lightgray";
|
||||
std::string label = "[" + std::to_string(q.id) + "] " + dotEsc(q.title);
|
||||
if (q.requiredLevel > 1) {
|
||||
label += "\\nlvl " + std::to_string(q.requiredLevel);
|
||||
}
|
||||
if (q.reward.xp > 0) {
|
||||
label += " " + std::to_string(q.reward.xp) + " XP";
|
||||
}
|
||||
out << " q" << q.id << " [label=\"" << label
|
||||
<< "\", fillcolor=" << color << "];\n";
|
||||
}
|
||||
// Edges: quest -> nextQuestId. Style chain-pointers to
|
||||
// missing quests differently so they stand out visually.
|
||||
int chainEdges = 0, brokenEdges = 0;
|
||||
for (const auto& q : quests) {
|
||||
if (q.nextQuestId == 0) continue;
|
||||
if (validIds.count(q.nextQuestId) == 0) {
|
||||
out << " q" << q.id << " -> q" << q.nextQuestId
|
||||
<< " [color=red, style=dashed, label=\"missing\"];\n";
|
||||
out << " q" << q.nextQuestId
|
||||
<< " [label=\"<missing> [" << q.nextQuestId
|
||||
<< "]\", fillcolor=mistyrose, style=\"filled,dashed\"];\n";
|
||||
brokenEdges++;
|
||||
} else {
|
||||
out << " q" << q.id << " -> q" << q.nextQuestId << ";\n";
|
||||
chainEdges++;
|
||||
}
|
||||
}
|
||||
out << "}\n";
|
||||
out.close();
|
||||
std::printf("Wrote %s\n", outPath.c_str());
|
||||
std::printf(" %zu quests, %d chain edges, %d broken (red/dashed)\n",
|
||||
quests.size(), chainEdges, brokenEdges);
|
||||
std::printf(" next: dot -Tpng %s -o quests.png\n", outPath.c_str());
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--validate") == 0 && i + 1 < argc) {
|
||||
std::string zoneDir = argv[++i];
|
||||
// Optional --json after the dir for machine-readable output
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue