mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
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.
This commit is contained in:
parent
2aa8187562
commit
81166346ef
7 changed files with 127 additions and 9 deletions
|
|
@ -252,6 +252,10 @@ public:
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||||
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
// NPC respawn callback (health 0 → >0, resets animation to idle)
|
||||||
|
using NpcRespawnCallback = std::function<void(uint64_t guid)>;
|
||||||
|
void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// Melee swing callback (for driving animation/SFX)
|
// Melee swing callback (for driving animation/SFX)
|
||||||
using MeleeSwingCallback = std::function<void()>;
|
using MeleeSwingCallback = std::function<void()>;
|
||||||
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
|
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
|
||||||
|
|
@ -683,6 +687,7 @@ private:
|
||||||
float swingTimer_ = 0.0f;
|
float swingTimer_ = 0.0f;
|
||||||
static constexpr float SWING_SPEED = 2.0f;
|
static constexpr float SWING_SPEED = 2.0f;
|
||||||
NpcDeathCallback npcDeathCallback_;
|
NpcDeathCallback npcDeathCallback_;
|
||||||
|
NpcRespawnCallback npcRespawnCallback_;
|
||||||
MeleeSwingCallback meleeSwingCallback_;
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
NpcSwingCallback npcSwingCallback_;
|
NpcSwingCallback npcSwingCallback_;
|
||||||
uint32_t localPlayerHealth_ = 0;
|
uint32_t localPlayerHealth_ = 0;
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ private:
|
||||||
int currentSequenceIndex = -1; // Index into M2Model::sequences
|
int currentSequenceIndex = -1; // Index into M2Model::sequences
|
||||||
float animationTime = 0.0f;
|
float animationTime = 0.0f;
|
||||||
bool animationLoop = true;
|
bool animationLoop = true;
|
||||||
|
bool isDead = false; // Prevents movement while in death state
|
||||||
std::vector<glm::mat4> boneMatrices; // Current bone transforms
|
std::vector<glm::mat4> boneMatrices; // Current bone transforms
|
||||||
|
|
||||||
// Geoset visibility — which submesh IDs to render
|
// Geoset visibility — which submesh IDs to render
|
||||||
|
|
|
||||||
|
|
@ -638,11 +638,33 @@ void Application::setupUICallbacks() {
|
||||||
break;
|
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<uint32_t, bool> factionMap;
|
std::unordered_map<uint32_t, bool> factionMap;
|
||||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||||
uint32_t id = dbc->getUInt32(i, 0);
|
uint32_t id = dbc->getUInt32(i, 0);
|
||||||
|
uint32_t factionGroup = dbc->getUInt32(i, 3);
|
||||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
||||||
bool hostile = (enemyGroup & playerFriendGroup) != 0;
|
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;
|
factionMap[id] = hostile;
|
||||||
}
|
}
|
||||||
gameHandler->setFactionHostileMap(std::move(factionMap));
|
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
|
// NPC swing callback (online mode) - play attack animation
|
||||||
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
|
gameHandler->setNpcSwingCallback([this](uint64_t guid) {
|
||||||
auto it = creatureInstances_.find(guid);
|
auto it = creatureInstances_.find(guid);
|
||||||
|
|
@ -1248,6 +1278,16 @@ void Application::spawnNpcs() {
|
||||||
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
|
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) {
|
gameHandler->setNpcSwingCallback([npcMgr, cr, app](uint64_t guid) {
|
||||||
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
||||||
if (instanceId == 0) {
|
if (instanceId == 0) {
|
||||||
|
|
|
||||||
|
|
@ -2652,7 +2652,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
for (const auto& [key, val] : block.fields) {
|
for (const auto& [key, val] : block.fields) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 24:
|
case 24: {
|
||||||
|
uint32_t oldHealth = unit->getHealth();
|
||||||
unit->setHealth(val);
|
unit->setHealth(val);
|
||||||
if (val == 0) {
|
if (val == 0) {
|
||||||
if (block.guid == autoAttackTarget) {
|
if (block.guid == autoAttackTarget) {
|
||||||
|
|
@ -2662,8 +2663,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) {
|
||||||
npcDeathCallback_(block.guid);
|
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;
|
break;
|
||||||
|
}
|
||||||
case 25: unit->setPower(val); break;
|
case 25: unit->setPower(val); break;
|
||||||
case 32: unit->setMaxHealth(val); break;
|
case 32: unit->setMaxHealth(val); break;
|
||||||
case 33: unit->setMaxPower(val); break;
|
case 33: unit->setMaxPower(val); break;
|
||||||
|
|
|
||||||
|
|
@ -736,12 +736,34 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
||||||
break;
|
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
|
// Second pass: classify each faction template
|
||||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||||
uint32_t id = dbc->getUInt32(i, 0);
|
uint32_t id = dbc->getUInt32(i, 0);
|
||||||
|
uint32_t factionGroup = dbc->getUInt32(i, 3);
|
||||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
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;
|
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;
|
factionHostile[id] = hostile;
|
||||||
}
|
}
|
||||||
LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(),
|
LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(),
|
||||||
|
|
|
||||||
|
|
@ -876,6 +876,13 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
||||||
auto& instance = it->second;
|
auto& instance = it->second;
|
||||||
auto& model = models[instance.modelId].data;
|
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
|
// Find animation sequence index by ID
|
||||||
instance.currentAnimationId = animationId;
|
instance.currentAnimationId = animationId;
|
||||||
instance.currentSequenceIndex = -1;
|
instance.currentSequenceIndex = -1;
|
||||||
|
|
@ -1437,6 +1444,10 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
|
||||||
if (it == instances.end()) return;
|
if (it == instances.end()) return;
|
||||||
|
|
||||||
auto& inst = it->second;
|
auto& inst = it->second;
|
||||||
|
|
||||||
|
// Don't move dead instances (corpses shouldn't slide around)
|
||||||
|
if (inst.isDead) return;
|
||||||
|
|
||||||
if (durationSeconds <= 0.0f) {
|
if (durationSeconds <= 0.0f) {
|
||||||
// Instant move (stop)
|
// Instant move (stop)
|
||||||
inst.position = destination;
|
inst.position = destination;
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
||||||
renderer->setTargetPosition(&targetGLPos);
|
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
|
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
||||||
float circleRadius = 1.5f;
|
float circleRadius = 1.5f;
|
||||||
if (target->getType() == game::ObjectType::UNIT) {
|
if (target->getType() == game::ObjectType::UNIT) {
|
||||||
|
|
@ -178,7 +178,20 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||||
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
||||||
} else if (unit->isHostile()) {
|
} 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<int32_t>(mobLv) - static_cast<int32_t>(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 {
|
} else {
|
||||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
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_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
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);
|
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||||
if (target->getType() == game::ObjectType::PLAYER) {
|
if (target->getType() == game::ObjectType::PLAYER) {
|
||||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
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) {
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||||
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||||
} else if (u->isHostile()) {
|
} 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<int32_t>(mobLv) - static_cast<int32_t>(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 {
|
} 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());
|
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) {
|
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
|
||||||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||||
ImGui::SameLine();
|
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
|
// Health bar
|
||||||
uint32_t hp = unit->getHealth();
|
uint32_t hp = unit->getHealth();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue