feat(editor): add --add-quest-objective for headless objective creation

--add-quest creates a quest shell but quests with no objectives never
complete in-game. This fills the gap:

  wowee_editor --add-quest-objective <zoneDir> <questIdx> \
    <kill|collect|talk|explore|escort|use> <targetName> [count]

Workflow:

  Z=custom_zones/MyZone
  wowee_editor --add-quest $Z 'Hunt Wolves' 100 100 250 5
  wowee_editor --add-quest-objective $Z 0 kill    'Wolf'      5
  wowee_editor --add-quest-objective $Z 0 collect 'Wolf Pelt' 3

Auto-generates a description from type+count+name so addons and
tooltips show something sensible ('Slay 5 Wolf', 'Collect 3 Wolf
Pelt', 'Talk to Mayor'). Designers can hand-edit quests.json after
the fact for bespoke prose; the auto-text is the floor, not the
ceiling.

Quest index matches --list-quests output (0-based). Out-of-range
index, unknown type, or missing quests.json all error with clear
messages and exit 1.

Verified: scaffolded zone, added 2 quests + 3 objectives across
them; --info-quests reports '1 kill, 1 collect, 1 talk'. Bad type
and bad index both rejected.
This commit is contained in:
Kelsi 2026-05-06 12:12:06 -07:00
parent fdc7ca7ee7
commit caa0df7e5e

View file

@ -411,6 +411,8 @@ static void printUsage(const char* argv0) {
std::printf(" Append one object placement to <zoneDir>/objects.json and exit\n");
std::printf(" --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]\n");
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(" --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");
@ -498,6 +500,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",
"--remove-creature", "--remove-object", "--remove-quest",
"--copy-zone",
"--build-woc", "--regen-collision", "--fix-zone",
@ -539,6 +542,11 @@ int main(int argc, char* argv[]) {
"--add-quest requires <zoneDir> <title>\n");
return 1;
}
if (std::strcmp(argv[i], "--add-quest-objective") == 0 && i + 4 >= argc) {
std::fprintf(stderr,
"--add-quest-objective requires <zoneDir> <questIdx> <type> <targetName>\n");
return 1;
}
if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--copy-zone requires <srcDir> <newName>\n");
@ -2463,6 +2471,95 @@ int main(int argc, char* argv[]) {
std::printf("Added quest '%s' to %s (now %zu total)\n",
title.c_str(), path.c_str(), qe.questCount());
return 0;
} else if (std::strcmp(argv[i], "--add-quest-objective") == 0 && i + 4 < argc) {
// Append a single objective to an existing quest. The quest
// must already exist (use --add-quest first); index is 0-based
// and matches --list-quests output.
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string typeStr = argv[++i];
std::string targetName = argv[++i];
std::string path = zoneDir + "/quests.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "add-quest-objective: %s not found — run --add-quest first\n",
path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "add-quest-objective: bad questIdx '%s'\n", idxStr.c_str());
return 1;
}
using OT = wowee::editor::QuestObjectiveType;
OT type;
if (typeStr == "kill") type = OT::KillCreature;
else if (typeStr == "collect") type = OT::CollectItem;
else if (typeStr == "talk") type = OT::TalkToNPC;
else if (typeStr == "explore") type = OT::ExploreArea;
else if (typeStr == "escort") type = OT::EscortNPC;
else if (typeStr == "use") type = OT::UseObject;
else {
std::fprintf(stderr,
"add-quest-objective: type must be kill/collect/talk/explore/escort/use, got '%s'\n",
typeStr.c_str());
return 1;
}
uint32_t count = 1;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try {
count = static_cast<uint32_t>(std::stoul(argv[++i]));
if (count == 0) count = 1;
} catch (...) {}
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "add-quest-objective: failed to load %s\n", path.c_str());
return 1;
}
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"add-quest-objective: questIdx %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
wowee::editor::QuestObjective obj;
obj.type = type;
obj.targetName = targetName;
obj.targetCount = count;
// Auto-generate a description from type+name+count so addons
// and tooltips have something useful by default. The user can
// edit quests.json directly if they want bespoke prose.
const char* verb = "complete";
switch (type) {
case OT::KillCreature: verb = "Slay"; break;
case OT::CollectItem: verb = "Collect"; break;
case OT::TalkToNPC: verb = "Talk to"; break;
case OT::ExploreArea: verb = "Explore"; break;
case OT::EscortNPC: verb = "Escort"; break;
case OT::UseObject: verb = "Use"; break;
}
obj.description = std::string(verb) + " " +
(count > 1 ? std::to_string(count) + " " : "") +
targetName;
// Quest is stored by value in the editor's vector; mutate via
// the non-const getter, which gives us a pointer we can write
// through.
wowee::editor::Quest* q = qe.getQuest(idx);
if (!q) {
std::fprintf(stderr, "add-quest-objective: getQuest(%d) returned null\n", idx);
return 1;
}
q->objectives.push_back(obj);
if (!qe.saveToFile(path)) {
std::fprintf(stderr, "add-quest-objective: failed to write %s\n",
path.c_str());
return 1;
}
std::printf("Added objective '%s' to quest %d ('%s'), now %zu objective(s)\n",
obj.description.c_str(), idx, q->title.c_str(),
q->objectives.size());
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