mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 00:03:50 +00:00
Fix item use (CMSG_USE_ITEM), mount tab, and inventory right-click
- Fix SMSG_ITEM_QUERY_SINGLE_RESPONSE parsing: read statsCount stat pairs instead of always 10, use 2 damage entries (MAX_ITEM_PROTO_DAMAGES), and parse item spell data (spellId + spellTrigger per slot) - Pass item spell ID in CMSG_USE_ITEM packet so server processes item use requests (spellId=0 caused silent server rejection) - Add spellId parameter to buildUseItem interface across all expansions - Fix spellbook mount tab to use SkillLine 777 (Mounts) instead of 762 (Riding), so known mount summon spells appear correctly - Fix inventory right-click: use IsItemHovered+IsMouseClicked instead of IsItemClicked for InvisibleButton (which only tracks left-clicks) - Fix SlotKind enum declaration order in inventory_screen.hpp
This commit is contained in:
parent
7982815a67
commit
c919477e74
9 changed files with 233 additions and 152 deletions
|
|
@ -51,8 +51,8 @@ public:
|
|||
}
|
||||
|
||||
/** Build CMSG_USE_ITEM (WotLK default: bag + slot + castCount + spellId + itemGuid + glyphIndex + castFlags + targets) */
|
||||
virtual network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) {
|
||||
return UseItemPacket::build(bagIndex, slotIndex, itemGuid);
|
||||
virtual network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) {
|
||||
return UseItemPacket::build(bagIndex, slotIndex, itemGuid, spellId);
|
||||
}
|
||||
|
||||
// --- Character Enumeration ---
|
||||
|
|
@ -313,7 +313,7 @@ public:
|
|||
const MovementInfo& info,
|
||||
uint64_t playerGuid = 0) override;
|
||||
network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override;
|
||||
network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) override;
|
||||
network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) override;
|
||||
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
||||
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
||||
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
||||
|
|
|
|||
|
|
@ -1512,6 +1512,12 @@ struct ItemQueryResponseData {
|
|||
int32_t spirit = 0;
|
||||
uint32_t sellPrice = 0;
|
||||
std::string subclassName;
|
||||
// Item spells (up to 5)
|
||||
struct ItemSpell {
|
||||
uint32_t spellId = 0;
|
||||
uint32_t spellTrigger = 0; // 0=Use, 1=Equip, 2=ChanceOnHit, 5=Learn
|
||||
};
|
||||
std::array<ItemSpell, 5> spells{};
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
|
|
@ -1879,7 +1885,7 @@ public:
|
|||
/** CMSG_USE_ITEM packet builder */
|
||||
class UseItemPacket {
|
||||
public:
|
||||
static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid);
|
||||
static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0);
|
||||
};
|
||||
|
||||
/** CMSG_AUTOEQUIP_ITEM packet builder */
|
||||
|
|
|
|||
|
|
@ -125,6 +125,19 @@ private:
|
|||
int heldBagSlotIndex = -1;
|
||||
game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
||||
|
||||
// Slot rendering with interaction support
|
||||
enum class SlotKind { BACKPACK, EQUIPMENT };
|
||||
|
||||
// Click-and-hold pickup tracking
|
||||
bool pickupPending_ = false;
|
||||
float pickupPressTime_ = 0.0f;
|
||||
SlotKind pickupSlotKind_ = SlotKind::BACKPACK;
|
||||
int pickupBackpackIndex_ = -1;
|
||||
int pickupBagIndex_ = -1;
|
||||
int pickupBagSlotIndex_ = -1;
|
||||
game::EquipSlot pickupEquipSlot_ = game::EquipSlot::NUM_SLOTS;
|
||||
static constexpr float kPickupHoldThreshold = 0.12f; // seconds
|
||||
|
||||
void renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper);
|
||||
void renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper);
|
||||
void renderBagWindow(const char* title, bool& isOpen, game::Inventory& inventory,
|
||||
|
|
@ -133,8 +146,6 @@ private:
|
|||
void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false);
|
||||
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0);
|
||||
|
||||
// Slot rendering with interaction support
|
||||
enum class SlotKind { BACKPACK, EQUIPMENT };
|
||||
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
float size, const char* label,
|
||||
SlotKind kind, int backpackIndex,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ private:
|
|||
bool skillLineDbLoaded = false;
|
||||
std::unordered_map<uint32_t, std::string> skillLineNames;
|
||||
std::unordered_map<uint32_t, uint32_t> skillLineCategories;
|
||||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine;
|
||||
std::unordered_multimap<uint32_t, uint32_t> spellToSkillLine;
|
||||
|
||||
// Categorized spell tabs
|
||||
std::vector<SpellTabInfo> spellTabs;
|
||||
|
|
|
|||
|
|
@ -7436,15 +7436,10 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
|
|||
|
||||
uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const {
|
||||
if (itemId == 0) return 0;
|
||||
uint64_t found = 0;
|
||||
for (const auto& [guid, info] : onlineItems_) {
|
||||
if (info.entry != itemId) continue;
|
||||
if (found != 0) {
|
||||
return 0; // Ambiguous
|
||||
}
|
||||
found = guid;
|
||||
if (info.entry == itemId) return guid;
|
||||
}
|
||||
return found;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void GameHandler::detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields) {
|
||||
|
|
@ -9309,6 +9304,14 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show chat message for non-talent spells
|
||||
const std::string& name = getSpellName(spellId);
|
||||
if (!name.empty()) {
|
||||
addSystemChatMessage("You have learned a new spell: " + name + ".");
|
||||
} else {
|
||||
addSystemChatMessage("You have learned a new spell.");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||
|
|
@ -10673,29 +10676,33 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) {
|
|||
void GameHandler::useItemBySlot(int backpackIndex) {
|
||||
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
|
||||
const auto& slot = inventory.getBackpackSlot(backpackIndex);
|
||||
if (slot.empty()) {
|
||||
LOG_WARNING("useItemBySlot: slot ", backpackIndex, " is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("useItemBySlot: backpackIndex=", backpackIndex, " itemId=", slot.item.itemId,
|
||||
" wowSlot=", 23 + backpackIndex);
|
||||
if (slot.empty()) return;
|
||||
|
||||
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
|
||||
if (itemGuid == 0) {
|
||||
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
|
||||
}
|
||||
LOG_INFO("useItemBySlot: itemGuid=0x", std::hex, itemGuid, std::dec);
|
||||
|
||||
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
|
||||
// Find the item's on-use spell ID from cached item info
|
||||
uint32_t useSpellId = 0;
|
||||
if (auto* info = getItemInfo(slot.item.itemId)) {
|
||||
for (const auto& sp : info->spells) {
|
||||
// SpellTrigger: 0=Use, 5=Learn
|
||||
if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) {
|
||||
useSpellId = sp.spellId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WoW inventory: equipment 0-18, bags 19-22, backpack 23-38
|
||||
auto packet = packetParsers_
|
||||
? packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid)
|
||||
: UseItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid);
|
||||
LOG_INFO("useItemBySlot: sending CMSG_USE_ITEM, packetSize=", packet.getSize());
|
||||
? packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId)
|
||||
: UseItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId);
|
||||
socket->send(packet);
|
||||
} else if (itemGuid == 0) {
|
||||
LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex);
|
||||
addSystemChatMessage("Cannot use that item right now.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10722,17 +10729,29 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) {
|
|||
" itemGuid=0x", std::hex, itemGuid, std::dec);
|
||||
|
||||
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
|
||||
// Find the item's on-use spell ID
|
||||
uint32_t useSpellId = 0;
|
||||
if (auto* info = getItemInfo(slot.item.itemId)) {
|
||||
for (const auto& sp : info->spells) {
|
||||
if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) {
|
||||
useSpellId = sp.spellId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WoW bag addressing: bagIndex = equip slot of bag container (19-22)
|
||||
// For CMSG_USE_ITEM: bag = 19+bagIndex, slot = slot within bag
|
||||
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
|
||||
auto packet = packetParsers_
|
||||
? packetParsers_->buildUseItem(wowBag, static_cast<uint8_t>(slotIndex), itemGuid)
|
||||
: UseItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex), itemGuid);
|
||||
? packetParsers_->buildUseItem(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId)
|
||||
: UseItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId);
|
||||
LOG_INFO("useItemInBag: sending CMSG_USE_ITEM, bag=", (int)wowBag, " slot=", slotIndex,
|
||||
" packetSize=", packet.getSize());
|
||||
socket->send(packet);
|
||||
} else if (itemGuid == 0) {
|
||||
LOG_WARNING("Use item in bag failed: missing item GUID for bag ", bagIndex, " slot ", slotIndex);
|
||||
addSystemChatMessage("Cannot use that item right now.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ network::Packet ClassicPacketParsers::buildCastSpell(uint32_t spellId, uint64_t
|
|||
// Vanilla 1.12.x: bag(u8) + slot(u8) + spellIndex(u8) + SpellCastTargets(u16)
|
||||
// NO spellId, itemGuid, glyphIndex, or castFlags fields (those are WotLK)
|
||||
// ============================================================================
|
||||
network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t /*itemGuid*/) {
|
||||
network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t /*itemGuid*/, uint32_t /*spellId*/) {
|
||||
network::Packet packet(wireOpcode(LogicalOpcode::CMSG_USE_ITEM));
|
||||
packet.writeUInt8(bagIndex);
|
||||
packet.writeUInt8(slotIndex);
|
||||
|
|
|
|||
|
|
@ -2398,94 +2398,62 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
uint32_t statsCount = packet.readUInt32();
|
||||
// Server always sends 10 stat pairs; statsCount tells how many are meaningful
|
||||
for (uint32_t i = 0; i < 10; i++) {
|
||||
// Server sends exactly statsCount stat pairs (not always 10).
|
||||
uint32_t statsToRead = std::min(statsCount, 10u);
|
||||
for (uint32_t i = 0; i < statsToRead; i++) {
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
packet.readUInt32(); // ScalingStatDistribution
|
||||
packet.readUInt32(); // ScalingStatValue
|
||||
|
||||
const size_t preDamagePos = packet.getReadPos();
|
||||
struct DamageParseResult {
|
||||
float damageMin = 0.0f;
|
||||
float damageMax = 0.0f;
|
||||
int32_t armor = 0;
|
||||
uint32_t delayMs = 0;
|
||||
bool ok = false;
|
||||
};
|
||||
auto parseDamageBlock = [&](int damageEntries) -> DamageParseResult {
|
||||
DamageParseResult r;
|
||||
packet.setReadPos(preDamagePos);
|
||||
bool haveWeaponDamage = false;
|
||||
for (int i = 0; i < damageEntries; i++) {
|
||||
float dmgMin = packet.readFloat();
|
||||
float dmgMax = packet.readFloat();
|
||||
uint32_t damageType = packet.readUInt32();
|
||||
if (!haveWeaponDamage && dmgMax > 0.0f) {
|
||||
if (damageType == 0 || r.damageMax <= 0.0f) {
|
||||
r.damageMin = dmgMin;
|
||||
r.damageMax = dmgMax;
|
||||
haveWeaponDamage = (damageType == 0);
|
||||
}
|
||||
// WotLK 3.3.5a: MAX_ITEM_PROTO_DAMAGES = 2
|
||||
bool haveWeaponDamage = false;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float dmgMin = packet.readFloat();
|
||||
float dmgMax = packet.readFloat();
|
||||
uint32_t damageType = packet.readUInt32();
|
||||
if (!haveWeaponDamage && dmgMax > 0.0f) {
|
||||
if (damageType == 0 || data.damageMax <= 0.0f) {
|
||||
data.damageMin = dmgMin;
|
||||
data.damageMax = dmgMax;
|
||||
haveWeaponDamage = (damageType == 0);
|
||||
}
|
||||
}
|
||||
|
||||
r.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||||
packet.readUInt32(); // HolyRes
|
||||
packet.readUInt32(); // FireRes
|
||||
packet.readUInt32(); // NatureRes
|
||||
packet.readUInt32(); // FrostRes
|
||||
packet.readUInt32(); // ShadowRes
|
||||
packet.readUInt32(); // ArcaneRes
|
||||
r.delayMs = packet.readUInt32();
|
||||
r.ok = true;
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
// All WoW versions (Classic, TBC, WotLK) use exactly 5 damage entries in
|
||||
// SMSG_ITEM_QUERY_SINGLE_RESPONSE. Default to 5. Fall back to 2 only if
|
||||
// the 5-entry parse fails or yields clearly implausible results for weapons.
|
||||
DamageParseResult parsed2 = parseDamageBlock(2);
|
||||
DamageParseResult parsed5 = parseDamageBlock(5);
|
||||
|
||||
auto looksWeaponItem = [&](const DamageParseResult& r) {
|
||||
return (data.itemClass == 2) && (r.damageMax > 0.0f) && (r.delayMs > 0);
|
||||
};
|
||||
|
||||
const DamageParseResult* chosen = &parsed5;
|
||||
if (parsed5.ok && parsed2.ok) {
|
||||
// Only prefer parsed2 if it identifies as a weapon and parsed5 doesn't.
|
||||
// This handles non-standard 2-entry servers for weapon items.
|
||||
if (looksWeaponItem(parsed2) && !looksWeaponItem(parsed5)) chosen = &parsed2;
|
||||
} else if (!parsed5.ok && parsed2.ok) {
|
||||
chosen = &parsed2;
|
||||
}
|
||||
int chosenDamageEntries = (chosen == &parsed5) ? 5 : 2;
|
||||
|
||||
data.damageMin = chosen->damageMin;
|
||||
data.damageMax = chosen->damageMax;
|
||||
data.armor = chosen->armor;
|
||||
data.delayMs = chosen->delayMs;
|
||||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||||
packet.readUInt32(); // HolyRes
|
||||
packet.readUInt32(); // FireRes
|
||||
packet.readUInt32(); // NatureRes
|
||||
packet.readUInt32(); // FrostRes
|
||||
packet.readUInt32(); // ShadowRes
|
||||
packet.readUInt32(); // ArcaneRes
|
||||
data.delayMs = packet.readUInt32();
|
||||
packet.readUInt32(); // AmmoType
|
||||
packet.readFloat(); // RangedModRange
|
||||
|
||||
// 5 item spells: SpellId, SpellTrigger, SpellCharges, SpellCooldown, SpellCategory, SpellCategoryCooldown
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (packet.getReadPos() + 24 > packet.getSize()) break;
|
||||
data.spells[i].spellId = packet.readUInt32();
|
||||
data.spells[i].spellTrigger = packet.readUInt32();
|
||||
packet.readUInt32(); // SpellCharges
|
||||
packet.readUInt32(); // SpellCooldown
|
||||
packet.readUInt32(); // SpellCategory
|
||||
packet.readUInt32(); // SpellCategoryCooldown
|
||||
}
|
||||
|
||||
data.valid = !data.name.empty();
|
||||
LOG_INFO("Item query: '", data.name, "' class=", data.itemClass,
|
||||
" invType=", data.inventoryType, " quality=", data.quality,
|
||||
" armor=", data.armor, " dmgEntries=", chosenDamageEntries,
|
||||
" statsCount=", statsCount, " sellPrice=", data.sellPrice);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -3142,13 +3110,13 @@ network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) {
|
||||
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
|
||||
packet.writeUInt8(bagIndex);
|
||||
packet.writeUInt8(slotIndex);
|
||||
packet.writeUInt8(0); // cast count
|
||||
packet.writeUInt32(0); // spell id
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt32(spellId); // spell id from item data
|
||||
packet.writeUInt64(itemGuid); // full 8-byte GUID
|
||||
packet.writeUInt32(0); // glyph index
|
||||
packet.writeUInt8(0); // cast flags
|
||||
// SpellCastTargets: self
|
||||
|
|
|
|||
|
|
@ -648,6 +648,11 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
cancelPickup(inventory);
|
||||
}
|
||||
|
||||
// Cancel pending pickup if mouse released before threshold
|
||||
if (pickupPending_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
pickupPending_ = false;
|
||||
}
|
||||
|
||||
if (separateBags_) {
|
||||
renderSeparateBags(inventory, moneyCopper);
|
||||
} else {
|
||||
|
|
@ -1341,7 +1346,9 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
|
||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
||||
// Drop held item on mouse release over empty slot
|
||||
if (ImGui::IsItemHovered() && holdingItem && validDrop &&
|
||||
ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
|
|
@ -1400,17 +1407,44 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
|
||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||
|
||||
// Left-click: pickup or place/swap
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
if (!holdingItem) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
pickupFromBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
pickupFromBag(inventory, bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
// Left mouse: hold to pick up, release to drop/swap
|
||||
if (!holdingItem) {
|
||||
// Start pickup tracking on mouse press
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
pickupPending_ = true;
|
||||
pickupPressTime_ = ImGui::GetTime();
|
||||
pickupSlotKind_ = kind;
|
||||
pickupBackpackIndex_ = backpackIndex;
|
||||
pickupBagIndex_ = bagIndex;
|
||||
pickupBagSlotIndex_ = bagSlotIndex;
|
||||
pickupEquipSlot_ = equipSlot;
|
||||
}
|
||||
// Check if held long enough to pick up
|
||||
if (pickupPending_ && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
|
||||
(ImGui::GetTime() - pickupPressTime_) >= kPickupHoldThreshold) {
|
||||
// Verify this is the same slot that was pressed
|
||||
bool sameSlot = (pickupSlotKind_ == kind);
|
||||
if (kind == SlotKind::BACKPACK && !isBagSlot)
|
||||
sameSlot = sameSlot && (pickupBackpackIndex_ == backpackIndex);
|
||||
else if (kind == SlotKind::BACKPACK && isBagSlot)
|
||||
sameSlot = sameSlot && (pickupBagIndex_ == bagIndex) && (pickupBagSlotIndex_ == bagSlotIndex);
|
||||
else if (kind == SlotKind::EQUIPMENT)
|
||||
sameSlot = sameSlot && (pickupEquipSlot_ == equipSlot);
|
||||
|
||||
if (sameSlot && ImGui::IsItemHovered()) {
|
||||
pickupPending_ = false;
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
pickupFromBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
pickupFromBag(inventory, bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// Drop/swap on mouse release over a filled slot
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
|
|
@ -1422,12 +1456,14 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
|
||||
// Right-click: bank deposit (if bank open), vendor sell (if vendor mode), or auto-equip/use
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) {
|
||||
LOG_INFO("Right-click slot: kind=", (int)kind,
|
||||
// Note: InvisibleButton only tracks left-click by default, so use IsItemHovered+IsMouseClicked
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) {
|
||||
LOG_WARNING("Right-click slot: kind=", (int)kind,
|
||||
" backpackIndex=", backpackIndex,
|
||||
" bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex,
|
||||
" vendorMode=", vendorMode_,
|
||||
" bankOpen=", gameHandler_->isBankOpen());
|
||||
" bankOpen=", gameHandler_->isBankOpen(),
|
||||
" item='", item.name, "' invType=", (int)item.inventoryType);
|
||||
if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
gameHandler_->attachItemFromBackpack(backpackIndex);
|
||||
} else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
|
|
@ -1444,12 +1480,18 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
||||
gameHandler_->unequipToBackpack(equipSlot);
|
||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
LOG_INFO("Right-click backpack item: name='", item.name,
|
||||
"' inventoryType=", (int)item.inventoryType,
|
||||
" itemId=", item.itemId);
|
||||
if (item.inventoryType > 0) {
|
||||
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
||||
} else {
|
||||
gameHandler_->useItemBySlot(backpackIndex);
|
||||
}
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
LOG_INFO("Right-click bag item: name='", item.name,
|
||||
"' inventoryType=", (int)item.inventoryType,
|
||||
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex);
|
||||
if (item.inventoryType > 0) {
|
||||
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -133,11 +133,17 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
uint32_t id = skillLineDbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
|
||||
uint32_t category = skillLineDbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
|
||||
std::string name = skillLineDbc->getString(i, slL ? (*slL)["Name"] : 3);
|
||||
if (id > 0 && !name.empty()) {
|
||||
skillLineNames[id] = name;
|
||||
if (id > 0) {
|
||||
if (!name.empty()) {
|
||||
skillLineNames[id] = name;
|
||||
}
|
||||
skillLineCategories[id] = category;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Spellbook: Loaded ", skillLineNames.size(), " skill line names, ",
|
||||
skillLineCategories.size(), " categories from SkillLine.dbc");
|
||||
} else {
|
||||
LOG_WARNING("Spellbook: Could not load SkillLine.dbc");
|
||||
}
|
||||
|
||||
auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc");
|
||||
|
|
@ -147,9 +153,12 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
|
|||
uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1);
|
||||
uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2);
|
||||
if (spellId > 0 && skillLineId > 0) {
|
||||
spellToSkillLine[spellId] = skillLineId;
|
||||
spellToSkillLine.emplace(spellId, skillLineId);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Spellbook: Loaded ", spellToSkillLine.size(), " spell-to-skillline mappings from SkillLineAbility.dbc");
|
||||
} else {
|
||||
LOG_WARNING("Spellbook: Could not load SkillLineAbility.dbc");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,9 +170,10 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& known
|
|||
static constexpr uint32_t CAT_PROFESSION = 11; // Primary professions
|
||||
static constexpr uint32_t CAT_SECONDARY = 9; // Secondary skills (Cooking, First Aid, Fishing, Riding, Companions)
|
||||
|
||||
// Special skill line IDs within category 9 that get their own tabs
|
||||
static constexpr uint32_t SKILLLINE_RIDING = 762; // Mounts
|
||||
static constexpr uint32_t SKILLLINE_COMPANIONS = 778; // Vanity/companion pets
|
||||
// Special skill line IDs that get their own tabs
|
||||
static constexpr uint32_t SKILLLINE_MOUNTS = 777; // Mount summon spells (category 7)
|
||||
static constexpr uint32_t SKILLLINE_RIDING = 762; // Riding skill ranks (category 9)
|
||||
static constexpr uint32_t SKILLLINE_COMPANIONS = 778; // Vanity/companion pets (category 7)
|
||||
|
||||
// Buckets
|
||||
std::map<uint32_t, std::vector<const SpellInfo*>> specSpells; // class spec trees
|
||||
|
|
@ -178,47 +188,72 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& known
|
|||
|
||||
const SpellInfo* info = &it->second;
|
||||
|
||||
auto slIt = spellToSkillLine.find(spellId);
|
||||
if (slIt != spellToSkillLine.end()) {
|
||||
// Check all skill lines this spell belongs to, prefer class (cat 7) > profession > secondary > special
|
||||
auto range = spellToSkillLine.equal_range(spellId);
|
||||
bool categorized = false;
|
||||
|
||||
uint32_t bestSkillLine = 0;
|
||||
int bestPriority = -1; // 4=class, 3=profession, 2=secondary, 1=mount/companion
|
||||
|
||||
for (auto slIt = range.first; slIt != range.second; ++slIt) {
|
||||
uint32_t skillLineId = slIt->second;
|
||||
|
||||
// Mounts: Riding skill line (762)
|
||||
if (skillLineId == SKILLLINE_RIDING) {
|
||||
mountSpells.push_back(info);
|
||||
if (skillLineId == SKILLLINE_MOUNTS || skillLineId == SKILLLINE_RIDING) {
|
||||
if (bestPriority < 1) { bestPriority = 1; bestSkillLine = SKILLLINE_MOUNTS; }
|
||||
continue;
|
||||
}
|
||||
|
||||
// Companions: vanity pets skill line (778)
|
||||
if (skillLineId == SKILLLINE_COMPANIONS) {
|
||||
companionSpells.push_back(info);
|
||||
if (bestPriority < 1) { bestPriority = 1; bestSkillLine = skillLineId; }
|
||||
continue;
|
||||
}
|
||||
|
||||
auto catIt = skillLineCategories.find(skillLineId);
|
||||
if (catIt != skillLineCategories.end()) {
|
||||
uint32_t cat = catIt->second;
|
||||
|
||||
// Class spec abilities
|
||||
if (cat == CAT_CLASS) {
|
||||
specSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Primary professions
|
||||
if (cat == CAT_PROFESSION) {
|
||||
profSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Secondary skills (Cooking, First Aid, Fishing)
|
||||
if (cat == CAT_SECONDARY) {
|
||||
profSpells[skillLineId].push_back(info);
|
||||
continue;
|
||||
if (cat == CAT_CLASS && bestPriority < 4) {
|
||||
bestPriority = 4; bestSkillLine = skillLineId;
|
||||
} else if (cat == CAT_PROFESSION && bestPriority < 3) {
|
||||
bestPriority = 3; bestSkillLine = skillLineId;
|
||||
} else if (cat == CAT_SECONDARY && bestPriority < 2) {
|
||||
bestPriority = 2; bestSkillLine = skillLineId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generalSpells.push_back(info);
|
||||
if (bestSkillLine > 0) {
|
||||
if (bestSkillLine == SKILLLINE_MOUNTS) {
|
||||
mountSpells.push_back(info);
|
||||
categorized = true;
|
||||
} else if (bestSkillLine == SKILLLINE_COMPANIONS) {
|
||||
companionSpells.push_back(info);
|
||||
categorized = true;
|
||||
} else {
|
||||
auto catIt = skillLineCategories.find(bestSkillLine);
|
||||
if (catIt != skillLineCategories.end()) {
|
||||
uint32_t cat = catIt->second;
|
||||
if (cat == CAT_CLASS) {
|
||||
specSpells[bestSkillLine].push_back(info);
|
||||
categorized = true;
|
||||
} else if (cat == CAT_PROFESSION || cat == CAT_SECONDARY) {
|
||||
profSpells[bestSkillLine].push_back(info);
|
||||
categorized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!categorized) {
|
||||
generalSpells.push_back(info);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Spellbook categorize: ", specSpells.size(), " spec groups, ",
|
||||
generalSpells.size(), " general, ", profSpells.size(), " prof groups, ",
|
||||
mountSpells.size(), " mounts, ", companionSpells.size(), " companions");
|
||||
for (const auto& [slId, spells] : specSpells) {
|
||||
auto nameIt = skillLineNames.find(slId);
|
||||
LOG_INFO(" Spec tab: skillLine=", slId, " name='",
|
||||
(nameIt != skillLineNames.end() ? nameIt->second : "?"), "' spells=", spells.size());
|
||||
}
|
||||
|
||||
auto byName = [](const SpellInfo* a, const SpellInfo* b) { return a->name < b->name; };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue