From d96496147f72f091e6ad66ded8c400e5828bfadf Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 11:57:55 -0700 Subject: [PATCH] feat(editor): add --random-populate-items seeded loot generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeded random items.json populator. Pulls a quality-banded prefix ("Worn" → "Eternal" depending on rolled quality) plus a noun ("Sword", "Tome", "Cloak"...) for plausible names, then randomizes itemLevel and stack size around quality-appropriate baselines. Useful for playtest loot tables that need bulk content without hand-typing each entry. Reproducible from --seed; respects --max-quality so a "trash loot" pass won't accidentally drop artifacts. Verified on a fresh zone: 8 items rolled with the seed=5 produces a sensible spread (1 epic Tome, 1 epic Cuirass, 1 uncommon Bow, 5 common/poor) and unique IDs starting at 1. Brings command count to 233. --- tools/editor/main.cpp | 120 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 1ae62577..b879bda3 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -571,6 +571,8 @@ static void printUsage(const char* argv0) { std::printf(" Append one item entry to /items.json (auto-creates the file)\n"); std::printf(" --random-populate-zone [--seed N] [--creatures N] [--objects N]\n"); std::printf(" Add random creatures/objects to a zone (seeded for reproducibility)\n"); + std::printf(" --random-populate-items [--seed N] [--count N] [--max-quality Q]\n"); + std::printf(" Generate random items.json entries (seeded; quality cap defaults to epic=4)\n"); std::printf(" --list-items [--json]\n"); std::printf(" Print every item in /items.json with quality colors and key fields\n"); std::printf(" --export-zone-items-md [out.md]\n"); @@ -1017,7 +1019,7 @@ int main(int argc, char* argv[]) { "--check-project-content", "--check-project-refs", "--export-zone-deps-md", "--export-zone-spawn-png", "--add-creature", "--add-object", "--add-quest", "--add-item", - "--random-populate-zone", + "--random-populate-zone", "--random-populate-items", "--list-items", "--info-item", "--set-item", "--export-zone-items-md", "--export-project-items-md", "--export-project-items-csv", "--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward", @@ -13291,6 +13293,122 @@ int main(int argc, char* argv[]) { std::printf(" objects : %d added (%zu total)\n", placedObjects, placer.getObjects().size()); return 0; + } else if (std::strcmp(argv[i], "--random-populate-items") == 0 && i + 1 < argc) { + // Seeded random items.json populator. Pulls a base name + // and a noun from inline word lists, picks a quality up + // to maxQuality, randomizes itemLevel and stack size + // around plausible defaults. Useful for playtest loot + // tables that need bulk content without hand-typing each + // entry. + // + // Flags: --seed N (default 7), --count N (default 30), + // --max-quality Q (default 4 = epic; 0..6 valid). + std::string zoneDir = argv[++i]; + uint32_t seed = 7; + int count = 30; + int maxQuality = 4; + while (i + 2 < argc && argv[i + 1][0] == '-') { + std::string flag = argv[++i]; + if (flag == "--seed") { + try { seed = static_cast(std::stoul(argv[++i])); } + catch (...) {} + } else if (flag == "--count") { + try { count = std::stoi(argv[++i]); } catch (...) {} + } else if (flag == "--max-quality") { + try { maxQuality = std::stoi(argv[++i]); } catch (...) {} + } else { + std::fprintf(stderr, + "random-populate-items: unknown flag '%s'\n", flag.c_str()); + return 1; + } + } + if (maxQuality < 0 || maxQuality > 6) maxQuality = 4; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "random-populate-items: %s has no zone.json\n", + zoneDir.c_str()); + return 1; + } + uint32_t rng = seed ? seed : 1u; + auto next01 = [&]() { + rng = rng * 1664525u + 1013904223u; + return (rng >> 8) / float(1 << 24); + }; + auto rangeI = [&](int a, int b) { + return a + static_cast(next01() * (b - a + 1)); + }; + // Inline name lexicon. {prefix, noun} → "Glowing Sword". + // Quality ramps prefix selection; rare+ items get fancier + // adjectives. + static const std::vector kPrefixes[5] = { + {"Worn", "Tattered", "Cracked", "Dented", "Faded"}, // poor + {"Common", "Plain", "Basic", "Simple", "Standard"}, // common + {"Sharp", "Sturdy", "Polished", "Reinforced", "Fine"}, // uncommon + {"Glowing", "Runed", "Enchanted", "Storm", "Mystic"}, // rare + {"Ancient", "Eternal", "Heroic", "Vengeful", "Soul"}, // epic + }; + static const std::vector kNouns = { + "Sword", "Mace", "Axe", "Dagger", "Staff", + "Bow", "Helm", "Cuirass", "Greaves", "Gauntlets", + "Ring", "Amulet", "Cloak", "Belt", "Boots", + "Potion", "Scroll", "Tome", "Wand", "Shield", + }; + // Open the items doc. + std::string ipath = zoneDir + "/items.json"; + nlohmann::json doc = nlohmann::json::object({{"items", + nlohmann::json::array()}}); + if (fs::exists(ipath)) { + std::ifstream in(ipath); + try { in >> doc; } catch (...) {} + if (!doc.contains("items") || !doc["items"].is_array()) { + doc["items"] = nlohmann::json::array(); + } + } + std::set used; + for (const auto& it : doc["items"]) { + if (it.contains("id") && it["id"].is_number_unsigned()) + used.insert(it["id"].get()); + } + int added = 0; + for (int n = 0; n < count; ++n) { + int q = std::min(maxQuality, rangeI(0, maxQuality)); + int qBucket = std::min(q, 4); + const auto& prefixes = kPrefixes[qBucket]; + std::string name = prefixes[rangeI(0, + static_cast(prefixes.size()) - 1)]; + name += " "; + name += kNouns[rangeI(0, static_cast(kNouns.size()) - 1)]; + uint32_t id = 1; + while (used.count(id)) ++id; + used.insert(id); + int ilvl = std::max(1, + rangeI(1, 5) + q * 12 + rangeI(-3, 3)); + doc["items"].push_back({ + {"id", id}, + {"name", name}, + {"quality", q}, + {"displayId", rangeI(1000, 9999)}, + {"itemLevel", ilvl}, + {"stackable", q == 0 || q == 1 ? rangeI(1, 20) : 1}, + }); + added++; + } + std::ofstream out(ipath); + if (!out) { + std::fprintf(stderr, + "random-populate-items: failed to write %s\n", + ipath.c_str()); + return 1; + } + out << doc.dump(2); + out.close(); + std::printf("random-populate-items: %s\n", ipath.c_str()); + std::printf(" seed : %u\n", seed); + std::printf(" added : %d\n", added); + std::printf(" total items : %zu\n", doc["items"].size()); + std::printf(" max quality : %d\n", maxQuality); + return 0; } else if (std::strcmp(argv[i], "--list-items") == 0 && i + 1 < argc) { // Inspect /items.json. Pretty-prints id / quality // / item level / display id / name as a table; also