diff --git a/CMakeLists.txt b/CMakeLists.txt index e860a4a0..67cbdd0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -634,6 +634,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_holidays.cpp src/pipeline/wowee_liquids.cpp src/pipeline/wowee_animations.cpp + src/pipeline/wowee_spell_visuals.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1416,6 +1417,7 @@ add_executable(wowee_editor tools/editor/cli_list_formats.cpp tools/editor/cli_info_magic.cpp tools/editor/cli_animations_catalog.cpp + tools/editor/cli_spell_visuals_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1528,6 +1530,7 @@ add_executable(wowee_editor src/pipeline/wowee_holidays.cpp src/pipeline/wowee_liquids.cpp src/pipeline/wowee_animations.cpp + src/pipeline/wowee_spell_visuals.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_spell_visuals.hpp b/include/pipeline/wowee_spell_visuals.hpp new file mode 100644 index 00000000..7aa695bf --- /dev/null +++ b/include/pipeline/wowee_spell_visuals.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Spell Visual Kit catalog (.wsvk) — novel +// replacement for Blizzard's SpellVisualKit.dbc + +// SpellVisualEffectName.dbc + the AzerothCore-style spell +// visual SQL data. The 47th open format added to the editor. +// +// Defines per-spell visual presentations: cast-bar effect +// model, projectile model + travel speed + arc gravity, +// impact effect model, hand effect on the caster, and the +// animations + sounds that fire at cast / channel / impact +// time. Spells reference a visualKitId here from Spell.dbc +// (or WSPL.spellId.visualKitId), so this catalog binds the +// "what happens visually" to the "what mechanically happens" +// in the spell catalog. +// +// Cross-references with previously-added formats: +// WSVK.entry.castAnimId → WANI.entry.animationId +// WSVK.entry.impactAnimId → WANI.entry.animationId +// WSVK.entry.precastAnimId → WANI.entry.animationId +// WSVK.entry.castSoundId → WSND.entry.soundId +// WSVK.entry.impactSoundId → WSND.entry.soundId +// +// Binary layout (little-endian): +// magic[4] = "WSVK" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// visualKitId (uint32) +// nameLen + name +// descLen + description +// castModelLen + castEffectModelPath +// projModelLen + projectileModelPath +// impactModelLen + impactEffectModelPath +// handModelLen + handEffectModelPath +// precastAnimId (uint32) +// castAnimId (uint32) +// impactAnimId (uint32) +// castSoundId (uint32) +// impactSoundId (uint32) +// projectileSpeed (float) — units/sec, 0=instant +// projectileGravity (float) — 0=straight line +// castDurationMs (uint32) +// impactRadius (float) — splash AoE in units +struct WoweeSpellVisualKit { + struct Entry { + uint32_t visualKitId = 0; + std::string name; + std::string description; + std::string castEffectModelPath; // M2 played during cast + std::string projectileModelPath; // M2 that flies to target + std::string impactEffectModelPath; // M2 played on impact + std::string handEffectModelPath; // M2 attached to caster hand + uint32_t precastAnimId = 0; // WANI cross-ref + uint32_t castAnimId = 0; // WANI cross-ref + uint32_t impactAnimId = 0; // WANI cross-ref + uint32_t castSoundId = 0; // WSND cross-ref + uint32_t impactSoundId = 0; // WSND cross-ref + float projectileSpeed = 0.0f; // 0 = instant hit + float projectileGravity = 0.0f; // 0 = straight line + uint32_t castDurationMs = 0; + float impactRadius = 0.0f; // 0 = single-target + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t visualKitId) const; +}; + +class WoweeSpellVisualKitLoader { +public: + static bool save(const WoweeSpellVisualKit& cat, + const std::string& basePath); + static WoweeSpellVisualKit load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-svk* variants. + // + // makeStarter — 3 visual kits (Frostbolt / Fireball / + // Healing) covering the canonical + // projectile + heal triad. + // makeCombat — 5 combat visuals (sword swing impact, + // arrow shot, ground pound, parry, + // deflect) with WANI animation refs. + // makeUtility — 4 utility visuals (portal/teleport, + // hearthstone return, mount summon, + // resurrection) with no projectile. + static WoweeSpellVisualKit makeStarter(const std::string& catalogName); + static WoweeSpellVisualKit makeCombat(const std::string& catalogName); + static WoweeSpellVisualKit makeUtility(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_spell_visuals.cpp b/src/pipeline/wowee_spell_visuals.cpp new file mode 100644 index 00000000..4bffb99d --- /dev/null +++ b/src/pipeline/wowee_spell_visuals.cpp @@ -0,0 +1,270 @@ +#include "pipeline/wowee_spell_visuals.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'V', 'K'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wsvk") { + base += ".wsvk"; + } + return base; +} + +} // namespace + +const WoweeSpellVisualKit::Entry* +WoweeSpellVisualKit::findById(uint32_t visualKitId) const { + for (const auto& e : entries) + if (e.visualKitId == visualKitId) return &e; + return nullptr; +} + +bool WoweeSpellVisualKitLoader::save(const WoweeSpellVisualKit& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.visualKitId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.castEffectModelPath); + writeStr(os, e.projectileModelPath); + writeStr(os, e.impactEffectModelPath); + writeStr(os, e.handEffectModelPath); + writePOD(os, e.precastAnimId); + writePOD(os, e.castAnimId); + writePOD(os, e.impactAnimId); + writePOD(os, e.castSoundId); + writePOD(os, e.impactSoundId); + writePOD(os, e.projectileSpeed); + writePOD(os, e.projectileGravity); + writePOD(os, e.castDurationMs); + writePOD(os, e.impactRadius); + } + return os.good(); +} + +WoweeSpellVisualKit WoweeSpellVisualKitLoader::load( + const std::string& basePath) { + WoweeSpellVisualKit out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.visualKitId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.castEffectModelPath) || + !readStr(is, e.projectileModelPath) || + !readStr(is, e.impactEffectModelPath) || + !readStr(is, e.handEffectModelPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.precastAnimId) || + !readPOD(is, e.castAnimId) || + !readPOD(is, e.impactAnimId) || + !readPOD(is, e.castSoundId) || + !readPOD(is, e.impactSoundId) || + !readPOD(is, e.projectileSpeed) || + !readPOD(is, e.projectileGravity) || + !readPOD(is, e.castDurationMs) || + !readPOD(is, e.impactRadius)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSpellVisualKitLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeStarter( + const std::string& catalogName) { + WoweeSpellVisualKit c; + c.name = catalogName; + { + WoweeSpellVisualKit::Entry e; + e.visualKitId = 1; e.name = "Frostbolt"; + e.description = "Mage frostbolt — slow icy projectile."; + e.castEffectModelPath = "Spells/Cast/Frost/cast_frost.m2"; + e.projectileModelPath = "Spells/Missiles/frostbolt.m2"; + e.impactEffectModelPath = "Spells/Impact/Frost/impact_frost.m2"; + e.handEffectModelPath = "Spells/Hand/frost_hand_glow.m2"; + e.castAnimId = 54; // ChannelCast from WANI combat + e.castSoundId = 100; // WSND cross-ref + e.impactSoundId = 101; + e.projectileSpeed = 25.0f; + e.castDurationMs = 2500; + e.impactRadius = 0.0f; // single-target + c.entries.push_back(e); + } + { + WoweeSpellVisualKit::Entry e; + e.visualKitId = 2; e.name = "Fireball"; + e.description = "Mage fireball — fast fiery projectile + AoE."; + e.castEffectModelPath = "Spells/Cast/Fire/cast_fire.m2"; + e.projectileModelPath = "Spells/Missiles/fireball.m2"; + e.impactEffectModelPath = "Spells/Impact/Fire/impact_fire.m2"; + e.handEffectModelPath = "Spells/Hand/fire_hand_glow.m2"; + e.castAnimId = 54; + e.castSoundId = 102; + e.impactSoundId = 103; + e.projectileSpeed = 35.0f; + e.castDurationMs = 3500; + e.impactRadius = 5.0f; // splash + c.entries.push_back(e); + } + { + WoweeSpellVisualKit::Entry e; + e.visualKitId = 3; e.name = "HealingTouch"; + e.description = "Druid healing — golden glow on caster + target."; + e.castEffectModelPath = "Spells/Cast/Holy/cast_holy.m2"; + e.impactEffectModelPath = "Spells/Impact/Holy/heal_glow.m2"; + e.handEffectModelPath = "Spells/Hand/holy_hand_glow.m2"; + e.castAnimId = 54; + e.castSoundId = 104; + e.impactSoundId = 105; + e.projectileSpeed = 0.0f; // instant — no projectile + e.castDurationMs = 3000; + c.entries.push_back(e); + } + return c; +} + +WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeCombat( + const std::string& catalogName) { + WoweeSpellVisualKit c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint32_t castAnim, uint32_t impactAnim, + float projSpeed, float gravity, + uint32_t durMs, float radius, + const char* castModel, const char* projModel, + const char* impactModel, const char* desc) { + WoweeSpellVisualKit::Entry e; + e.visualKitId = id; e.name = name; e.description = desc; + e.castEffectModelPath = castModel; + e.projectileModelPath = projModel; + e.impactEffectModelPath = impactModel; + e.castAnimId = castAnim; + e.impactAnimId = impactAnim; + e.projectileSpeed = projSpeed; + e.projectileGravity = gravity; + e.castDurationMs = durMs; + e.impactRadius = radius; + c.entries.push_back(e); + }; + add(100, "SwordImpact", 17, 0, 0.0f, 0.0f, 0, 0.0f, + "", "", "Spells/Impact/Physical/sword_hit.m2", + "Sword strike sparks on impact — no cast effect."); + add(101, "ArrowShot", 40, 0, 60.0f, 0.05f, 800, 0.0f, + "Spells/Cast/Physical/draw_bow.m2", + "Spells/Missiles/arrow.m2", + "Spells/Impact/Physical/arrow_hit.m2", + "Arrow with slight gravity drop."); + add(102, "GroundPound", 18, 0, 0.0f, 0.0f, 1200, 8.0f, + "Spells/Cast/Physical/heave.m2", "", + "Spells/Impact/Physical/ground_shockwave.m2", + "AoE ground pound — large impact radius."); + add(103, "ParryFlash", 53, 0, 0.0f, 0.0f, 500, 0.0f, + "Spells/Cast/Physical/parry_flash.m2", "", "", + "Quick parry sparks — instant cast."); + add(104, "DeflectShield", 0, 0, 0.0f, 0.0f, 500, 0.0f, + "Spells/Cast/Physical/shield_deflect.m2", "", "", + "Shield reflection visual — no projectile."); + return c; +} + +WoweeSpellVisualKit WoweeSpellVisualKitLoader::makeUtility( + const std::string& catalogName) { + WoweeSpellVisualKit c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t durMs, + const char* castModel, const char* impactModel, + const char* desc) { + WoweeSpellVisualKit::Entry e; + e.visualKitId = id; e.name = name; e.description = desc; + e.castEffectModelPath = castModel; + e.impactEffectModelPath = impactModel; + e.castAnimId = 54; // ChannelCast + e.castDurationMs = durMs; + c.entries.push_back(e); + }; + add(200, "PortalCast", 10000, + "Spells/Cast/Arcane/portal_cast.m2", + "Spells/Impact/Arcane/portal_open.m2", + "Mage portal — long channel + persistent doorway."); + add(201, "HearthstoneReturn", 10000, + "Spells/Cast/Arcane/hearth_glow.m2", + "Spells/Impact/Arcane/hearth_arrive.m2", + "Hearthstone teleport — channel + arrival flash."); + add(202, "MountSummon", 1500, + "Spells/Cast/Nature/mount_summon.m2", "", + "Quick mount spawn animation."); + add(203, "Resurrect", 5000, + "Spells/Cast/Holy/resurrect_cast.m2", + "Spells/Impact/Holy/resurrect_target.m2", + "Resurrect — golden beam + corpse glow."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 2767faa4..43311628 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -138,6 +138,8 @@ const char* const kArgRequired[] = { "--gen-animations", "--gen-animations-combat", "--gen-animations-movement", "--info-wani", "--validate-wani", "--export-wani-json", "--import-wani-json", + "--gen-svk", "--gen-svk-combat", "--gen-svk-utility", + "--info-wsvk", "--validate-wsvk", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index c5b3f976..758af492 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -76,6 +76,7 @@ #include "cli_list_formats.hpp" #include "cli_info_magic.hpp" #include "cli_animations_catalog.hpp" +#include "cli_spell_visuals_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -193,6 +194,7 @@ constexpr DispatchFn kDispatchTable[] = { handleListFormats, handleInfoMagic, handleAnimationsCatalog, + handleSpellVisualsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index af860393..1148ad97 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1365,6 +1365,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wani to a human-editable JSON sidecar (defaults to .wani.json)\n"); std::printf(" --import-wani-json [out-base]\n"); std::printf(" Import a .wani.json sidecar back into binary .wani (accepts behaviorTier int OR name string)\n"); + std::printf(" --gen-svk [name]\n"); + std::printf(" Emit .wsvk starter: 3 visual kits (Frostbolt / Fireball / HealingTouch) with projectile + impact + hand effects\n"); + std::printf(" --gen-svk-combat [name]\n"); + std::printf(" Emit .wsvk 5 combat visuals (sword/arrow/groundpound/parry/deflect) with WANI animation cross-refs\n"); + std::printf(" --gen-svk-utility [name]\n"); + std::printf(" Emit .wsvk 4 utility visuals (portal/hearthstone/mount-summon/resurrect) with no projectile\n"); + std::printf(" --info-wsvk [--json]\n"); + std::printf(" Print WSVK entries (id / cast+impact anim IDs / projectile speed+gravity / cast duration / AoE radius / sounds / name)\n"); + std::printf(" --validate-wsvk [--json]\n"); + std::printf(" Static checks: id>0+unique, name not empty, no negative speeds/radii, projectile-model + speed coherence, no-effect warning\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_info_magic.cpp b/tools/editor/cli_info_magic.cpp index 126e7f5f..5446e6a4 100644 --- a/tools/editor/cli_info_magic.cpp +++ b/tools/editor/cli_info_magic.cpp @@ -67,6 +67,7 @@ constexpr MagicEntry kMagicTable[] = { {{'W','H','O','L'}, ".whol", "holiday", "--info-whol", "Holiday catalog"}, {{'W','L','I','Q'}, ".wliq", "liquids", "--info-wliq", "Liquid catalog"}, {{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"}, + {{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index b8542b5c..7a3cae46 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -70,6 +70,7 @@ constexpr FormatRow kFormats[] = { {"WHOL", ".whol", "holiday", "Holidays.dbc + game_event", "Calendar holiday + event catalog"}, {"WLIQ", ".wliq", "liquids", "LiquidType.dbc", "Liquid material catalog (water/lava/slime)"}, {"WANI", ".wani", "anim", "AnimationData.dbc", "Animation ID + fallback + weapon-flag catalog"}, + {"WSVK", ".wsvk", "spellfx", "SpellVisualKit.dbc + SpellVisFx", "Spell visual kit (cast/proj/impact effects)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_spell_visuals_catalog.cpp b/tools/editor/cli_spell_visuals_catalog.cpp new file mode 100644 index 00000000..69b902fb --- /dev/null +++ b/tools/editor/cli_spell_visuals_catalog.cpp @@ -0,0 +1,253 @@ +#include "cli_spell_visuals_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_spell_visuals.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWsvkExt(std::string base) { + stripExt(base, ".wsvk"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSpellVisualKit& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSpellVisualKitLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wsvk\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSpellVisualKit& c, + const std::string& base) { + std::printf("Wrote %s.wsvk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" visuals : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterVisualKits"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsvkExt(base); + auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-svk")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCombat(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CombatVisualKits"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsvkExt(base); + auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeCombat(name); + if (!saveOrError(c, base, "gen-svk-combat")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenUtility(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "UtilityVisualKits"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWsvkExt(base); + auto c = wowee::pipeline::WoweeSpellVisualKitLoader::makeUtility(name); + if (!saveOrError(c, base, "gen-svk-utility")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWsvkExt(base); + if (!wowee::pipeline::WoweeSpellVisualKitLoader::exists(base)) { + std::fprintf(stderr, "WSVK not found: %s.wsvk\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellVisualKitLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wsvk"] = base + ".wsvk"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"visualKitId", e.visualKitId}, + {"name", e.name}, + {"description", e.description}, + {"castEffectModelPath", e.castEffectModelPath}, + {"projectileModelPath", e.projectileModelPath}, + {"impactEffectModelPath", e.impactEffectModelPath}, + {"handEffectModelPath", e.handEffectModelPath}, + {"precastAnimId", e.precastAnimId}, + {"castAnimId", e.castAnimId}, + {"impactAnimId", e.impactAnimId}, + {"castSoundId", e.castSoundId}, + {"impactSoundId", e.impactSoundId}, + {"projectileSpeed", e.projectileSpeed}, + {"projectileGravity", e.projectileGravity}, + {"castDurationMs", e.castDurationMs}, + {"impactRadius", e.impactRadius}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSVK: %s.wsvk\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" visuals : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id castAnim impAnim speed grav dur(ms) AoE castSnd impSnd name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %5u %5u %5.1f %4.2f %5u %4.1f %5u %5u %s\n", + e.visualKitId, e.castAnimId, e.impactAnimId, + e.projectileSpeed, e.projectileGravity, + e.castDurationMs, e.impactRadius, + e.castSoundId, e.impactSoundId, e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWsvkExt(base); + if (!wowee::pipeline::WoweeSpellVisualKitLoader::exists(base)) { + std::fprintf(stderr, + "validate-wsvk: WSVK not found: %s.wsvk\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSpellVisualKitLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.visualKitId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.visualKitId == 0) + errors.push_back(ctx + ": visualKitId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.projectileSpeed < 0.0f) + errors.push_back(ctx + ": projectileSpeed " + + std::to_string(e.projectileSpeed) + + " negative (use 0 for instant)"); + if (e.projectileGravity < 0.0f) + errors.push_back(ctx + ": projectileGravity " + + std::to_string(e.projectileGravity) + + " negative (use 0 for straight line)"); + if (e.impactRadius < 0.0f) + errors.push_back(ctx + ": impactRadius " + + std::to_string(e.impactRadius) + + " negative (use 0 for single-target)"); + // Projectile model + zero speed = projectile defined + // but never travels. Inverse: speed > 0 + no model = + // invisible projectile. Both are usually mistakes. + if (!e.projectileModelPath.empty() && + e.projectileSpeed == 0.0f) { + warnings.push_back(ctx + + ": projectileModelPath set but projectileSpeed=0 " + "(model never travels)"); + } + if (e.projectileModelPath.empty() && + e.projectileSpeed > 0.0f) { + warnings.push_back(ctx + + ": projectileSpeed > 0 but no projectileModelPath " + "(invisible projectile)"); + } + // No effect model AND no animation AND no sound = the + // visual kit has no observable effect at all. + if (e.castEffectModelPath.empty() && + e.impactEffectModelPath.empty() && + e.handEffectModelPath.empty() && + e.castAnimId == 0 && e.impactAnimId == 0 && + e.castSoundId == 0 && e.impactSoundId == 0) { + warnings.push_back(ctx + + ": no models, animations, or sounds — visual kit has no observable effect"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.visualKitId) { + errors.push_back(ctx + ": duplicate visualKitId"); + break; + } + } + idsSeen.push_back(e.visualKitId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wsvk"] = base + ".wsvk"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wsvk: %s.wsvk\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu visual kits, all visualKitIds unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleSpellVisualsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-svk") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-svk-combat") == 0 && i + 1 < argc) { + outRc = handleGenCombat(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-svk-utility") == 0 && i + 1 < argc) { + outRc = handleGenUtility(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wsvk") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wsvk") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_spell_visuals_catalog.hpp b/tools/editor/cli_spell_visuals_catalog.hpp new file mode 100644 index 00000000..29deaa1e --- /dev/null +++ b/tools/editor/cli_spell_visuals_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSpellVisualsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee