Add Warrior Charge ability with ribbon trail visual effect

Implements charge rush-to-target for spell IDs 100/6178/11578 with
smoothstep lerp movement, vertical red-orange ribbon trail, dust puffs,
client-side range validation, and sound fallback chain.
This commit is contained in:
Kelsi 2026-02-19 21:13:13 -08:00
parent da49593268
commit e163813dee
9 changed files with 761 additions and 2 deletions

View file

@ -16,6 +16,7 @@
#include "rendering/sky_system.hpp"
#include "rendering/swim_effects.hpp"
#include "rendering/mount_dust.hpp"
#include "rendering/charge_effect.hpp"
#include "rendering/levelup_effect.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
@ -359,6 +360,13 @@ bool Renderer::initialize(core::Window* win) {
// Create level-up effect (model loaded later via loadLevelUpEffect)
levelUpEffect = std::make_unique<LevelUpEffect>();
// Create charge effect (point-sprite particles + optional M2 models)
chargeEffect = std::make_unique<ChargeEffect>();
if (!chargeEffect->initialize()) {
LOG_WARNING("Failed to initialize charge effect");
chargeEffect.reset();
}
// Create character renderer
characterRenderer = std::make_unique<CharacterRenderer>();
if (!characterRenderer->initialize()) {
@ -1509,12 +1517,20 @@ void Renderer::updateCharacterAnimation() {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::CHARGE:
// Stay in CHARGE until charging_ is cleared
break;
}
if (forceMelee) {
newState = CharAnimState::MELEE_SWING;
}
if (charging_) {
newState = CharAnimState::CHARGE;
}
if (newState != charAnimState) {
charAnimState = newState;
}
@ -1573,6 +1589,10 @@ void Renderer::updateCharacterAnimation() {
loop = false;
break;
case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break;
case CharAnimState::CHARGE:
animId = ANIM_RUN;
loop = true;
break;
}
uint32_t currentAnimId = 0;
@ -1632,6 +1652,34 @@ void Renderer::triggerLevelUpEffect(const glm::vec3& position) {
levelUpEffect->trigger(position);
}
void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
if (!chargeEffect) return;
// Lazy-load M2 models on first use
if (!chargeEffect->isActive() && m2Renderer) {
if (!cachedAssetManager) {
cachedAssetManager = core::Application::getInstance().getAssetManager();
}
if (cachedAssetManager) {
chargeEffect->tryLoadM2Models(m2Renderer.get(), cachedAssetManager);
}
}
chargeEffect->start(position, direction);
}
void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) {
if (chargeEffect) {
chargeEffect->emit(position, direction);
}
}
void Renderer::stopChargeEffect() {
if (chargeEffect) {
chargeEffect->stop();
}
}
void Renderer::triggerMeleeSwing() {
if (!characterRenderer || characterInstanceId == 0) return;
if (meleeSwingCooldown > 0.0f) return;
@ -2008,6 +2056,10 @@ void Renderer::update(float deltaTime) {
if (levelUpEffect) {
levelUpEffect->update(deltaTime);
}
// Update charge effect
if (chargeEffect) {
chargeEffect->update(deltaTime);
}
auto sky2 = std::chrono::high_resolution_clock::now();
skyTime += std::chrono::duration<float, std::milli>(sky2 - sky1).count();
@ -2685,6 +2737,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
mountDust->render(*camera);
}
// Render charge effect (red haze + dust)
if (chargeEffect && camera) {
chargeEffect->render(*camera);
}
// Compute view/projection once for all sub-renderers
const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f);
const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f);