mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 16:30:15 +00:00
Add quest markers (! and ?) above NPCs and on minimap
Parse SMSG_QUESTGIVER_STATUS and SMSG_QUESTGIVER_STATUS_MULTIPLE packets to track per-NPC quest status, render yellow/gray ! and ? markers in 3D world space above NPC heads with distance-based scaling, and show corresponding dots on the minimap.
This commit is contained in:
parent
46407cfdeb
commit
1fe5fffc33
5 changed files with 224 additions and 2 deletions
|
|
@ -20,6 +20,19 @@ namespace network { class WorldSocket; class Packet; }
|
|||
|
||||
namespace game {
|
||||
|
||||
/**
|
||||
* Quest giver status values (WoW 3.3.5a)
|
||||
*/
|
||||
enum class QuestGiverStatus : uint8_t {
|
||||
NONE = 0,
|
||||
UNAVAILABLE = 1,
|
||||
INCOMPLETE = 5, // ? (gray)
|
||||
REWARD_REP = 6,
|
||||
AVAILABLE_LOW = 7, // ! (gray, low-level)
|
||||
AVAILABLE = 8, // ! (yellow)
|
||||
REWARD = 10 // ? (yellow)
|
||||
};
|
||||
|
||||
/**
|
||||
* World connection state
|
||||
*/
|
||||
|
|
@ -360,6 +373,13 @@ public:
|
|||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||
void abandonQuest(uint32_t questId);
|
||||
|
||||
// Quest giver status (! and ? markers)
|
||||
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
|
||||
auto it = npcQuestStatus_.find(guid);
|
||||
return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE;
|
||||
}
|
||||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
||||
|
||||
// Vendor
|
||||
void openVendor(uint64_t npcGuid);
|
||||
void closeVendor();
|
||||
|
|
@ -672,6 +692,9 @@ private:
|
|||
// Quest log
|
||||
std::vector<QuestLogEntry> questLog_;
|
||||
|
||||
// Quest giver status per NPC
|
||||
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
|
||||
|
||||
// Faction hostility lookup (populated from FactionTemplate.dbc)
|
||||
std::unordered_map<uint32_t, bool> factionHostileMap_;
|
||||
bool isHostileFaction(uint32_t factionTemplateId) const {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ enum class Opcode : uint16_t {
|
|||
// ---- Phase 5: Quests ----
|
||||
CMSG_QUESTGIVER_STATUS_QUERY = 0x182,
|
||||
SMSG_QUESTGIVER_STATUS = 0x183,
|
||||
SMSG_QUESTGIVER_STATUS_MULTIPLE = 0x198,
|
||||
CMSG_QUESTGIVER_HELLO = 0x184,
|
||||
CMSG_QUESTGIVER_QUERY_QUEST = 0x186,
|
||||
SMSG_QUESTGIVER_QUEST_DETAILS = 0x188,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ private:
|
|||
void renderDeathScreen(game::GameHandler& gameHandler);
|
||||
void renderEscapeMenu();
|
||||
void renderSettingsWindow();
|
||||
void renderQuestMarkers(game::GameHandler& gameHandler);
|
||||
void renderMinimapMarkers(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
|
|||
|
|
@ -1228,9 +1228,31 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::SMSG_BUY_FAILED:
|
||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||
case Opcode::SMSG_QUESTGIVER_STATUS:
|
||||
LOG_DEBUG("Ignoring SMSG_QUESTGIVER_STATUS");
|
||||
break;
|
||||
case Opcode::SMSG_QUESTGIVER_STATUS: {
|
||||
// uint64 npcGuid + uint8 status
|
||||
if (packet.getSize() - packet.getReadPos() >= 9) {
|
||||
uint64_t npcGuid = packet.readUInt64();
|
||||
uint8_t status = packet.readUInt8();
|
||||
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
|
||||
LOG_DEBUG("SMSG_QUESTGIVER_STATUS: guid=0x", std::hex, npcGuid, std::dec, " status=", (int)status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTGIVER_STATUS_MULTIPLE: {
|
||||
// uint32 count, then count * (uint64 guid + uint8 status)
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t count = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (packet.getSize() - packet.getReadPos() < 9) break;
|
||||
uint64_t npcGuid = packet.readUInt64();
|
||||
uint8_t status = packet.readUInt8();
|
||||
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
|
||||
}
|
||||
LOG_DEBUG("SMSG_QUESTGIVER_STATUS_MULTIPLE: ", count, " entries");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS:
|
||||
handleQuestDetails(packet);
|
||||
break;
|
||||
|
|
@ -2897,6 +2919,9 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
|
|||
rebuildOnlineInventory();
|
||||
}
|
||||
|
||||
// Clean up quest giver status
|
||||
npcQuestStatus_.erase(data.guid);
|
||||
|
||||
tabCycleStale = true;
|
||||
LOG_INFO("Entity count: ", entityManager.getEntityCount());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderGossipWindow(gameHandler);
|
||||
renderQuestDetailsWindow(gameHandler);
|
||||
renderVendorWindow(gameHandler);
|
||||
renderQuestMarkers(gameHandler);
|
||||
renderMinimapMarkers(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
renderEscapeMenu();
|
||||
renderSettingsWindow();
|
||||
|
|
@ -2723,4 +2725,173 @@ void GameScreen::renderSettingsWindow() {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) {
|
||||
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
||||
if (statuses.empty()) return;
|
||||
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
if (!camera || !window) return;
|
||||
|
||||
float screenW = static_cast<float>(window->getWidth());
|
||||
float screenH = static_cast<float>(window->getHeight());
|
||||
glm::mat4 viewProj = camera->getViewProjectionMatrix();
|
||||
auto* drawList = ImGui::GetForegroundDrawList();
|
||||
|
||||
for (const auto& [guid, status] : statuses) {
|
||||
// Only show markers for available (!) and reward/completable (?)
|
||||
const char* marker = nullptr;
|
||||
ImU32 color = IM_COL32(255, 210, 0, 255); // yellow
|
||||
if (status == game::QuestGiverStatus::AVAILABLE) {
|
||||
marker = "!";
|
||||
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
||||
marker = "!";
|
||||
color = IM_COL32(160, 160, 160, 255); // gray
|
||||
} else if (status == game::QuestGiverStatus::REWARD) {
|
||||
marker = "?";
|
||||
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
||||
marker = "?";
|
||||
color = IM_COL32(160, 160, 160, 255); // gray
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get entity position (canonical coords)
|
||||
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
||||
if (!entity) continue;
|
||||
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Get model height for offset
|
||||
float heightOffset = 3.0f;
|
||||
glm::vec3 boundsCenter;
|
||||
float boundsRadius = 0.0f;
|
||||
if (core::Application::getInstance().getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
||||
heightOffset = boundsRadius * 2.0f + 1.0f;
|
||||
}
|
||||
renderPos.z += heightOffset;
|
||||
|
||||
// Project to screen
|
||||
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
||||
if (clipPos.w <= 0.0f) continue;
|
||||
|
||||
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
||||
float sx = (ndc.x + 1.0f) * 0.5f * screenW;
|
||||
float sy = (1.0f - ndc.y) * 0.5f * screenH;
|
||||
|
||||
// Skip if off-screen
|
||||
if (sx < -50 || sx > screenW + 50 || sy < -50 || sy > screenH + 50) continue;
|
||||
|
||||
// Scale text size based on distance
|
||||
float dist = clipPos.w;
|
||||
float fontSize = std::clamp(800.0f / dist, 14.0f, 48.0f);
|
||||
|
||||
// Draw outlined text: 4 shadow copies then main text
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImU32 outlineColor = IM_COL32(0, 0, 0, 220);
|
||||
float off = std::max(1.0f, fontSize * 0.06f);
|
||||
ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, marker);
|
||||
float tx = sx - textSize.x * 0.5f;
|
||||
float ty = sy - textSize.y * 0.5f;
|
||||
|
||||
drawList->AddText(font, fontSize, ImVec2(tx - off, ty), outlineColor, marker);
|
||||
drawList->AddText(font, fontSize, ImVec2(tx + off, ty), outlineColor, marker);
|
||||
drawList->AddText(font, fontSize, ImVec2(tx, ty - off), outlineColor, marker);
|
||||
drawList->AddText(font, fontSize, ImVec2(tx, ty + off), outlineColor, marker);
|
||||
drawList->AddText(font, fontSize, ImVec2(tx, ty), color, marker);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
||||
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
||||
if (statuses.empty()) return;
|
||||
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
auto* minimap = renderer ? renderer->getMinimap() : nullptr;
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
if (!camera || !minimap || !window) return;
|
||||
|
||||
float screenW = static_cast<float>(window->getWidth());
|
||||
|
||||
// Minimap parameters (matching minimap.cpp)
|
||||
float mapSize = 200.0f;
|
||||
float margin = 10.0f;
|
||||
float mapRadius = mapSize * 0.5f;
|
||||
float centerX = screenW - margin - mapRadius;
|
||||
float centerY = margin + mapRadius;
|
||||
float viewRadius = 400.0f;
|
||||
|
||||
// Player position in render coords
|
||||
auto& mi = gameHandler.getMovementInfo();
|
||||
glm::vec3 playerRender = core::coords::canonicalToRender(glm::vec3(mi.x, mi.y, mi.z));
|
||||
|
||||
// Camera bearing for minimap rotation
|
||||
glm::vec3 fwd = camera->getForward();
|
||||
float bearing = std::atan2(-fwd.x, fwd.y);
|
||||
float cosB = std::cos(bearing);
|
||||
float sinB = std::sin(bearing);
|
||||
|
||||
auto* drawList = ImGui::GetForegroundDrawList();
|
||||
|
||||
for (const auto& [guid, status] : statuses) {
|
||||
ImU32 dotColor;
|
||||
const char* marker = nullptr;
|
||||
if (status == game::QuestGiverStatus::AVAILABLE) {
|
||||
dotColor = IM_COL32(255, 210, 0, 255);
|
||||
marker = "!";
|
||||
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
||||
dotColor = IM_COL32(160, 160, 160, 255);
|
||||
marker = "!";
|
||||
} else if (status == game::QuestGiverStatus::REWARD) {
|
||||
dotColor = IM_COL32(255, 210, 0, 255);
|
||||
marker = "?";
|
||||
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
||||
dotColor = IM_COL32(160, 160, 160, 255);
|
||||
marker = "?";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
||||
if (!entity) continue;
|
||||
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 npcRender = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Offset from player in render coords
|
||||
float dx = npcRender.x - playerRender.x;
|
||||
float dy = npcRender.y - playerRender.y;
|
||||
|
||||
// Rotate by camera bearing (minimap north-up rotation)
|
||||
float rx = dx * cosB - dy * sinB;
|
||||
float ry = dx * sinB + dy * cosB;
|
||||
|
||||
// Scale to minimap pixels
|
||||
float px = rx / viewRadius * mapRadius;
|
||||
float py = -ry / viewRadius * mapRadius; // screen Y is inverted
|
||||
|
||||
// Clamp to circle
|
||||
float distFromCenter = std::sqrt(px * px + py * py);
|
||||
if (distFromCenter > mapRadius - 4.0f) {
|
||||
float scale = (mapRadius - 4.0f) / distFromCenter;
|
||||
px *= scale;
|
||||
py *= scale;
|
||||
}
|
||||
|
||||
float sx = centerX + px;
|
||||
float sy = centerY + py;
|
||||
|
||||
// Draw dot with marker text
|
||||
drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, dotColor);
|
||||
ImFont* font = ImGui::GetFont();
|
||||
ImVec2 textSize = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, marker);
|
||||
drawList->AddText(font, 11.0f,
|
||||
ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f),
|
||||
IM_COL32(0, 0, 0, 255), marker);
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue