Kelsidavis-WoWee/tools/editor/npc_presets.cpp
Kelsi 2980ca83e7 feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.

Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
  Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format

Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)

Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel

NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
  (Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON

Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format

Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed

Known issues:
- M2/WMO rendering may not display on first placement (frame index
  sync between update/render was misaligned — now fixed but untested
  end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00

191 lines
7.6 KiB
C++

#include "npc_presets.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/asset_manifest.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <set>
namespace wowee {
namespace editor {
const char* NpcPresets::getCategoryName(CreatureCategory cat) {
static const char* names[] = {
"Critters", "Beasts", "Humanoids", "Undead", "Demons",
"Elementals", "Dragonkin", "Giants", "Mechanical", "Mounts", "Bosses", "Other"
};
return names[static_cast<int>(cat)];
}
std::string NpcPresets::prettifyName(const std::string& dirName) const {
std::string result;
for (size_t i = 0; i < dirName.size(); i++) {
char c = dirName[i];
if (i == 0) {
result += static_cast<char>(std::toupper(c));
} else if (std::isupper(c) && i > 0 && std::islower(dirName[i-1])) {
result += ' ';
result += c;
} else {
result += c;
}
}
return result;
}
CreatureCategory NpcPresets::classifyCreature(const std::string& name) const {
// Critters
static const char* critters[] = {
"rabbit", "rat", "chicken", "frog", "snake", "squirrel", "deer", "sheep",
"cow", "pig", "parrot", "seagull", "beetle", "cockroach", "crab", "prairie",
"butterfly", "firefly", "maggot", "toad", "mouse", "hare", "penguin",
"babycrocodile", "babyelekk", "bearcub", "cat", "smallfish",
"kitten", "skunk", "ladybug", "gazelle", "gilamonster"
};
// Beasts
static const char* beasts[] = {
"bear", "boar", "wolf", "lion", "tiger", "raptor", "gorilla", "hyena",
"scorpid", "spider", "bat", "vulture", "crocolisk", "tallstrider",
"kodo", "elekk", "warp", "ravager", "serpent", "devilsaur", "crochet",
"plainstrider", "stag", "moose", "worg", "rhino", "mammoth", "jormungar",
"shoveltusk", "basilisk", "carrionbird", "condor", "hippogryph",
"windserpent", "thunderlizard", "turtle", "silithid", "wasp", "moth",
"nether", "cat", "arcticcondor"
};
// Humanoids
static const char* humanoids[] = {
"human", "orc", "dwarf", "nightelf", "undead", "tauren", "gnome", "troll",
"bloodelf", "draenei", "goblin", "ogre", "murloc", "naga", "satyr",
"centaur", "furbolg", "gnoll", "kobold", "trogg", "harpy", "pirate",
"bandit", "vrykul", "tuskarr", "wolvar", "arakkoa", "ethereal",
"broken", "fleshgiant", "kvaldir", "pygmy", "taunka"
};
// Undead
static const char* undead[] = {
"skeleton", "zombie", "ghoul", "ghost", "banshee", "lich", "wraith",
"abomination", "geist", "shade", "spectre", "boneguard", "bonespider",
"bonegolem", "crypt", "necro", "plague", "scourge", "val"
};
// Demons
static const char* demons[] = {
"demon", "felguard", "imp", "infernal", "doomguard", "succubus",
"voidwalker", "felhound", "eredar", "pitlord", "dreadlord",
"abyssal", "felboar", "darkhound", "terrorfiend"
};
// Elementals
static const char* elementals[] = {
"elemental", "fire", "water", "air", "earth", "arcane", "storm",
"lava", "bog", "ooze", "slime", "revenant", "totem"
};
// Dragonkin
static const char* dragonkin[] = {
"dragon", "drake", "whelp", "wyrm", "dragonspawn", "drakonid",
"nether", "proto", "celestialdragon"
};
// Giants
static const char* giants[] = {
"giant", "ettin", "gronn", "colossus", "titan", "mountain", "sea"
};
// Mechanical
static const char* mechanical[] = {
"mechanical", "robot", "golem", "harvest", "shredder", "gyro",
"bomber", "tank", "turret", "cannon", "siege"
};
// Mounts
static const char* mounts[] = {
"mount", "horse", "hawkstrider", "raptor", "mechanostrider",
"nightsaber", "ram", "kodo", "skeletal", "broom", "carpet",
"gryphon", "wyvern", "hippogryph", "netherdrake", "protodrake"
};
// Boss
static const char* bosses[] = {
"arthas", "illidan", "kelthuzad", "ragnaros", "onyxia", "nefarian",
"alexstrasza", "malygos", "sartharion", "yoggsaron", "lichking",
"brutallus", "bloodqueen", "anubarak"
};
auto matches = [&](const char* list[], size_t count) {
for (size_t i = 0; i < count; i++) {
if (name.find(list[i]) != std::string::npos) return true;
}
return false;
};
if (matches(critters, sizeof(critters)/sizeof(critters[0]))) return CreatureCategory::Critter;
if (matches(mounts, sizeof(mounts)/sizeof(mounts[0]))) return CreatureCategory::Mount;
if (matches(bosses, sizeof(bosses)/sizeof(bosses[0]))) return CreatureCategory::Boss;
if (matches(undead, sizeof(undead)/sizeof(undead[0]))) return CreatureCategory::Undead;
if (matches(demons, sizeof(demons)/sizeof(demons[0]))) return CreatureCategory::Demon;
if (matches(dragonkin, sizeof(dragonkin)/sizeof(dragonkin[0]))) return CreatureCategory::Dragonkin;
if (matches(elementals, sizeof(elementals)/sizeof(elementals[0]))) return CreatureCategory::Elemental;
if (matches(giants, sizeof(giants)/sizeof(giants[0]))) return CreatureCategory::Giant;
if (matches(mechanical, sizeof(mechanical)/sizeof(mechanical[0]))) return CreatureCategory::Mechanical;
if (matches(humanoids, sizeof(humanoids)/sizeof(humanoids[0]))) return CreatureCategory::Humanoid;
if (matches(beasts, sizeof(beasts)/sizeof(beasts[0]))) return CreatureCategory::Beast;
return CreatureCategory::Other;
}
uint32_t NpcPresets::estimateLevel(const std::string& /*dirName*/) const {
return 10;
}
uint32_t NpcPresets::estimateHealth(uint32_t level) const {
return 50 + level * 80;
}
void NpcPresets::initialize(pipeline::AssetManager* am) {
if (initialized_ || !am) return;
initialized_ = true;
byCategory_.resize(static_cast<size_t>(CreatureCategory::COUNT));
const auto& entries = am->getManifest().getEntries();
std::set<std::string> seen;
for (const auto& [path, entry] : entries) {
if (!path.starts_with("creature\\")) continue;
if (!path.ends_with(".m2")) continue;
// Extract directory name (creature type)
auto firstSlash = path.find('\\');
auto secondSlash = path.find('\\', firstSlash + 1);
if (secondSlash == std::string::npos) continue;
std::string dirName = path.substr(firstSlash + 1, secondSlash - firstSlash - 1);
if (seen.count(dirName)) continue;
seen.insert(dirName);
// Get the actual M2 file path
std::string modelFile = path;
NpcPreset preset;
preset.name = prettifyName(dirName);
preset.modelPath = modelFile;
preset.category = classifyCreature(dirName);
preset.defaultLevel = estimateLevel(dirName);
preset.defaultHealth = estimateHealth(preset.defaultLevel);
preset.defaultHostile = (preset.category != CreatureCategory::Critter &&
preset.category != CreatureCategory::Mount);
presets_.push_back(preset);
byCategory_[static_cast<size_t>(preset.category)].push_back(preset);
}
// Sort each category alphabetically
for (auto& cat : byCategory_) {
std::sort(cat.begin(), cat.end(),
[](const NpcPreset& a, const NpcPreset& b) { return a.name < b.name; });
}
std::sort(presets_.begin(), presets_.end(),
[](const NpcPreset& a, const NpcPreset& b) { return a.name < b.name; });
LOG_INFO("NPC presets: ", presets_.size(), " creatures in ", seen.size(), " types");
}
const std::vector<NpcPreset>& NpcPresets::getByCategory(CreatureCategory cat) const {
return byCategory_[static_cast<size_t>(cat)];
}
} // namespace editor
} // namespace wowee