feat(editor): add --add-quest-reward-item and --set-quest-reward

Closes the quest-authoring CLI: --add-quest creates a quest, then
--add-quest-objective adds completion conditions, and now these add
the rewards.

  wowee_editor --add-quest-reward-item $Z 0 'Item:Sword' 'Item:Shield' 'Item:Potion'
  wowee_editor --set-quest-reward      $Z 0 --xp 999 --gold 5 --silver 30

--add-quest-reward-item greedy-consumes any trailing positional args
that don't start with '-', so a whole loot table can be added in one
invocation rather than N round-trips through load+save.

--set-quest-reward updates only the fields explicitly passed (--xp /
--gold / --silver / --copper) — avoids the round-trip-and-clobber
footgun of a 'replace whole reward' command. Field flags are
order-independent. At least one flag is required (else error).

Both validate questIdx against quest count and bail with exit 1 on
out-of-range / missing file / bad numeric value.

Verified: scaffolded zone, added quest with default xp=100, batch-
added 3 item rewards in one shot, then set xp=999/gold=5/silver=30
in one shot. --info-quests reports total XP=999, with-items=1.
This commit is contained in:
Kelsi 2026-05-06 12:18:20 -07:00
parent 705899d6a7
commit 2048496aaf

View file

@ -413,6 +413,10 @@ static void printUsage(const char* argv0) {
std::printf(" Append one quest to <zoneDir>/quests.json and exit\n");
std::printf(" --add-quest-objective <zoneDir> <questIdx> <kill|collect|talk|explore|escort|use> <targetName> [count]\n");
std::printf(" Append one objective to a quest by index\n");
std::printf(" --add-quest-reward-item <zoneDir> <questIdx> <itemPath> [more...]\n");
std::printf(" Append item reward(s) to a quest's reward.itemRewards list\n");
std::printf(" --set-quest-reward <zoneDir> <questIdx> [--xp N] [--gold N] [--silver N] [--copper N]\n");
std::printf(" Update XP/coin reward fields on a quest by index\n");
std::printf(" --remove-creature <zoneDir> <index>\n");
std::printf(" Remove creature at given 0-based index from <zoneDir>/creatures.json\n");
std::printf(" --remove-object <zoneDir> <index>\n");
@ -507,7 +511,7 @@ int main(int argc, char* argv[]) {
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
"--validate-whm", "--validate-all", "--zone-summary",
"--scaffold-zone", "--add-creature", "--add-object", "--add-quest",
"--add-quest-objective",
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
"--remove-creature", "--remove-object", "--remove-quest",
"--copy-zone",
"--build-woc", "--regen-collision", "--fix-zone",
@ -554,6 +558,16 @@ int main(int argc, char* argv[]) {
"--add-quest-objective requires <zoneDir> <questIdx> <type> <targetName>\n");
return 1;
}
if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 >= argc) {
std::fprintf(stderr,
"--add-quest-reward-item requires <zoneDir> <questIdx> <itemPath>\n");
return 1;
}
if (std::strcmp(argv[i], "--set-quest-reward") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--set-quest-reward requires <zoneDir> <questIdx> [--xp N] [--gold N] [--silver N] [--copper N]\n");
return 1;
}
if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--copy-zone requires <srcDir> <newName>\n");
@ -2794,6 +2808,124 @@ int main(int argc, char* argv[]) {
obj.description.c_str(), idx, q->title.c_str(),
q->objectives.size());
return 0;
} else if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 < argc) {
// Append one or more item rewards to a quest. Multiple paths
// can be passed in a single invocation:
// --add-quest-reward-item zone 0 'Item:Sword' 'Item:Shield'
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/quests.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "add-quest-reward-item: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "add-quest-reward-item: bad questIdx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "add-quest-reward-item: failed to load %s\n", path.c_str());
return 1;
}
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"add-quest-reward-item: questIdx %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
wowee::editor::Quest* q = qe.getQuest(idx);
if (!q) return 1;
int added = 0;
// Greedy-consume any remaining args that don't start with '-'
// so the caller can batch-add a whole loot table in one shot.
while (i + 1 < argc && argv[i + 1][0] != '-') {
q->reward.itemRewards.push_back(argv[++i]);
added++;
}
if (added == 0) {
std::fprintf(stderr, "add-quest-reward-item: need at least one itemPath\n");
return 1;
}
if (!qe.saveToFile(path)) {
std::fprintf(stderr, "add-quest-reward-item: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Added %d item reward(s) to quest %d ('%s'), now %zu total\n",
added, idx, q->title.c_str(), q->reward.itemRewards.size());
return 0;
} else if (std::strcmp(argv[i], "--set-quest-reward") == 0 && i + 2 < argc) {
// Update XP / coin reward fields on an existing quest. Each
// field is optional — only the ones explicitly passed are
// changed. This avoids the round-trip-and-clobber footgun of
// a "replace whole reward" command.
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/quests.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "set-quest-reward: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "set-quest-reward: bad questIdx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "set-quest-reward: failed to load %s\n", path.c_str());
return 1;
}
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"set-quest-reward: questIdx %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
wowee::editor::Quest* q = qe.getQuest(idx);
if (!q) return 1;
int changed = 0;
auto consumeUint = [&](const char* flag, uint32_t& target) {
if (i + 2 < argc && std::strcmp(argv[i + 1], flag) == 0) {
try {
target = static_cast<uint32_t>(std::stoul(argv[i + 2]));
i += 2;
changed++;
return true;
} catch (...) {
std::fprintf(stderr, "set-quest-reward: bad %s value '%s'\n",
flag, argv[i + 2]);
}
}
return false;
};
// Loop until no more recognised flags consume their value —
// order-independent, so callers can pass --gold then --xp.
bool any = true;
while (any) {
any = false;
if (consumeUint("--xp", q->reward.xp)) any = true;
if (consumeUint("--gold", q->reward.gold)) any = true;
if (consumeUint("--silver", q->reward.silver)) any = true;
if (consumeUint("--copper", q->reward.copper)) any = true;
}
if (changed == 0) {
std::fprintf(stderr,
"set-quest-reward: no fields changed — pass --xp / --gold / --silver / --copper\n");
return 1;
}
if (!qe.saveToFile(path)) {
std::fprintf(stderr, "set-quest-reward: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Updated %d field(s) on quest %d ('%s'): xp=%u gold=%u silver=%u copper=%u\n",
changed, idx, q->title.c_str(),
q->reward.xp, q->reward.gold,
q->reward.silver, q->reward.copper);
return 0;
} else if (std::strcmp(argv[i], "--remove-creature") == 0 && i + 2 < argc) {
// Remove a creature spawn by 0-based index. Pair with
// --info-creatures (or your editor) to find the right index