diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d81258b..a7ac83be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1344,6 +1344,7 @@ add_executable(wowee_editor tools/editor/cli_random.cpp tools/editor/cli_items_export.cpp tools/editor/cli_items_mutate.cpp + tools/editor/cli_zone_create.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_zone_create.cpp b/tools/editor/cli_zone_create.cpp new file mode 100644 index 00000000..9d62328e --- /dev/null +++ b/tools/editor/cli_zone_create.cpp @@ -0,0 +1,203 @@ +#include "cli_zone_create.hpp" + +#include "zone_manifest.hpp" +#include "npc_spawner.hpp" +#include "object_placer.hpp" +#include "quest_editor.hpp" +#include "terrain_editor.hpp" +#include "terrain_biomes.hpp" +#include "wowee_terrain.hpp" + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleScaffoldZone(int& i, int argc, char** argv) { + // Generate a minimal valid empty zone — useful for kickstarting + // a new authoring session without needing to launch the GUI. + std::string rawName = argv[++i]; + int sx = 32, sy = 32; + if (i + 2 < argc) { + int parsedX = std::atoi(argv[i + 1]); + int parsedY = std::atoi(argv[i + 2]); + if (parsedX >= 0 && parsedX <= 63 && + parsedY >= 0 && parsedY <= 63) { + sx = parsedX; sy = parsedY; + i += 2; + } + } + // Slugify name to match unpackZone / server module rules. + std::string slug; + for (char c : rawName) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + slug += c; + } else if (c == ' ') { + slug += '_'; + } + } + if (slug.empty()) { + std::fprintf(stderr, "--scaffold-zone: name '%s' has no valid characters\n", + rawName.c_str()); + return 1; + } + namespace fs = std::filesystem; + std::string dir = "custom_zones/" + slug; + if (fs::exists(dir)) { + std::fprintf(stderr, "--scaffold-zone: directory already exists: %s\n", + dir.c_str()); + return 1; + } + fs::create_directories(dir); + + // Blank flat terrain at the requested tile. + auto terrain = wowee::editor::TerrainEditor::createBlankTerrain( + sx, sy, 100.0f, wowee::editor::Biome::Grassland); + std::string base = dir + "/" + slug + "_" + + std::to_string(sx) + "_" + std::to_string(sy); + wowee::editor::WoweeTerrain::exportOpen(terrain, base, sx, sy); + + // Minimal zone.json + wowee::editor::ZoneManifest manifest; + manifest.mapName = slug; + manifest.displayName = rawName; + manifest.mapId = 9000; + manifest.baseHeight = 100.0f; + manifest.tiles.push_back({sx, sy}); + manifest.save(dir + "/zone.json"); + + std::printf("Scaffolded zone: %s\n", dir.c_str()); + std::printf(" tile : (%d, %d)\n", sx, sy); + std::printf(" files : %s.wot, %s.whm, zone.json\n", + slug.c_str(), slug.c_str()); + std::printf(" next step: run editor without args, then File → Open Zone\n"); + return 0; +} + +int handleMvpZone(int& i, int argc, char** argv) { + // Quick-start: scaffold + populate one of each content type + // (1 creature, 1 object, 1 quest with objective + reward). + // Useful for demos, screenshot bait, smoke tests of the + // bake/validate pipeline. The zone goes from empty to + // 'something to look at' in one command. + std::string rawName = argv[++i]; + int sx = 32, sy = 32; + if (i + 2 < argc) { + int parsedX = std::atoi(argv[i + 1]); + int parsedY = std::atoi(argv[i + 2]); + if (parsedX >= 0 && parsedX <= 63 && + parsedY >= 0 && parsedY <= 63) { + sx = parsedX; sy = parsedY; + i += 2; + } + } + // Reuse scaffold-zone's slug logic. + std::string slug; + for (char c : rawName) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') slug += c; + else if (c == ' ') slug += '_'; + } + if (slug.empty()) { + std::fprintf(stderr, + "mvp-zone: name '%s' has no valid characters\n", + rawName.c_str()); + return 1; + } + namespace fs = std::filesystem; + std::string dir = "custom_zones/" + slug; + if (fs::exists(dir)) { + std::fprintf(stderr, + "mvp-zone: directory already exists: %s\n", dir.c_str()); + return 1; + } + fs::create_directories(dir); + // Scaffold terrain. + auto terrain = wowee::editor::TerrainEditor::createBlankTerrain( + sx, sy, 100.0f, wowee::editor::Biome::Grassland); + std::string base = dir + "/" + slug + "_" + + std::to_string(sx) + "_" + std::to_string(sy); + wowee::editor::WoweeTerrain::exportOpen(terrain, base, sx, sy); + // Manifest. + wowee::editor::ZoneManifest zm; + zm.mapName = slug; + zm.displayName = rawName; + zm.mapId = 9000; + zm.baseHeight = 100.0f; + zm.tiles.push_back({sx, sy}); + zm.hasCreatures = true; + zm.save(dir + "/zone.json"); + // Position the demo content roughly centered in the tile. + // Tile (32, 32) is the WoW map origin; tile centers are at + // 533.33-yard intervals from there. + float centerX = (32.0f - sy) * 533.33333f - 266.667f; + float centerY = (32.0f - sx) * 533.33333f - 266.667f; + float centerZ = 100.0f; + // Demo creature. + wowee::editor::NpcSpawner sp; + wowee::editor::CreatureSpawn c; + c.name = "Demo Wolf"; + c.position = {centerX, centerY, centerZ}; + c.level = 5; + c.health = 100; + c.minDamage = 5; c.maxDamage = 10; + c.displayId = 11430; // any valid id; renderer falls back if absent + sp.getSpawns().push_back(c); + sp.saveToFile(dir + "/creatures.json"); + // Demo object — a tree placement near the creature. + wowee::editor::ObjectPlacer op; + wowee::editor::PlacedObject po; + po.type = wowee::editor::PlaceableType::M2; + po.path = "World/Generic/Tree.m2"; + po.position = {centerX + 5.0f, centerY, centerZ}; + po.scale = 1.0f; + op.getObjects().push_back(po); + op.saveToFile(dir + "/objects.json"); + // Demo quest with objective + XP reward. + wowee::editor::QuestEditor qe; + wowee::editor::Quest q; + q.title = "Welcome to " + rawName; + q.requiredLevel = 1; + q.questGiverNpcId = c.id; // self-referential so refs check passes + q.turnInNpcId = c.id; + q.reward.xp = 100; + wowee::editor::QuestObjective obj; + obj.type = wowee::editor::QuestObjectiveType::KillCreature; + obj.targetName = "Demo Wolf"; + obj.targetCount = 1; + obj.description = "Slay the Demo Wolf"; + q.objectives.push_back(obj); + qe.addQuest(q); + qe.saveToFile(dir + "/quests.json"); + std::printf("Created demo zone: %s\n", dir.c_str()); + std::printf(" tile : (%d, %d)\n", sx, sy); + std::printf(" contents : 1 creature, 1 object, 1 quest (with objective + reward)\n"); + std::printf(" next : wowee_editor --info-zone-tree %s\n", dir.c_str()); + (void)argc; + return 0; +} + +} // namespace + +bool handleZoneCreate(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--scaffold-zone") == 0 && i + 1 < argc) { + outRc = handleScaffoldZone(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--mvp-zone") == 0 && i + 1 < argc) { + outRc = handleMvpZone(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_zone_create.hpp b/tools/editor/cli_zone_create.hpp new file mode 100644 index 00000000..ab0b1ede --- /dev/null +++ b/tools/editor/cli_zone_create.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the zone-creation handlers. Both kickstart a new +// authoring session by generating a new zone directory under +// custom_zones/ — empty for --scaffold-zone, populated with +// one of each content type for --mvp-zone. +// --scaffold-zone minimal valid empty zone (terrain + manifest) +// --mvp-zone scaffold + 1 creature + 1 object + 1 quest +// +// Returns true if matched; outRc holds the exit code. +bool handleZoneCreate(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index aa0b1c76..156d04bc 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -45,6 +45,7 @@ #include "cli_random.hpp" #include "cli_items_export.hpp" #include "cli_items_mutate.hpp" +#include "cli_zone_create.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -517,6 +518,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleItemsMutate(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleZoneCreate(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1445,166 +1449,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--scaffold-zone") == 0 && i + 1 < argc) { - // Generate a minimal valid empty zone — useful for kickstarting - // a new authoring session without needing to launch the GUI. - std::string rawName = argv[++i]; - int sx = 32, sy = 32; - if (i + 2 < argc) { - int parsedX = std::atoi(argv[i + 1]); - int parsedY = std::atoi(argv[i + 2]); - if (parsedX >= 0 && parsedX <= 63 && - parsedY >= 0 && parsedY <= 63) { - sx = parsedX; sy = parsedY; - i += 2; - } - } - // Slugify name to match unpackZone / server module rules. - std::string slug; - for (char c : rawName) { - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || c == '_' || c == '-') { - slug += c; - } else if (c == ' ') { - slug += '_'; - } - } - if (slug.empty()) { - std::fprintf(stderr, "--scaffold-zone: name '%s' has no valid characters\n", - rawName.c_str()); - return 1; - } - namespace fs = std::filesystem; - std::string dir = "custom_zones/" + slug; - if (fs::exists(dir)) { - std::fprintf(stderr, "--scaffold-zone: directory already exists: %s\n", - dir.c_str()); - return 1; - } - fs::create_directories(dir); - - // Blank flat terrain at the requested tile. - auto terrain = wowee::editor::TerrainEditor::createBlankTerrain( - sx, sy, 100.0f, wowee::editor::Biome::Grassland); - std::string base = dir + "/" + slug + "_" + - std::to_string(sx) + "_" + std::to_string(sy); - wowee::editor::WoweeTerrain::exportOpen(terrain, base, sx, sy); - - // Minimal zone.json - wowee::editor::ZoneManifest manifest; - manifest.mapName = slug; - manifest.displayName = rawName; - manifest.mapId = 9000; - manifest.baseHeight = 100.0f; - manifest.tiles.push_back({sx, sy}); - manifest.save(dir + "/zone.json"); - - std::printf("Scaffolded zone: %s\n", dir.c_str()); - std::printf(" tile : (%d, %d)\n", sx, sy); - std::printf(" files : %s.wot, %s.whm, zone.json\n", - slug.c_str(), slug.c_str()); - std::printf(" next step: run editor without args, then File → Open Zone\n"); - return 0; - } else if (std::strcmp(argv[i], "--mvp-zone") == 0 && i + 1 < argc) { - // Quick-start: scaffold + populate one of each content type - // (1 creature, 1 object, 1 quest with objective + reward). - // Useful for demos, screenshot bait, smoke tests of the - // bake/validate pipeline. The zone goes from empty to - // 'something to look at' in one command. - std::string rawName = argv[++i]; - int sx = 32, sy = 32; - if (i + 2 < argc) { - int parsedX = std::atoi(argv[i + 1]); - int parsedY = std::atoi(argv[i + 2]); - if (parsedX >= 0 && parsedX <= 63 && - parsedY >= 0 && parsedY <= 63) { - sx = parsedX; sy = parsedY; - i += 2; - } - } - // Reuse scaffold-zone's slug logic. - std::string slug; - for (char c : rawName) { - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || c == '_' || c == '-') slug += c; - else if (c == ' ') slug += '_'; - } - if (slug.empty()) { - std::fprintf(stderr, - "mvp-zone: name '%s' has no valid characters\n", - rawName.c_str()); - return 1; - } - namespace fs = std::filesystem; - std::string dir = "custom_zones/" + slug; - if (fs::exists(dir)) { - std::fprintf(stderr, - "mvp-zone: directory already exists: %s\n", dir.c_str()); - return 1; - } - fs::create_directories(dir); - // Scaffold terrain. - auto terrain = wowee::editor::TerrainEditor::createBlankTerrain( - sx, sy, 100.0f, wowee::editor::Biome::Grassland); - std::string base = dir + "/" + slug + "_" + - std::to_string(sx) + "_" + std::to_string(sy); - wowee::editor::WoweeTerrain::exportOpen(terrain, base, sx, sy); - // Manifest. - wowee::editor::ZoneManifest zm; - zm.mapName = slug; - zm.displayName = rawName; - zm.mapId = 9000; - zm.baseHeight = 100.0f; - zm.tiles.push_back({sx, sy}); - zm.hasCreatures = true; - zm.save(dir + "/zone.json"); - // Position the demo content roughly centered in the tile. - // Tile (32, 32) is the WoW map origin; tile centers are at - // 533.33-yard intervals from there. - float centerX = (32.0f - sy) * 533.33333f - 266.667f; - float centerY = (32.0f - sx) * 533.33333f - 266.667f; - float centerZ = 100.0f; - // Demo creature. - wowee::editor::NpcSpawner sp; - wowee::editor::CreatureSpawn c; - c.name = "Demo Wolf"; - c.position = {centerX, centerY, centerZ}; - c.level = 5; - c.health = 100; - c.minDamage = 5; c.maxDamage = 10; - c.displayId = 11430; // any valid id; renderer falls back if absent - sp.getSpawns().push_back(c); - sp.saveToFile(dir + "/creatures.json"); - // Demo object — a tree placement near the creature. - wowee::editor::ObjectPlacer op; - wowee::editor::PlacedObject po; - po.type = wowee::editor::PlaceableType::M2; - po.path = "World/Generic/Tree.m2"; - po.position = {centerX + 5.0f, centerY, centerZ}; - po.scale = 1.0f; - op.getObjects().push_back(po); - op.saveToFile(dir + "/objects.json"); - // Demo quest with objective + XP reward. - wowee::editor::QuestEditor qe; - wowee::editor::Quest q; - q.title = "Welcome to " + rawName; - q.requiredLevel = 1; - q.questGiverNpcId = c.id; // self-referential so refs check passes - q.turnInNpcId = c.id; - q.reward.xp = 100; - wowee::editor::QuestObjective obj; - obj.type = wowee::editor::QuestObjectiveType::KillCreature; - obj.targetName = "Demo Wolf"; - obj.targetCount = 1; - obj.description = "Slay the Demo Wolf"; - q.objectives.push_back(obj); - qe.addQuest(q); - qe.saveToFile(dir + "/quests.json"); - std::printf("Created demo zone: %s\n", dir.c_str()); - std::printf(" tile : (%d, %d)\n", sx, sy); - std::printf(" contents : 1 creature, 1 object, 1 quest (with objective + reward)\n"); - std::printf(" next : wowee_editor --info-zone-tree %s\n", dir.c_str()); - return 0; } else if (std::strcmp(argv[i], "--add-tile") == 0 && i + 3 < argc) { // Extend an existing zone with another ADT tile. Zones can // span multiple tiles (e.g. a continent fragment), but