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
This commit is contained in:
Kelsi 2026-03-18 00:06:39 -07:00
parent 395a8f77c4
commit 5a5c2dcda3
3 changed files with 43 additions and 3 deletions

View file

@ -1171,6 +1171,10 @@ public:
bool isPlayerGhost() const { return releasedSpirit_; } bool isPlayerGhost() const { return releasedSpirit_; }
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
bool showResurrectDialog() const { return resurrectRequestPending_; } 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_; } const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
bool showTalentWipeConfirmDialog() const { return talentWipePending_; } bool showTalentWipeConfirmDialog() const { return talentWipePending_; }
uint32_t getTalentWipeCost() const { return talentWipeCost_; } uint32_t getTalentWipeCost() const { return talentWipeCost_; }
@ -3314,6 +3318,7 @@ private:
uint64_t pendingSpiritHealerGuid_ = 0; uint64_t pendingSpiritHealerGuid_ = 0;
bool resurrectPending_ = false; bool resurrectPending_ = false;
bool resurrectRequestPending_ = false; bool resurrectRequestPending_ = false;
bool selfResAvailable_ = false; // SMSG_PRE_RESURRECT received — Reincarnation/Twisting Nether
// ---- Talent wipe confirm dialog ---- // ---- Talent wipe confirm dialog ----
bool talentWipePending_ = false; bool talentWipePending_ = false;
uint64_t talentWipeNpcGuid_ = 0; uint64_t talentWipeNpcGuid_ = 0;

View file

@ -7316,8 +7316,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
// ---- Pre-resurrect state ---- // ---- Pre-resurrect state ----
case Opcode::SMSG_PRE_RESURRECT: { case Opcode::SMSG_PRE_RESURRECT: {
// packed GUID of the player to enter pre-resurrect // SMSG_PRE_RESURRECT: packed GUID of the player who can self-resurrect.
(void)UpdateObjectParser::readPackedGuid(packet); // 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; break;
} }
@ -9193,6 +9200,7 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
movementInfo.jumpXYSpeed = 0.0f; movementInfo.jumpXYSpeed = 0.0f;
resurrectPending_ = false; resurrectPending_ = false;
resurrectRequestPending_ = false; resurrectRequestPending_ = false;
selfResAvailable_ = false;
onTaxiFlight_ = false; onTaxiFlight_ = false;
taxiMountActive_ = false; taxiMountActive_ = false;
taxiActivatePending_ = false; taxiActivatePending_ = false;
@ -10985,6 +10993,7 @@ void GameHandler::forceClearTaxiAndMovementState() {
vehicleId_ = 0; vehicleId_ = 0;
resurrectPending_ = false; resurrectPending_ = false;
resurrectRequestPending_ = false; resurrectRequestPending_ = false;
selfResAvailable_ = false;
playerDead_ = false; playerDead_ = false;
releasedSpirit_ = false; releasedSpirit_ = false;
corpseGuid_ = 0; corpseGuid_ = 0;
@ -11886,6 +11895,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
} else if (wasDead && !nowDead) { } else if (wasDead && !nowDead) {
playerDead_ = false; playerDead_ = false;
releasedSpirit_ = false; releasedSpirit_ = false;
selfResAvailable_ = false;
LOG_INFO("Player resurrected (dynamic flags)"); LOG_INFO("Player resurrected (dynamic flags)");
} }
} else if (entity->getType() == ObjectType::UNIT) { } else if (entity->getType() == ObjectType::UNIT) {
@ -12167,6 +12177,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
playerDead_ = false; playerDead_ = false;
repopPending_ = false; repopPending_ = false;
resurrectPending_ = false; resurrectPending_ = false;
selfResAvailable_ = false;
corpseMapId_ = 0; // corpse reclaimed corpseMapId_ = 0; // corpse reclaimed
corpseGuid_ = 0; corpseGuid_ = 0;
corpseReclaimAvailableMs_ = 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); 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) { void GameHandler::activateSpiritHealer(uint64_t npcGuid) {
if (state != WorldState::IN_WORLD || !socket) return; if (state != WorldState::IN_WORLD || !socket) return;
pendingSpiritHealerGuid_ = npcGuid; pendingSpiritHealerGuid_ = npcGuid;

View file

@ -15364,8 +15364,10 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
// "Release Spirit" dialog centered on screen // "Release Spirit" dialog centered on screen
const bool hasSelfRes = gameHandler.canSelfRes();
float dlgW = 280.0f; 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::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
@ -15399,6 +15401,19 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
ImGui::Spacing(); ImGui::Spacing();
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 // Center the Release Spirit button
float btnW = 180.0f; float btnW = 180.0f;
ImGui::SetCursorPosX((dlgW - btnW) / 2); ImGui::SetCursorPosX((dlgW - btnW) / 2);