mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
06ad676be1
commit
315adfbe93
7 changed files with 245 additions and 3 deletions
|
|
@ -95,5 +95,14 @@
|
|||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||
},
|
||||
"SpellVisual": {
|
||||
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||
},
|
||||
"SpellVisualKit": {
|
||||
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||
},
|
||||
"SpellVisualEffectName": {
|
||||
"ID": 0, "FilePath": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,5 +111,14 @@
|
|||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||
"Threshold8": 46, "Threshold9": 47
|
||||
},
|
||||
"SpellVisual": {
|
||||
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||
},
|
||||
"SpellVisualKit": {
|
||||
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||
},
|
||||
"SpellVisualEffectName": {
|
||||
"ID": 0, "FilePath": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,5 +108,14 @@
|
|||
"Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33,
|
||||
"Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37,
|
||||
"Threshold8": 38, "Threshold9": 39
|
||||
},
|
||||
"SpellVisual": {
|
||||
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||
},
|
||||
"SpellVisualKit": {
|
||||
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||
},
|
||||
"SpellVisualEffectName": {
|
||||
"ID": 0, "FilePath": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,5 +116,14 @@
|
|||
},
|
||||
"LFGDungeons": {
|
||||
"ID": 0, "Name": 1
|
||||
},
|
||||
"SpellVisual": {
|
||||
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||
},
|
||||
"SpellVisualKit": {
|
||||
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||
},
|
||||
"SpellVisualEffectName": {
|
||||
"ID": 0, "FilePath": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -3265,10 +3265,24 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
handleSpellDamageLog(packet);
|
||||
break;
|
||||
case Opcode::SMSG_PLAY_SPELL_VISUAL: {
|
||||
// Minimal parse: uint64 casterGuid, uint32 visualId
|
||||
// uint64 casterGuid + uint32 visualId
|
||||
if (packet.getSize() - packet.getReadPos() < 12) break;
|
||||
packet.readUInt64();
|
||||
packet.readUInt32();
|
||||
uint64_t casterGuid = packet.readUInt64();
|
||||
uint32_t visualId = packet.readUInt32();
|
||||
if (visualId == 0) break;
|
||||
// Resolve caster world position and spawn the effect
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (!renderer) break;
|
||||
glm::vec3 spawnPos;
|
||||
if (casterGuid == playerGuid) {
|
||||
spawnPos = renderer->getCharacterPosition();
|
||||
} else {
|
||||
auto entity = entityManager.getEntity(casterGuid);
|
||||
if (!entity) break;
|
||||
glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||
spawnPos = core::coords::canonicalToRender(canonical);
|
||||
}
|
||||
renderer->playSpellVisual(visualId, spawnPos);
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_SPELLHEALLOG:
|
||||
|
|
|
|||
|
|
@ -2627,6 +2627,180 @@ void Renderer::stopChargeEffect() {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Spell Visual Effects ────────────────────────────────────────────────────
|
||||
|
||||
void Renderer::loadSpellVisualDbc() {
|
||||
if (spellVisualDbcLoaded_) return;
|
||||
spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure
|
||||
|
||||
if (!cachedAssetManager) {
|
||||
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
||||
}
|
||||
if (!cachedAssetManager) return;
|
||||
|
||||
auto* layout = pipeline::getActiveDBCLayout();
|
||||
const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr;
|
||||
const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr;
|
||||
const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr;
|
||||
|
||||
uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2;
|
||||
uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8;
|
||||
uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11;
|
||||
uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5;
|
||||
uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2;
|
||||
|
||||
// Load SpellVisualEffectName.dbc — ID → M2 path
|
||||
auto fxDbc = cachedAssetManager->loadDBC("SpellVisualEffectName.dbc");
|
||||
if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) {
|
||||
LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=",
|
||||
fxDbc ? fxDbc->getFieldCount() : 0, ")");
|
||||
return;
|
||||
}
|
||||
std::unordered_map<uint32_t, std::string> effectPaths; // effectNameId → path
|
||||
for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) {
|
||||
uint32_t id = fxDbc->getUInt32(i, 0);
|
||||
std::string p = fxDbc->getString(i, fxFilePathField);
|
||||
if (id && !p.empty()) effectPaths[id] = p;
|
||||
}
|
||||
|
||||
// Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID
|
||||
auto kitDbc = cachedAssetManager->loadDBC("SpellVisualKit.dbc");
|
||||
std::unordered_map<uint32_t, uint32_t> kitToEffectName; // kitId → effectNameId
|
||||
if (kitDbc && kitDbc->isLoaded()) {
|
||||
uint32_t fc = kitDbc->getFieldCount();
|
||||
for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) {
|
||||
uint32_t kitId = kitDbc->getUInt32(i, 0);
|
||||
if (!kitId) continue;
|
||||
// Prefer SpecialEffect0, fall back to BaseEffect
|
||||
uint32_t eff = 0;
|
||||
if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field);
|
||||
if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField);
|
||||
if (eff) kitToEffectName[kitId] = eff;
|
||||
}
|
||||
}
|
||||
|
||||
// Load SpellVisual.dbc — visualId → M2 path via kit chain
|
||||
auto svDbc = cachedAssetManager->loadDBC("SpellVisual.dbc");
|
||||
if (!svDbc || !svDbc->isLoaded()) {
|
||||
LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable");
|
||||
return;
|
||||
}
|
||||
uint32_t svFc = svDbc->getFieldCount();
|
||||
uint32_t loaded = 0;
|
||||
for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) {
|
||||
uint32_t vid = svDbc->getUInt32(i, 0);
|
||||
if (!vid) continue;
|
||||
|
||||
std::string path;
|
||||
|
||||
// Try CastKit → SpellVisualKit → SpecialEffect0 path
|
||||
if (svCastKitField < svFc) {
|
||||
uint32_t kitId = svDbc->getUInt32(i, svCastKitField);
|
||||
if (kitId) {
|
||||
auto kitIt = kitToEffectName.find(kitId);
|
||||
if (kitIt != kitToEffectName.end()) {
|
||||
auto fxIt = effectPaths.find(kitIt->second);
|
||||
if (fxIt != effectPaths.end()) path = fxIt->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: MissileModel directly references SpellVisualEffectName
|
||||
if (path.empty() && svMissileField < svFc) {
|
||||
uint32_t missileEff = svDbc->getUInt32(i, svMissileField);
|
||||
if (missileEff) {
|
||||
auto fxIt = effectPaths.find(missileEff);
|
||||
if (fxIt != effectPaths.end()) path = fxIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
spellVisualModelPath_[vid] = path;
|
||||
++loaded;
|
||||
}
|
||||
}
|
||||
LOG_INFO("SpellVisual: loaded ", loaded, " visual→M2 mappings (of ",
|
||||
svDbc->getRecordCount(), " records)");
|
||||
}
|
||||
|
||||
void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition) {
|
||||
if (!m2Renderer || visualId == 0) return;
|
||||
|
||||
if (!cachedAssetManager)
|
||||
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
||||
if (!cachedAssetManager) return;
|
||||
|
||||
if (!spellVisualDbcLoaded_) loadSpellVisualDbc();
|
||||
|
||||
// Find the M2 path for this visual
|
||||
auto pathIt = spellVisualModelPath_.find(visualId);
|
||||
if (pathIt == spellVisualModelPath_.end()) return; // No model for this visual
|
||||
|
||||
const std::string& modelPath = pathIt->second;
|
||||
|
||||
// Get or assign a model ID for this path
|
||||
auto midIt = spellVisualModelIds_.find(modelPath);
|
||||
uint32_t modelId = 0;
|
||||
if (midIt != spellVisualModelIds_.end()) {
|
||||
modelId = midIt->second;
|
||||
} else {
|
||||
if (nextSpellVisualModelId_ >= 999800) {
|
||||
LOG_WARNING("SpellVisual: model ID pool exhausted");
|
||||
return;
|
||||
}
|
||||
modelId = nextSpellVisualModelId_++;
|
||||
spellVisualModelIds_[modelPath] = modelId;
|
||||
}
|
||||
|
||||
// Load the M2 model if not already loaded
|
||||
if (!m2Renderer->hasModel(modelId)) {
|
||||
auto m2Data = cachedAssetManager->readFile(modelPath);
|
||||
if (m2Data.empty()) {
|
||||
LOG_DEBUG("SpellVisual: could not read model: ", modelPath);
|
||||
return;
|
||||
}
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty() && model.particleEmitters.empty()) {
|
||||
LOG_DEBUG("SpellVisual: empty model: ", modelPath);
|
||||
return;
|
||||
}
|
||||
// Load skin file for WotLK-format M2s
|
||||
if (model.version >= 264) {
|
||||
std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin";
|
||||
auto skinData = cachedAssetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
if (!m2Renderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath);
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath);
|
||||
}
|
||||
|
||||
// Spawn instance at world position
|
||||
uint32_t instanceId = m2Renderer->createInstance(modelId, worldPosition,
|
||||
glm::vec3(0.0f), 1.0f);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId);
|
||||
return;
|
||||
}
|
||||
activeSpellVisuals_.push_back({instanceId, 0.0f});
|
||||
LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId,
|
||||
" model=", modelPath);
|
||||
}
|
||||
|
||||
void Renderer::updateSpellVisuals(float deltaTime) {
|
||||
if (activeSpellVisuals_.empty() || !m2Renderer) return;
|
||||
for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) {
|
||||
it->elapsed += deltaTime;
|
||||
if (it->elapsed >= SPELL_VISUAL_DURATION) {
|
||||
m2Renderer->removeInstance(it->instanceId);
|
||||
it = activeSpellVisuals_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::triggerMeleeSwing() {
|
||||
if (!characterRenderer || characterInstanceId == 0) return;
|
||||
if (meleeSwingCooldown > 0.0f) return;
|
||||
|
|
@ -3012,6 +3186,8 @@ void Renderer::update(float deltaTime) {
|
|||
if (chargeEffect) {
|
||||
chargeEffect->update(deltaTime);
|
||||
}
|
||||
// Update transient spell visual instances
|
||||
updateSpellVisuals(deltaTime);
|
||||
|
||||
|
||||
// Launch M2 doodad animation on background thread (overlaps with character animation + audio)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue