From 2c41f7804b13846c30ac20c98ebced0fc15f2aec Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:53:23 -0700 Subject: [PATCH] feat(editor): add --clone-quest for templating quests with shared shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Common designer workflow: build a base quest with objectives + rewards once, then make N variants ('Slay Wolves' / 'Slay Bears' / 'Slay Tigers') with the same shape. Doing this through individual --add-* commands means re-typing all objectives + items each time. --clone-quest deep-copies an existing quest in one shot: wowee_editor --clone-quest $Z 0 # 'Foo' -> 'Foo (copy)' wowee_editor --clone-quest $Z 0 'Slay Bears' # custom title Carries: - All objectives (deep copy via vector value-copy) - All item rewards - XP / coin reward fields - requiredLevel, giver, turnIn Resets: - id (set to 0; addQuest auto-assigns a fresh one) - nextQuestId (cleared — chaining the clone to the same next quest would corrupt chain semantics; user can re-set it explicitly) Verified: scaffolded zone, added quest with 2 objectives + 1 item + xp=250. Cloned twice (default name, custom name). Result: 3 quests in list, both clones have full objective list and reward intact. --- tools/editor/main.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index c0977303..f90f9130 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -429,6 +429,8 @@ static void printUsage(const char* argv0) { std::printf(" Append one objective to a quest by index\n"); std::printf(" --remove-quest-objective \n"); std::printf(" Remove the objective at given 0-based index from a quest\n"); + std::printf(" --clone-quest [newTitle]\n"); + std::printf(" Duplicate a quest (with all objectives + rewards) and append it\n"); std::printf(" --add-quest-reward-item [more...]\n"); std::printf(" Append item reward(s) to a quest's reward.itemRewards list\n"); std::printf(" --set-quest-reward [--xp N] [--gold N] [--silver N] [--copper N]\n"); @@ -549,7 +551,7 @@ int main(int argc, char* argv[]) { "--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles", "--add-creature", "--add-object", "--add-quest", "--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward", - "--remove-quest-objective", + "--remove-quest-objective", "--clone-quest", "--remove-creature", "--remove-object", "--remove-quest", "--copy-zone", "--rename-zone", "--build-woc", "--regen-collision", "--fix-zone", @@ -604,6 +606,11 @@ int main(int argc, char* argv[]) { "--remove-quest-objective requires \n"); return 1; } + if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 >= argc) { + std::fprintf(stderr, + "--clone-quest requires \n"); + return 1; + } if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 >= argc) { std::fprintf(stderr, "--add-quest-reward-item requires \n"); @@ -3692,6 +3699,64 @@ int main(int argc, char* argv[]) { removedDesc.c_str(), oIdx, qIdx, q->title.c_str(), q->objectives.size()); return 0; + } else if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 < argc) { + // Duplicate a quest. Useful for templating: create a base + // quest with objectives + rewards once, then clone N times + // for variants ('Slay Wolves', 'Slay Bears' with the same + // shape). Optional newTitle replaces the cloned copy's title; + // omit to get ' (copy)'. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string newTitle; + if (i + 1 < argc && argv[i + 1][0] != '-') { + newTitle = argv[++i]; + } + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "clone-quest: %s not found\n", path.c_str()); + return 1; + } + int qIdx; + try { qIdx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "clone-quest: bad questIdx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "clone-quest: failed to load %s\n", path.c_str()); + return 1; + } + if (qIdx < 0 || qIdx >= static_cast(qe.questCount())) { + std::fprintf(stderr, + "clone-quest: questIdx %d out of range [0, %zu)\n", + qIdx, qe.questCount()); + return 1; + } + // Deep-copy by value via vector iteration; .objectives and + // .reward are STL containers so the copy is automatic. + wowee::editor::Quest clone = qe.getQuests()[qIdx]; + // Reset id so the editor's auto-id sequence assigns a fresh + // one — addQuest does this internally if id==0. + clone.id = 0; + // Reset chain link too — copying a chained quest with the + // same nextQuestId would corrupt the chain semantics. + clone.nextQuestId = 0; + clone.title = newTitle.empty() + ? (clone.title + " (copy)") + : newTitle; + qe.addQuest(clone); + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "clone-quest: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Cloned quest %d -> '%s' (now %zu total)\n", + qIdx, clone.title.c_str(), qe.questCount()); + std::printf(" carried %zu objective(s), %zu item reward(s), xp=%u\n", + clone.objectives.size(), + clone.reward.itemRewards.size(), + clone.reward.xp); + 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: