Handle gossip POI, combat clearing, dismount, spell log miss, and loot notifications

- SMSG_GOSSIP_POI: parse map POI markers (x/y/icon/name) from quest NPCs, render as
  cyan diamonds on the minimap with hover tooltips for quest navigation
- SMSG_ATTACKSWING_DEADTARGET: clear auto-attack when target dies mid-swing
- SMSG_CANCEL_COMBAT: server-side combat reset (clears autoAttacking + target)
- SMSG_BREAK_TARGET / SMSG_CLEAR_TARGET: server-side targeting clears
- SMSG_DISMOUNT: server-forced dismount triggers mountCallback(0)
- SMSG_MOUNTRESULT / SMSG_DISMOUNTRESULT: mount feedback in system chat
- SMSG_LOOT_ALL_PASSED: "Everyone passed on [Item]" system message, clears loot roll
- SMSG_LOOT_ITEM_NOTIFY / SMSG_LOOT_SLOT_CHANGED: consumed
- SMSG_SPELLLOGMISS: decode miss/dodge/parry/block from spell casts into combat text
- SMSG_ENVIRONMENTALDAMAGELOG: environmental damage (drowning/lava/fall) in combat text
- GossipPoi struct + gossipPois_ vector in GameHandler with public getters/clearers
This commit is contained in:
Kelsi 2026-03-09 14:38:45 -07:00
parent bd3bd1b5a6
commit f89840a6aa
3 changed files with 189 additions and 0 deletions

View file

@ -825,6 +825,17 @@ public:
bool isQuestDetailsOpen() const { return questDetailsOpen; }
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
// Gossip / quest map POI markers (SMSG_GOSSIP_POI)
struct GossipPoi {
float x = 0.0f; // WoW canonical X (north)
float y = 0.0f; // WoW canonical Y (west)
uint32_t icon = 0; // POI icon type
uint32_t data = 0;
std::string name;
};
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
void clearGossipPois() { gossipPois_.clear(); }
// Quest turn-in
bool isQuestRequestItemsOpen() const { return questRequestItemsOpen_; }
const QuestRequestItemsData& getQuestRequestItems() const { return currentQuestRequestItems_; }
@ -1778,6 +1789,7 @@ private:
// Gossip
bool gossipWindowOpen = false;
GossipMessageData currentGossip;
std::vector<GossipPoi> gossipPois_;
void performGameObjectInteractionNow(uint64_t guid);

View file

@ -1819,6 +1819,157 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
// ---- Gossip POI (quest map markers) ----
case Opcode::SMSG_GOSSIP_POI: {
// uint32 flags + float x + float y + uint32 icon + uint32 data + string name
if (packet.getSize() - packet.getReadPos() < 20) break;
/*uint32_t flags =*/ packet.readUInt32();
float poiX = packet.readFloat(); // WoW canonical coords
float poiY = packet.readFloat();
uint32_t icon = packet.readUInt32();
uint32_t data = packet.readUInt32();
std::string name = packet.readString();
GossipPoi poi;
poi.x = poiX;
poi.y = poiY;
poi.icon = icon;
poi.data = data;
poi.name = std::move(name);
gossipPois_.push_back(std::move(poi));
LOG_DEBUG("SMSG_GOSSIP_POI: x=", poiX, " y=", poiY, " icon=", icon);
break;
}
// ---- Combat clearing ----
case Opcode::SMSG_ATTACKSWING_DEADTARGET:
// Target died mid-swing: clear auto-attack
autoAttacking = false;
autoAttackTarget = 0;
break;
case Opcode::SMSG_CANCEL_COMBAT:
// Server-side combat state reset
autoAttacking = false;
autoAttackTarget = 0;
autoAttackRequested_ = false;
break;
case Opcode::SMSG_BREAK_TARGET:
// Server breaking our targeting (PvP flag, etc.)
// uint64 guid — consume; target cleared if it matches
if (packet.getSize() - packet.getReadPos() >= 8) {
uint64_t bGuid = packet.readUInt64();
if (bGuid == targetGuid) targetGuid = 0;
}
break;
case Opcode::SMSG_CLEAR_TARGET:
// uint64 guid — server cleared targeting on a unit (or 0 = clear all)
if (packet.getSize() - packet.getReadPos() >= 8) {
uint64_t cGuid = packet.readUInt64();
if (cGuid == 0 || cGuid == targetGuid) targetGuid = 0;
}
break;
// ---- Server-forced dismount ----
case Opcode::SMSG_DISMOUNT:
// No payload — server forcing dismount
currentMountDisplayId_ = 0;
if (mountCallback_) mountCallback_(0);
break;
case Opcode::SMSG_MOUNTRESULT: {
// uint32 result: 0=error, 1=invalid, 2=not in range, 3=already mounted, 4=ok
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t result = packet.readUInt32();
if (result != 4) {
const char* msgs[] = { "Cannot mount here.", "Invalid mount spell.", "Too far away to mount.", "Already mounted." };
addSystemChatMessage(result < 4 ? msgs[result] : "Cannot mount.");
}
break;
}
case Opcode::SMSG_DISMOUNTRESULT: {
// uint32 result: 0=ok, others=error
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t result = packet.readUInt32();
if (result != 0) addSystemChatMessage("Cannot dismount here.");
break;
}
// ---- Loot notifications ----
case Opcode::SMSG_LOOT_ALL_PASSED: {
// uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId
if (packet.getSize() - packet.getReadPos() < 24) break;
/*uint64_t objGuid =*/ packet.readUInt64();
/*uint32_t slot =*/ packet.readUInt32();
uint32_t itemId = packet.readUInt32();
auto* info = getItemInfo(itemId);
char buf[256];
std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].",
info ? info->name.c_str() : std::to_string(itemId).c_str());
addSystemChatMessage(buf);
pendingLootRollActive_ = false;
break;
}
case Opcode::SMSG_LOOT_ITEM_NOTIFY:
// uint64 looterGuid + uint64 lootGuid + uint32 itemId + uint32 count — consume
packet.setReadPos(packet.getSize());
break;
case Opcode::SMSG_LOOT_SLOT_CHANGED:
// uint64 objectGuid + uint32 slot + ... — consume
packet.setReadPos(packet.getSize());
break;
// ---- Spell log miss ----
case Opcode::SMSG_SPELLLOGMISS: {
// packed_guid caster + packed_guid target + uint8 isCrit + uint32 count
// + count × (uint64 victimGuid + uint8 missInfo)
if (packet.getSize() - packet.getReadPos() < 2) break;
uint64_t casterGuid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 2) break;
/*uint64_t targetGuidLog =*/ UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 5) break;
/*uint8_t isCrit =*/ packet.readUInt8();
uint32_t count = packet.readUInt32();
count = std::min(count, 32u);
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 9; ++i) {
/*uint64_t victimGuid =*/ packet.readUInt64();
uint8_t missInfo = packet.readUInt8();
// Show combat text only for local player's spell misses
if (casterGuid == playerGuid) {
static const CombatTextEntry::Type missTypes[] = {
CombatTextEntry::MISS, // 0=MISS
CombatTextEntry::DODGE, // 1=DODGE
CombatTextEntry::PARRY, // 2=PARRY
CombatTextEntry::BLOCK, // 3=BLOCK
CombatTextEntry::MISS, // 4=EVADE → show as MISS
CombatTextEntry::MISS, // 5=IMMUNE → show as MISS
CombatTextEntry::MISS, // 6=DEFLECT
CombatTextEntry::MISS, // 7=ABSORB
CombatTextEntry::MISS, // 8=RESIST
};
CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] : CombatTextEntry::MISS;
addCombatText(ct, 0, 0, true);
}
}
break;
}
// ---- Environmental damage log ----
case Opcode::SMSG_ENVIRONMENTALDAMAGELOG: {
// uint64 victimGuid + uint8 envDamageType + uint32 damage + uint32 absorb + uint32 resist
if (packet.getSize() - packet.getReadPos() < 21) break;
uint64_t victimGuid = packet.readUInt64();
/*uint8_t envType =*/ packet.readUInt8();
uint32_t damage = packet.readUInt32();
/*uint32_t absorb =*/ packet.readUInt32();
/*uint32_t resist =*/ packet.readUInt32();
if (victimGuid == playerGuid && damage > 0) {
addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast<int32_t>(damage), 0, false);
}
break;
}
// ---- Creature Movement ----
case Opcode::SMSG_MONSTER_MOVE:
handleMonsterMove(packet);

View file

@ -7378,6 +7378,32 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
IM_COL32(0, 0, 0, 255), marker);
}
// Gossip POI markers (quest / NPC navigation targets)
for (const auto& poi : gameHandler.getGossipPois()) {
// Convert WoW canonical coords to render coords for minimap projection
glm::vec3 poiRender = core::coords::canonicalToRender(glm::vec3(poi.x, poi.y, 0.0f));
float sx = 0.0f, sy = 0.0f;
if (!projectToMinimap(poiRender, sx, sy)) continue;
// Draw as a cyan diamond with tooltip on hover
const float d = 5.0f;
ImVec2 pts[4] = {
{ sx, sy - d },
{ sx + d, sy },
{ sx, sy + d },
{ sx - d, sy },
};
drawList->AddConvexPolyFilled(pts, 4, IM_COL32(0, 210, 255, 220));
drawList->AddPolyline(pts, 4, IM_COL32(255, 255, 255, 160), true, 1.0f);
// Show name label if cursor is within ~8px
ImVec2 cursorPos = ImGui::GetMousePos();
float dx = cursorPos.x - sx, dy = cursorPos.y - sy;
if (!poi.name.empty() && (dx * dx + dy * dy) < 64.0f) {
ImGui::SetTooltip("%s", poi.name.c_str());
}
}
auto applyMuteState = [&]() {
auto* activeRenderer = core::Application::getInstance().getRenderer();
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;