From 36fed15d43d70a3743b295eb89d7cf65bcd120f3 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 18:30:11 -0700 Subject: [PATCH] feat: separate cast/impact kit paths in spell visual DBC lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadSpellVisualDbc() now builds two distinct maps: spellVisualCastPath_ — visualId → M2 via SpellVisual.CastKit chain spellVisualImpactPath_ — visualId → M2 via SpellVisual.ImpactKit chain playSpellVisual() accepts useImpactKit=false (default, cast) / true (impact). SMSG_PLAY_SPELL_IMPACT passes useImpactKit=true so impact effects (explosions, debuff indicators) use the ImpactKit model instead of the CastKit model. Added ImpactKit field to all four dbc_layouts.json files. --- include/rendering/renderer.hpp | 13 +++--- src/game/game_handler.cpp | 2 +- src/rendering/renderer.cpp | 74 +++++++++++++++++++--------------- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 7c08a738..1f33d2f4 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -154,8 +154,10 @@ public: void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); - // Spell visual effects (SMSG_PLAY_SPELL_VISUAL) - void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition); + // Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT) + // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path + void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit = false); 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); @@ -328,11 +330,12 @@ private: pipeline::AssetManager* cachedAssetManager = nullptr; - // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL + // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT struct SpellVisualInstance { uint32_t instanceId; float elapsed; }; std::vector activeSpellVisuals_; - std::unordered_map spellVisualModelPath_; // visualId → resolved M2 path - std::unordered_map spellVisualModelIds_; // M2 path → M2Renderer modelId + std::unordered_map spellVisualCastPath_; // visualId → cast M2 path + std::unordered_map spellVisualImpactPath_; // visualId → impact M2 path + std::unordered_map spellVisualModelIds_; // M2 path → M2Renderer modelId uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799 bool spellVisualDbcLoaded_ = false; void loadSpellVisualDbc(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d7432666..a4ec7081 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7431,7 +7431,7 @@ void GameHandler::handlePacket(network::Packet& packet) { glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); spawnPos = core::coords::canonicalToRender(canonical); } - renderer->playSpellVisual(impVisualId, spawnPos); + renderer->playSpellVisual(impVisualId, spawnPos, /*useImpactKit=*/true); break; } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index e04ca9ea..4da8bad7 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2644,11 +2644,13 @@ void Renderer::loadSpellVisualDbc() { const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr; uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2; + uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3; 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; + // Helper to look up effectName path from a kit ID // Load SpellVisualEffectName.dbc — ID → M2 path auto fxDbc = cachedAssetManager->loadDBC("SpellVisualEffectName.dbc"); if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) { @@ -2679,50 +2681,57 @@ void Renderer::loadSpellVisualDbc() { } } - // Load SpellVisual.dbc — visualId → M2 path via kit chain + // Helper: resolve path for a given kit ID + auto kitPath = [&](uint32_t kitId) -> std::string { + if (!kitId) return {}; + auto kitIt = kitToEffectName.find(kitId); + if (kitIt == kitToEffectName.end()) return {}; + auto fxIt = effectPaths.find(kitIt->second); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + auto missilePath = [&](uint32_t effId) -> std::string { + if (!effId) return {}; + auto fxIt = effectPaths.find(effId); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + + // Load SpellVisual.dbc — visualId → cast/impact M2 paths 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; + uint32_t loadedCast = 0, loadedImpact = 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; - } - } + // Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svCastKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svCastKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; } } - // 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; + // Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svImpactKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svImpactKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; } } } - LOG_INFO("SpellVisual: loaded ", loaded, " visual→M2 mappings (of ", - svDbc->getRecordCount(), " records)"); + LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact, + " visual→M2 mappings (of ", svDbc->getRecordCount(), " records)"); } -void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition) { +void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit) { if (!m2Renderer || visualId == 0) return; if (!cachedAssetManager) @@ -2731,9 +2740,10 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition 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 + // Select cast or impact path map + auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; + auto pathIt = pathMap.find(visualId); + if (pathIt == pathMap.end()) return; // No model for this visual const std::string& modelPath = pathIt->second;