From 5a5c2dcda34ab4a7e38b869f3d809c34aca163c0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 00:06:39 -0700 Subject: [PATCH] feat: implement self-resurrection (Reincarnation/Twisting Nether) SMSG_PRE_RESURRECT was silently discarded; Shamans with Reincarnation and Warlocks with Twisting Nether could never see or use the self-res ability. Now: - SMSG_PRE_RESURRECT sets selfResAvailable_ flag when addressed to the local player - Death dialog gains a "Use Self-Resurrection" button (blue, shown above Release Spirit) when the flag is set - Clicking it sends CMSG_SELF_RES (empty body) and clears the flag - selfResAvailable_ is cleared on all resurrection and session-reset paths so it never bleeds across deaths or logins --- include/game/game_handler.hpp | 5 +++++ src/game/game_handler.cpp | 24 ++++++++++++++++++++++-- src/ui/game_screen.cpp | 17 ++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d917890a..f8480d2b 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1171,6 +1171,10 @@ public: bool isPlayerGhost() const { return releasedSpirit_; } bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } bool showResurrectDialog() const { return resurrectRequestPending_; } + /** True when SMSG_PRE_RESURRECT arrived — Reincarnation/Twisting Nether available. */ + bool canSelfRes() const { return selfResAvailable_; } + /** Send CMSG_SELF_RES to use Reincarnation / Twisting Nether. */ + void useSelfRes(); const std::string& getResurrectCasterName() const { return resurrectCasterName_; } bool showTalentWipeConfirmDialog() const { return talentWipePending_; } uint32_t getTalentWipeCost() const { return talentWipeCost_; } @@ -3314,6 +3318,7 @@ private: uint64_t pendingSpiritHealerGuid_ = 0; bool resurrectPending_ = false; bool resurrectRequestPending_ = false; + bool selfResAvailable_ = false; // SMSG_PRE_RESURRECT received — Reincarnation/Twisting Nether // ---- Talent wipe confirm dialog ---- bool talentWipePending_ = false; uint64_t talentWipeNpcGuid_ = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4ab52388..28ffc8e1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7316,8 +7316,15 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Pre-resurrect state ---- case Opcode::SMSG_PRE_RESURRECT: { - // packed GUID of the player to enter pre-resurrect - (void)UpdateObjectParser::readPackedGuid(packet); + // SMSG_PRE_RESURRECT: packed GUID of the player who can self-resurrect. + // Sent when the dead player has Reincarnation (Shaman), Twisting Nether (Warlock), + // or Deathpact (Death Knight passive). The client must send CMSG_SELF_RES to accept. + uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (targetGuid == playerGuid || targetGuid == 0) { + selfResAvailable_ = true; + LOG_INFO("SMSG_PRE_RESURRECT: self-resurrection available (guid=0x", + std::hex, targetGuid, std::dec, ")"); + } break; } @@ -9193,6 +9200,7 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { movementInfo.jumpXYSpeed = 0.0f; resurrectPending_ = false; resurrectRequestPending_ = false; + selfResAvailable_ = false; onTaxiFlight_ = false; taxiMountActive_ = false; taxiActivatePending_ = false; @@ -10985,6 +10993,7 @@ void GameHandler::forceClearTaxiAndMovementState() { vehicleId_ = 0; resurrectPending_ = false; resurrectRequestPending_ = false; + selfResAvailable_ = false; playerDead_ = false; releasedSpirit_ = false; corpseGuid_ = 0; @@ -11886,6 +11895,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } else if (wasDead && !nowDead) { playerDead_ = false; releasedSpirit_ = false; + selfResAvailable_ = false; LOG_INFO("Player resurrected (dynamic flags)"); } } else if (entity->getType() == ObjectType::UNIT) { @@ -12167,6 +12177,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem playerDead_ = false; repopPending_ = false; resurrectPending_ = false; + selfResAvailable_ = false; corpseMapId_ = 0; // corpse reclaimed corpseGuid_ = 0; corpseReclaimAvailableMs_ = 0; @@ -13967,6 +13978,15 @@ void GameHandler::reclaimCorpse() { LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, corpseGuid_, std::dec); } +void GameHandler::useSelfRes() { + if (!selfResAvailable_ || !socket) return; + // CMSG_SELF_RES: empty body — server confirms resurrection via SMSG_UPDATE_OBJECT. + network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES)); + socket->send(pkt); + selfResAvailable_ = false; + LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)"); +} + void GameHandler::activateSpiritHealer(uint64_t npcGuid) { if (state != WorldState::IN_WORLD || !socket) return; pendingSpiritHealerGuid_ = npcGuid; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 808a922b..c3eed8d6 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -15364,8 +15364,10 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); // "Release Spirit" dialog centered on screen + const bool hasSelfRes = gameHandler.canSelfRes(); float dlgW = 280.0f; - float dlgH = 130.0f; + // Extra height when self-res button is available + float dlgH = hasSelfRes ? 170.0f : 130.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); @@ -15399,6 +15401,19 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::Spacing(); ImGui::Spacing(); + // Self-resurrection button (Reincarnation / Twisting Nether / Deathpact) + if (hasSelfRes) { + float btnW2 = 220.0f; + ImGui::SetCursorPosX((dlgW - btnW2) / 2); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.55f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.5f, 0.75f, 1.0f)); + if (ImGui::Button("Use Self-Resurrection", ImVec2(btnW2, 30))) { + gameHandler.useSelfRes(); + } + ImGui::PopStyleColor(2); + ImGui::Spacing(); + } + // Center the Release Spirit button float btnW = 180.0f; ImGui::SetCursorPosX((dlgW - btnW) / 2);