feat(editor): add --mvp-zone for one-command demo zone setup

Quick-start: scaffold a zone AND populate one of each content type
(1 creature, 1 object, 1 quest with objective + XP reward) in a
single command. Goes from empty filesystem to 'something to look at'
without 7 chained --add-* commands:

  wowee_editor --mvp-zone 'Demo Land' 30 30

  Created demo zone: custom_zones/Demo_Land
    tile     : (30, 30)
    contents : 1 creature, 1 object, 1 quest (with objective + reward)
    next     : wowee_editor --info-zone-tree custom_zones/Demo_Land

  Demo Land/
  ├─ Manifest ...
  ├─ Tiles (1) — (30, 30)
  ├─ Creatures (1) — lvl 5 Demo Wolf
  ├─ Objects (1) — m2 World/Generic/Tree.m2
  ├─ Quests (1) — [1] Welcome to Demo Land (lvl 1, 100 XP)
  │     └─ kill ×1 Demo Wolf

Demo content is positioned roughly at tile center (533.33-yard
intervals from origin tile 32/32). Quest references the demo
creature's auto-id so --check-zone-refs passes immediately.

Use cases:
- Smoke-testing the bake/validate pipeline
- Screenshot bait for docs / blog posts
- Editor onboarding (open a zone in the GUI to see the format)
- CI sanity check (does our editor still produce a viewable zone?)

Verified end-to-end: --mvp-zone 'Demo Land' → --info-zone-tree
shows all 4 sections populated correctly, file list matches
expected 6 files.
This commit is contained in:
Kelsi 2026-05-06 16:25:23 -07:00
parent 9c7b6aebfc
commit dbf973e29e

View file

@ -524,6 +524,8 @@ static void printUsage(const char* argv0) {
std::printf(" --for-each-zone <projectDir> -- <cmd...>\n");
std::printf(" Run <cmd...> for every zone in <projectDir>; '{}' in cmd is replaced with the zone path\n");
std::printf(" --scaffold-zone <name> [tx ty] Create a blank zone in custom_zones/<name>/ and exit\n");
std::printf(" --mvp-zone <name> [tx ty]\n");
std::printf(" Scaffold + add a creature + object + quest (with objective+reward) for quick demos\n");
std::printf(" --add-tile <zoneDir> <tx> <ty> [baseHeight]\n");
std::printf(" Add a new ADT tile to an existing zone (extends the manifest's tiles list)\n");
std::printf(" --remove-tile <zoneDir> <tx> <ty>\n");
@ -788,7 +790,7 @@ int main(int argc, char* argv[]) {
"--export-zone-summary-md", "--export-quest-graph",
"--export-zone-csv", "--export-zone-html", "--export-project-html",
"--export-zone-checksum",
"--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles",
"--scaffold-zone", "--mvp-zone", "--add-tile", "--remove-tile", "--list-tiles",
"--for-each-zone", "--zone-stats", "--info-tilemap",
"--list-zone-deps", "--check-zone-refs", "--check-zone-content",
"--export-zone-deps-md",
@ -9682,6 +9684,106 @@ int main(int argc, char* argv[]) {
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