mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(pipeline): add WSVK (Wowee Spell Visual Kit) catalog
47th open format — replaces SpellVisualKit.dbc + SpellVisualEffectName.dbc plus the AzerothCore-style spell visual SQL data. 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. Cross-references with prior formats — castAnimId / impactAnimId / precastAnimId point at WANI.animationId, castSoundId / impactSoundId point at WSND.soundId. Spell catalogs (WSPL) will reference visualKitId here to bind "what mechanically happens" to "what plays visually." CLI: --gen-svk (3-kit Frostbolt/Fireball/HealingTouch starter showing projectile + AoE + heal patterns), --gen-svk-combat (5 melee/ranged with WANI animation refs), --gen-svk-utility (4 portal/hearth/mount/resurrect with no projectile), --info-wsvk, --validate-wsvk with --json variants. Validator catches id=0/duplicates, missing name, negative speeds/radii, projectile-model + speed coherence (model without speed = never travels; speed without model = invisible), and a no-effect catch-all (no models + no anims + no sounds).
This commit is contained in:
parent
441b962c3f
commit
beca69352a
10 changed files with 658 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
105
include/pipeline/wowee_spell_visuals.hpp
Normal file
105
include/pipeline/wowee_spell_visuals.hpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Entry> 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
|
||||
270
src/pipeline/wowee_spell_visuals.cpp
Normal file
270
src/pipeline/wowee_spell_visuals.cpp
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
#include "pipeline/wowee_spell_visuals.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'S', 'V', 'K'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
|
||||
template <typename T>
|
||||
void writePOD(std::ofstream& os, const T& v) {
|
||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPOD(std::ifstream& is, T& v) {
|
||||
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
||||
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
||||
}
|
||||
|
||||
void writeStr(std::ofstream& os, const std::string& s) {
|
||||
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1365,6 +1365,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wani to a human-editable JSON sidecar (defaults to <base>.wani.json)\n");
|
||||
std::printf(" --import-wani-json <json-path> [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 <wsvk-base> [name]\n");
|
||||
std::printf(" Emit .wsvk starter: 3 visual kits (Frostbolt / Fireball / HealingTouch) with projectile + impact + hand effects\n");
|
||||
std::printf(" --gen-svk-combat <wsvk-base> [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 <wsvk-base> [name]\n");
|
||||
std::printf(" Emit .wsvk 4 utility visuals (portal/hearthstone/mount-summon/resurrect) with no projectile\n");
|
||||
std::printf(" --info-wsvk <wsvk-base> [--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 <wsvk-base> [--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 <wow-base> [zoneName]\n");
|
||||
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
|
||||
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
253
tools/editor/cli_spell_visuals_catalog.cpp
Normal file
253
tools/editor/cli_spell_visuals_catalog.cpp
Normal file
|
|
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::vector<uint32_t> 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
|
||||
11
tools/editor/cli_spell_visuals_catalog.hpp
Normal file
11
tools/editor/cli_spell_visuals_catalog.hpp
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue