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
This commit is contained in:
Kelsi 2026-02-06 17:27:20 -08:00
parent 046111d037
commit 7436420cd1
9 changed files with 270 additions and 102 deletions

View file

@ -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();

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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<uint32_t> 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<uint32_t, bool> 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<uint32_t>(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<std::string> 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<uint32_t> 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<uint32_t, bool> 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<uint8_t>(activeChar->race));
}
}
// Spawn player model for online mode
if (gameHandler) {
const game::Character* activeChar = gameHandler->getActiveCharacter();

View file

@ -2562,7 +2562,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
auto unit = std::static_pointer_cast<Unit>(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<Entity> 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) {

View file

@ -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<uint16_t>(Opcode::CMSG_REPOP_REQUEST));
packet.writeUInt8(0); // auto-release flag (0 = manual)
return packet;
}
} // namespace game
} // namespace wowee

View file

@ -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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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
// ============================================================