Fix classic combat desync and separate attack intent from confirmed combat

Combat state/UI:

- Split attack intent from server-confirmed auto attack in GameHandler.

- Add explicit combat state helpers (intent, confirmed, combat-with-target).

- Update player/target frame and renderer in-combat indicators to use confirmed combat.

- Add intent-only visual state in UI to avoid false combat feedback.

Animation correctness:

- Correct combat-ready animation IDs to canonical ready stances (22/23/24/25).

- Remove hit/wound-like stance behavior caused by incorrect ready IDs.

Classic/TBC/Turtle melee reliability:

- Tighten melee sync while attack intent is active: faster swing resend, tighter facing threshold, and faster facing cadence for classic-like expansions.

- Send immediate movement heartbeat after facing corrections and during stationary melee sync windows.

- Handle melee error opcodes explicitly (NOTINRANGE/BADFACING/NOTSTANDING/CANT_ATTACK) and immediately resync facing/swing where appropriate.

- On ATTACKSTOP with persistent intent, immediately reassert ATTACKSWING.

- Increase movement heartbeat cadence during active classic-like melee intent.

Server compatibility/anti-cheat stability:

- Restrict legacy force-movement/speed/teleport ACK behavior for classic-like flows to avoid unsolicited order acknowledgements.

- Preserve local movement state updates while preventing bad-order ACK noise.
This commit is contained in:
Kelsi 2026-02-20 03:38:12 -08:00
parent cb044e3985
commit eaba378b5b
4 changed files with 129 additions and 48 deletions

View file

@ -413,6 +413,14 @@ public:
void startAutoAttack(uint64_t targetGuid); void startAutoAttack(uint64_t targetGuid);
void stopAutoAttack(); void stopAutoAttack();
bool isAutoAttacking() const { return autoAttacking; } bool isAutoAttacking() const { return autoAttacking; }
bool hasAutoAttackIntent() const { return autoAttackRequested_; }
bool isInCombat() const { return autoAttacking || !hostileAttackers_.empty(); }
bool isInCombatWith(uint64_t guid) const {
return guid != 0 &&
((autoAttacking && autoAttackTarget == guid) ||
(hostileAttackers_.count(guid) > 0));
}
uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; }
bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; } const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
void updateCombatText(float deltaTime); void updateCombatText(float deltaTime);
@ -1359,6 +1367,7 @@ private:
// ---- Phase 2: Combat ---- // ---- Phase 2: Combat ----
bool autoAttacking = false; bool autoAttacking = false;
bool autoAttackRequested_ = false; // local intent (CMSG_ATTACKSWING sent)
uint64_t autoAttackTarget = 0; uint64_t autoAttackTarget = 0;
bool autoAttackOutOfRange_ = false; bool autoAttackOutOfRange_ = false;
float autoAttackResendTimer_ = 0.0f; // Re-send CMSG_ATTACKSWING every ~1s while attacking float autoAttackResendTimer_ = 0.0f; // Re-send CMSG_ATTACKSWING every ~1s while attacking

View file

@ -89,6 +89,10 @@ bool isActiveExpansion(const char* expansionId) {
return profile->id == expansionId; return profile->id == expansionId;
} }
bool isClassicLikeExpansion() {
return isActiveExpansion("classic") || isActiveExpansion("turtle");
}
std::string formatCopperAmount(uint32_t amount) { std::string formatCopperAmount(uint32_t amount) {
uint32_t gold = amount / 10000; uint32_t gold = amount / 10000;
uint32_t silver = (amount / 100) % 100; uint32_t silver = (amount / 100) % 100;
@ -667,7 +671,11 @@ void GameHandler::update(float deltaTime) {
timeSinceLastPing = 0.0f; timeSinceLastPing = 0.0f;
} }
float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_) ? 0.25f : moveHeartbeatInterval_; const bool classicLikeCombatSync =
autoAttackRequested_ && (isClassicLikeExpansion() || isActiveExpansion("tbc"));
float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_)
? 0.25f
: (classicLikeCombatSync ? 0.05f : moveHeartbeatInterval_);
if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) { if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) {
sendMovement(Opcode::MSG_MOVE_HEARTBEAT); sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
timeSinceLastMoveHeartbeat_ = 0.0f; timeSinceLastMoveHeartbeat_ = 0.0f;
@ -865,28 +873,37 @@ void GameHandler::update(float deltaTime) {
auto distanceStart = std::chrono::high_resolution_clock::now(); auto distanceStart = std::chrono::high_resolution_clock::now();
// Leave combat if auto-attack target is too far away (leash range) // Leave combat if auto-attack target is too far away (leash range)
// Also re-send CMSG_ATTACKSWING every second to resume after server SMSG_ATTACKSTOP // and keep melee intent tightly synced while stationary.
if (autoAttacking && autoAttackTarget != 0) { if (autoAttackRequested_ && autoAttackTarget != 0) {
auto targetEntity = entityManager.getEntity(autoAttackTarget); auto targetEntity = entityManager.getEntity(autoAttackTarget);
if (targetEntity) { if (targetEntity) {
float dx = movementInfo.x - targetEntity->getX(); float dx = movementInfo.x - targetEntity->getX();
float dy = movementInfo.y - targetEntity->getY(); float dy = movementInfo.y - targetEntity->getY();
float dist = std::sqrt(dx * dx + dy * dy); float dist = std::sqrt(dx * dx + dy * dy);
const bool classicLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (dist > 40.0f) { if (dist > 40.0f) {
stopAutoAttack(); stopAutoAttack();
LOG_INFO("Left combat: target too far (", dist, " yards)"); LOG_INFO("Left combat: target too far (", dist, " yards)");
} else if (state == WorldState::IN_WORLD && socket) { } else if (state == WorldState::IN_WORLD && socket) {
autoAttackResendTimer_ += deltaTime; autoAttackResendTimer_ += deltaTime;
autoAttackFacingSyncTimer_ += deltaTime; autoAttackFacingSyncTimer_ += deltaTime;
if (autoAttackResendTimer_ >= 1.0f) {
// Re-request swing more aggressively until server confirms active loop.
float resendInterval = 1.0f;
if (!autoAttacking || autoAttackOutOfRange_) {
resendInterval = classicLike ? 0.25f : 0.50f;
}
if (autoAttackResendTimer_ >= resendInterval) {
autoAttackResendTimer_ = 0.0f; autoAttackResendTimer_ = 0.0f;
auto pkt = AttackSwingPacket::build(autoAttackTarget); auto pkt = AttackSwingPacket::build(autoAttackTarget);
socket->send(pkt); socket->send(pkt);
} }
// Keep server-facing aligned with our current melee target. // Keep server-facing aligned with our current melee target.
// Some vanilla-family realms become strict about front-arc checks unless // Some vanilla-family realms become strict about front-arc checks unless
// the client sends explicit facing updates while stationary. // the client sends explicit facing updates while stationary.
if (autoAttackFacingSyncTimer_ >= 0.20f) { const float facingSyncInterval = classicLike ? 0.10f : 0.20f;
if (autoAttackFacingSyncTimer_ >= facingSyncInterval) {
autoAttackFacingSyncTimer_ = 0.0f; autoAttackFacingSyncTimer_ = 0.0f;
float toTargetX = targetEntity->getX() - movementInfo.x; float toTargetX = targetEntity->getX() - movementInfo.x;
float toTargetY = targetEntity->getY() - movementInfo.y; float toTargetY = targetEntity->getY() - movementInfo.y;
@ -895,10 +912,16 @@ void GameHandler::update(float deltaTime) {
float diff = desired - movementInfo.orientation; float diff = desired - 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);
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);
if (std::abs(diff) > 0.12f) { // ~7 degrees const float facingThreshold = classicLike ? 0.035f : 0.12f; // ~2deg / ~7deg
if (std::abs(diff) > facingThreshold) {
movementInfo.orientation = desired; movementInfo.orientation = desired;
sendMovement(Opcode::MSG_MOVE_SET_FACING); sendMovement(Opcode::MSG_MOVE_SET_FACING);
// Follow facing update with a heartbeat to tighten server range/facing checks.
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
} }
} else if (classicLike) {
// Keep stationary melee position/facing fresh for strict vanilla-family checks.
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
} }
} }
} }
@ -1521,6 +1544,35 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_ATTACKSTOP: case Opcode::SMSG_ATTACKSTOP:
handleAttackStop(packet); handleAttackStop(packet);
break; break;
case Opcode::SMSG_ATTACKSWING_NOTINRANGE:
autoAttackOutOfRange_ = true;
if (autoAttackRequested_ && autoAttackTarget != 0 && socket) {
auto pkt = AttackSwingPacket::build(autoAttackTarget);
socket->send(pkt);
}
break;
case Opcode::SMSG_ATTACKSWING_BADFACING:
if (autoAttackRequested_ && autoAttackTarget != 0) {
auto targetEntity = entityManager.getEntity(autoAttackTarget);
if (targetEntity) {
float toTargetX = targetEntity->getX() - movementInfo.x;
float toTargetY = targetEntity->getY() - movementInfo.y;
if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) {
movementInfo.orientation = std::atan2(-toTargetY, toTargetX);
sendMovement(Opcode::MSG_MOVE_SET_FACING);
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
}
}
if (socket) {
auto pkt = AttackSwingPacket::build(autoAttackTarget);
socket->send(pkt);
}
}
break;
case Opcode::SMSG_ATTACKSWING_NOTSTANDING:
case Opcode::SMSG_ATTACKSWING_CANT_ATTACK:
autoAttackOutOfRange_ = false;
break;
case Opcode::SMSG_ATTACKERSTATEUPDATE: case Opcode::SMSG_ATTACKERSTATEUPDATE:
handleAttackerStateUpdate(packet); handleAttackerStateUpdate(packet);
break; break;
@ -7813,7 +7865,10 @@ void GameHandler::startAutoAttack(uint64_t targetGuid) {
if (isMounted()) { if (isMounted()) {
dismount(); dismount();
} }
autoAttacking = true; autoAttackRequested_ = true;
// Keep combat animation/state server-authoritative. We only flip autoAttacking
// on SMSG_ATTACKSTART where attackerGuid == playerGuid.
autoAttacking = false;
autoAttackTarget = targetGuid; autoAttackTarget = targetGuid;
autoAttackOutOfRange_ = false; autoAttackOutOfRange_ = false;
autoAttackResendTimer_ = 0.0f; autoAttackResendTimer_ = 0.0f;
@ -7826,7 +7881,8 @@ void GameHandler::startAutoAttack(uint64_t targetGuid) {
} }
void GameHandler::stopAutoAttack() { void GameHandler::stopAutoAttack() {
if (!autoAttacking) return; if (!autoAttacking && !autoAttackRequested_) return;
autoAttackRequested_ = false;
autoAttacking = false; autoAttacking = false;
autoAttackTarget = 0; autoAttackTarget = 0;
autoAttackOutOfRange_ = false; autoAttackOutOfRange_ = false;
@ -7871,6 +7927,7 @@ void GameHandler::handleAttackStart(network::Packet& packet) {
if (!AttackStartParser::parse(packet, data)) return; if (!AttackStartParser::parse(packet, data)) return;
if (data.attackerGuid == playerGuid) { if (data.attackerGuid == playerGuid) {
autoAttackRequested_ = true;
autoAttacking = true; autoAttacking = true;
autoAttackTarget = data.victimGuid; autoAttackTarget = data.victimGuid;
} else if (data.victimGuid == playerGuid && data.attackerGuid != 0) { } else if (data.victimGuid == playerGuid && data.attackerGuid != 0) {
@ -7906,12 +7963,16 @@ void GameHandler::handleAttackStop(network::Packet& packet) {
AttackStopData data; AttackStopData data;
if (!AttackStopParser::parse(packet, data)) return; if (!AttackStopParser::parse(packet, data)) return;
// Don't clear autoAttacking on SMSG_ATTACKSTOP - the server sends this // Keep intent, but clear server-confirmed active state until ATTACKSTART resumes.
// when the attack loop pauses (out of range, etc). The player's intent
// to attack persists until target dies or player explicitly cancels.
// We'll re-send CMSG_ATTACKSWING periodically in the update loop.
if (data.attackerGuid == playerGuid) { if (data.attackerGuid == playerGuid) {
autoAttacking = false;
LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)"); LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)");
if (autoAttackRequested_ && autoAttackTarget != 0 && socket) {
// Classic-family servers may emit transient ATTACKSTOP when range/facing jitters.
// Reassert melee intent immediately instead of waiting for periodic resend.
auto pkt = AttackSwingPacket::build(autoAttackTarget);
socket->send(pkt);
}
} else if (data.victimGuid == playerGuid) { } else if (data.victimGuid == playerGuid) {
hostileAttackers_.erase(data.attackerGuid); hostileAttackers_.erase(data.attackerGuid);
} }
@ -7982,9 +8043,15 @@ void GameHandler::handleForceRunSpeedChange(network::Packet& packet) {
// Always ACK the speed change to prevent server stall. // Always ACK the speed change to prevent server stall.
// Packet format mirrors movement packets: packed guid + counter + movement info + new speed. // Packet format mirrors movement packets: packed guid + counter + movement info + new speed.
if (socket) { if (socket && !isClassicLikeExpansion()) {
network::Packet ack(wireOpcode(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK)); network::Packet ack(wireOpcode(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK));
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
ack.writeUInt64(playerGuid); // CMaNGOS expects full GUID for force speed ACKs
} else {
MovementPacket::writePackedGuid(ack, playerGuid); MovementPacket::writePackedGuid(ack, playerGuid);
}
ack.writeUInt32(counter); ack.writeUInt32(counter);
MovementInfo wire = movementInfo; MovementInfo wire = movementInfo;
@ -8055,13 +8122,19 @@ void GameHandler::handleForceMoveRootState(network::Packet& packet, bool rooted)
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ROOT); movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::ROOT);
} }
if (!socket) return; if (!socket || isClassicLikeExpansion()) return;
uint16_t ackWire = wireOpcode(rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK uint16_t ackWire = wireOpcode(rooted ? Opcode::CMSG_FORCE_MOVE_ROOT_ACK
: Opcode::CMSG_FORCE_MOVE_UNROOT_ACK); : Opcode::CMSG_FORCE_MOVE_UNROOT_ACK);
if (ackWire == 0xFFFF) return; if (ackWire == 0xFFFF) return;
network::Packet ack(ackWire); network::Packet ack(ackWire);
const bool legacyGuidAck =
isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
if (legacyGuidAck) {
ack.writeUInt64(playerGuid); // CMaNGOS expects full GUID for root/unroot ACKs
} else {
MovementPacket::writePackedGuid(ack, playerGuid); MovementPacket::writePackedGuid(ack, playerGuid);
}
ack.writeUInt32(counter); ack.writeUInt32(counter);
MovementInfo wire = movementInfo; MovementInfo wire = movementInfo;
@ -10927,24 +11000,14 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
// Send the ack back to the server // Send the ack back to the server
// Client→server MSG_MOVE_TELEPORT_ACK: u64 guid + u32 counter + u32 time // Client→server MSG_MOVE_TELEPORT_ACK: u64 guid + u32 counter + u32 time
if (socket) { if (socket && !isClassicLikeExpansion()) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK)); network::Packet ack(wireOpcode(Opcode::MSG_MOVE_TELEPORT_ACK));
// Write packed guid const bool legacyGuidAck =
uint8_t mask = 0; isActiveExpansion("classic") || isActiveExpansion("tbc") || isActiveExpansion("turtle");
uint8_t bytes[8]; if (legacyGuidAck) {
int byteCount = 0; ack.writeUInt64(playerGuid); // CMaNGOS expects full GUID for teleport ACK
uint64_t g = playerGuid; } else {
for (int i = 0; i < 8; i++) { MovementPacket::writePackedGuid(ack, playerGuid);
uint8_t b = static_cast<uint8_t>(g & 0xFF);
g >>= 8;
if (b != 0) {
mask |= (1 << i);
bytes[byteCount++] = b;
}
}
ack.writeUInt8(mask);
for (int i = 0; i < byteCount; i++) {
ack.writeUInt8(bytes[i]);
} }
ack.writeUInt32(counter); ack.writeUInt32(counter);
ack.writeUInt32(moveTime); ack.writeUInt32(moveTime);

View file

@ -960,9 +960,11 @@ void Renderer::updateCharacterAnimation() {
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle) constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim) constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount
constexpr uint32_t ANIM_READY_UNARMED = 7; // Combat ready stance (unarmed) // Canonical player ready stances (AnimationData.dbc)
constexpr uint32_t ANIM_READY_1H = 8; // Combat ready stance (1H weapon) constexpr uint32_t ANIM_READY_UNARMED = 22; // ReadyUnarmed
constexpr uint32_t ANIM_READY_2H = 9; // Combat ready stance (2H weapon) constexpr uint32_t ANIM_READY_1H = 23; // Ready1H
constexpr uint32_t ANIM_READY_2H = 24; // Ready2H
constexpr uint32_t ANIM_READY_2H_L = 25; // Ready2HL (some 2H left-handed rigs)
constexpr uint32_t ANIM_FLY_IDLE = 158; // Flying mount idle/hover constexpr uint32_t ANIM_FLY_IDLE = 158; // Flying mount idle/hover
constexpr uint32_t ANIM_FLY_FORWARD = 159; // Flying mount forward constexpr uint32_t ANIM_FLY_FORWARD = 159; // Flying mount forward
@ -1613,7 +1615,9 @@ void Renderer::updateCharacterAnimation() {
break; break;
case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break;
case CharAnimState::COMBAT_IDLE: case CharAnimState::COMBAT_IDLE:
animId = pickFirstAvailable({ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_UNARMED}, ANIM_STAND); animId = pickFirstAvailable(
{ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED},
ANIM_STAND);
loop = true; loop = true;
break; break;
case CharAnimState::CHARGE: case CharAnimState::CHARGE:

View file

@ -397,7 +397,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
// Update renderer face-target position and selection circle // Update renderer face-target position and selection circle
auto* renderer = core::Application::getInstance().getRenderer(); auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) { if (renderer) {
renderer->setInCombat(gameHandler.isAutoAttacking()); renderer->setInCombat(gameHandler.isInCombat());
static glm::vec3 targetGLPos; static glm::vec3 targetGLPos;
if (gameHandler.hasTarget()) { if (gameHandler.hasTarget()) {
auto target = gameHandler.getTarget(); auto target = gameHandler.getTarget();
@ -1535,11 +1535,15 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
const bool inCombatConfirmed = gameHandler.isInCombat();
const bool attackIntentOnly = gameHandler.hasAutoAttackIntent() && !inCombatConfirmed;
ImVec4 playerBorder = isDead ImVec4 playerBorder = isDead
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
: (gameHandler.isAutoAttacking() : (inCombatConfirmed
? ImVec4(1.0f, 0.2f, 0.2f, 1.0f) ? ImVec4(1.0f, 0.2f, 0.2f, 1.0f)
: ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); : (attackIntentOnly
? ImVec4(1.0f, 0.7f, 0.2f, 1.0f)
: ImVec4(0.4f, 0.4f, 0.4f, 1.0f)));
ImGui::PushStyleColor(ImGuiCol_Border, playerBorder); ImGui::PushStyleColor(ImGuiCol_Border, playerBorder);
if (ImGui::Begin("##PlayerFrame", nullptr, flags)) { if (ImGui::Begin("##PlayerFrame", nullptr, flags)) {
@ -1678,18 +1682,19 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
bool isHostileTarget = gameHandler.isHostileAttacker(target->getGuid()); const uint64_t targetGuid = target->getGuid();
if (!isHostileTarget && target->getType() == game::ObjectType::UNIT) { const bool confirmedCombatWithTarget = gameHandler.isInCombatWith(targetGuid);
auto u = std::static_pointer_cast<game::Unit>(target); const bool intentTowardTarget =
isHostileTarget = u->isHostile(); gameHandler.hasAutoAttackIntent() &&
} gameHandler.getAutoAttackTargetGuid() == targetGuid &&
!confirmedCombatWithTarget;
ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f); ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f);
if (isHostileTarget) { if (confirmedCombatWithTarget) {
float t = ImGui::GetTime(); float t = ImGui::GetTime();
float pulse = (std::fmod(t, 0.6f) < 0.3f) ? 1.0f : 0.0f; float pulse = (std::fmod(t, 0.6f) < 0.3f) ? 1.0f : 0.0f;
borderColor = ImVec4(1.0f, 0.1f, 0.1f, pulse); borderColor = ImVec4(1.0f, 0.1f, 0.1f, pulse);
} else if (gameHandler.isAutoAttacking()) { } else if (intentTowardTarget) {
borderColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); borderColor = ImVec4(1.0f, 0.7f, 0.2f, 1.0f);
} }
ImGui::PushStyleColor(ImGuiCol_Border, borderColor); ImGui::PushStyleColor(ImGuiCol_Border, borderColor);