fix: Warrior Charge sent 3x SET_FACING by falling through to generic facing

Charge already computed facing and sent SET_FACING, but then fell through
to both the melee-ability facing block and the generic targeted-spell
facing block — sending up to 3 SET_FACING + 1 HEARTBEAT per cast. Added
facingHandled flag so only one block sends facing, reducing redundant
network traffic that could trigger server-side movement validation.
This commit is contained in:
Kelsi 2026-03-29 17:52:51 -07:00
parent b9ecc26f50
commit ec24bcd910

View file

@ -242,6 +242,10 @@ void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
// Self-targeted spells like hearthstone should not send a target
if (spellId == 8690) target = 0;
// Track whether a spell-specific block already handled facing so the generic
// facing block below doesn't send redundant SET_FACING packets.
bool facingHandled = false;
// Warrior Charge (ranks 1-3): client-side range check + charge callback
if (spellId == 100 || spellId == 6178 || spellId == 11578) {
if (target == 0) {
@ -266,23 +270,20 @@ void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
owner_.addSystemChatMessage("Out of range.");
return;
}
// Face the target before sending the cast packet
float yaw = std::atan2(-dy, dx);
owner_.movementInfo.orientation = yaw;
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
if (owner_.chargeCallback_) {
owner_.chargeCallback_(target, tx, ty, tz);
}
facingHandled = true;
}
// Instant melee abilities: client-side range + facing check
{
if (!facingHandled) {
owner_.loadSpellNameCache();
bool isMeleeAbility = false;
auto cacheIt = owner_.spellNameCache_.find(spellId);
if (cacheIt != owner_.spellNameCache_.end() && cacheIt->second.schoolMask == 1) {
isMeleeAbility = true;
}
bool isMeleeAbility = (cacheIt != owner_.spellNameCache_.end() && cacheIt->second.schoolMask == 1);
if (isMeleeAbility && target != 0) {
auto entity = owner_.getEntityManager().getEntity(target);
if (entity) {
@ -297,28 +298,31 @@ void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
float yaw = std::atan2(-dy, dx);
owner_.movementInfo.orientation = yaw;
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
facingHandled = true;
}
}
}
// Face the target before casting any targeted spell (server checks facing arc).
// Send both SET_FACING and a HEARTBEAT so the server has the updated orientation
// before it processes the cast packet.
if (target != 0) {
// Only send if a spell-specific block above didn't already handle facing,
// to avoid redundant SET_FACING packets that waste bandwidth.
if (!facingHandled && target != 0) {
auto entity = owner_.getEntityManager().getEntity(target);
if (entity) {
float dx = entity->getX() - owner_.movementInfo.x;
float dy = entity->getY() - owner_.movementInfo.y;
float lenSq = dx * dx + dy * dy;
if (lenSq > 0.01f) {
// Canonical yaw convention: atan2(-dy, dx) where X=north, Y=west
float canonYaw = std::atan2(-dy, dx);
owner_.movementInfo.orientation = canonYaw;
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
owner_.sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
}
}
}
// Heartbeat ensures the server has the updated orientation before the cast packet.
if (target != 0) {
owner_.sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
}
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildCastSpell(spellId, target, ++castCount_)