From c44477fbee8df9854530c35f26b4509db709c09d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 22:31:56 -0700 Subject: [PATCH] Implement corpse reclaim: store death position and show Resurrect button When a player releases spirit, the server sends SMSG_DEATH_RELEASE_LOC with the corpse map and position. Store this so the ghost can reclaim. New flow: - SMSG_DEATH_RELEASE_LOC now stores corpseMapId_/corpseX_/Y_/Z_ instead of logging and discarding - canReclaimCorpse(): true when ghost is on same map within 40 yards of stored corpse position - reclaimCorpse(): sends CMSG_RECLAIM_CORPSE (no payload) - renderReclaimCorpseButton(): shows "Resurrect from Corpse" button at bottom-center when canReclaimCorpse() is true --- include/game/game_handler.hpp | 6 ++++++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 31 +++++++++++++++++++++++++------ src/ui/game_screen.cpp | 29 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2e53dda9..af0fac2e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -736,6 +736,10 @@ public: bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } bool showResurrectDialog() const { return resurrectRequestPending_; } const std::string& getResurrectCasterName() const { return resurrectCasterName_; } + /** True when ghost is within 40 yards of corpse position (same map). */ + bool canReclaimCorpse() const; + /** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */ + void reclaimCorpse(); void releaseSpirit(); void acceptResurrect(); void declineResurrect(); @@ -2150,6 +2154,8 @@ private: float serverPitchRate_ = 3.14159f; bool playerDead_ = false; bool releasedSpirit_ = false; + uint32_t corpseMapId_ = 0; + float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f; // Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially std::array playerRunes_ = [] { std::array r{}; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 49d429b6..db27a073 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -228,6 +228,7 @@ private: void renderTrainerWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); void renderDeathScreen(game::GameHandler& gameHandler); + void renderReclaimCorpseButton(game::GameHandler& gameHandler); void renderResurrectDialog(game::GameHandler& gameHandler); void renderEscapeMenu(); void renderSettingsWindow(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index db003428..6eced5fe 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2021,13 +2021,14 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_DEATH_RELEASE_LOC: { - // uint32 mapId + float x + float y + float z — spirit healer position + // uint32 mapId + float x + float y + float z — corpse/spirit healer position if (packet.getSize() - packet.getReadPos() >= 16) { - uint32_t mapId = packet.readUInt32(); - float x = packet.readFloat(); - float y = packet.readFloat(); - float z = packet.readFloat(); - LOG_INFO("SMSG_DEATH_RELEASE_LOC: map=", mapId, " x=", x, " y=", y, " z=", z); + corpseMapId_ = packet.readUInt32(); + corpseX_ = packet.readFloat(); + corpseY_ = packet.readFloat(); + corpseZ_ = packet.readFloat(); + LOG_INFO("SMSG_DEATH_RELEASE_LOC: map=", corpseMapId_, + " x=", corpseX_, " y=", corpseY_, " z=", corpseZ_); } break; } @@ -9459,6 +9460,24 @@ void GameHandler::releaseSpirit() { } } +bool GameHandler::canReclaimCorpse() const { + if (!releasedSpirit_ || corpseMapId_ == 0) return false; + // Only if ghost is on the same map as their corpse + if (currentMapId_ != corpseMapId_) return false; + // Must be within 40 yards (server also validates proximity) + float dx = movementInfo.x - corpseX_; + float dy = movementInfo.y - corpseY_; + float dz = movementInfo.z - corpseZ_; + return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f); +} + +void GameHandler::reclaimCorpse() { + if (!canReclaimCorpse() || !socket) return; + network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE)); + socket->send(packet); + LOG_INFO("Sent CMSG_RECLAIM_CORPSE"); +} + 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 69d1668e..d238f461 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -433,6 +433,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); + renderReclaimCorpseButton(gameHandler); renderResurrectDialog(gameHandler); renderChatBubbles(gameHandler); renderEscapeMenu(); @@ -7013,6 +7014,34 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) { + if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + float btnW = 220.0f, btnH = 36.0f; + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, btnH + 16.0f), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f)); + if (ImGui::Begin("##ReclaimCorpse", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus)) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f)); + if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) { + gameHandler.reclaimCorpse(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); +} + void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) { if (!gameHandler.showResurrectDialog()) return;