mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add quest details dialog, fix vendor UI, suppress both-button clicks
Parse SMSG_QUESTGIVER_QUEST_DETAILS and show quest text with Accept/ Decline buttons. Vendor window now shows item names with quality colors, stat tooltips on hover, player money, and closes properly via X button. Suppress left/right-click targeting and interaction when the other mouse button is held (both-button run forward).
This commit is contained in:
parent
60be428250
commit
67a3da3bae
6 changed files with 227 additions and 5 deletions
|
|
@ -322,16 +322,25 @@ public:
|
|||
void interactWithNpc(uint64_t guid);
|
||||
void selectGossipOption(uint32_t optionId);
|
||||
void selectGossipQuest(uint32_t questId);
|
||||
void acceptQuest();
|
||||
void declineQuest();
|
||||
void closeGossip();
|
||||
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
||||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||||
bool isQuestDetailsOpen() const { return questDetailsOpen; }
|
||||
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
|
||||
|
||||
// Vendor
|
||||
void openVendor(uint64_t npcGuid);
|
||||
void closeVendor();
|
||||
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count);
|
||||
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint8_t count);
|
||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
||||
auto it = itemInfoCache_.find(itemId);
|
||||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callbacks
|
||||
|
|
@ -461,6 +470,7 @@ private:
|
|||
void handleLootRemoved(network::Packet& packet);
|
||||
void handleGossipMessage(network::Packet& packet);
|
||||
void handleGossipComplete(network::Packet& packet);
|
||||
void handleQuestDetails(network::Packet& packet);
|
||||
void handleListInventory(network::Packet& packet);
|
||||
LootResponseData generateLocalLoot(uint64_t guid);
|
||||
void simulateLootResponse(const LootResponseData& data);
|
||||
|
|
@ -602,6 +612,10 @@ private:
|
|||
bool gossipWindowOpen = false;
|
||||
GossipMessageData currentGossip;
|
||||
|
||||
// Quest details
|
||||
bool questDetailsOpen = false;
|
||||
QuestDetailsData currentQuestDetails;
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
ListInventoryData currentVendorItems;
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,24 @@ public:
|
|||
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||
};
|
||||
|
||||
/** SMSG_QUESTGIVER_QUEST_DETAILS data (simplified) */
|
||||
struct QuestDetailsData {
|
||||
uint64_t npcGuid = 0;
|
||||
uint32_t questId = 0;
|
||||
std::string title;
|
||||
std::string details; // Quest description text
|
||||
std::string objectives; // Objectives text
|
||||
uint32_t suggestedPlayers = 0;
|
||||
uint32_t rewardMoney = 0;
|
||||
uint32_t rewardXp = 0;
|
||||
};
|
||||
|
||||
/** SMSG_QUESTGIVER_QUEST_DETAILS parser */
|
||||
class QuestDetailsParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, QuestDetailsData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Vendor
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ private:
|
|||
void renderBuffBar(game::GameHandler& gameHandler);
|
||||
void renderLootWindow(game::GameHandler& gameHandler);
|
||||
void renderGossipWindow(game::GameHandler& gameHandler);
|
||||
void renderQuestDetailsWindow(game::GameHandler& gameHandler);
|
||||
void renderVendorWindow(game::GameHandler& gameHandler);
|
||||
void renderTeleporterPanel();
|
||||
void renderEscapeMenu();
|
||||
|
|
|
|||
|
|
@ -1092,6 +1092,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||
case Opcode::SMSG_QUESTGIVER_STATUS:
|
||||
case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS:
|
||||
handleQuestDetails(packet);
|
||||
break;
|
||||
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
|
||||
case Opcode::SMSG_QUESTGIVER_OFFER_REWARD:
|
||||
case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE:
|
||||
|
|
@ -3538,6 +3540,31 @@ void GameHandler::selectGossipQuest(uint32_t questId) {
|
|||
gossipWindowOpen = false;
|
||||
}
|
||||
|
||||
void GameHandler::handleQuestDetails(network::Packet& packet) {
|
||||
QuestDetailsData data;
|
||||
if (!QuestDetailsParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_QUEST_DETAILS");
|
||||
return;
|
||||
}
|
||||
currentQuestDetails = data;
|
||||
questDetailsOpen = true;
|
||||
gossipWindowOpen = false;
|
||||
}
|
||||
|
||||
void GameHandler::acceptQuest() {
|
||||
if (!questDetailsOpen || state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = QuestgiverAcceptQuestPacket::build(
|
||||
currentQuestDetails.npcGuid, currentQuestDetails.questId);
|
||||
socket->send(packet);
|
||||
questDetailsOpen = false;
|
||||
currentQuestDetails = QuestDetailsData{};
|
||||
}
|
||||
|
||||
void GameHandler::declineQuest() {
|
||||
questDetailsOpen = false;
|
||||
currentQuestDetails = QuestDetailsData{};
|
||||
}
|
||||
|
||||
void GameHandler::closeGossip() {
|
||||
gossipWindowOpen = false;
|
||||
currentGossip = GossipMessageData{};
|
||||
|
|
@ -3549,6 +3576,11 @@ void GameHandler::openVendor(uint64_t npcGuid) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::closeVendor() {
|
||||
vendorWindowOpen = false;
|
||||
currentVendorItems = ListInventoryData{};
|
||||
}
|
||||
|
||||
void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint8_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = BuyItemPacket::build(vendorGuid, itemId, slot, count);
|
||||
|
|
@ -3605,6 +3637,11 @@ void GameHandler::handleListInventory(network::Packet& packet) {
|
|||
if (!ListInventoryParser::parse(packet, currentVendorItems)) return;
|
||||
vendorWindowOpen = true;
|
||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||
|
||||
// Query item info for all vendor items so we can show names
|
||||
for (const auto& item : currentVendorItems.items) {
|
||||
queryItemInfo(item.itemId, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1835,6 +1835,44 @@ network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t qu
|
|||
return packet;
|
||||
}
|
||||
|
||||
bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) {
|
||||
if (packet.getSize() < 20) return false;
|
||||
data.npcGuid = packet.readUInt64();
|
||||
/*informUnit*/ packet.readUInt64();
|
||||
data.questId = packet.readUInt32();
|
||||
data.title = packet.readString();
|
||||
data.details = packet.readString();
|
||||
data.objectives = packet.readString();
|
||||
/*activateAccept*/ packet.readUInt8();
|
||||
/*flags*/ packet.readUInt32();
|
||||
data.suggestedPlayers = packet.readUInt32();
|
||||
/*isFinished*/ packet.readUInt8();
|
||||
|
||||
// Reward choice items
|
||||
uint32_t choiceCount = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < choiceCount && i < 6; i++) {
|
||||
packet.readUInt32(); // itemId
|
||||
packet.readUInt32(); // count
|
||||
packet.readUInt32(); // displayInfo
|
||||
}
|
||||
|
||||
// Reward items
|
||||
uint32_t rewardCount = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < rewardCount && i < 4; i++) {
|
||||
packet.readUInt32(); // itemId
|
||||
packet.readUInt32(); // count
|
||||
packet.readUInt32(); // displayInfo
|
||||
}
|
||||
|
||||
data.rewardMoney = packet.readUInt32();
|
||||
if (packet.getReadPos() < packet.getSize()) {
|
||||
data.rewardXp = packet.readUInt32();
|
||||
}
|
||||
|
||||
LOG_INFO("Quest details: id=", data.questId, " title='", data.title, "'");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
||||
data.npcGuid = packet.readUInt64();
|
||||
data.menuId = packet.readUInt32();
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderBuffBar(gameHandler);
|
||||
renderLootWindow(gameHandler);
|
||||
renderGossipWindow(gameHandler);
|
||||
renderQuestDetailsWindow(gameHandler);
|
||||
renderVendorWindow(gameHandler);
|
||||
renderEscapeMenu();
|
||||
renderSettingsWindow();
|
||||
|
|
@ -405,7 +406,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
// Left-click targeting (when mouse not captured by UI)
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||
// Suppress when right button is held (both-button run)
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
|
|
@ -445,7 +447,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
// Right-click on target for NPC interaction / loot / auto-attack
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT)) {
|
||||
// Suppress when left button is held (both-button run)
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
||||
if (gameHandler.hasTarget()) {
|
||||
auto target = gameHandler.getTarget();
|
||||
if (target) {
|
||||
|
|
@ -1632,6 +1635,79 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Quest Details Window
|
||||
// ============================================================
|
||||
|
||||
void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
|
||||
if (!gameHandler.isQuestDetailsOpen()) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
||||
|
||||
bool open = true;
|
||||
const auto& quest = gameHandler.getQuestDetails();
|
||||
if (ImGui::Begin(quest.title.c_str(), &open)) {
|
||||
// Quest description
|
||||
if (!quest.details.empty()) {
|
||||
ImGui::TextWrapped("%s", quest.details.c_str());
|
||||
}
|
||||
|
||||
// Objectives
|
||||
if (!quest.objectives.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Objectives:");
|
||||
ImGui::TextWrapped("%s", quest.objectives.c_str());
|
||||
}
|
||||
|
||||
// Rewards
|
||||
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:");
|
||||
if (quest.rewardXp > 0) {
|
||||
ImGui::Text(" %u experience", quest.rewardXp);
|
||||
}
|
||||
if (quest.rewardMoney > 0) {
|
||||
uint32_t gold = quest.rewardMoney / 10000;
|
||||
uint32_t silver = (quest.rewardMoney % 10000) / 100;
|
||||
uint32_t copper = quest.rewardMoney % 100;
|
||||
if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper);
|
||||
else if (silver > 0) ImGui::Text(" %us %uc", silver, copper);
|
||||
else ImGui::Text(" %uc", copper);
|
||||
}
|
||||
}
|
||||
|
||||
if (quest.suggestedPlayers > 1) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
|
||||
"Suggested players: %u", quest.suggestedPlayers);
|
||||
}
|
||||
|
||||
// Accept / Decline buttons
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button("Accept", ImVec2(buttonW, 0))) {
|
||||
gameHandler.acceptQuest();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Decline", ImVec2(buttonW, 0))) {
|
||||
gameHandler.declineQuest();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
gameHandler.declineQuest();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Vendor Window (Phase 5)
|
||||
// ============================================================
|
||||
|
|
@ -1649,6 +1725,14 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
if (ImGui::Begin("Vendor", &open)) {
|
||||
const auto& vendor = gameHandler.getVendorItems();
|
||||
|
||||
// Show player money
|
||||
uint64_t money = gameHandler.getMoneyCopper();
|
||||
uint32_t mg = static_cast<uint32_t>(money / 10000);
|
||||
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
||||
uint32_t mc = static_cast<uint32_t>(money % 100);
|
||||
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
||||
ImGui::Separator();
|
||||
|
||||
if (vendor.items.empty()) {
|
||||
ImGui::TextDisabled("This vendor has nothing for sale.");
|
||||
} else {
|
||||
|
|
@ -1659,18 +1743,49 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Quality colors (matching WoW)
|
||||
static const ImVec4 qualityColors[] = {
|
||||
ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0 Poor (gray)
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1 Common (white)
|
||||
ImVec4(0.12f, 1.0f, 0.0f, 1.0f), // 2 Uncommon (green)
|
||||
ImVec4(0.0f, 0.44f, 0.87f, 1.0f), // 3 Rare (blue)
|
||||
ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4 Epic (purple)
|
||||
ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5 Legendary (orange)
|
||||
};
|
||||
|
||||
for (const auto& item : vendor.items) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(item.slot));
|
||||
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::Text("Item %u", item.itemId);
|
||||
auto* info = gameHandler.getItemInfo(item.itemId);
|
||||
if (info && info->valid) {
|
||||
uint32_t q = info->quality < 6 ? info->quality : 1;
|
||||
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
||||
// Tooltip with stats on hover
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextColored(qualityColors[q], "%s", info->name.c_str());
|
||||
if (info->armor > 0) ImGui::Text("Armor: %d", info->armor);
|
||||
if (info->stamina > 0) ImGui::Text("+%d Stamina", info->stamina);
|
||||
if (info->strength > 0) ImGui::Text("+%d Strength", info->strength);
|
||||
if (info->agility > 0) ImGui::Text("+%d Agility", info->agility);
|
||||
if (info->intellect > 0) ImGui::Text("+%d Intellect", info->intellect);
|
||||
if (info->spirit > 0) ImGui::Text("+%d Spirit", info->spirit);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
} else {
|
||||
ImGui::Text("Item %u", item.itemId);
|
||||
}
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
uint32_t g = item.buyPrice / 10000;
|
||||
uint32_t s = (item.buyPrice / 100) % 100;
|
||||
uint32_t c = item.buyPrice % 100;
|
||||
bool canAfford = money >= item.buyPrice;
|
||||
if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::Text("%ug %us %uc", g, s, c);
|
||||
if (!canAfford) ImGui::PopStyleColor();
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
if (item.maxCount < 0) {
|
||||
|
|
@ -1694,8 +1809,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
|
||||
if (!open) {
|
||||
// Close vendor - just hide UI, no server packet needed
|
||||
// The vendor window state will be reset on next interaction
|
||||
gameHandler.closeVendor();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue