From 90edb3bc077ac5dca2ce04b3a82033eb96db82fe Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Mar 2026 05:52:47 -0700 Subject: [PATCH] feat: use M2 animation duration for spell visual lifetime Spell visual effects previously used a fixed 3.5s duration for all effects, causing some to linger too long and overlap during combat. Now queries the M2 model's default animation duration via the new getInstanceAnimDuration() method and clamps it to 0.5-5s. Effects without animations fall back to a 2s default. This makes spell impacts feel more responsive and reduces visual clutter. --- include/rendering/m2_renderer.hpp | 1 + include/rendering/renderer.hpp | 9 +++++++-- src/rendering/m2_renderer.cpp | 12 ++++++++++++ src/rendering/renderer.cpp | 11 ++++++++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 1f19b46e..08d83d32 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -323,6 +323,7 @@ public: void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform); void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen); + float getInstanceAnimDuration(uint32_t instanceId) const; void removeInstance(uint32_t instanceId); void removeInstances(const std::vector& instanceIds); void setSkipCollision(uint32_t instanceId, bool skip); diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 588fa3af..1a99a6f4 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -334,7 +334,11 @@ private: pipeline::AssetManager* cachedAssetManager = nullptr; // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT - struct SpellVisualInstance { uint32_t instanceId; float elapsed; }; + struct SpellVisualInstance { + uint32_t instanceId; + float elapsed; + float duration; // per-instance lifetime in seconds (from M2 anim or default) + }; std::vector activeSpellVisuals_; std::unordered_map spellVisualCastPath_; // visualId → cast M2 path std::unordered_map spellVisualImpactPath_; // visualId → impact M2 path @@ -343,7 +347,8 @@ private: bool spellVisualDbcLoaded_ = false; void loadSpellVisualDbc(); void updateSpellVisuals(float deltaTime); - static constexpr float SPELL_VISUAL_DURATION = 3.5f; + static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f; + static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f; uint32_t currentZoneId = 0; std::string currentZoneName; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 40ffd4b1..365bf7c3 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -3956,6 +3956,18 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) { } } +float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const { + auto idxIt = instanceIndexById.find(instanceId); + if (idxIt == instanceIndexById.end()) return 0.0f; + const auto& inst = instances[idxIt->second]; + if (!inst.cachedModel) return 0.0f; + const auto& seqs = inst.cachedModel->sequences; + if (seqs.empty()) return 0.0f; + int seqIdx = inst.currentSequenceIndex; + if (seqIdx < 0 || seqIdx >= static_cast(seqs.size())) seqIdx = 0; + return seqs[seqIdx].duration; // in milliseconds +} + void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) { auto idxIt = instanceIndexById.find(instanceId); if (idxIt == instanceIndexById.end()) return; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 2d942c23..c3241337 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2892,16 +2892,21 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId); return; } - activeSpellVisuals_.push_back({instanceId, 0.0f}); + // Determine lifetime from M2 animation duration (clamp to reasonable range) + float animDurMs = m2Renderer->getInstanceAnimDuration(instanceId); + float duration = (animDurMs > 100.0f) + ? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION) + : SPELL_VISUAL_DEFAULT_DURATION; + activeSpellVisuals_.push_back({instanceId, 0.0f, duration}); LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId, - " model=", modelPath); + " duration=", duration, "s 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) { + if (it->elapsed >= it->duration) { m2Renderer->removeInstance(it->instanceId); it = activeSpellVisuals_.erase(it); } else {