feat: implement SMSG_PLAY_SPELL_VISUAL with SpellVisual DBC chain lookup

Parse SMSG_PLAY_SPELL_VISUAL (casterGuid + visualId) and spawn a
transient M2 spell effect at the caster's world position.

DBC chain: SpellVisual.dbc → SpellVisualKit.dbc → SpellVisualEffectName.dbc
Lookup priority: CastKit.SpecialEffect0, fallback to MissileModel.
Models are lazy-loaded and cached by path; instances auto-expire after 3.5s.
DBC layouts added to all four expansion layout files (Classic/TBC/WotLK/Turtle).
This commit is contained in:
Kelsi 2026-03-17 18:23:05 -07:00
parent 06ad676be1
commit 315adfbe93
7 changed files with 245 additions and 3 deletions

View file

@ -6,6 +6,7 @@
#include <vector>
#include <future>
#include <cstddef>
#include <unordered_map>
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
@ -152,6 +153,9 @@ public:
void playEmote(const std::string& emoteName);
void triggerLevelUpEffect(const glm::vec3& position);
void cancelEmote();
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL)
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition);
bool isEmoteActive() const { return emoteActive; }
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
static uint32_t getEmoteDbcId(const std::string& emoteName);
@ -323,6 +327,18 @@ private:
glm::mat4 computeLightSpaceMatrix();
pipeline::AssetManager* cachedAssetManager = nullptr;
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL
struct SpellVisualInstance { uint32_t instanceId; float elapsed; };
std::vector<SpellVisualInstance> activeSpellVisuals_;
std::unordered_map<uint32_t, std::string> spellVisualModelPath_; // visualId → resolved M2 path
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
bool spellVisualDbcLoaded_ = false;
void loadSpellVisualDbc();
void updateSpellVisuals(float deltaTime);
static constexpr float SPELL_VISUAL_DURATION = 3.5f;
uint32_t currentZoneId = 0;
std::string currentZoneName;
bool inTavern_ = false;