Kelsidavis-WoWee/src/game/combat_handler.cpp

1519 lines
65 KiB
C++
Raw Normal View History

#include "game/combat_handler.hpp"
#include "game/game_handler.hpp"
#include "game/game_utils.hpp"
#include "game/packet_parsers.hpp"
#include "game/entity.hpp"
#include "game/update_field_table.hpp"
#include "game/opcode_table.hpp"
#include "rendering/renderer.hpp"
#include "audio/combat_sound_manager.hpp"
#include "audio/activity_sound_manager.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include "network/world_socket.hpp"
#include <algorithm>
#include <cmath>
#include <ctime>
#include <random>
namespace wowee {
namespace game {
CombatHandler::CombatHandler(GameHandler& owner)
: owner_(owner) {}
void CombatHandler::registerOpcodes(DispatchTable& table) {
// ---- Combat clearing ----
table[Opcode::SMSG_ATTACKSWING_DEADTARGET] = [this](network::Packet& /*packet*/) {
autoAttacking_ = false;
autoAttackTarget_ = 0;
};
table[Opcode::SMSG_THREAT_CLEAR] = [this](network::Packet& /*packet*/) {
threatLists_.clear();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
};
table[Opcode::SMSG_THREAT_REMOVE] = [this](network::Packet& packet) {
if (!packet.hasRemaining(1)) return;
uint64_t unitGuid = packet.readPackedGuid();
if (!packet.hasRemaining(1)) return;
uint64_t victimGuid = packet.readPackedGuid();
auto it = threatLists_.find(unitGuid);
if (it != threatLists_.end()) {
auto& list = it->second;
list.erase(std::remove_if(list.begin(), list.end(),
[victimGuid](const ThreatEntry& e){ return e.victimGuid == victimGuid; }),
list.end());
if (list.empty()) threatLists_.erase(it);
}
};
table[Opcode::SMSG_CANCEL_COMBAT] = [this](network::Packet& /*packet*/) {
autoAttacking_ = false;
autoAttackTarget_ = 0;
autoAttackRequested_ = false;
};
// ---- Attack/combat delegates ----
table[Opcode::SMSG_ATTACKSTART] = [this](network::Packet& packet) { handleAttackStart(packet); };
table[Opcode::SMSG_ATTACKSTOP] = [this](network::Packet& packet) { handleAttackStop(packet); };
table[Opcode::SMSG_ATTACKSWING_NOTINRANGE] = [this](network::Packet& /*packet*/) {
autoAttackOutOfRange_ = true;
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("Target is too far away.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
};
table[Opcode::SMSG_ATTACKSWING_BADFACING] = [this](network::Packet& /*packet*/) {
if (autoAttackRequested_ && autoAttackTarget_ != 0) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto targetEntity = owner_.getEntityManager().getEntity(autoAttackTarget_);
if (targetEntity) {
float toTargetX = targetEntity->getX() - owner_.movementInfo.x;
float toTargetY = targetEntity->getY() - owner_.movementInfo.y;
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
owner_.movementInfo.orientation = std::atan2(-toTargetY, toTargetX);
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
}
}
}
};
table[Opcode::SMSG_ATTACKSWING_NOTSTANDING] = [this](network::Packet& /*packet*/) {
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("You need to stand up to fight.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
};
table[Opcode::SMSG_ATTACKSWING_CANT_ATTACK] = [this](network::Packet& /*packet*/) {
stopAutoAttack();
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("You can't attack that.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
};
table[Opcode::SMSG_ATTACKERSTATEUPDATE] = [this](network::Packet& packet) { handleAttackerStateUpdate(packet); };
table[Opcode::SMSG_AI_REACTION] = [this](network::Packet& packet) {
if (!packet.hasRemaining(12)) return;
uint64_t guid = packet.readUInt64();
uint32_t reaction = packet.readUInt32();
if (reaction == 2 && owner_.npcAggroCallback_) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().getEntity(guid);
if (entity)
owner_.npcAggroCallback_(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
}
};
table[Opcode::SMSG_SPELLNONMELEEDAMAGELOG] = [this](network::Packet& packet) { handleSpellDamageLog(packet); };
table[Opcode::SMSG_SPELLHEALLOG] = [this](network::Packet& packet) { handleSpellHealLog(packet); };
// ---- Environmental damage ----
table[Opcode::SMSG_ENVIRONMENTAL_DAMAGE_LOG] = [this](network::Packet& packet) {
// uint64 victimGuid + uint8 envDmgType + uint32 damage + uint32 absorbed + uint32 resisted
// envDmgType: 0=Exhausted(fatigue), 1=Drowning, 2=Fall, 3=Lava, 4=Slime, 5=Fire
if (!packet.hasRemaining(21)) { packet.setReadPos(packet.getSize()); return; }
uint64_t victimGuid = packet.readUInt64();
uint8_t envType = packet.readUInt8();
uint32_t dmg = packet.readUInt32();
uint32_t envAbs = packet.readUInt32();
uint32_t envRes = packet.readUInt32();
if (victimGuid == owner_.playerGuid) {
// Environmental damage: pass envType via powerType field for display differentiation
if (dmg > 0)
addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast<int32_t>(dmg), 0, false, envType, 0, victimGuid);
if (envAbs > 0)
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(envAbs), 0, false, 0, 0, victimGuid);
if (envRes > 0)
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(envRes), 0, false, 0, 0, victimGuid);
}
packet.setReadPos(packet.getSize());
};
// ---- Threat updates ----
for (auto op : {Opcode::SMSG_HIGHEST_THREAT_UPDATE,
Opcode::SMSG_THREAT_UPDATE}) {
table[op] = [this](network::Packet& packet) {
// Both packets share the same format:
// packed_guid (unit) + packed_guid (highest-threat target or target, unused here)
// + uint32 count + count × (packed_guid victim + uint32 threat)
if (!packet.hasRemaining(1)) return;
uint64_t unitGuid = packet.readPackedGuid();
if (!packet.hasRemaining(1)) return;
(void)packet.readPackedGuid(); // highest-threat / current target
if (!packet.hasRemaining(4)) return;
uint32_t cnt = packet.readUInt32();
if (cnt > 100) { packet.setReadPos(packet.getSize()); return; } // sanity
std::vector<ThreatEntry> list;
list.reserve(cnt);
for (uint32_t i = 0; i < cnt; ++i) {
if (!packet.hasRemaining(1)) return;
ThreatEntry entry;
entry.victimGuid = packet.readPackedGuid();
if (!packet.hasRemaining(4)) return;
entry.threat = packet.readUInt32();
list.push_back(entry);
}
// Sort descending by threat so highest is first
std::sort(list.begin(), list.end(),
[](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; });
threatLists_[unitGuid] = std::move(list);
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {});
};
}
// ---- Forced faction reactions ----
table[Opcode::SMSG_SET_FORCED_REACTIONS] = [this](network::Packet& packet) { handleSetForcedReactions(packet); };
// ---- Entity delta updates: health / power / combo / PvP / proc ----
table[Opcode::SMSG_HEALTH_UPDATE] = [this](network::Packet& p) { handleHealthUpdate(p); };
table[Opcode::SMSG_POWER_UPDATE] = [this](network::Packet& p) { handlePowerUpdate(p); };
table[Opcode::SMSG_UPDATE_COMBO_POINTS] = [this](network::Packet& p) { handleUpdateComboPoints(p); };
table[Opcode::SMSG_PVP_CREDIT] = [this](network::Packet& p) { handlePvpCredit(p); };
table[Opcode::SMSG_PROCRESIST] = [this](network::Packet& p) { handleProcResist(p); };
// SMSG_ENVIRONMENTALDAMAGELOG is an alias for SMSG_ENVIRONMENTAL_DAMAGE_LOG
// (registered above at line 108 with envType forwarding). No separate handler needed.
table[Opcode::SMSG_SPELLDAMAGESHIELD] = [this](network::Packet& p) { handleSpellDamageShield(p); };
table[Opcode::SMSG_SPELLORDAMAGE_IMMUNE] = [this](network::Packet& p) { handleSpellOrDamageImmune(p); };
table[Opcode::SMSG_RESISTLOG] = [this](network::Packet& p) { handleResistLog(p); };
// ---- Pet feedback ----
table[Opcode::SMSG_PET_TAME_FAILURE] = [this](network::Packet& p) { handlePetTameFailure(p); };
table[Opcode::SMSG_PET_ACTION_FEEDBACK] = [this](network::Packet& p) { handlePetActionFeedback(p); };
table[Opcode::SMSG_PET_CAST_FAILED] = [this](network::Packet& p) { handlePetCastFailed(p); };
table[Opcode::SMSG_PET_BROKEN] = [this](network::Packet& p) { handlePetBroken(p); };
table[Opcode::SMSG_PET_LEARNED_SPELL] = [this](network::Packet& p) { handlePetLearnedSpell(p); };
table[Opcode::SMSG_PET_UNLEARNED_SPELL] = [this](network::Packet& p) { handlePetUnlearnedSpell(p); };
table[Opcode::SMSG_PET_MODE] = [this](network::Packet& p) { handlePetMode(p); };
// ---- Resurrect ----
table[Opcode::SMSG_RESURRECT_FAILED] = [this](network::Packet& p) { handleResurrectFailed(p); };
}
// ============================================================
// Auto-attack
// ============================================================
void CombatHandler::startAutoAttack(uint64_t targetGuid) {
// Can't attack yourself
if (targetGuid == owner_.playerGuid) return;
if (targetGuid == 0) return;
// Dismount when entering combat
if (owner_.isMounted()) {
owner_.dismount();
}
// Client-side melee range gate to avoid starting "swing forever" loops when
// target is already clearly out of range.
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (auto target = owner_.getEntityManager().getEntity(targetGuid)) {
float dx = owner_.movementInfo.x - target->getLatestX();
float dy = owner_.movementInfo.y - target->getLatestY();
float dz = owner_.movementInfo.z - target->getLatestZ();
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
if (dist3d > 8.0f) {
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("Target is too far away.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
return;
}
}
autoAttackRequested_ = true;
autoAttackRetryPending_ = true;
// Keep combat animation/state server-authoritative. We only flip autoAttacking
// on SMSG_ATTACKSTART where attackerGuid == playerGuid.
autoAttacking_ = false;
autoAttackTarget_ = targetGuid;
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
autoAttackResendTimer_ = 0.0f;
autoAttackFacingSyncTimer_ = 0.0f;
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
auto packet = AttackSwingPacket::build(targetGuid);
owner_.socket->send(packet);
}
LOG_INFO("Starting auto-attack on 0x", std::hex, targetGuid, std::dec);
}
void CombatHandler::stopAutoAttack() {
if (!autoAttacking_ && !autoAttackRequested_) return;
autoAttackRequested_ = false;
autoAttacking_ = false;
autoAttackRetryPending_ = false;
autoAttackTarget_ = 0;
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
autoAttackResendTimer_ = 0.0f;
autoAttackFacingSyncTimer_ = 0.0f;
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
auto packet = AttackStopPacket::build();
owner_.socket->send(packet);
}
LOG_INFO("Stopping auto-attack");
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PLAYER_LEAVE_COMBAT", {});
}
// ============================================================
// Combat text
// ============================================================
void CombatHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType,
uint64_t srcGuid, uint64_t dstGuid) {
CombatTextEntry entry;
entry.type = type;
entry.amount = amount;
entry.spellId = spellId;
entry.age = 0.0f;
entry.isPlayerSource = isPlayerSource;
entry.powerType = powerType;
entry.srcGuid = srcGuid;
entry.dstGuid = dstGuid;
// Random horizontal stagger so simultaneous hits don't stack vertically
static std::mt19937 rng(std::random_device{}());
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
entry.xSeed = dist(rng);
combatText_.push_back(entry);
// Persistent combat log — use explicit GUIDs if provided, else fall back to
// player/current-target (the old behaviour for events without specific participants).
CombatLogEntry log;
log.type = type;
log.amount = amount;
log.spellId = spellId;
log.isPlayerSource = isPlayerSource;
log.powerType = powerType;
log.timestamp = std::time(nullptr);
// If the caller provided an explicit destination GUID but left source GUID as 0,
// preserve "unknown/no source" (e.g. environmental damage) instead of
// backfilling from current target.
uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid
: ((dstGuid != 0) ? 0 : (isPlayerSource ? owner_.playerGuid : owner_.targetGuid));
uint64_t effectiveDst = (dstGuid != 0) ? dstGuid
: (isPlayerSource ? owner_.targetGuid : owner_.playerGuid);
log.sourceName = owner_.lookupName(effectiveSrc);
log.targetName = (effectiveDst != 0) ? owner_.lookupName(effectiveDst) : std::string{};
if (combatLog_.size() >= MAX_COMBAT_LOG)
combatLog_.pop_front();
combatLog_.push_back(std::move(log));
// Fire COMBAT_LOG_EVENT_UNFILTERED for Lua addons
// Args: subevent, sourceGUID, sourceName, 0 (sourceFlags), destGUID, destName, 0 (destFlags), spellId, spellName, amount
if (owner_.addonEventCallback_) {
static const char* kSubevents[] = {
"SWING_DAMAGE", "SPELL_DAMAGE", "SPELL_HEAL", "SWING_MISSED", "SWING_MISSED",
"SWING_MISSED", "SWING_MISSED", "SWING_MISSED", "SPELL_DAMAGE", "SPELL_HEAL",
"SPELL_PERIODIC_DAMAGE", "SPELL_PERIODIC_HEAL", "ENVIRONMENTAL_DAMAGE",
"SPELL_ENERGIZE", "SPELL_DRAIN", "PARTY_KILL", "SPELL_MISSED", "SPELL_ABSORBED",
"SPELL_MISSED", "SPELL_MISSED", "SPELL_MISSED", "SPELL_AURA_APPLIED",
"SPELL_DISPEL", "SPELL_STOLEN", "SPELL_INTERRUPT", "SPELL_INSTAKILL",
"PARTY_KILL", "SWING_DAMAGE", "SWING_DAMAGE"
};
const char* subevent = (type < sizeof(kSubevents)/sizeof(kSubevents[0]))
? kSubevents[type] : "UNKNOWN";
char srcBuf[32], dstBuf[32];
snprintf(srcBuf, sizeof(srcBuf), "0x%016llX", (unsigned long long)effectiveSrc);
snprintf(dstBuf, sizeof(dstBuf), "0x%016llX", (unsigned long long)effectiveDst);
std::string spellName = (spellId != 0) ? owner_.getSpellName(spellId) : std::string{};
std::string timestamp = std::to_string(static_cast<double>(std::time(nullptr)));
owner_.addonEventCallback_("COMBAT_LOG_EVENT_UNFILTERED", {
timestamp, subevent,
srcBuf, log.sourceName, "0",
dstBuf, log.targetName, "0",
std::to_string(spellId), spellName,
std::to_string(amount)
});
}
}
bool CombatHandler::shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId) {
if (spellId == 0) return false;
const auto now = std::chrono::steady_clock::now();
constexpr auto kRecentWindow = std::chrono::seconds(1);
while (!recentSpellstealLogs_.empty() &&
now - recentSpellstealLogs_.front().timestamp > kRecentWindow) {
recentSpellstealLogs_.pop_front();
}
for (auto it = recentSpellstealLogs_.begin(); it != recentSpellstealLogs_.end(); ++it) {
if (it->casterGuid == casterGuid &&
it->victimGuid == victimGuid &&
it->spellId == spellId) {
recentSpellstealLogs_.erase(it);
return false;
}
}
if (recentSpellstealLogs_.size() >= MAX_RECENT_SPELLSTEAL_LOGS)
recentSpellstealLogs_.pop_front();
recentSpellstealLogs_.push_back({casterGuid, victimGuid, spellId, now});
return true;
}
void CombatHandler::updateCombatText(float deltaTime) {
for (auto& entry : combatText_) {
entry.age += deltaTime;
}
combatText_.erase(
std::remove_if(combatText_.begin(), combatText_.end(),
[](const CombatTextEntry& e) { return e.isExpired(); }),
combatText_.end());
}
// ============================================================
// Packet handlers
// ============================================================
void CombatHandler::autoTargetAttacker(uint64_t attackerGuid) {
if (attackerGuid == 0 || attackerGuid == owner_.playerGuid) return;
if (owner_.targetGuid != 0) return;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
if (!owner_.getEntityManager().hasEntity(attackerGuid)) return;
owner_.setTarget(attackerGuid);
}
void CombatHandler::handleAttackStart(network::Packet& packet) {
AttackStartData data;
if (!AttackStartParser::parse(packet, data)) return;
if (data.attackerGuid == owner_.playerGuid) {
autoAttackRequested_ = true;
autoAttacking_ = true;
autoAttackRetryPending_ = false;
autoAttackTarget_ = data.victimGuid;
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PLAYER_ENTER_COMBAT", {});
} else if (data.victimGuid == owner_.playerGuid && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
autoTargetAttacker(data.attackerGuid);
// Play aggro sound when NPC attacks player
if (owner_.npcAggroCallback_) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().getEntity(data.attackerGuid);
if (entity && entity->getType() == ObjectType::UNIT) {
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
owner_.npcAggroCallback_(data.attackerGuid, pos);
}
}
}
// Force both participants to face each other at combat start.
// Uses atan2(-dy, dx): canonical orientation convention where the West/Y
// component is negated (renderYaw = orientation + 90°, model-forward = render+X).
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto attackerEnt = owner_.getEntityManager().getEntity(data.attackerGuid);
auto victimEnt = owner_.getEntityManager().getEntity(data.victimGuid);
if (attackerEnt && victimEnt) {
float dx = victimEnt->getX() - attackerEnt->getX();
float dy = victimEnt->getY() - attackerEnt->getY();
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
attackerEnt->setOrientation(std::atan2(-dy, dx)); // attacker → victim
victimEnt->setOrientation (std::atan2( dy, -dx)); // victim → attacker
}
}
}
void CombatHandler::handleAttackStop(network::Packet& packet) {
AttackStopData data;
if (!AttackStopParser::parse(packet, data)) return;
// Keep intent, but clear server-confirmed active state until ATTACKSTART resumes.
if (data.attackerGuid == owner_.playerGuid) {
autoAttacking_ = false;
autoAttackRetryPending_ = autoAttackRequested_;
autoAttackResendTimer_ = 0.0f;
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
} else if (data.victimGuid == owner_.playerGuid) {
hostileAttackers_.erase(data.attackerGuid);
}
}
void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
AttackerStateUpdateData data;
if (!owner_.packetParsers_->parseAttackerStateUpdate(packet, data)) return;
bool isPlayerAttacker = (data.attackerGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat
if (isPlayerAttacker) {
lastMeleeSwingMs_ = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
}
if (!isPlayerAttacker && owner_.npcSwingCallback_) {
owner_.npcSwingCallback_(data.attackerGuid);
}
if (isPlayerTarget && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
autoTargetAttacker(data.attackerGuid);
}
// Play combat sounds via CombatSoundManager + character vocalizations
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* csm = renderer->getCombatSoundManager()) {
auto weaponSize = audio::CombatSoundManager::WeaponSize::MEDIUM;
if (data.isMiss()) {
csm->playWeaponMiss(false);
} else if (data.victimState == 1 || data.victimState == 2) {
// Dodge/parry — swing whoosh but no impact
csm->playWeaponSwing(weaponSize, false);
} else {
// Hit — swing + flesh impact
csm->playWeaponSwing(weaponSize, data.isCrit());
csm->playImpact(weaponSize, audio::CombatSoundManager::ImpactType::FLESH, data.isCrit());
}
}
// Character vocalizations
if (auto* asm_ = renderer->getActivitySoundManager()) {
if (isPlayerAttacker && !data.isMiss() && data.victimState != 1 && data.victimState != 2) {
asm_->playAttackGrunt();
}
if (isPlayerTarget && !data.isMiss() && data.victimState != 1 && data.victimState != 2) {
asm_->playWound(data.isCrit());
}
}
}
if (data.isMiss()) {
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 1) {
addCombatText(CombatTextEntry::DODGE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 2) {
addCombatText(CombatTextEntry::PARRY, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 4) {
// VICTIMSTATE_BLOCKS: show reduced damage and the blocked amount
if (data.totalDamage > 0)
addCombatText(CombatTextEntry::MELEE_DAMAGE, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
addCombatText(CombatTextEntry::BLOCK, static_cast<int32_t>(data.blocked), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 5) {
// VICTIMSTATE_EVADE: NPC evaded (out of combat zone).
addCombatText(CombatTextEntry::EVADE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 6) {
// VICTIMSTATE_IS_IMMUNE: Target is immune to this attack.
addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 7) {
// VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect).
addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else {
CombatTextEntry::Type type;
if (data.isCrit())
type = CombatTextEntry::CRIT_DAMAGE;
else if (data.isCrushing())
type = CombatTextEntry::CRUSHING;
else if (data.isGlancing())
type = CombatTextEntry::GLANCING;
else
type = CombatTextEntry::MELEE_DAMAGE;
addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
// Show partial absorb/resist from sub-damage entries
uint32_t totalAbsorbed = 0, totalResisted = 0;
for (const auto& sub : data.subDamages) {
totalAbsorbed += sub.absorbed;
totalResisted += sub.resisted;
}
if (totalAbsorbed > 0)
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(totalAbsorbed), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
if (totalResisted > 0)
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(totalResisted), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
}
}
void CombatHandler::handleSpellDamageLog(network::Packet& packet) {
SpellDamageLogData data;
if (!owner_.packetParsers_->parseSpellDamageLog(packet, data)) return;
bool isPlayerSource = (data.attackerGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
if (isPlayerTarget && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
autoTargetAttacker(data.attackerGuid);
}
auto type = data.isCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::SPELL_DAMAGE;
if (data.damage > 0)
addCombatText(type, static_cast<int32_t>(data.damage), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid);
if (data.absorbed > 0)
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(data.absorbed), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid);
if (data.resisted > 0)
addCombatText(CombatTextEntry::RESIST, static_cast<int32_t>(data.resisted), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid);
}
void CombatHandler::handleSpellHealLog(network::Packet& packet) {
SpellHealLogData data;
if (!owner_.packetParsers_->parseSpellHealLog(packet, data)) return;
bool isPlayerSource = (data.casterGuid == owner_.playerGuid);
bool isPlayerTarget = (data.targetGuid == owner_.playerGuid);
if (!isPlayerSource && !isPlayerTarget) return; // Not our combat
auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL;
addCombatText(type, static_cast<int32_t>(data.heal), data.spellId, isPlayerSource, 0, data.casterGuid, data.targetGuid);
if (data.absorbed > 0)
addCombatText(CombatTextEntry::ABSORB, static_cast<int32_t>(data.absorbed), data.spellId, isPlayerSource, 0, data.casterGuid, data.targetGuid);
}
void CombatHandler::handleSetForcedReactions(network::Packet& packet) {
if (!packet.hasRemaining(4)) return;
uint32_t count = packet.readUInt32();
if (count > 64) {
LOG_WARNING("SMSG_SET_FORCED_REACTIONS: suspicious count ", count, ", ignoring");
packet.setReadPos(packet.getSize());
return;
}
forcedReactions_.clear();
for (uint32_t i = 0; i < count; ++i) {
if (!packet.hasRemaining(8)) break;
uint32_t factionId = packet.readUInt32();
uint32_t reaction = packet.readUInt32();
forcedReactions_[factionId] = static_cast<uint8_t>(reaction);
}
LOG_INFO("SMSG_SET_FORCED_REACTIONS: ", forcedReactions_.size(), " faction overrides");
}
// ============================================================
// Per-frame update
// ============================================================
void CombatHandler::updateAutoAttack(float deltaTime) {
// Decrement range warn cooldown
if (autoAttackRangeWarnCooldown_ > 0.0f) {
autoAttackRangeWarnCooldown_ = std::max(0.0f, autoAttackRangeWarnCooldown_ - deltaTime);
}
// Leave combat if auto-attack target is too far away (leash range)
// and keep melee intent tightly synced while stationary.
if (autoAttackRequested_ && autoAttackTarget_ != 0) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto targetEntity = owner_.getEntityManager().getEntity(autoAttackTarget_);
if (targetEntity) {
const float targetX = targetEntity->getLatestX();
const float targetY = targetEntity->getLatestY();
const float targetZ = targetEntity->getLatestZ();
float dx = owner_.movementInfo.x - targetX;
float dy = owner_.movementInfo.y - targetY;
float dz = owner_.movementInfo.z - targetZ;
float dist = std::sqrt(dx * dx + dy * dy);
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
const bool classicLike = isPreWotlk();
if (dist > 40.0f) {
stopAutoAttack();
LOG_INFO("Left combat: target too far (", dist, " yards)");
} else if (owner_.isInWorld()) {
bool allowResync = true;
const float meleeRange = classicLike ? 5.25f : 5.75f;
if (dist3d > meleeRange) {
autoAttackOutOfRange_ = true;
autoAttackOutOfRangeTime_ += deltaTime;
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("Target is too far away.");
owner_.addUIError("Target is too far away.");
autoAttackRangeWarnCooldown_ = 1.25f;
}
// Stop chasing stale swings when the target remains out of range.
if (autoAttackOutOfRangeTime_ > 2.0f && dist3d > 9.0f) {
stopAutoAttack();
owner_.addSystemChatMessage("Auto-attack stopped: target out of range.");
allowResync = false;
}
} else {
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
}
if (allowResync) {
autoAttackResendTimer_ += deltaTime;
autoAttackFacingSyncTimer_ += deltaTime;
// Classic/Turtle servers do not tolerate steady attack-start
// reissues well. Only retry once after local start or an
// explicit server-side attack stop while intent is still set.
const float resendInterval = classicLike ? 1.0f : 0.50f;
if (!autoAttacking_ && !autoAttackOutOfRange_ && autoAttackRetryPending_ &&
autoAttackResendTimer_ >= resendInterval) {
autoAttackResendTimer_ = 0.0f;
autoAttackRetryPending_ = false;
auto pkt = AttackSwingPacket::build(autoAttackTarget_);
owner_.socket->send(pkt);
}
// Keep server-facing aligned while trying to acquire melee.
const float facingSyncInterval = classicLike ? 0.25f : 0.20f;
const bool allowPeriodicFacingSync = !classicLike || !autoAttacking_;
if (allowPeriodicFacingSync &&
autoAttackFacingSyncTimer_ >= facingSyncInterval) {
autoAttackFacingSyncTimer_ = 0.0f;
float toTargetX = targetX - owner_.movementInfo.x;
float toTargetY = targetY - owner_.movementInfo.y;
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
float desired = std::atan2(-toTargetY, toTargetX);
float diff = desired - owner_.movementInfo.orientation;
while (diff > static_cast<float>(M_PI)) diff -= 2.0f * static_cast<float>(M_PI);
while (diff < -static_cast<float>(M_PI)) diff += 2.0f * static_cast<float>(M_PI);
const float facingThreshold = classicLike ? 0.035f : 0.12f;
if (std::abs(diff) > facingThreshold) {
owner_.movementInfo.orientation = desired;
owner_.sendMovement(Opcode::MSG_MOVE_SET_FACING);
}
}
}
}
}
}
}
// Keep active melee attackers visually facing the player as positions change.
if (!hostileAttackers_.empty()) {
for (uint64_t attackerGuid : hostileAttackers_) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto attacker = owner_.getEntityManager().getEntity(attackerGuid);
if (!attacker) continue;
float dx = owner_.movementInfo.x - attacker->getX();
float dy = owner_.movementInfo.y - attacker->getY();
if (std::abs(dx) < 0.01f && std::abs(dy) < 0.01f) continue;
attacker->setOrientation(std::atan2(-dy, dx));
}
}
}
// ============================================================
// State management
// ============================================================
void CombatHandler::resetAllCombatState() {
hostileAttackers_.clear();
combatText_.clear();
autoAttacking_ = false;
autoAttackRequested_ = false;
autoAttackRetryPending_ = false;
autoAttackTarget_ = 0;
autoAttackOutOfRange_ = false;
autoAttackOutOfRangeTime_ = 0.0f;
autoAttackRangeWarnCooldown_ = 0.0f;
autoAttackResendTimer_ = 0.0f;
autoAttackFacingSyncTimer_ = 0.0f;
lastMeleeSwingMs_ = 0;
}
void CombatHandler::removeHostileAttacker(uint64_t guid) {
hostileAttackers_.erase(guid);
}
void CombatHandler::clearCombatText() {
combatText_.clear();
}
void CombatHandler::removeCombatTextForGuid(uint64_t guid) {
combatText_.erase(
std::remove_if(combatText_.begin(), combatText_.end(),
[guid](const CombatTextEntry& e) {
return e.dstGuid == guid;
}),
combatText_.end());
}
// ============================================================
// Moved opcode handlers (from GameHandler::registerOpcodeHandlers)
// ============================================================
void CombatHandler::handleHealthUpdate(network::Packet& packet) {
const bool huTbc = isActiveExpansion("tbc");
if (!packet.hasRemaining(huTbc ? 8u : 2u) ) return;
uint64_t guid = huTbc ? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(4)) return;
uint32_t hp = packet.readUInt32();
if (auto* unit = owner_.getUnitByGuid(guid)) unit->setHealth(hp);
if (guid != 0) {
auto unitId = owner_.guidToUnitId(guid);
if (!unitId.empty()) owner_.fireAddonEvent("UNIT_HEALTH", {unitId});
}
}
void CombatHandler::handlePowerUpdate(network::Packet& packet) {
const bool puTbc = isActiveExpansion("tbc");
if (!packet.hasRemaining(puTbc ? 8u : 2u) ) return;
uint64_t guid = puTbc ? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(5)) return;
uint8_t powerType = packet.readUInt8();
uint32_t value = packet.readUInt32();
if (auto* unit = owner_.getUnitByGuid(guid)) unit->setPowerByType(powerType, value);
if (guid != 0) {
auto unitId = owner_.guidToUnitId(guid);
if (!unitId.empty()) {
owner_.fireAddonEvent("UNIT_POWER", {unitId});
if (guid == owner_.playerGuid) {
owner_.fireAddonEvent("ACTIONBAR_UPDATE_USABLE", {});
owner_.fireAddonEvent("SPELL_UPDATE_USABLE", {});
}
}
}
}
void CombatHandler::handleUpdateComboPoints(network::Packet& packet) {
const bool cpTbc = isActiveExpansion("tbc");
if (!packet.hasRemaining(cpTbc ? 8u : 2u) ) return;
uint64_t target = cpTbc ? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(1)) return;
owner_.comboPoints_ = packet.readUInt8();
owner_.comboTarget_ = target;
LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target,
std::dec, " points=", static_cast<int>(owner_.comboPoints_));
owner_.fireAddonEvent("PLAYER_COMBO_POINTS", {});
}
void CombatHandler::handlePvpCredit(network::Packet& packet) {
if (packet.hasRemaining(16)) {
uint32_t honor = packet.readUInt32();
uint64_t victimGuid = packet.readUInt64();
uint32_t rank = packet.readUInt32();
LOG_INFO("SMSG_PVP_CREDIT: honor=", honor, " victim=0x", std::hex, victimGuid, std::dec, " rank=", rank);
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
owner_.addSystemChatMessage(msg);
if (honor > 0) addCombatText(CombatTextEntry::HONOR_GAIN, static_cast<int32_t>(honor), 0, true);
if (owner_.pvpHonorCallback_) owner_.pvpHonorCallback_(honor, victimGuid, rank);
owner_.fireAddonEvent("CHAT_MSG_COMBAT_HONOR_GAIN", {msg});
}
}
void CombatHandler::handleProcResist(network::Packet& packet) {
const bool prUsesFullGuid = isActiveExpansion("tbc");
auto readPrGuid = [&]() -> uint64_t {
if (prUsesFullGuid)
return (packet.hasRemaining(8)) ? packet.readUInt64() : 0;
return packet.readPackedGuid();
};
if (!packet.hasRemaining(prUsesFullGuid ? 8u : 1u) || (!prUsesFullGuid && !packet.hasFullPackedGuid())) { packet.skipAll(); return; }
uint64_t caster = readPrGuid();
if (!packet.hasRemaining(prUsesFullGuid ? 8u : 1u) || (!prUsesFullGuid && !packet.hasFullPackedGuid())) { packet.skipAll(); return; }
uint64_t victim = readPrGuid();
if (!packet.hasRemaining(4)) return;
uint32_t spellId = packet.readUInt32();
if (victim == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
else if (caster == owner_.playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
packet.skipAll();
}
// ============================================================
// Environmental / reflect / immune / resist
// ============================================================
void CombatHandler::handleSpellDamageShield(network::Packet& packet) {
// Classic: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + schoolMask(4)
// TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4)
// WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4)
const bool shieldTbc = isActiveExpansion("tbc");
const bool shieldWotlkLike = !isClassicLikeExpansion() && !shieldTbc;
const auto shieldRem = [&]() { return packet.getRemainingSize(); };
const size_t shieldMinSz = shieldTbc ? 24u : 2u;
if (!packet.hasRemaining(shieldMinSz)) {
packet.skipAll(); return;
}
if (!shieldTbc && (!packet.hasFullPackedGuid())) {
packet.skipAll(); return;
}
uint64_t victimGuid = shieldTbc
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(shieldTbc ? 8u : 1u) || (!shieldTbc && !packet.hasFullPackedGuid())) {
packet.skipAll(); return;
}
uint64_t casterGuid = shieldTbc
? packet.readUInt64() : packet.readPackedGuid();
const size_t shieldTailSize = shieldWotlkLike ? 16u : 12u;
if (shieldRem() < shieldTailSize) {
packet.skipAll(); return;
}
uint32_t shieldSpellId = packet.readUInt32();
uint32_t damage = packet.readUInt32();
if (shieldWotlkLike)
/*uint32_t absorbed =*/ packet.readUInt32();
/*uint32_t school =*/ packet.readUInt32();
// Show combat text: damage shield reflect
if (casterGuid == owner_.playerGuid) {
// We have a damage shield that reflected damage
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, true, 0, casterGuid, victimGuid);
} else if (victimGuid == owner_.playerGuid) {
// A damage shield hit us (e.g. target's Thorns)
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(damage), shieldSpellId, false, 0, casterGuid, victimGuid);
}
}
void CombatHandler::handleSpellOrDamageImmune(network::Packet& packet) {
// WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType
// TBC: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8
const bool immuneUsesFullGuid = isActiveExpansion("tbc");
const size_t minSz = immuneUsesFullGuid ? 21u : 2u;
if (!packet.hasRemaining(minSz)) {
packet.skipAll(); return;
}
if (!immuneUsesFullGuid && !packet.hasFullPackedGuid()) {
packet.skipAll(); return;
}
uint64_t casterGuid = immuneUsesFullGuid
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(immuneUsesFullGuid ? 8u : 2u) || (!immuneUsesFullGuid && !packet.hasFullPackedGuid())) {
packet.skipAll(); return;
}
uint64_t victimGuid = immuneUsesFullGuid
? packet.readUInt64() : packet.readPackedGuid();
if (!packet.hasRemaining(5)) return;
uint32_t immuneSpellId = packet.readUInt32();
/*uint8_t saveType =*/ packet.readUInt8();
// Show IMMUNE text when the player is the caster (we hit an immune target)
// or the victim (we are immune)
if (casterGuid == owner_.playerGuid || victimGuid == owner_.playerGuid) {
addCombatText(CombatTextEntry::IMMUNE, 0, immuneSpellId,
casterGuid == owner_.playerGuid, 0, casterGuid, victimGuid);
}
}
void CombatHandler::handleResistLog(network::Packet& packet) {
// WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId
// + float resistFactor + uint32 targetRes + uint32 resistedValue + ...
// TBC: same layout but full uint64 GUIDs
// Show RESIST combat text when player resists an incoming spell.
const bool rlUsesFullGuid = isActiveExpansion("tbc");
auto rl_rem = [&]() { return packet.getRemainingSize(); };
if (rl_rem() < 4) { packet.skipAll(); return; }
/*uint32_t hitInfo =*/ packet.readUInt32();
if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)
|| (!rlUsesFullGuid && !packet.hasFullPackedGuid())) {
packet.skipAll(); return;
}
uint64_t attackerGuid = rlUsesFullGuid
? packet.readUInt64() : packet.readPackedGuid();
if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)
|| (!rlUsesFullGuid && !packet.hasFullPackedGuid())) {
packet.skipAll(); return;
}
uint64_t victimGuid = rlUsesFullGuid
? packet.readUInt64() : packet.readPackedGuid();
if (rl_rem() < 4) { packet.skipAll(); return; }
uint32_t spellId = packet.readUInt32();
// Resist payload includes:
// float resistFactor + uint32 targetResistance + uint32 resistedValue.
// Require the full payload so truncated packets cannot synthesize
// zero-value resist events.
if (rl_rem() < 12) { packet.skipAll(); return; }
/*float resistFactor =*/ packet.readFloat();
/*uint32_t targetRes =*/ packet.readUInt32();
int32_t resistedAmount = static_cast<int32_t>(packet.readUInt32());
// Show RESIST when the player is involved on either side.
if (resistedAmount > 0 && victimGuid == owner_.playerGuid) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid);
} else if (resistedAmount > 0 && attackerGuid == owner_.playerGuid) {
addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid);
}
packet.skipAll();
}
// ============================================================
// Pet feedback
// ============================================================
void CombatHandler::handlePetTameFailure(network::Packet& packet) {
static const char* reasons[] = {
"Invalid creature", "Too many pets", "Already tamed",
"Wrong faction", "Level too low", "Creature not tameable",
"Can't control", "Can't command"
};
if (packet.hasRemaining(1)) {
uint8_t reason = packet.readUInt8();
const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason";
std::string s = std::string("Failed to tame: ") + msg;
owner_.addUIError(s);
owner_.addSystemChatMessage(s);
}
}
void CombatHandler::handlePetActionFeedback(network::Packet& packet) {
static const char* kPetFeedback[] = {
nullptr,
"Your pet is dead.", "Your pet has nothing to attack.",
"Your pet cannot attack that target.", "That target is too far away.",
"Your pet cannot find a path to the target.",
"Your pet cannot attack an immune target.",
};
if (!packet.hasRemaining(1)) return;
uint8_t msg = packet.readUInt8();
if (msg > 0 && msg < 7 && kPetFeedback[msg]) owner_.addSystemChatMessage(kPetFeedback[msg]);
packet.skipAll();
}
void CombatHandler::handlePetCastFailed(network::Packet& packet) {
// WotLK: castCount(1) + spellId(4) + reason(1)
// Classic/TBC: spellId(4) + reason(1) (no castCount)
const bool hasCount = isActiveExpansion("wotlk");
const size_t minSize = hasCount ? 6u : 5u;
if (packet.hasRemaining(minSize)) {
if (hasCount) /*uint8_t castCount =*/ packet.readUInt8();
uint32_t spellId = packet.readUInt32();
uint8_t reason = (packet.hasRemaining(1))
? packet.readUInt8() : 0;
LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId,
" reason=", static_cast<int>(reason));
if (reason != 0) {
const char* reasonStr = getSpellCastResultString(reason);
const std::string& sName = owner_.getSpellName(spellId);
std::string errMsg;
if (reasonStr && *reasonStr)
errMsg = sName.empty() ? reasonStr : (sName + ": " + reasonStr);
else
errMsg = sName.empty() ? "Pet spell failed." : (sName + ": Pet spell failed.");
owner_.addSystemChatMessage(errMsg);
}
}
packet.skipAll();
}
void CombatHandler::handlePetBroken(network::Packet& packet) {
// Pet bond broken (died or forcibly dismissed) — clear pet state
owner_.petGuid_ = 0;
owner_.petSpellList_.clear();
owner_.petAutocastSpells_.clear();
memset(owner_.petActionSlots_, 0, sizeof(owner_.petActionSlots_));
owner_.addSystemChatMessage("Your pet has died.");
LOG_INFO("SMSG_PET_BROKEN: pet bond broken");
packet.skipAll();
}
void CombatHandler::handlePetLearnedSpell(network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t spellId = packet.readUInt32();
owner_.petSpellList_.push_back(spellId);
const std::string& sname = owner_.getSpellName(spellId);
owner_.addSystemChatMessage("Your pet has learned " + (sname.empty() ? "a new ability." : sname + "."));
LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId);
owner_.fireAddonEvent("PET_BAR_UPDATE", {});
}
packet.skipAll();
}
void CombatHandler::handlePetUnlearnedSpell(network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t spellId = packet.readUInt32();
owner_.petSpellList_.erase(
std::remove(owner_.petSpellList_.begin(), owner_.petSpellList_.end(), spellId),
owner_.petSpellList_.end());
owner_.petAutocastSpells_.erase(spellId);
LOG_DEBUG("SMSG_PET_UNLEARNED_SPELL: spellId=", spellId);
}
packet.skipAll();
}
void CombatHandler::handlePetMode(network::Packet& packet) {
// uint64 petGuid, uint32 mode
// mode bits: low byte = command state, next byte = react state
if (packet.hasRemaining(12)) {
uint64_t modeGuid = packet.readUInt64();
uint32_t mode = packet.readUInt32();
if (modeGuid == owner_.petGuid_) {
owner_.petCommand_ = static_cast<uint8_t>(mode & 0xFF);
owner_.petReact_ = static_cast<uint8_t>((mode >> 8) & 0xFF);
LOG_DEBUG("SMSG_PET_MODE: command=", static_cast<int>(owner_.petCommand_),
" react=", static_cast<int>(owner_.petReact_));
}
}
packet.skipAll();
}
// ============================================================
// Resurrect
// ============================================================
void CombatHandler::handleResurrectFailed(network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t reason = packet.readUInt32();
const char* msg = (reason == 1) ? "The target cannot be resurrected right now."
: (reason == 2) ? "Cannot resurrect in this area."
: "Resurrection failed.";
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
}
}
// ============================================================
// Targeting
// ============================================================
void CombatHandler::setTarget(uint64_t guid) {
if (guid == owner_.targetGuid) return;
// Save previous target
if (owner_.targetGuid != 0) {
owner_.lastTargetGuid = owner_.targetGuid;
}
owner_.targetGuid = guid;
// Clear stale aura data from the previous target so the buff bar shows
// an empty state until the server sends SMSG_AURA_UPDATE_ALL for the new target.
if (owner_.spellHandler_) for (auto& slot : owner_.spellHandler_->targetAuras_) slot = AuraSlot{};
// Clear previous target's cast bar on target change
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)
// Inform server of target selection (Phase 1)
if (owner_.isInWorld()) {
auto packet = SetSelectionPacket::build(guid);
owner_.socket->send(packet);
}
if (guid != 0) {
LOG_INFO("Target set: 0x", std::hex, guid, std::dec);
}
owner_.fireAddonEvent("PLAYER_TARGET_CHANGED", {});
}
void CombatHandler::clearTarget() {
if (owner_.targetGuid != 0) {
LOG_INFO("Target cleared");
// Zero the GUID before firing the event so callbacks/addons that query
// the current target see null (consistent with setTarget which updates
// targetGuid before the event).
owner_.targetGuid = 0;
owner_.fireAddonEvent("PLAYER_TARGET_CHANGED", {});
} else {
owner_.targetGuid = 0;
}
owner_.tabCycleIndex = -1;
owner_.tabCycleStale = true;
}
std::shared_ptr<Entity> CombatHandler::getTarget() const {
if (owner_.targetGuid == 0) return nullptr;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
return owner_.getEntityManager().getEntity(owner_.targetGuid);
}
void CombatHandler::setFocus(uint64_t guid) {
owner_.focusGuid = guid;
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
if (guid != 0) {
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().getEntity(guid);
if (entity) {
std::string name;
auto unit = std::dynamic_pointer_cast<Unit>(entity);
if (unit && !unit->getName().empty()) {
name = unit->getName();
}
if (name.empty()) name = owner_.lookupName(guid);
if (name.empty()) name = "Unknown";
owner_.addSystemChatMessage("Focus set: " + name);
LOG_INFO("Focus set: 0x", std::hex, guid, std::dec);
}
}
}
void CombatHandler::clearFocus() {
if (owner_.focusGuid != 0) {
owner_.addSystemChatMessage("Focus cleared.");
LOG_INFO("Focus cleared");
}
owner_.focusGuid = 0;
owner_.fireAddonEvent("PLAYER_FOCUS_CHANGED", {});
}
std::shared_ptr<Entity> CombatHandler::getFocus() const {
if (owner_.focusGuid == 0) return nullptr;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
return owner_.getEntityManager().getEntity(owner_.focusGuid);
}
void CombatHandler::setMouseoverGuid(uint64_t guid) {
if (owner_.mouseoverGuid_ != guid) {
owner_.mouseoverGuid_ = guid;
owner_.fireAddonEvent("UPDATE_MOUSEOVER_UNIT", {});
}
}
void CombatHandler::targetLastTarget() {
if (owner_.lastTargetGuid == 0) {
owner_.addSystemChatMessage("No previous target.");
return;
}
// Swap current and last target
uint64_t temp = owner_.targetGuid;
setTarget(owner_.lastTargetGuid);
owner_.lastTargetGuid = temp;
}
void CombatHandler::targetEnemy(bool reverse) {
// Get list of hostile entities
std::vector<uint64_t> hostiles;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto& entities = owner_.getEntityManager().getEntities();
for (const auto& [guid, entity] : entities) {
if (entity->getType() == ObjectType::UNIT) {
auto unit = std::dynamic_pointer_cast<Unit>(entity);
if (unit && guid != owner_.playerGuid && unit->isHostile()) {
hostiles.push_back(guid);
}
}
}
if (hostiles.empty()) {
owner_.addSystemChatMessage("No enemies in range.");
return;
}
// Find current target in list
auto it = std::find(hostiles.begin(), hostiles.end(), owner_.targetGuid);
if (it == hostiles.end()) {
// Not currently targeting a hostile, target first one
setTarget(reverse ? hostiles.back() : hostiles.front());
} else {
// Cycle to next/previous
if (reverse) {
if (it == hostiles.begin()) {
setTarget(hostiles.back());
} else {
setTarget(*(--it));
}
} else {
++it;
if (it == hostiles.end()) {
setTarget(hostiles.front());
} else {
setTarget(*it);
}
}
}
}
void CombatHandler::targetFriend(bool reverse) {
// Get list of friendly entities (players)
std::vector<uint64_t> friendlies;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto& entities = owner_.getEntityManager().getEntities();
for (const auto& [guid, entity] : entities) {
if (entity->getType() == ObjectType::PLAYER && guid != owner_.playerGuid) {
friendlies.push_back(guid);
}
}
if (friendlies.empty()) {
owner_.addSystemChatMessage("No friendly targets in range.");
return;
}
// Find current target in list
auto it = std::find(friendlies.begin(), friendlies.end(), owner_.targetGuid);
if (it == friendlies.end()) {
// Not currently targeting a friend, target first one
setTarget(reverse ? friendlies.back() : friendlies.front());
} else {
// Cycle to next/previous
if (reverse) {
if (it == friendlies.begin()) {
setTarget(friendlies.back());
} else {
setTarget(*(--it));
}
} else {
++it;
if (it == friendlies.end()) {
setTarget(friendlies.front());
} else {
setTarget(*it);
}
}
}
}
void CombatHandler::tabTarget(float playerX, float playerY, float playerZ) {
// Helper: returns true if the entity is a living hostile that can be tab-targeted.
auto isValidTabTarget = [&](const std::shared_ptr<Entity>& e) -> bool {
if (!e) return false;
const uint64_t guid = e->getGuid();
auto* unit = dynamic_cast<Unit*>(e.get());
if (!unit) return false;
if (unit->getHealth() == 0) {
auto lootIt = owner_.localLootState_.find(guid);
if (lootIt == owner_.localLootState_.end() || lootIt->second.data.items.empty()) {
return false;
}
return true;
}
const bool hostileByFaction = unit->isHostile();
const bool hostileByCombat = isAggressiveTowardPlayer(guid);
if (!hostileByFaction && !hostileByCombat) return false;
return true;
};
// Rebuild cycle list if stale (entity added/removed since last tab press).
if (owner_.tabCycleStale) {
owner_.tabCycleList.clear();
owner_.tabCycleIndex = -1;
struct EntityDist { uint64_t guid; float distance; };
std::vector<EntityDist> sortable;
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
auto t = entity->getType();
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
if (guid == owner_.playerGuid) continue;
if (!isValidTabTarget(entity)) continue;
float dx = entity->getX() - playerX;
float dy = entity->getY() - playerY;
float dz = entity->getZ() - playerZ;
sortable.push_back({guid, std::sqrt(dx*dx + dy*dy + dz*dz)});
}
std::sort(sortable.begin(), sortable.end(),
[](const EntityDist& a, const EntityDist& b) { return a.distance < b.distance; });
for (const auto& ed : sortable) {
owner_.tabCycleList.push_back(ed.guid);
}
owner_.tabCycleStale = false;
}
if (owner_.tabCycleList.empty()) {
clearTarget();
return;
}
// Advance through the cycle, skipping any entry that has since died or
// turned friendly (e.g. NPC killed between two tab presses).
int tries = static_cast<int>(owner_.tabCycleList.size());
while (tries-- > 0) {
owner_.tabCycleIndex = (owner_.tabCycleIndex + 1) % static_cast<int>(owner_.tabCycleList.size());
uint64_t guid = owner_.tabCycleList[owner_.tabCycleIndex];
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().getEntity(guid);
if (isValidTabTarget(entity)) {
setTarget(guid);
return;
}
}
// All cached entries are stale — clear target and force a fresh rebuild next time.
owner_.tabCycleStale = true;
clearTarget();
}
void CombatHandler::assistTarget() {
if (owner_.state != WorldState::IN_WORLD) {
LOG_WARNING("Cannot assist: not in world");
return;
}
if (owner_.targetGuid == 0) {
owner_.addSystemChatMessage("You must target someone to assist.");
return;
}
auto target = getTarget();
if (!target) {
owner_.addSystemChatMessage("Invalid target.");
return;
}
// Get target name
std::string targetName = "Target";
if (target->getType() == ObjectType::PLAYER) {
auto player = std::static_pointer_cast<Player>(target);
if (!player->getName().empty()) {
targetName = player->getName();
}
} else if (target->getType() == ObjectType::UNIT) {
auto unit = std::static_pointer_cast<Unit>(target);
targetName = unit->getName();
}
// Try to read target GUID from update fields (UNIT_FIELD_TARGET)
uint64_t assistTargetGuid = 0;
const auto& fields = target->getFields();
auto it = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_LO));
if (it != fields.end()) {
assistTargetGuid = it->second;
auto it2 = fields.find(fieldIndex(UF::UNIT_FIELD_TARGET_HI));
if (it2 != fields.end()) {
assistTargetGuid |= (static_cast<uint64_t>(it2->second) << 32);
}
}
if (assistTargetGuid == 0) {
owner_.addSystemChatMessage(targetName + " has no target.");
LOG_INFO("Assist: ", targetName, " has no target");
return;
}
// Set our target to their target
setTarget(assistTargetGuid);
LOG_INFO("Assisting ", targetName, ", now targeting GUID: 0x", std::hex, assistTargetGuid, std::dec);
}
// ============================================================
// PvP
// ============================================================
void CombatHandler::togglePvp() {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot toggle PvP: not in world or not connected");
return;
}
auto packet = TogglePvpPacket::build();
owner_.socket->send(packet);
refactor(game): extract EntityController from GameHandler (step 1.3) Moves entity lifecycle, name/creature/game-object caches, transport GUID tracking, and the entire update-object pipeline out of GameHandler into a new EntityController class (friend-class pattern, same as CombatHandler et al.). What moved: - applyUpdateObjectBlock() — 1,520-line core of all entity creation, field updates, and movement application - processOutOfRangeObjects() / finalizeUpdateObjectBatch() - handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject() - handleNameQueryResponse() / handleCreatureQueryResponse() - handleGameObjectQueryResponse() / handleGameObjectPageText() - handlePageTextQueryResponse() - enqueueUpdateObjectWork() / processPendingUpdateObjectWork() - playerNameCache, playerClassRaceCache_, pendingNameQueries - creatureInfoCache, pendingCreatureQueries - gameObjectInfoCache_, pendingGameObjectQueries_ - transportGuids_, serverUpdatedTransportGuids_ - EntityManager (accessed by other handlers via getEntityManager()) 8 opcodes re-registered by EntityController::registerOpcodes(): SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT, SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE, SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT, SMSG_PAGE_TEXT_QUERY_RESPONSE Other handler files (combat, movement, social, spell, inventory, quest, chat) updated to access EntityManager via getEntityManager() and the name cache via getPlayerNameCache() — no logic changes. Also included: - .clang-tidy: add modernize-use-nodiscard, modernize-use-designated-initializers; set -std=c++20 in ExtraArgs - test.sh: prepend clang's own resource include dir before GCC's to silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs Line counts: entity_controller.hpp 147 lines (new) entity_controller.cpp 2172 lines (new) game_handler.cpp 8095 lines (was 10143, −2048) Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
auto entity = owner_.getEntityManager().getEntity(owner_.playerGuid);
bool currentlyPvp = false;
if (entity) {
currentlyPvp = (entity->getField(59) & 0x00001000) != 0;
}
if (currentlyPvp) {
owner_.addSystemChatMessage("PvP flag disabled.");
} else {
owner_.addSystemChatMessage("PvP flag enabled.");
}
LOG_INFO("Toggled PvP flag");
}
// ============================================================
// Death / Resurrection
// ============================================================
void CombatHandler::releaseSpirit() {
if (owner_.socket && owner_.state == WorldState::IN_WORLD) {
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
if (owner_.repopPending_ && now - static_cast<int64_t>(owner_.lastRepopRequestMs_) < 1000) {
return;
}
auto packet = RepopRequestPacket::build();
owner_.socket->send(packet);
owner_.selfResAvailable_ = false;
owner_.repopPending_ = true;
owner_.lastRepopRequestMs_ = static_cast<uint64_t>(now);
LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)");
network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY));
owner_.socket->send(cq);
}
}
bool CombatHandler::canReclaimCorpse() const {
if (!owner_.releasedSpirit_ || owner_.corpseGuid_ == 0 || owner_.corpseMapId_ == 0) return false;
if (owner_.currentMapId_ != owner_.corpseMapId_) return false;
float dx = owner_.movementInfo.x - owner_.corpseY_;
float dy = owner_.movementInfo.y - owner_.corpseX_;
float dz = owner_.movementInfo.z - owner_.corpseZ_;
return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f);
}
float CombatHandler::getCorpseReclaimDelaySec() const {
if (owner_.corpseReclaimAvailableMs_ == 0) return 0.0f;
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
if (nowMs >= owner_.corpseReclaimAvailableMs_) return 0.0f;
return static_cast<float>(owner_.corpseReclaimAvailableMs_ - nowMs) / 1000.0f;
}
void CombatHandler::reclaimCorpse() {
if (!canReclaimCorpse() || !owner_.socket) return;
if (owner_.corpseGuid_ == 0) {
LOG_WARNING("reclaimCorpse: corpse GUID not yet known (corpse object not received); cannot reclaim");
return;
}
auto packet = ReclaimCorpsePacket::build(owner_.corpseGuid_);
owner_.socket->send(packet);
LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, owner_.corpseGuid_, std::dec);
}
void CombatHandler::useSelfRes() {
if (!owner_.selfResAvailable_ || !owner_.socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES));
owner_.socket->send(pkt);
owner_.selfResAvailable_ = false;
LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)");
}
void CombatHandler::activateSpiritHealer(uint64_t npcGuid) {
if (!owner_.isInWorld()) return;
owner_.pendingSpiritHealerGuid_ = npcGuid;
auto packet = SpiritHealerActivatePacket::build(npcGuid);
owner_.socket->send(packet);
owner_.resurrectPending_ = true;
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x", std::hex, npcGuid, std::dec);
}
void CombatHandler::acceptResurrect() {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
if (owner_.resurrectIsSpiritHealer_) {
auto activate = SpiritHealerActivatePacket::build(owner_.resurrectCasterGuid_);
owner_.socket->send(activate);
LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
} else {
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, true);
owner_.socket->send(resp);
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (accept) for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
}
owner_.resurrectRequestPending_ = false;
owner_.resurrectPending_ = true;
}
void CombatHandler::declineResurrect() {
if (owner_.state != WorldState::IN_WORLD || !owner_.socket || !owner_.resurrectRequestPending_) return;
auto resp = ResurrectResponsePacket::build(owner_.resurrectCasterGuid_, false);
owner_.socket->send(resp);
LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (decline) for 0x",
std::hex, owner_.resurrectCasterGuid_, std::dec);
owner_.resurrectRequestPending_ = false;
}
// ============================================================
// XP
// ============================================================
uint32_t CombatHandler::killXp(uint32_t playerLevel, uint32_t victimLevel) {
if (playerLevel == 0 || victimLevel == 0) return 0;
int32_t grayLevel;
if (playerLevel <= 5) grayLevel = 0;
else if (playerLevel <= 39) grayLevel = static_cast<int32_t>(playerLevel) - 5 - static_cast<int32_t>(playerLevel) / 10;
else if (playerLevel <= 59) grayLevel = static_cast<int32_t>(playerLevel) - 1 - static_cast<int32_t>(playerLevel) / 5;
else grayLevel = static_cast<int32_t>(playerLevel) - 9;
if (static_cast<int32_t>(victimLevel) <= grayLevel) return 0;
uint32_t baseXp = 45 + 5 * victimLevel;
int32_t diff = static_cast<int32_t>(victimLevel) - static_cast<int32_t>(playerLevel);
float multiplier = 1.0f + diff * 0.05f;
if (multiplier < 0.1f) multiplier = 0.1f;
if (multiplier > 2.0f) multiplier = 2.0f;
return static_cast<uint32_t>(baseXp * multiplier);
}
void CombatHandler::handleXpGain(network::Packet& packet) {
XpGainData data;
if (!XpGainParser::parse(packet, data)) return;
addCombatText(CombatTextEntry::XP_GAIN, static_cast<int32_t>(data.totalXp), 0, true);
std::string msg;
if (data.victimGuid != 0 && data.type == 0) {
std::string victimName = owner_.lookupName(data.victimGuid);
if (!victimName.empty())
msg = victimName + " dies, you gain " + std::to_string(data.totalXp) + " experience.";
else
msg = "You gain " + std::to_string(data.totalXp) + " experience.";
} else {
msg = "You gain " + std::to_string(data.totalXp) + " experience.";
}
if (data.groupBonus > 0) {
msg += " (+" + std::to_string(data.groupBonus) + " group bonus)";
}
owner_.addSystemChatMessage(msg);
owner_.fireAddonEvent("CHAT_MSG_COMBAT_XP_GAIN", {msg, std::to_string(data.totalXp)});
}
} // namespace game
} // namespace wowee