From 7436420cd1963973541d4b2f412deff6f7a9fbea Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 17:27:20 -0800 Subject: [PATCH] Add player death handling, race-aware faction hostility, and all-race texture support - Death screen with "Release Spirit" button sends CMSG_REPOP_REQUEST - Detect player death/resurrection via health updates (VALUES and CREATE) - Faction hostility map now built per-character race instead of hardcoded Human - CharSections.dbc texture lookup enabled for all races (was Human-only) - Fallback texture paths use race folder names instead of hardcoded Human - Player name in unit frame is clickable for self-targeting --- include/core/application.hpp | 1 + include/game/game_handler.hpp | 5 + include/game/opcodes.hpp | 3 + include/game/world_packets.hpp | 6 + include/ui/game_screen.hpp | 1 + src/core/application.cpp | 248 ++++++++++++++++++++------------- src/game/game_handler.cpp | 29 +++- src/game/world_packets.cpp | 10 ++ src/ui/game_screen.cpp | 69 ++++++++- 9 files changed, 270 insertions(+), 102 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index c4ec59dc..d961c9b1 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -82,6 +82,7 @@ private: std::string getPlayerModelPath() const; static const char* mapIdToName(uint32_t mapId); void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); + void buildFactionHostilityMap(uint8_t playerRace); void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation); void despawnOnlineCreature(uint64_t guid); void buildCreatureDisplayLookups(); diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5125420d..547bb477 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -314,6 +314,10 @@ public: uint64_t getPlayerGuid() const { return playerGuid; } void setPlayerGuid(uint64_t guid) { playerGuid = guid; } + // Player death state + bool isPlayerDead() const { return playerDead_; } + void releaseSpirit(); + // ---- Phase 4: Group ---- void inviteToGroup(const std::string& playerName); void acceptGroupInvite(); @@ -693,6 +697,7 @@ private: uint32_t localPlayerHealth_ = 0; uint32_t localPlayerMaxHealth_ = 0; uint32_t localPlayerLevel_ = 1; + bool playerDead_ = false; struct NpcAggroEntry { uint64_t guid; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index dee0c10b..131b97eb 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -164,6 +164,9 @@ enum class Opcode : uint16_t { SMSG_ITEM_QUERY_SINGLE_RESPONSE = 0x058, CMSG_AUTOEQUIP_ITEM = 0x10A, SMSG_INVENTORY_CHANGE_FAILURE = 0x112, + + // ---- Death/Respawn ---- + CMSG_REPOP_REQUEST = 0x015A, }; } // namespace game diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 4e4e95cd..3ce88764 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1268,5 +1268,11 @@ public: static bool parse(network::Packet& packet, ListInventoryData& data); }; +/** CMSG_REPOP_REQUEST packet builder */ +class RepopRequestPacket { +public: + static network::Packet build(); +}; + } // namespace game } // namespace wowee diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 8bd09b92..602d8f32 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -140,6 +140,7 @@ private: void renderQuestDetailsWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); void renderTeleporterPanel(); + void renderDeathScreen(game::GameHandler& gameHandler); void renderEscapeMenu(); void renderSettingsWindow(); diff --git a/src/core/application.cpp b/src/core/application.cpp index 3ac4b421..0f804034 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -629,96 +629,7 @@ void Application::setupUICallbacks() { loadOnlineWorldTerrain(mapId, x, y, z); }); - // Load faction hostility map from FactionTemplate.dbc + Faction.dbc - if (assetManager && assetManager->isInitialized()) { - auto ftDbc = assetManager->loadDBC("FactionTemplate.dbc"); - auto fDbc = assetManager->loadDBC("Faction.dbc"); - if (ftDbc && ftDbc->isLoaded()) { - // Build set of hostile parent faction IDs from Faction.dbc base reputation - // Faction.dbc: field 0=ID, fields 13-16=ReputationBase[4], fields 5-8=ReputationRaceMask[4] - // Human = race 1 = raceMask bit 0 (0x1) - std::unordered_set hostileParentFactions; - if (fDbc && fDbc->isLoaded()) { - for (uint32_t i = 0; i < fDbc->getRecordCount(); i++) { - uint32_t factionId = fDbc->getUInt32(i, 0); - // Check each of the 4 reputation slots for Human race mask - for (int slot = 0; slot < 4; slot++) { - uint32_t raceMask = fDbc->getUInt32(i, 2 + slot); // ReputationRaceMask[4] at fields 2-5 - if (raceMask & 0x1) { // Human race bit - int32_t baseRep = fDbc->getInt32(i, 10 + slot); // ReputationBase[4] at fields 10-13 - if (baseRep < 0) { - hostileParentFactions.insert(factionId); - } - break; - } - } - } - LOG_INFO("Faction.dbc: ", hostileParentFactions.size(), " factions hostile to Humans"); - } - - // Get player faction template data - uint32_t playerFriendGroup = 0; - uint32_t playerEnemyGroup = 0; - uint32_t playerFactionId = 0; - for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) { - if (ftDbc->getUInt32(i, 0) == 1) { // Human player faction template - playerFriendGroup = ftDbc->getUInt32(i, 4) | ftDbc->getUInt32(i, 3); - playerEnemyGroup = ftDbc->getUInt32(i, 5); - playerFactionId = ftDbc->getUInt32(i, 1); - break; - } - } - - // Build hostility map for each faction template - std::unordered_map factionMap; - for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) { - uint32_t id = ftDbc->getUInt32(i, 0); - uint32_t parentFaction = ftDbc->getUInt32(i, 1); - uint32_t factionGroup = ftDbc->getUInt32(i, 3); - uint32_t friendGroup = ftDbc->getUInt32(i, 4); - uint32_t enemyGroup = ftDbc->getUInt32(i, 5); - - // 1. Symmetric group check (WoW's actual hostility formula) - bool hostile = (enemyGroup & playerFriendGroup) != 0 - || (factionGroup & playerEnemyGroup) != 0; - - // 2. Monster factionGroup bit (8) - if (!hostile && (factionGroup & 8) != 0) { - hostile = true; - } - - // 3. Individual enemy faction IDs (fields 6-9) - if (!hostile && playerFactionId > 0) { - for (int e = 6; e <= 9; e++) { - if (ftDbc->getUInt32(i, e) == playerFactionId) { - hostile = true; - break; - } - } - } - - // 4. Parent faction base reputation check (Faction.dbc) - if (!hostile && parentFaction > 0) { - if (hostileParentFactions.count(parentFaction)) { - hostile = true; - } - } - - // 5. If explicitly friendly (friendGroup includes player), override to non-hostile - if (hostile && (friendGroup & playerFriendGroup) != 0) { - hostile = false; - } - - factionMap[id] = hostile; - } - - uint32_t hostileCount = 0; - for (const auto& [fid, h] : factionMap) { if (h) hostileCount++; } - gameHandler->setFactionHostileMap(std::move(factionMap)); - LOG_INFO("Loaded faction hostility: ", hostileCount, "/", ftDbc->getRecordCount(), - " hostile (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")"); - } - } + // Faction hostility map is built in buildFactionHostilityMap() when character enters world // Creature spawn callback (online mode) - spawn creature models gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { @@ -850,16 +761,30 @@ void Application::spawnPlayerCharacter() { LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'"); } - // Look up underwear textures from CharSections.dbc (humans only for now) - bool useCharSections = (spRace_ == game::Race::HUMAN); + // Look up textures from CharSections.dbc for all races + bool useCharSections = true; uint32_t targetRaceId = static_cast(spRace_); uint32_t targetSexId = (spGender_ == game::Gender::FEMALE) ? 1u : 0u; - std::string bodySkinPath = (spGender_ == game::Gender::FEMALE) - ? "Character\\Human\\Female\\HumanFemaleSkin00_00.blp" - : "Character\\Human\\Male\\HumanMaleSkin00_00.blp"; - std::string pelvisPath = (spGender_ == game::Gender::FEMALE) - ? "Character\\Human\\Female\\HumanFemaleNakedPelvisSkin00_00.blp" - : "Character\\Human\\Male\\HumanMaleNakedPelvisSkin00_00.blp"; + + // Race name for fallback texture paths + const char* raceFolderName = "Human"; + switch (spRace_) { + case game::Race::HUMAN: raceFolderName = "Human"; break; + case game::Race::ORC: raceFolderName = "Orc"; break; + case game::Race::DWARF: raceFolderName = "Dwarf"; break; + case game::Race::NIGHT_ELF: raceFolderName = "NightElf"; break; + case game::Race::UNDEAD: raceFolderName = "Scourge"; break; + case game::Race::TAUREN: raceFolderName = "Tauren"; break; + case game::Race::GNOME: raceFolderName = "Gnome"; break; + case game::Race::TROLL: raceFolderName = "Troll"; break; + case game::Race::BLOOD_ELF: raceFolderName = "BloodElf"; break; + case game::Race::DRAENEI: raceFolderName = "Draenei"; break; + default: break; + } + const char* genderFolder = (spGender_ == game::Gender::FEMALE) ? "Female" : "Male"; + std::string raceGender = std::string(raceFolderName) + genderFolder; + std::string bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp"; + std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp"; std::string faceLowerTexturePath; std::vector underwearPaths; @@ -955,7 +880,7 @@ void Application::spawnPlayerCharacter() { if (!hairTexturePath.empty()) { tex.filename = hairTexturePath; } else if (tex.filename.empty()) { - tex.filename = "Character\\Human\\Hair00_00.blp"; + tex.filename = std::string("Character\\") + raceFolderName + "\\Hair00_00.blp"; } } else if (tex.type == 8 && tex.filename.empty()) { if (!underwearPaths.empty()) { @@ -1819,6 +1744,123 @@ void Application::teleportTo(int presetIndex) { LOG_INFO("Teleport to ", preset.label, " complete"); } +void Application::buildFactionHostilityMap(uint8_t playerRace) { + if (!assetManager || !assetManager->isInitialized() || !gameHandler) return; + + auto ftDbc = assetManager->loadDBC("FactionTemplate.dbc"); + auto fDbc = assetManager->loadDBC("Faction.dbc"); + if (!ftDbc || !ftDbc->isLoaded()) return; + + // Race enum → race mask bit: race 1=0x1, 2=0x2, 3=0x4, 4=0x8, 5=0x10, 6=0x20, 7=0x40, 8=0x80, 10=0x200, 11=0x400 + uint32_t playerRaceMask = 0; + if (playerRace >= 1 && playerRace <= 8) { + playerRaceMask = 1u << (playerRace - 1); + } else if (playerRace == 10) { + playerRaceMask = 0x200; // Blood Elf + } else if (playerRace == 11) { + playerRaceMask = 0x400; // Draenei + } + + // Race → player faction template ID + // Human=1, Orc=2, Dwarf=3, NightElf=4, Undead=5, Tauren=6, Gnome=115, Troll=116, BloodElf=1610, Draenei=1629 + uint32_t playerFtId = 0; + switch (playerRace) { + case 1: playerFtId = 1; break; // Human + case 2: playerFtId = 2; break; // Orc + case 3: playerFtId = 3; break; // Dwarf + case 4: playerFtId = 4; break; // Night Elf + case 5: playerFtId = 5; break; // Undead + case 6: playerFtId = 6; break; // Tauren + case 7: playerFtId = 115; break; // Gnome + case 8: playerFtId = 116; break; // Troll + case 10: playerFtId = 1610; break; // Blood Elf + case 11: playerFtId = 1629; break; // Draenei + default: playerFtId = 1; break; + } + + // Build set of hostile parent faction IDs from Faction.dbc base reputation + std::unordered_set hostileParentFactions; + if (fDbc && fDbc->isLoaded()) { + for (uint32_t i = 0; i < fDbc->getRecordCount(); i++) { + uint32_t factionId = fDbc->getUInt32(i, 0); + for (int slot = 0; slot < 4; slot++) { + uint32_t raceMask = fDbc->getUInt32(i, 2 + slot); // ReputationRaceMask[4] at fields 2-5 + if (raceMask & playerRaceMask) { + int32_t baseRep = fDbc->getInt32(i, 10 + slot); // ReputationBase[4] at fields 10-13 + if (baseRep < 0) { + hostileParentFactions.insert(factionId); + } + break; + } + } + } + LOG_INFO("Faction.dbc: ", hostileParentFactions.size(), " factions hostile to race ", (int)playerRace); + } + + // Get player faction template data + uint32_t playerFriendGroup = 0; + uint32_t playerEnemyGroup = 0; + uint32_t playerFactionId = 0; + for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) { + if (ftDbc->getUInt32(i, 0) == playerFtId) { + playerFriendGroup = ftDbc->getUInt32(i, 4) | ftDbc->getUInt32(i, 3); + playerEnemyGroup = ftDbc->getUInt32(i, 5); + playerFactionId = ftDbc->getUInt32(i, 1); + break; + } + } + + // Build hostility map for each faction template + std::unordered_map factionMap; + for (uint32_t i = 0; i < ftDbc->getRecordCount(); i++) { + uint32_t id = ftDbc->getUInt32(i, 0); + uint32_t parentFaction = ftDbc->getUInt32(i, 1); + uint32_t factionGroup = ftDbc->getUInt32(i, 3); + uint32_t friendGroup = ftDbc->getUInt32(i, 4); + uint32_t enemyGroup = ftDbc->getUInt32(i, 5); + + // 1. Symmetric group check + bool hostile = (enemyGroup & playerFriendGroup) != 0 + || (factionGroup & playerEnemyGroup) != 0; + + // 2. Monster factionGroup bit (8) + if (!hostile && (factionGroup & 8) != 0) { + hostile = true; + } + + // 3. Individual enemy faction IDs (fields 6-9) + if (!hostile && playerFactionId > 0) { + for (int e = 6; e <= 9; e++) { + if (ftDbc->getUInt32(i, e) == playerFactionId) { + hostile = true; + break; + } + } + } + + // 4. Parent faction base reputation check (Faction.dbc) + if (!hostile && parentFaction > 0) { + if (hostileParentFactions.count(parentFaction)) { + hostile = true; + } + } + + // 5. If explicitly friendly (friendGroup includes player), override to non-hostile + if (hostile && (friendGroup & playerFriendGroup) != 0) { + hostile = false; + } + + factionMap[id] = hostile; + } + + uint32_t hostileCount = 0; + for (const auto& [fid, h] : factionMap) { if (h) hostileCount++; } + gameHandler->setFactionHostileMap(std::move(factionMap)); + LOG_INFO("Faction hostility for race ", (int)playerRace, " (FT ", playerFtId, "): ", + hostileCount, "/", ftDbc->getRecordCount(), + " hostile (friendGroup=0x", std::hex, playerFriendGroup, ", enemyGroup=0x", playerEnemyGroup, std::dec, ")"); +} + void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z) { if (!renderer || !assetManager || !assetManager->isInitialized()) { LOG_WARNING("Cannot load online terrain: renderer or assets not ready"); @@ -1884,6 +1926,14 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float showProgress("Loading character model...", 0.05f); + // Build faction hostility map for this character's race + if (gameHandler) { + const game::Character* activeChar = gameHandler->getActiveCharacter(); + if (activeChar) { + buildFactionHostilityMap(static_cast(activeChar->race)); + } + } + // Spawn player model for online mode if (gameHandler) { const game::Character* activeChar = gameHandler->getActiveCharacter(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 181506ae..fa3fff10 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2562,7 +2562,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { auto unit = std::static_pointer_cast(entity); for (const auto& [key, val] : block.fields) { switch (key) { - case 24: unit->setHealth(val); break; + case 24: + unit->setHealth(val); + // Detect dead player on login + if (block.guid == playerGuid && val == 0) { + playerDead_ = true; + LOG_INFO("Player logged in dead"); + } + break; case 25: unit->setPower(val); break; case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; @@ -2659,11 +2666,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == autoAttackTarget) { stopAutoAttack(); } + // Player death + if (block.guid == playerGuid) { + playerDead_ = true; + stopAutoAttack(); + LOG_INFO("Player died!"); + } // Trigger death animation for NPC units if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) { npcDeathCallback_(block.guid); } } else if (oldHealth == 0 && val > 0) { + // Player resurrection + if (block.guid == playerGuid) { + playerDead_ = false; + LOG_INFO("Player resurrected!"); + } // Respawn: health went from 0 to >0, reset animation if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) { npcRespawnCallback_(block.guid); @@ -2948,6 +2966,15 @@ std::shared_ptr GameHandler::getTarget() const { return entityManager.getEntity(targetGuid); } +void GameHandler::releaseSpirit() { + if (!playerDead_) return; + if (socket && state == WorldState::IN_WORLD) { + auto packet = RepopRequestPacket::build(); + socket->send(packet); + LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)"); + } +} + void GameHandler::tabTarget(float playerX, float playerY, float playerZ) { // Rebuild cycle list if stale if (tabCycleStale) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 986b9270..8460efb0 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2112,5 +2112,15 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data return true; } +// ============================================================ +// Death/Respawn +// ============================================================ + +network::Packet RepopRequestPacket::build() { + network::Packet packet(static_cast(Opcode::CMSG_REPOP_REQUEST)); + packet.writeUInt8(0); // auto-release flag (0 = manual) + return packet; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 62a97b04..df14413f 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -88,6 +88,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderGossipWindow(gameHandler); renderQuestDetailsWindow(gameHandler); renderVendorWindow(gameHandler); + renderDeathScreen(gameHandler); renderEscapeMenu(); renderSettingsWindow(); @@ -662,8 +663,12 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { playerHp = playerMaxHp; } - // Name in green (friendly player color) - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%s", playerName.c_str()); + // Name in green (friendly player color) — clickable for self-target + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); + if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) { + gameHandler.setTarget(gameHandler.getPlayerGuid()); + } + ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("Lv %u", playerLevel); @@ -2276,6 +2281,66 @@ void GameScreen::renderEscapeMenu() { ImGui::End(); } +// ============================================================ +// Death Screen +// ============================================================ + +void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { + if (!gameHandler.isPlayerDead()) 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; + + // Dark red overlay covering the whole screen + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.0f, 0.0f, 0.45f)); + ImGui::Begin("##DeathOverlay", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing); + ImGui::End(); + ImGui::PopStyleColor(); + + // "Release Spirit" dialog centered on screen + float dlgW = 280.0f; + float dlgH = 100.0f; + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.0f, 0.0f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); + + if (ImGui::Begin("##DeathDialog", nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { + + ImGui::Spacing(); + // Center "You are dead." text + const char* deathText = "You are dead."; + float textW = ImGui::CalcTextSize(deathText).x; + ImGui::SetCursorPosX((dlgW - textW) / 2); + ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Center the Release Spirit button + float btnW = 180.0f; + ImGui::SetCursorPosX((dlgW - btnW) / 2); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f)); + if (ImGui::Button("Release Spirit", ImVec2(btnW, 30))) { + gameHandler.releaseSpirit(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Settings Window // ============================================================