diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 53029471..58c49af3 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -466,6 +466,24 @@ public: bool canUseWeaponSubclass(uint32_t subClass) const { return (weaponProficiency_ >> subClass) & 1u; } bool canUseArmorSubclass(uint32_t subClass) const { return (armorProficiency_ >> subClass) & 1u; } + // Minimap pings from party members + struct MinimapPing { + uint64_t senderGuid = 0; + float wowX = 0.0f; // canonical WoW X (north) + float wowY = 0.0f; // canonical WoW Y (west) + float age = 0.0f; // seconds since received + static constexpr float LIFETIME = 5.0f; + bool isExpired() const { return age >= LIFETIME; } + }; + const std::vector& getMinimapPings() const { return minimapPings_; } + void tickMinimapPings(float dt) { + for (auto& p : minimapPings_) p.age += dt; + minimapPings_.erase( + std::remove_if(minimapPings_.begin(), minimapPings_.end(), + [](const MinimapPing& p){ return p.isExpired(); }), + minimapPings_.end()); + } + bool isCasting() const { return casting; } bool isGameObjectInteractionCasting() const { return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0; @@ -1698,6 +1716,7 @@ private: std::unordered_map spellCooldowns; // spellId -> remaining seconds uint32_t weaponProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=2 uint32_t armorProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=4 + std::vector minimapPings_; uint8_t castCount = 0; bool casting = false; uint32_t currentCastSpellId = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0dec7072..ab206173 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -779,6 +779,7 @@ void GameHandler::update(float deltaTime) { // Update combat text (Phase 2) updateCombatText(deltaTime); + tickMinimapPings(deltaTime); // Update taxi landing cooldown if (taxiLandingCooldown_ > 0.0f) { @@ -2588,10 +2589,21 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_FISH_ESCAPED: addSystemChatMessage("Your fish escaped!"); break; - case Opcode::MSG_MINIMAP_PING: - // Minimap ping from a party member — consume; no visual support yet. - packet.setReadPos(packet.getSize()); + case Opcode::MSG_MINIMAP_PING: { + // SMSG: packed_guid + float posX (canonical WoW Y=west) + float posY (canonical WoW X=north) + if (packet.getSize() - packet.getReadPos() < 1) break; + uint64_t senderGuid = UpdateObjectParser::readPackedGuid(packet); + if (packet.getSize() - packet.getReadPos() < 8) break; + float pingX = packet.readFloat(); // server sends map-coord X (east-west) + float pingY = packet.readFloat(); // server sends map-coord Y (north-south) + MinimapPing ping; + ping.senderGuid = senderGuid; + ping.wowX = pingY; // canonical WoW X = north = server's posY + ping.wowY = pingX; // canonical WoW Y = west = server's posX + ping.age = 0.0f; + minimapPings_.push_back(ping); break; + } case Opcode::SMSG_ZONE_UNDER_ATTACK: { // uint32 areaId if (packet.getSize() - packet.getReadPos() >= 4) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f5ab928e..84d8cb1c 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8040,6 +8040,25 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } + // Minimap pings from party members + for (const auto& ping : gameHandler.getMinimapPings()) { + glm::vec3 pingRender = core::coords::canonicalToRender(glm::vec3(ping.wowX, ping.wowY, 0.0f)); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(pingRender, sx, sy)) continue; + + float t = ping.age / game::GameHandler::MinimapPing::LIFETIME; + float alpha = 1.0f - t; + float pulse = 1.0f + 1.5f * t; // expands outward as it fades + + ImU32 col = IM_COL32(255, 220, 0, static_cast(alpha * 200)); + ImU32 col2 = IM_COL32(255, 150, 0, static_cast(alpha * 100)); + float r1 = 4.0f * pulse; + float r2 = 8.0f * pulse; + drawList->AddCircle(ImVec2(sx, sy), r1, col, 16, 2.0f); + drawList->AddCircle(ImVec2(sx, sy), r2, col2, 16, 1.0f); + drawList->AddCircleFilled(ImVec2(sx, sy), 2.5f, col); + } + auto applyMuteState = [&]() { auto* activeRenderer = core::Application::getInstance().getRenderer(); float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f;