diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index 529e0e8b..e8ceee38 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -65,5 +65,173 @@ struct SpellCooldownEntry { uint32_t categoryCooldownMs; }; +/** + * Get human-readable spell cast failure reason (WoW 3.3.5a SpellCastResult) + */ +inline const char* getSpellCastResultString(uint8_t result) { + switch (result) { + case 0: return "Spell failed"; + case 1: return "Affects dead target"; + case 2: return "Already at full health"; + case 3: return "Already at full mana"; + case 4: return "Already at full power"; + case 5: return "Already being tamed"; + case 6: return "Already have charm"; + case 7: return "Already have summon"; + case 8: return "Already open"; + case 9: return "Aura bounced"; + case 10: return "Autopilot in use"; + case 11: return "Bad implicit targets"; + case 12: return "Bad targets"; + case 13: return "Can't be charmed"; + case 14: return "Can't be disenchanted"; + case 15: return "Can't be disenchanted (skill)"; + case 16: return "Can't be milled"; + case 17: return "Can't be prospected"; + case 18: return "Can't cast on tapped"; + case 19: return "Can't duel while invisible"; + case 20: return "Can't duel while stealthed"; + case 21: return "Can't stealth"; + case 22: return "Caster aurastate"; + case 23: return "Caster dead"; + case 24: return "Charmed"; + case 25: return "Chest in use"; + case 26: return "Confused"; + case 27: return "Don't report"; + case 28: return "Equipped item"; + case 29: return "Equipped item (class)"; + case 30: return "Equipped item (class2)"; + case 31: return "Equipped item (level)"; + case 32: return "Error"; + case 33: return "Fizzle"; + case 34: return "Fleeing"; + case 35: return "Food too low level"; + case 36: return "Highlighted rune needed"; + case 37: return "Immune"; + case 38: return "Interrupted"; + case 39: return "Interrupted (combat)"; + case 40: return "Invalid item"; + case 41: return "Item already enchanted"; + case 42: return "Item gone"; + case 43: return "Item not found"; + case 44: return "Item not ready"; + case 45: return "Level requirement"; + case 46: return "Line of sight"; + case 47: return "Lowlevel"; + case 48: return "Low castlevel"; + case 49: return "Mainhand empty"; + case 50: return "Moving"; + case 51: return "Must be behind target"; + case 52: return "Need ammo"; + case 53: return "Need ammo pouch"; + case 54: return "Need exotic ammo"; + case 55: return "Need more items"; + case 56: return "No path"; + case 57: return "Not behind"; + case 58: return "Not fishable"; + case 59: return "Not flying"; + case 60: return "Not here"; + case 61: return "Not infront"; + case 62: return "Not in control"; + case 63: return "Not known"; + case 64: return "Not mounted"; + case 65: return "Not on taxi"; + case 66: return "Not on transport"; + case 67: return "Not ready"; + case 68: return "Not shapeshift"; + case 69: return "Not standing"; + case 70: return "Not tradeable"; + case 71: return "Not trading"; + case 72: return "Not unsheathed"; + case 73: return "Not while ghost"; + case 74: return "Not while looting"; + case 75: return "No charges remain"; + case 76: return "No champion"; + case 77: return "No combo points"; + case 78: return "No dueling"; + case 79: return "No endurance"; + case 80: return "No fish"; + case 81: return "No items while shapeshifted"; + case 82: return "No mounts allowed here"; + case 83: return "No pet"; + case 84: return "No power"; + case 85: return "Nothing to dispel"; + case 86: return "Nothing to steal"; + case 87: return "Only above water"; + case 88: return "Only daytime"; + case 89: return "Only indoors"; + case 90: return "Only mounted"; + case 91: return "Only nighttime"; + case 92: return "Only outdoors"; + case 93: return "Only shapeshift"; + case 94: return "Only stealthed"; + case 95: return "Only underwater"; + case 96: return "Out of range"; + case 97: return "Pacified"; + case 98: return "Possessed"; + case 99: return "Reagents"; + case 100: return "Requires area"; + case 101: return "Requires spell focus"; + case 102: return "Rooted"; + case 103: return "Silenced"; + case 104: return "Spell in progress"; + case 105: return "Spell learned"; + case 106: return "Spell unavailable"; + case 107: return "Stunned"; + case 108: return "Targets dead"; + case 109: return "Target not dead"; + case 110: return "Target not in party"; + case 111: return "Target not in raid"; + case 112: return "Target friendly"; + case 113: return "Target is player"; + case 114: return "Target is player controlled"; + case 115: return "Target not dead"; + case 116: return "Target not in party"; + case 117: return "Target not player"; + case 118: return "Target no pockets"; + case 119: return "Target no weapons"; + case 120: return "Target out of range"; + case 121: return "Target unskinnable"; + case 122: return "Thirst satiated"; + case 123: return "Too close"; + case 124: return "Too many of item"; + case 125: return "Totem category"; + case 126: return "Totems"; + case 127: return "Training points"; + case 128: return "Try again"; + case 129: return "Unit not behind"; + case 130: return "Unit not infront"; + case 131: return "Wrong pet food"; + case 132: return "Not while fatigued"; + case 133: return "Target not in instance"; + case 134: return "Not while trading"; + case 135: return "Target not in raid"; + case 136: return "Target feign dead"; + case 137: return "Disabled by power scaling"; + case 138: return "Quest players only"; + case 139: return "Not idle"; + case 140: return "Not inactive"; + case 141: return "Partial playtime"; + case 142: return "No playtime"; + case 143: return "Not in battleground"; + case 144: return "Not in raid instance"; + case 145: return "Only in arena"; + case 146: return "Target locked to raid instance"; + case 147: return "On use enchant"; + case 148: return "Not on ground"; + case 149: return "Custom error"; + case 150: return "Can't open lock"; + case 151: return "Wrong artifact equipped"; + case 173: return "Not enough mana"; + case 174: return "Not enough health"; + case 175: return "Not enough holy power"; + case 176: return "Not enough rage"; + case 177: return "Not enough energy"; + case 178: return "Not enough runes"; + case 179: return "Not enough runic power"; + default: return nullptr; + } +} + } // namespace game } // namespace wowee diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 81785f06..f2d97279 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -122,6 +122,7 @@ public: void setTargetPosition(const glm::vec3* pos); bool isMoving() const; void triggerMeleeSwing(); + void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } // Selection circle for targeted entity void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color); @@ -254,6 +255,7 @@ private: float meleeSwingCooldown = 0.0f; float meleeAnimDurationMs = 0.0f; uint32_t meleeAnimId = 0; + uint32_t equippedWeaponInvType_ = 0; bool terrainEnabled = true; bool terrainLoaded = false; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 54a7517f..5e4f012d 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -76,7 +76,9 @@ private: // Item icon cache: displayInfoId -> GL texture std::unordered_map iconCache_; +public: GLuint getItemIcon(uint32_t displayInfoId); +private: // Character model preview std::unique_ptr charPreview_; diff --git a/src/core/application.cpp b/src/core/application.cpp index 368a8a25..1b781fe4 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -787,6 +787,21 @@ void Application::spawnPlayerCharacter() { std::string faceLowerTexturePath; std::vector underwearPaths; + // Extract appearance bytes for texture lookups + uint8_t charSkinId = 0, charFaceId = 0, charHairStyleId = 0, charHairColorId = 0; + if (gameHandler) { + const game::Character* activeChar = gameHandler->getActiveCharacter(); + if (activeChar) { + charSkinId = activeChar->appearanceBytes & 0xFF; + charFaceId = (activeChar->appearanceBytes >> 8) & 0xFF; + charHairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF; + charHairColorId = (activeChar->appearanceBytes >> 24) & 0xFF; + LOG_INFO("Appearance: skin=", (int)charSkinId, " face=", (int)charFaceId, + " hairStyle=", (int)charHairStyleId, " hairColor=", (int)charHairColorId); + } + } + + std::string hairTexturePath; if (useCharSections) { auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); if (charSectionsDbc) { @@ -794,6 +809,7 @@ void Application::spawnPlayerCharacter() { bool foundSkin = false; bool foundUnderwear = false; bool foundFaceLower = false; + bool foundHair = false; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { uint32_t raceId = charSectionsDbc->getUInt32(r, 1); uint32_t sexId = charSectionsDbc->getUInt32(r, 2); @@ -803,23 +819,37 @@ void Application::spawnPlayerCharacter() { if (raceId != targetRaceId || sexId != targetSexId) continue; - if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == 0) { + // Section 0 = skin: match by colorIndex = skin byte + if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { std::string tex1 = charSectionsDbc->getString(r, 4); if (!tex1.empty()) { bodySkinPath = tex1; foundSkin = true; - LOG_INFO(" DBC body skin: ", bodySkinPath); + LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", (int)charSkinId, ")"); } - } else if (baseSection == 3 && colorIndex == 0) { - (void)variationIndex; - } else if (baseSection == 1 && !foundFaceLower && variationIndex == 0 && colorIndex == 0) { + } + // Section 3 = hair: match variation=hairStyle, color=hairColor + else if (baseSection == 3 && !foundHair && + variationIndex == charHairStyleId && colorIndex == charHairColorId) { + hairTexturePath = charSectionsDbc->getString(r, 4); + if (!hairTexturePath.empty()) { + foundHair = true; + LOG_INFO(" DBC hair texture: ", hairTexturePath, + " (style=", (int)charHairStyleId, " color=", (int)charHairColorId, ")"); + } + } + // Section 1 = face lower: match variation=faceId + else if (baseSection == 1 && !foundFaceLower && + variationIndex == charFaceId && colorIndex == charSkinId) { std::string tex1 = charSectionsDbc->getString(r, 4); if (!tex1.empty()) { faceLowerTexturePath = tex1; foundFaceLower = true; LOG_INFO(" DBC face texture: ", faceLowerTexturePath); } - } else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == 0) { + } + // Section 4 = underwear + else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) { for (int f = 4; f <= 6; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { @@ -829,36 +859,19 @@ void Application::spawnPlayerCharacter() { } foundUnderwear = true; } + + if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break; + } + + if (!foundHair) { + LOG_WARNING("No DBC hair match for style=", (int)charHairStyleId, + " color=", (int)charHairColorId, + " race=", targetRaceId, " sex=", targetSexId); } } else { LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures"); } - // Look up hair texture from CharSections.dbc section 3 - std::string hairTexturePath; - if (gameHandler) { - const game::Character* activeChar = gameHandler->getActiveCharacter(); - if (activeChar) { - uint8_t hairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF; - uint8_t hairColorId = (activeChar->appearanceBytes >> 24) & 0xFF; - for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, 1); - uint32_t sexId = charSectionsDbc->getUInt32(r, 2); - uint32_t section = charSectionsDbc->getUInt32(r, 3); - uint32_t variation = charSectionsDbc->getUInt32(r, 8); - uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9); - if (raceId != targetRaceId || sexId != targetSexId) continue; - if (section != 3) continue; - if (variation != hairStyleId) continue; - if (colorIdx != hairColorId) continue; - hairTexturePath = charSectionsDbc->getString(r, 4); - LOG_INFO(" DBC hair texture: ", hairTexturePath, - " (style=", (int)hairStyleId, " color=", (int)hairColorId, ")"); - break; - } - } - } - for (auto& tex : model.textures) { if (tex.type == 1 && tex.filename.empty()) { tex.filename = bodySkinPath; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 218e84b4..41ea983f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3309,7 +3309,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { if (casting) return; // Already casting - uint64_t target = targetGuid != 0 ? targetGuid : targetGuid; + uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid; auto packet = CastSpellPacket::build(spellId, target, ++castCount); socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); @@ -3389,11 +3389,16 @@ void GameHandler::handleCastFailed(network::Packet& packet) { currentCastSpellId = 0; castTimeRemaining = 0.0f; - // Add system message about failed cast + // Add system message about failed cast with readable reason + const char* reason = getSpellCastResultString(data.result); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; - msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")"; + if (reason) { + msg.message = reason; + } else { + msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")"; + } addLocalChatMessage(msg); } @@ -4310,8 +4315,8 @@ void GameHandler::simulateXpGain(uint64_t victimGuid, uint32_t totalXp) { packet.writeUInt64(victimGuid); packet.writeUInt32(totalXp); packet.writeUInt8(0); // kill XP - packet.writeFloat(0.0f); - packet.writeUInt32(0); // group bonus + packet.writeFloat(1.0f); // group rate (1.0 = solo, no bonus) + packet.writeUInt8(0); // RAF flag handleXpGain(packet); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 409064db..986b9270 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1326,16 +1326,19 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa data.containerSlots = packet.readUInt32(); uint32_t statsCount = packet.readUInt32(); - for (uint32_t i = 0; i < statsCount && i < 10; i++) { + // Server always sends 10 stat pairs; statsCount tells how many are meaningful + for (uint32_t i = 0; i < 10; i++) { uint32_t statType = packet.readUInt32(); int32_t statValue = static_cast(packet.readUInt32()); - switch (statType) { - case 3: data.agility = statValue; break; - case 4: data.strength = statValue; break; - case 5: data.intellect = statValue; break; - case 6: data.spirit = statValue; break; - case 7: data.stamina = statValue; break; - default: break; + if (i < statsCount) { + switch (statType) { + case 3: data.agility = statValue; break; + case 4: data.strength = statValue; break; + case 5: data.intellect = statValue; break; + case 6: data.spirit = statValue; break; + case 7: data.stamina = statValue; break; + default: break; + } } } @@ -1585,9 +1588,13 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) { data.totalXp = packet.readUInt32(); data.type = packet.readUInt8(); if (data.type == 0) { - // Kill XP: has group bonus float (unused) + group bonus uint32 - packet.readFloat(); - data.groupBonus = packet.readUInt32(); + // Kill XP: float groupRate (1.0 = solo) + uint8 RAF flag + float groupRate = packet.readFloat(); + packet.readUInt8(); // RAF bonus flag + // Group bonus = total - (total / rate); only if grouped (rate > 1) + if (groupRate > 1.0f) { + data.groupBonus = data.totalXp - static_cast(data.totalXp / groupRate); + } } LOG_INFO("XP gain: ", data.totalXp, " xp (type=", static_cast(data.type), ")"); return data.totalXp > 0; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1cc094d8..3abb4e24 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -391,9 +391,26 @@ uint32_t Renderer::resolveMeleeAnimId() { return 0.0f; }; - // Prefer weapon attacks (1H=17, 2H=18) over unarmed (16); 19-21 are other variants - const uint32_t attackCandidates[] = {17, 18, 16, 19, 20, 21}; - for (uint32_t id : attackCandidates) { + // Select animation priority based on equipped weapon type + // WoW inventory types: 17 = 2H weapon, 13/21 = 1H, 0 = unarmed + // WoW anim IDs: 16 = unarmed, 17 = 1H attack, 18 = 2H attack + const uint32_t* attackCandidates; + size_t candidateCount; + static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; + static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; + static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; + if (equippedWeaponInvType_ == 17) { // INVTYPE_2HWEAPON + attackCandidates = candidates2H; + candidateCount = 6; + } else if (equippedWeaponInvType_ == 0) { + attackCandidates = candidatesUnarmed; + candidateCount = 6; + } else { + attackCandidates = candidates1H; + candidateCount = 6; + } + for (size_t ci = 0; ci < candidateCount; ci++) { + uint32_t id = attackCandidates[ci]; if (characterRenderer->hasAnimation(characterInstanceId, id)) { meleeAnimId = id; meleeAnimDurationMs = findDuration(id); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e240913e..9f80e52c 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -149,6 +149,12 @@ void GameScreen::render(game::GameHandler& gameHandler) { core::Application::getInstance().loadEquippedWeapons(); gameHandler.notifyEquipmentChanged(); inventoryScreen.markPreviewDirty(); + // Update renderer weapon type for animation selection + auto* r = core::Application::getInstance().getRenderer(); + if (r) { + const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND); + r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType); + } } // Update renderer face-target position and selection circle @@ -1723,14 +1729,73 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { ImGui::Separator(); } - // Items + // Items with icons and labels + constexpr float iconSize = 32.0f; for (const auto& item : loot.items) { ImGui::PushID(item.slotIndex); - char label[64]; - snprintf(label, sizeof(label), "Item %u (x%u)", item.itemId, item.count); - if (ImGui::Selectable(label)) { + + // Get item info for name and quality + const auto* info = gameHandler.getItemInfo(item.itemId); + std::string itemName = info && !info->name.empty() + ? info->name + : "Item #" + std::to_string(item.itemId); + game::ItemQuality quality = info + ? static_cast(info->quality) + : game::ItemQuality::COMMON; + ImVec4 qColor = InventoryScreen::getQualityColor(quality); + + // Get item icon + uint32_t displayId = item.displayInfoId; + if (displayId == 0 && info) displayId = info->displayInfoId; + GLuint iconTex = inventoryScreen.getItemIcon(displayId); + + ImVec2 cursor = ImGui::GetCursorScreenPos(); + float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f); + + // Invisible selectable for click handling + if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) { gameHandler.lootItem(item.slotIndex); } + bool hovered = ImGui::IsItemHovered(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + // Draw hover highlight + if (hovered) { + drawList->AddRectFilled(cursor, + ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f, + cursor.y + rowH), + IM_COL32(255, 255, 255, 30)); + } + + // Draw icon + if (iconTex) { + drawList->AddImage((ImTextureID)(uintptr_t)iconTex, + cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize)); + drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), + ImGui::ColorConvertFloat4ToU32(qColor)); + } else { + drawList->AddRectFilled(cursor, + ImVec2(cursor.x + iconSize, cursor.y + iconSize), + IM_COL32(40, 40, 50, 200)); + drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), + IM_COL32(80, 80, 80, 200)); + } + + // Draw item name + float textX = cursor.x + iconSize + 6.0f; + float textY = cursor.y + 2.0f; + drawList->AddText(ImVec2(textX, textY), + ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str()); + + // Draw count if > 1 + if (item.count > 1) { + char countStr[32]; + snprintf(countStr, sizeof(countStr), "x%u", item.count); + float countY = textY + ImGui::GetTextLineHeight(); + drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr); + } + ImGui::PopID(); } @@ -1924,6 +1989,9 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); ImGui::Separator(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell"); + ImGui::Separator(); + if (vendor.items.empty()) { ImGui::TextDisabled("This vendor has nothing for sale."); } else { diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 97efdfe6..e2bd5a12 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -642,7 +642,9 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { // Stats panel ImGui::Spacing(); + ImGui::Spacing(); ImGui::Separator(); + ImGui::Spacing(); renderStatsPanel(inventory, gameHandler.getPlayerLevel()); ImGui::End();