feat: handle SMSG_PET_UNLEARN_CONFIRM with pet talent respec dialog

Parses the pet talent wipe confirm packet (petGuid + cost), shows a
confirmation dialog matching the player talent reset UX, and sends
CMSG_PET_UNLEARN_TALENTS on confirmation. Completes the pet talent
respec flow for Hunters/Warlocks on WotLK servers.
This commit is contained in:
Kelsi 2026-03-17 21:13:27 -07:00
parent 67c8101f67
commit 49ba89dfc3
4 changed files with 103 additions and 1 deletions

View file

@ -1176,6 +1176,11 @@ public:
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
void confirmTalentWipe();
void cancelTalentWipe() { talentWipePending_ = false; }
// Pet talent respec confirm
bool showPetUnlearnDialog() const { return petUnlearnPending_; }
uint32_t getPetUnlearnCost() const { return petUnlearnCost_; }
void confirmPetUnlearn();
void cancelPetUnlearn() { petUnlearnPending_ = false; }
/** True when ghost is within 40 yards of corpse position (same map). */
bool canReclaimCorpse() const;
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
@ -3305,6 +3310,10 @@ private:
bool talentWipePending_ = false;
uint64_t talentWipeNpcGuid_ = 0;
uint32_t talentWipeCost_ = 0;
// ---- Pet talent respec confirm dialog ----
bool petUnlearnPending_ = false;
uint64_t petUnlearnGuid_ = 0;
uint32_t petUnlearnCost_ = 0;
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
uint64_t resurrectCasterGuid_ = 0;
std::string resurrectCasterName_;

View file

@ -358,6 +358,7 @@ private:
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
void renderResurrectDialog(game::GameHandler& gameHandler);
void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler);
void renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler);
void renderEscapeMenu();
void renderSettingsWindow();
void applyGraphicsPreset(GraphicsPreset preset);

View file

@ -7660,7 +7660,16 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_PET_GUIDS:
case Opcode::SMSG_PET_DISMISS_SOUND:
case Opcode::SMSG_PET_ACTION_SOUND:
case Opcode::SMSG_PET_UNLEARN_CONFIRM:
case Opcode::SMSG_PET_UNLEARN_CONFIRM: {
// uint64 petGuid + uint32 cost (copper)
if (packet.getSize() - packet.getReadPos() >= 12) {
petUnlearnGuid_ = packet.readUInt64();
petUnlearnCost_ = packet.readUInt32();
petUnlearnPending_ = true;
}
packet.setReadPos(packet.getSize());
break;
}
case Opcode::SMSG_PET_UPDATE_COMBO_POINTS:
packet.setReadPos(packet.getSize());
break;
@ -18867,6 +18876,20 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) {
addSystemChatMessage(msg);
}
void GameHandler::confirmPetUnlearn() {
if (!petUnlearnPending_) return;
petUnlearnPending_ = false;
if (state != WorldState::IN_WORLD || !socket) return;
// Respond with CMSG_PET_UNLEARN_TALENTS (no payload in 3.3.5a)
network::Packet pkt(wireOpcode(Opcode::CMSG_PET_UNLEARN_TALENTS));
socket->send(pkt);
LOG_INFO("confirmPetUnlearn: sent CMSG_PET_UNLEARN_TALENTS");
addSystemChatMessage("Pet talent reset confirmed.");
petUnlearnGuid_ = 0;
petUnlearnCost_ = 0;
}
void GameHandler::confirmTalentWipe() {
if (!talentWipePending_) return;
talentWipePending_ = false;

View file

@ -741,6 +741,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderReclaimCorpseButton(gameHandler);
renderResurrectDialog(gameHandler);
renderTalentWipeConfirmDialog(gameHandler);
renderPetUnlearnConfirmDialog(gameHandler);
renderChatBubbles(gameHandler);
renderEscapeMenu();
renderSettingsWindow();
@ -15568,6 +15569,74 @@ void GameScreen::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) {
ImGui::PopStyleVar();
}
void GameScreen::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) {
if (!gameHandler.showPetUnlearnDialog()) 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;
float dlgW = 340.0f;
float dlgH = 130.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f));
if (ImGui::Begin("##PetUnlearnDialog", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
ImGui::Spacing();
uint32_t cost = gameHandler.getPetUnlearnCost();
uint32_t gold = cost / 10000;
uint32_t silver = (cost % 10000) / 100;
uint32_t copper = cost % 100;
char costStr[64];
if (gold > 0)
std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper);
else if (silver > 0)
std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper);
else
std::snprintf(costStr, sizeof(costStr), "%uc", copper);
std::string text = std::string("Reset your pet's talents for ") + costStr + "?";
float textW = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2));
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str());
ImGui::Spacing();
ImGui::SetCursorPosX(8.0f);
ImGui::TextDisabled("All pet talent points will be refunded.");
ImGui::Spacing();
float btnW = 110.0f;
float spacing = 20.0f;
ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f));
if (ImGui::Button("Confirm##petunlearn", ImVec2(btnW, 30))) {
gameHandler.confirmPetUnlearn();
}
ImGui::PopStyleColor(2);
ImGui::SameLine(0, spacing);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Cancel##petunlearn", ImVec2(btnW, 30))) {
gameHandler.cancelPetUnlearn();
}
ImGui::PopStyleColor(2);
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
// ============================================================
// Settings Window
// ============================================================