Kelsidavis-WoWee/include/rendering/spell_visual_system.hpp
Pavel Okhlopkov b79d9b8fea feat(rendering): implement spell visual effects with bone-tracked ribbons and particles
Add complete spell visual pipeline resolving the DBC chain
(Spell → SpellVisual → SpellVisualKit → SpellVisualEffectName → M2)
with precast/cast/impact phases, bone-attached positioning, and
automatic dual-hand mirroring.

Ribbon rendering fixes:
- Parse visibility track as uint8 (was read as float, suppressing
  all ribbon edges due to ~1.4e-45 failing the >0.5 check)
- Filter garbage emitters with bone=UINT_MAX unconditionally
- Guard against NaN spine positions from corrupt bone data
- Resolve ribbon textures via direct index, not textureLookup table
- Fall back to bone 0 when ribbon bone index is out of range

Particle rendering fixes:
- Reduce spell particle scale from 5x to 1.5x (was oversized)
- Exempt spell effect instances from position-based deduplication

Spell handler integration:
- Trigger precast visuals on SMSG_SPELL_START with server castTimeMs
- Trigger cast/impact visuals on SMSG_SPELL_GO
- Cancel precast visuals on cast interrupt/failure/movement

M2 classifier expansion:
- Add AmbientEmitterType enum for sound system integration
- Add 20+ foliage tokens, 4 spell effect tokens, isSmallFoliage flag
- Add markModelAsSpellEffect() to override disableAnimation

DBC layouts:
- Add SpellVisualID field to Spell.dbc for all expansion configs

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-07 11:27:59 +03:00

82 lines
3.2 KiB
C++

#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <glm/glm.hpp>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering {
class M2Renderer;
class Renderer;
class CharacterRenderer;
class SpellVisualSystem {
public:
SpellVisualSystem() = default;
~SpellVisualSystem() = default;
// Initialize with references to the M2 renderer and parent renderer
void initialize(M2Renderer* m2Renderer, Renderer* renderer);
void shutdown();
// Spawn a spell visual at a world position.
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
bool useImpactKit = false);
// Spawn a precast visual effect at a world position.
// castTimeMs: server cast time in milliseconds (0 = use anim duration).
void playSpellVisualPrecast(uint32_t visualId, const glm::vec3& worldPosition,
uint32_t castTimeMs = 0);
// Advance lifetime timers and remove expired instances.
void update(float deltaTime);
// Remove all active precast visual instances (cast canceled/interrupted).
void cancelAllPrecastVisuals();
// Remove all active spell visual instances and reset caches.
// Called on map change / combat reset.
void reset();
private:
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT
struct SpellVisualInstance {
uint32_t instanceId;
float elapsed;
float duration; // per-instance lifetime in seconds (from M2 anim or default)
bool isPrecast; // true for precast effects (removed on cancel/interrupt)
uint32_t attachmentId; // character attachment point to track (0=none/static)
};
void loadSpellVisualDbc();
M2Renderer* m2Renderer_ = nullptr;
Renderer* renderer_ = nullptr;
pipeline::AssetManager* cachedAssetManager_ = nullptr;
std::vector<SpellVisualInstance> activeSpellVisuals_;
std::unordered_map<uint32_t, std::string> spellVisualPrecastPath_; // visualId → precast M2 path
std::unordered_map<uint32_t, std::string> spellVisualCastPath_; // visualId → cast M2 path
std::unordered_map<uint32_t, std::string> spellVisualImpactPath_; // visualId → impact M2 path
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
std::unordered_set<uint32_t> spellVisualFailedModels_; // modelIds that failed to load (negative cache)
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
bool spellVisualDbcLoaded_ = false;
static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f;
static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f;
// Determine character attachment point from model path keywords
static uint32_t classifyAttachmentId(const std::string& modelPath);
// Apply height offset based on model path keywords (Hand → hands, Chest → chest, Base → ground)
static glm::vec3 applyEffectHeightOffset(const glm::vec3& basePos, const std::string& modelPath);
};
} // namespace rendering
} // namespace wowee