From 6927c19d72d30a178ea8f034ae8800a1d5b15a69 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:57:31 -0700 Subject: [PATCH] feat(editor): add --clone-creature symmetric to --clone-quest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Designer workflow: archetype one 'patrol guard' with stats + faction + behavior + equipment dialed in, then stamp it across spawn points around a town: for x in 100 110 120 130; do wowee_editor --clone-creature $Z 0 "Guard $x" $x 200 50 done Defaults give safe behavior: - Position offset by (5, 0, 0) yards so the copy doesn't z-fight the original when no offset is passed. - Name appended with ' (copy)' if no newName given. - id reset (auto-assigned by NpcSpawner). - Patrol path NOT offset — patrol points are world-space waypoints, not relative to the spawn; cloning that vector unchanged matches designer intent. Re-author the path on the clone if needed. Verified: scaffolded zone, added Guard, cloned twice (default offset, custom name + 3D offset). list-creatures shows 3 spawns with correctly differentiated positions and names. --- tools/editor/main.cpp | 78 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index ca1d8b81..fc8b6423 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -432,6 +432,8 @@ static void printUsage(const char* argv0) { 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(" --clone-creature [newName] [dx dy dz]\n"); + std::printf(" Duplicate a creature spawn (defaults: ' (copy)' offset by 5 yards)\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"); @@ -554,7 +556,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", "--clone-quest", + "--remove-quest-objective", "--clone-quest", "--clone-creature", "--remove-creature", "--remove-object", "--remove-quest", "--copy-zone", "--rename-zone", "--build-woc", "--regen-collision", "--fix-zone", @@ -614,6 +616,11 @@ int main(int argc, char* argv[]) { "--clone-quest requires \n"); return 1; } + if (std::strcmp(argv[i], "--clone-creature") == 0 && i + 2 >= argc) { + std::fprintf(stderr, + "--clone-creature 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"); @@ -3835,6 +3842,75 @@ int main(int argc, char* argv[]) { clone.reward.itemRewards.size(), clone.reward.xp); return 0; + } else if (std::strcmp(argv[i], "--clone-creature") == 0 && i + 2 < argc) { + // Duplicate a creature spawn. Common workflow: design one + // 'patrol guard' archetype, then clone it across spawn points + // around a town. Preserves stats, faction, behavior, equipment; + // resets id and offsets position by 5 yards by default so the + // copy doesn't z-fight with the original. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string newName; + float dx = 5.0f, dy = 0.0f, dz = 0.0f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + newName = argv[++i]; + } + // Optional 3-axis offset after newName. + if (i + 3 < argc && argv[i + 1][0] != '-') { + try { + dx = std::stof(argv[++i]); + dy = std::stof(argv[++i]); + dz = std::stof(argv[++i]); + } catch (...) { + std::fprintf(stderr, "clone-creature: bad offset coordinate\n"); + return 1; + } + } + std::string path = zoneDir + "/creatures.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "clone-creature: %s not found\n", path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "clone-creature: bad idx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::NpcSpawner sp; + if (!sp.loadFromFile(path)) { + std::fprintf(stderr, "clone-creature: failed to load %s\n", path.c_str()); + return 1; + } + if (idx < 0 || idx >= static_cast(sp.spawnCount())) { + std::fprintf(stderr, + "clone-creature: idx %d out of range [0, %zu)\n", + idx, sp.spawnCount()); + return 1; + } + // Deep-copy by value; CreatureSpawn is POD-ish (vectors for + // patrol points copy automatically). + wowee::editor::CreatureSpawn clone = sp.getSpawns()[idx]; + clone.id = 0; // addCreature auto-assigns a fresh id + clone.name = newName.empty() + ? (clone.name + " (copy)") + : newName; + clone.position.x += dx; + clone.position.y += dy; + clone.position.z += dz; + // Patrol path is intentionally NOT offset — patrol points are + // typically authored as world-space waypoints, not relative to + // the spawn. Designers re-author the path if needed. + sp.getSpawns().push_back(clone); + if (!sp.saveToFile(path)) { + std::fprintf(stderr, "clone-creature: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Cloned creature %d -> '%s' at (%.1f, %.1f, %.1f) (now %zu total)\n", + idx, clone.name.c_str(), + clone.position.x, clone.position.y, clone.position.z, + sp.spawnCount()); + 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: