mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 00:03:50 +00:00
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:
parent
cb044e3985
commit
eaba378b5b
4 changed files with 129 additions and 48 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
MovementPacket::writePackedGuid(ack, playerGuid);
|
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);
|
||||||
|
}
|
||||||
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);
|
||||||
MovementPacket::writePackedGuid(ack, playerGuid);
|
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);
|
||||||
|
}
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue