feat: track PvP corpse-reclaim delay and show countdown in UI

SMSG_CORPSE_RECLAIM_DELAY is now stored as an absolute expiry timestamp
(steady_clock ms) instead of being discarded after a chat message.

GameHandler::getCorpseReclaimDelaySec() returns remaining seconds (0 when
reclaim is available). The "Resurrect from Corpse" button now:
- Disables and shows the remaining seconds when a PvP delay is active
- Shows the usual "Corpse: N yards" helper text when available
Also resets corpseReclaimAvailableMs_ on world/session teardown.
This commit is contained in:
Kelsi 2026-03-17 23:52:45 -07:00
parent 2acab47eee
commit b0046fa777
3 changed files with 55 additions and 19 deletions

View file

@ -1183,6 +1183,8 @@ public:
void cancelPetUnlearn() { petUnlearnPending_ = false; }
/** True when ghost is within 40 yards of corpse position (same map). */
bool canReclaimCorpse() const;
/** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */
float getCorpseReclaimDelaySec() const;
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
float getCorpseDistance() const {
if (corpseMapId_ == 0 || currentMapId_ != corpseMapId_) return -1.0f;
@ -3298,6 +3300,9 @@ private:
uint32_t corpseMapId_ = 0;
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
uint64_t corpseGuid_ = 0;
// Absolute time (ms since epoch) when PvP corpse-reclaim delay expires.
// 0 means no active delay (reclaim allowed immediately upon proximity).
uint64_t corpseReclaimAvailableMs_ = 0;
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
std::array<RuneSlot, 6> playerRunes_ = [] {
std::array<RuneSlot, 6> r{};

View file

@ -2645,13 +2645,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_CORPSE_RECLAIM_DELAY: {
// uint32 delayMs before player can reclaim corpse
// uint32 delayMs before player can reclaim corpse (PvP deaths)
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t delayMs = packet.readUInt32();
uint32_t delaySec = (delayMs + 999) / 1000;
addSystemChatMessage("You can reclaim your corpse in " +
std::to_string(delaySec) + " seconds.");
LOG_DEBUG("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms");
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
corpseReclaimAvailableMs_ = nowMs + delayMs;
LOG_INFO("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms");
}
break;
}
@ -9085,6 +9086,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
playerDead_ = false;
releasedSpirit_ = false;
corpseGuid_ = 0;
corpseReclaimAvailableMs_ = 0;
targetGuid = 0;
focusGuid = 0;
lastTargetGuid = 0;
@ -13941,6 +13943,15 @@ bool GameHandler::canReclaimCorpse() const {
return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f);
}
float GameHandler::getCorpseReclaimDelaySec() const {
if (corpseReclaimAvailableMs_ == 0) return 0.0f;
auto nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
if (nowMs >= corpseReclaimAvailableMs_) return 0.0f;
return static_cast<float>(corpseReclaimAvailableMs_ - nowMs) / 1000.0f;
}
void GameHandler::reclaimCorpse() {
if (!canReclaimCorpse() || !socket) return;
// CMSG_RECLAIM_CORPSE requires the corpse object's own GUID.

View file

@ -15421,28 +15421,48 @@ void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) {
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
float delaySec = gameHandler.getCorpseReclaimDelaySec();
bool onDelay = (delaySec > 0.0f);
float btnW = 220.0f, btnH = 36.0f;
float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.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::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), 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);
float corpDist = gameHandler.getCorpseDistance();
if (corpDist >= 0.0f) {
char distBuf[48];
snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist);
float dw = ImGui::CalcTextSize(distBuf).x;
ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f);
ImGui::TextDisabled("%s", distBuf);
if (onDelay) {
// Greyed-out button while PvP reclaim timer ticks down
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
ImGui::BeginDisabled(true);
char delayLabel[64];
snprintf(delayLabel, sizeof(delayLabel), "Resurrect from Corpse (%.0fs)", delaySec);
ImGui::Button(delayLabel, ImVec2(btnW, btnH));
ImGui::EndDisabled();
ImGui::PopStyleColor(2);
const char* waitMsg = "You cannot reclaim your corpse yet.";
float tw = ImGui::CalcTextSize(waitMsg).x;
ImGui::SetCursorPosX((btnW + 16.0f - tw) * 0.5f);
ImGui::TextColored(ImVec4(0.8f, 0.5f, 0.2f, 1.0f), "%s", waitMsg);
} else {
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);
float corpDist = gameHandler.getCorpseDistance();
if (corpDist >= 0.0f) {
char distBuf[48];
snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist);
float dw = ImGui::CalcTextSize(distBuf).x;
ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f);
ImGui::TextDisabled("%s", distBuf);
}
}
}
ImGui::End();