From 81166346ef0401060cfb3e654b45f12846e4a86d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 16:47:07 -0800 Subject: [PATCH] Fix respawned corpse movement, faction hostility, and add WoW-canonical mob level colors Reset NPC animation to idle when health goes from 0 to >0 (respawn), prevent dead NPCs from being moved by server movement packets. Fix faction hostility to check factionGroup Monster bit and individual enemy arrays, not just enemyGroup. Add level-based mob coloring: grey (no XP), green (easy), yellow (even), orange (hard), red (very hard) for target frame and selection circle. --- include/game/game_handler.hpp | 5 +++ include/rendering/character_renderer.hpp | 1 + src/core/application.cpp | 40 +++++++++++++++++++++ src/game/game_handler.cpp | 9 ++++- src/game/npc_manager.cpp | 24 ++++++++++++- src/rendering/character_renderer.cpp | 11 ++++++ src/ui/game_screen.cpp | 46 ++++++++++++++++++++---- 7 files changed, 127 insertions(+), 9 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 1881cde6..01502483 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -252,6 +252,10 @@ public: using NpcDeathCallback = std::function; void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); } + // NPC respawn callback (health 0 → >0, resets animation to idle) + using NpcRespawnCallback = std::function; + void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); } + // Melee swing callback (for driving animation/SFX) using MeleeSwingCallback = std::function; void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); } @@ -683,6 +687,7 @@ private: float swingTimer_ = 0.0f; static constexpr float SWING_SPEED = 2.0f; NpcDeathCallback npcDeathCallback_; + NpcRespawnCallback npcRespawnCallback_; MeleeSwingCallback meleeSwingCallback_; NpcSwingCallback npcSwingCallback_; uint32_t localPlayerHealth_ = 0; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 921dc3c4..0c072978 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -121,6 +121,7 @@ private: int currentSequenceIndex = -1; // Index into M2Model::sequences float animationTime = 0.0f; bool animationLoop = true; + bool isDead = false; // Prevents movement while in death state std::vector boneMatrices; // Current bone transforms // Geoset visibility — which submesh IDs to render diff --git a/src/core/application.cpp b/src/core/application.cpp index 13858eec..bd6349f4 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -638,11 +638,33 @@ void Application::setupUICallbacks() { break; } } + // Find player's parent faction ID for individual enemy checks + uint32_t playerFactionId = 0; + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + if (dbc->getUInt32(i, 0) == 1) { + playerFactionId = dbc->getUInt32(i, 1); + break; + } + } std::unordered_map factionMap; for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, 0); + uint32_t factionGroup = dbc->getUInt32(i, 3); uint32_t enemyGroup = dbc->getUInt32(i, 5); bool hostile = (enemyGroup & playerFriendGroup) != 0; + // Monster factionGroup bit (4) = hostile to players + if (!hostile && (factionGroup & 4) != 0) { + hostile = true; + } + // Check individual enemy faction IDs (fields 6-9) + if (!hostile && playerFactionId > 0) { + for (int e = 6; e <= 9; e++) { + if (dbc->getUInt32(i, e) == playerFactionId) { + hostile = true; + break; + } + } + } factionMap[id] = hostile; } gameHandler->setFactionHostileMap(std::move(factionMap)); @@ -679,6 +701,14 @@ void Application::setupUICallbacks() { } }); + // NPC respawn callback (online mode) - reset to idle animation + gameHandler->setNpcRespawnCallback([this](uint64_t guid) { + auto it = creatureInstances_.find(guid); + if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { + renderer->getCharacterRenderer()->playAnimation(it->second, 0, true); // Idle + } + }); + // NPC swing callback (online mode) - play attack animation gameHandler->setNpcSwingCallback([this](uint64_t guid) { auto it = creatureInstances_.find(guid); @@ -1248,6 +1278,16 @@ void Application::spawnNpcs() { cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death } }); + gameHandler->setNpcRespawnCallback([npcMgr, cr, app](uint64_t guid) { + uint32_t instanceId = npcMgr->findRenderInstanceId(guid); + if (instanceId == 0) { + auto it = app->creatureInstances_.find(guid); + if (it != app->creatureInstances_.end()) instanceId = it->second; + } + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle + } + }); gameHandler->setNpcSwingCallback([npcMgr, cr, app](uint64_t guid) { uint32_t instanceId = npcMgr->findRenderInstanceId(guid); if (instanceId == 0) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 46a0b19e..181506ae 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2652,7 +2652,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { auto unit = std::static_pointer_cast(entity); for (const auto& [key, val] : block.fields) { switch (key) { - case 24: + case 24: { + uint32_t oldHealth = unit->getHealth(); unit->setHealth(val); if (val == 0) { if (block.guid == autoAttackTarget) { @@ -2662,8 +2663,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) { npcDeathCallback_(block.guid); } + } else if (oldHealth == 0 && val > 0) { + // Respawn: health went from 0 to >0, reset animation + if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) { + npcRespawnCallback_(block.guid); + } } break; + } case 25: unit->setPower(val); break; case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; diff --git a/src/game/npc_manager.cpp b/src/game/npc_manager.cpp index da79cc25..2030ea9e 100644 --- a/src/game/npc_manager.cpp +++ b/src/game/npc_manager.cpp @@ -736,12 +736,34 @@ void NpcManager::initialize(pipeline::AssetManager* am, break; } } + // Find player's parent faction ID for individual enemy checks + uint32_t playerFactionId = 0; + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + if (dbc->getUInt32(i, 0) == 1) { + playerFactionId = dbc->getUInt32(i, 1); // Faction (parent) + break; + } + } // Second pass: classify each faction template for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, 0); + uint32_t factionGroup = dbc->getUInt32(i, 3); uint32_t enemyGroup = dbc->getUInt32(i, 5); - // Hostile only if creature's enemy groups overlap player's faction/friend groups + // Check group-level hostility bool hostile = (enemyGroup & playerFriendGroup) != 0; + // Check if creature is a Monster type (factionGroup bit 4) + if (!hostile && (factionGroup & 4) != 0) { + hostile = true; + } + // Check individual enemy faction IDs (fields 6-9) + if (!hostile && playerFactionId > 0) { + for (int e = 6; e <= 9; e++) { + if (dbc->getUInt32(i, e) == playerFactionId) { + hostile = true; + break; + } + } + } factionHostile[id] = hostile; } LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(), diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 46de569a..978a47d7 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -876,6 +876,13 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId, auto& instance = it->second; auto& model = models[instance.modelId].data; + // Track death state for preventing movement while dead + if (animationId == 1) { + instance.isDead = true; + } else if (instance.isDead && animationId == 0) { + instance.isDead = false; // Respawned + } + // Find animation sequence index by ID instance.currentAnimationId = animationId; instance.currentSequenceIndex = -1; @@ -1437,6 +1444,10 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des if (it == instances.end()) return; auto& inst = it->second; + + // Don't move dead instances (corpses shouldn't slide around) + if (inst.isDead) return; + if (durationSeconds <= 0.0f) { // Instant move (stop) inst.position = destination; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 44e59d44..62a97b04 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -170,7 +170,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ())); renderer->setTargetPosition(&targetGLPos); - // Selection circle color: red=hostile, green=friendly, gray=dead + // Selection circle color: WoW-canonical level-based colors glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow float circleRadius = 1.5f; if (target->getType() == game::ObjectType::UNIT) { @@ -178,7 +178,20 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) { circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead) } else if (unit->isHostile()) { - circleColor = glm::vec3(1.0f, 0.2f, 0.2f); // red (hostile) + uint32_t playerLv = gameHandler.getPlayerLevel(); + uint32_t mobLv = unit->getLevel(); + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) { + circleColor = glm::vec3(0.6f, 0.6f, 0.6f); // grey + } else if (diff >= 10) { + circleColor = glm::vec3(1.0f, 0.1f, 0.1f); // red + } else if (diff >= 5) { + circleColor = glm::vec3(1.0f, 0.5f, 0.1f); // orange + } else if (diff >= -2) { + circleColor = glm::vec3(1.0f, 1.0f, 0.1f); // yellow + } else { + circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green + } } else { circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly) } @@ -724,7 +737,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; - // Determine hostility color for border and name + // Determine hostility/level color for border and name (WoW-canonical) ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f); if (target->getType() == game::ObjectType::PLAYER) { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); @@ -733,9 +746,23 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (u->getHealth() == 0 && u->getMaxHealth() > 0) { hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); } else if (u->isHostile()) { - hostileColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); + // WoW level-based color for hostile mobs + uint32_t playerLv = gameHandler.getPlayerLevel(); + uint32_t mobLv = u->getLevel(); + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) { + hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP + } else if (diff >= 10) { + hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard + } else if (diff >= 5) { + hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard + } else if (diff >= -2) { + hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even + } else { + hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy + } } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly } } @@ -751,11 +778,16 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::TextColored(nameColor, "%s", name.c_str()); - // Level (for units/players) + // Level (for units/players) — colored by difficulty if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) { auto unit = std::static_pointer_cast(target); ImGui::SameLine(); - ImGui::TextDisabled("Lv %u", unit->getLevel()); + // Level color matches the hostility/difficulty color + ImVec4 levelColor = hostileColor; + if (target->getType() == game::ObjectType::PLAYER) { + levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + } + ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); // Health bar uint32_t hp = unit->getHealth();