mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Fix combat facing, tab-target filtering, and spirit healer resurrection
- Add Entity::setOrientation() to update facing without cancelling movement - Force attacker and victim to face each other on SMSG_ATTACKSTART - Fix orientation sign error in MonsterMove: use atan2(-dy, dx) throughout so NPCs don't glide backward; clamp FacingAngle moves that are >90° off travel vector - Tab-target: skip dead units and non-hostiles at both build and advance time; stale entries (killed between presses) are skipped inline rather than cycling to them - Spirit healer resurrection: detect same-map SMSG_NEW_WORLD with resurrectPending_ and skip the full world reload/entity clear, preventing the fall-forever bug
This commit is contained in:
parent
24be81c679
commit
2dffba63d8
2 changed files with 111 additions and 14 deletions
|
|
@ -66,6 +66,8 @@ public:
|
||||||
float getY() const { return y; }
|
float getY() const { return y; }
|
||||||
float getZ() const { return z; }
|
float getZ() const { return z; }
|
||||||
float getOrientation() const { return orientation; }
|
float getOrientation() const { return orientation; }
|
||||||
|
// Update orientation only, without disrupting an in-progress movement interpolation.
|
||||||
|
void setOrientation(float o) { orientation = o; }
|
||||||
|
|
||||||
void setPosition(float px, float py, float pz, float o) {
|
void setPosition(float px, float py, float pz, float o) {
|
||||||
x = px;
|
x = px;
|
||||||
|
|
|
||||||
|
|
@ -5688,26 +5688,33 @@ void GameHandler::declineResurrect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
||||||
// Rebuild cycle list if stale
|
// 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;
|
||||||
|
auto* unit = dynamic_cast<Unit*>(e.get());
|
||||||
|
if (!unit) return false; // Not a unit (shouldn't happen after type filter)
|
||||||
|
if (unit->getHealth() == 0) return false; // Dead / corpse
|
||||||
|
if (!unit->isHostile()) return false; // Friendly
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rebuild cycle list if stale (entity added/removed since last tab press).
|
||||||
if (tabCycleStale) {
|
if (tabCycleStale) {
|
||||||
tabCycleList.clear();
|
tabCycleList.clear();
|
||||||
tabCycleIndex = -1;
|
tabCycleIndex = -1;
|
||||||
|
|
||||||
struct EntityDist {
|
struct EntityDist { uint64_t guid; float distance; };
|
||||||
uint64_t guid;
|
|
||||||
float distance;
|
|
||||||
};
|
|
||||||
std::vector<EntityDist> sortable;
|
std::vector<EntityDist> sortable;
|
||||||
|
|
||||||
for (const auto& [guid, entity] : entityManager.getEntities()) {
|
for (const auto& [guid, entity] : entityManager.getEntities()) {
|
||||||
auto t = entity->getType();
|
auto t = entity->getType();
|
||||||
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
|
||||||
if (guid == playerGuid) continue; // Don't tab-target self
|
if (guid == playerGuid) continue;
|
||||||
|
if (!isValidTabTarget(entity)) continue; // Skip dead / non-hostile
|
||||||
float dx = entity->getX() - playerX;
|
float dx = entity->getX() - playerX;
|
||||||
float dy = entity->getY() - playerY;
|
float dy = entity->getY() - playerY;
|
||||||
float dz = entity->getZ() - playerZ;
|
float dz = entity->getZ() - playerZ;
|
||||||
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
sortable.push_back({guid, std::sqrt(dx*dx + dy*dy + dz*dz)});
|
||||||
sortable.push_back({guid, dist});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(sortable.begin(), sortable.end(),
|
std::sort(sortable.begin(), sortable.end(),
|
||||||
|
|
@ -5724,8 +5731,22 @@ void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tabCycleIndex = (tabCycleIndex + 1) % static_cast<int>(tabCycleList.size());
|
// Advance through the cycle, skipping any entry that has since died or
|
||||||
setTarget(tabCycleList[tabCycleIndex]);
|
// turned friendly (e.g. NPC killed between two tab presses).
|
||||||
|
int tries = static_cast<int>(tabCycleList.size());
|
||||||
|
while (tries-- > 0) {
|
||||||
|
tabCycleIndex = (tabCycleIndex + 1) % static_cast<int>(tabCycleList.size());
|
||||||
|
uint64_t guid = tabCycleList[tabCycleIndex];
|
||||||
|
auto entity = entityManager.getEntity(guid);
|
||||||
|
if (isValidTabTarget(entity)) {
|
||||||
|
setTarget(guid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All cached entries are stale — clear target and force a fresh rebuild next time.
|
||||||
|
tabCycleStale = true;
|
||||||
|
clearTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
|
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
|
||||||
|
|
@ -6763,6 +6784,20 @@ void GameHandler::handleAttackStart(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
auto attackerEnt = entityManager.getEntity(data.attackerGuid);
|
||||||
|
auto victimEnt = entityManager.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 GameHandler::handleAttackStop(network::Packet& packet) {
|
void GameHandler::handleAttackStop(network::Packet& packet) {
|
||||||
|
|
@ -7217,21 +7252,45 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
// FacingAngle - server specifies exact angle
|
// FacingAngle - server specifies exact angle
|
||||||
orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
|
||||||
} else if (data.moveType == 3) {
|
} else if (data.moveType == 3) {
|
||||||
// FacingTarget - face toward the target entity
|
// FacingTarget - face toward the target entity.
|
||||||
|
// Canonical orientation uses atan2(-dy, dx): the West/Y component
|
||||||
|
// must be negated because renderYaw = orientation + 90° and
|
||||||
|
// model-forward = render +X, so the sign convention flips.
|
||||||
auto target = entityManager.getEntity(data.facingTarget);
|
auto target = entityManager.getEntity(data.facingTarget);
|
||||||
if (target) {
|
if (target) {
|
||||||
float dx = target->getX() - entity->getX();
|
float dx = target->getX() - entity->getX();
|
||||||
float dy = target->getY() - entity->getY();
|
float dy = target->getY() - entity->getY();
|
||||||
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
orientation = std::atan2(dy, dx);
|
orientation = std::atan2(-dy, dx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal move - face toward destination
|
// Normal move - face toward destination.
|
||||||
float dx = destCanonical.x - entity->getX();
|
float dx = destCanonical.x - entity->getX();
|
||||||
float dy = destCanonical.y - entity->getY();
|
float dy = destCanonical.y - entity->getY();
|
||||||
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
|
||||||
orientation = std::atan2(dy, dx);
|
orientation = std::atan2(-dy, dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti-backward-glide: if the computed orientation is more than 90° away from
|
||||||
|
// the actual travel direction, snap to the travel direction. FacingTarget
|
||||||
|
// (moveType 3) is deliberately different from travel dir, so skip it there.
|
||||||
|
if (data.moveType != 3) {
|
||||||
|
glm::vec3 startCanonical = core::coords::serverToCanonical(
|
||||||
|
glm::vec3(data.x, data.y, data.z));
|
||||||
|
float travelDx = destCanonical.x - startCanonical.x;
|
||||||
|
float travelDy = destCanonical.y - startCanonical.y;
|
||||||
|
float travelLen = std::sqrt(travelDx * travelDx + travelDy * travelDy);
|
||||||
|
if (travelLen > 0.5f) {
|
||||||
|
float travelAngle = std::atan2(-travelDy, travelDx);
|
||||||
|
float diff = orientation - travelAngle;
|
||||||
|
// Normalise diff to [-π, π]
|
||||||
|
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) > static_cast<float>(M_PI) * 0.5f) {
|
||||||
|
orientation = travelAngle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9270,6 +9329,42 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
||||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
||||||
" orient=", orientation);
|
" orient=", orientation);
|
||||||
|
|
||||||
|
// Detect same-map spirit healer resurrection: the server uses SMSG_NEW_WORLD
|
||||||
|
// to reposition the player at the graveyard on the same map. A full world
|
||||||
|
// reload is not needed and causes terrain to vanish, making the player fall
|
||||||
|
// forever. Just reposition and send the ack.
|
||||||
|
const bool isSameMap = (mapId == currentMapId_);
|
||||||
|
const bool isResurrection = resurrectPending_;
|
||||||
|
if (isSameMap && isResurrection) {
|
||||||
|
LOG_INFO("SMSG_NEW_WORLD same-map resurrection — skipping world reload");
|
||||||
|
|
||||||
|
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ));
|
||||||
|
movementInfo.x = canonical.x;
|
||||||
|
movementInfo.y = canonical.y;
|
||||||
|
movementInfo.z = canonical.z;
|
||||||
|
movementInfo.orientation = core::coords::serverToCanonicalYaw(orientation);
|
||||||
|
movementInfo.flags = 0;
|
||||||
|
movementInfo.flags2 = 0;
|
||||||
|
|
||||||
|
resurrectPending_ = false;
|
||||||
|
resurrectRequestPending_ = false;
|
||||||
|
releasedSpirit_ = false;
|
||||||
|
playerDead_ = false;
|
||||||
|
repopPending_ = false;
|
||||||
|
pendingSpiritHealerGuid_ = 0;
|
||||||
|
resurrectCasterGuid_ = 0;
|
||||||
|
hostileAttackers_.clear();
|
||||||
|
stopAutoAttack();
|
||||||
|
tabCycleStale = true;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
|
||||||
|
socket->send(ack);
|
||||||
|
LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK (resurrection)");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
currentMapId_ = mapId;
|
currentMapId_ = mapId;
|
||||||
|
|
||||||
// Update player position
|
// Update player position
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue