diff --git a/src/core/application.cpp b/src/core/application.cpp index 67b373a9..c595ae57 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -823,6 +823,35 @@ void Application::update(float deltaTime) { } } + // Keep creature render instances aligned with authoritative entity positions. + // This prevents desync where target circles move with server entities but + // creature models remain at stale spawn positions. + if (renderer && gameHandler && renderer->getCharacterRenderer()) { + auto* charRenderer = renderer->getCharacterRenderer(); + glm::vec3 playerPos(0.0f); + bool havePlayerPos = false; + if (auto playerEntity = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid())) { + playerPos = glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()); + havePlayerPos = true; + } + const float syncRadiusSq = 320.0f * 320.0f; + for (const auto& [guid, instanceId] : creatureInstances_) { + auto entity = gameHandler->getEntityManager().getEntity(guid); + if (!entity || entity->getType() != game::ObjectType::UNIT) continue; + + glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); + if (havePlayerPos) { + glm::vec3 d = canonical - playerPos; + if (glm::dot(d, d) > syncRadiusSq) continue; + } + + glm::vec3 renderPos = core::coords::canonicalToRender(canonical); + charRenderer->setInstancePosition(instanceId, renderPos); + float renderYaw = entity->getOrientation() + glm::radians(90.0f); + charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw)); + } + } + // Movement heartbeat is sent from GameHandler::update() to avoid // duplicate packets from multiple update loops. diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f9bf9cab..7bebf3dc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3471,6 +3471,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; + constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; bool unitInitiallyDead = false; const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH); const uint16_t ufPower = fieldIndex(UF::UNIT_FIELD_POWER1); @@ -3501,7 +3502,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { else if (key == ufDynFlags) { unit->setDynamicFlags(val); if (block.objectType == ObjectType::UNIT && - (val & UNIT_DYNFLAG_DEAD) != 0) { + ((val & UNIT_DYNFLAG_DEAD) != 0 || (val & UNIT_DYNFLAG_LOOTABLE) != 0)) { unitInitiallyDead = true; } } @@ -3849,6 +3850,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; + constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; uint32_t oldDisplayId = unit->getDisplayId(); bool displayIdChanged = false; bool npcDeathNotified = false; @@ -3997,6 +3999,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } else if (creatureSpawnCallback_) { creatureSpawnCallback_(block.guid, unit->getDisplayId(), unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + bool isDeadNow = (unit->getHealth() == 0) || + ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); + if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { + npcDeathCallback_(block.guid); + npcDeathNotified = true; + } } if (entity->getType() == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); @@ -8490,27 +8498,6 @@ void GameHandler::handleLootResponse(network::Packet& packet) { } if (currentLoot.gold > 0) { - // Some servers don't send SMSG_LOOT_MONEY_NOTIFY consistently. - // Announce money immediately on loot response as a fallback. - auto it = localLootState_.find(currentLoot.lootGuid); - bool alreadyAnnounced = (it != localLootState_.end() && it->second.moneyTaken); - if (!alreadyAnnounced) { - addSystemChatMessage("Looted: " + formatCopperAmount(currentLoot.gold)); - auto* renderer = core::Application::getInstance().getRenderer(); - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { - if (currentLoot.gold >= 10000) { - sfx->playLootCoinLarge(); - } else { - sfx->playLootCoinSmall(); - } - } - } - if (it != localLootState_.end()) { - it->second.moneyTaken = true; - } - } - if (state == WorldState::IN_WORLD && socket) { // Auto-loot gold by sending CMSG_LOOT_MONEY (server handles the rest) auto pkt = LootMoneyPacket::build();