#include "game/inventory.hpp" #include "core/logger.hpp" #include namespace wowee { namespace game { static const ItemSlot EMPTY_SLOT{}; Inventory::Inventory() = default; const ItemSlot& Inventory::getBackpackSlot(int index) const { if (index < 0 || index >= BACKPACK_SLOTS) return EMPTY_SLOT; return backpack[index]; } bool Inventory::setBackpackSlot(int index, const ItemDef& item) { if (index < 0 || index >= BACKPACK_SLOTS) return false; backpack[index].item = item; return true; } bool Inventory::clearBackpackSlot(int index) { if (index < 0 || index >= BACKPACK_SLOTS) return false; backpack[index].item = ItemDef{}; return true; } const ItemSlot& Inventory::getEquipSlot(EquipSlot slot) const { int idx = static_cast(slot); if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return EMPTY_SLOT; return equipment[idx]; } bool Inventory::setEquipSlot(EquipSlot slot, const ItemDef& item) { int idx = static_cast(slot); if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false; equipment[idx].item = item; return true; } bool Inventory::clearEquipSlot(EquipSlot slot) { int idx = static_cast(slot); if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false; equipment[idx].item = ItemDef{}; return true; } const ItemSlot& Inventory::getKeyringSlot(int index) const { if (index < 0 || index >= KEYRING_SLOTS) return EMPTY_SLOT; return keyring_[index]; } bool Inventory::setKeyringSlot(int index, const ItemDef& item) { if (index < 0 || index >= KEYRING_SLOTS) return false; keyring_[index].item = item; return true; } bool Inventory::clearKeyringSlot(int index) { if (index < 0 || index >= KEYRING_SLOTS) return false; keyring_[index].item = ItemDef{}; return true; } int Inventory::getBagSize(int bagIndex) const { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0; return bags[bagIndex].size; } void Inventory::setBagSize(int bagIndex, int size) { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return; bags[bagIndex].size = std::min(size, MAX_BAG_SIZE); } const ItemSlot& Inventory::getBagSlot(int bagIndex, int slotIndex) const { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return EMPTY_SLOT; if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return EMPTY_SLOT; return bags[bagIndex].slots[slotIndex]; } bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false; if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false; bags[bagIndex].slots[slotIndex].item = item; return true; } bool Inventory::clearBagSlot(int bagIndex, int slotIndex) { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false; if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false; bags[bagIndex].slots[slotIndex].item = ItemDef{}; return true; } const ItemSlot& Inventory::getBankSlot(int index) const { if (index < 0 || index >= BANK_SLOTS) return EMPTY_SLOT; return bankSlots_[index]; } bool Inventory::setBankSlot(int index, const ItemDef& item) { if (index < 0 || index >= BANK_SLOTS) return false; bankSlots_[index].item = item; return true; } bool Inventory::clearBankSlot(int index) { if (index < 0 || index >= BANK_SLOTS) return false; bankSlots_[index].item = ItemDef{}; return true; } const ItemSlot& Inventory::getBankBagSlot(int bagIndex, int slotIndex) const { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return EMPTY_SLOT; if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return EMPTY_SLOT; return bankBags_[bagIndex].slots[slotIndex]; } bool Inventory::setBankBagSlot(int bagIndex, int slotIndex, const ItemDef& item) { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return false; if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return false; bankBags_[bagIndex].slots[slotIndex].item = item; return true; } bool Inventory::clearBankBagSlot(int bagIndex, int slotIndex) { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return false; if (slotIndex < 0 || slotIndex >= bankBags_[bagIndex].size) return false; bankBags_[bagIndex].slots[slotIndex].item = ItemDef{}; return true; } int Inventory::getBankBagSize(int bagIndex) const { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return 0; return bankBags_[bagIndex].size; } void Inventory::setBankBagSize(int bagIndex, int size) { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return; bankBags_[bagIndex].size = std::min(size, MAX_BAG_SIZE); } const ItemSlot& Inventory::getBankBagItem(int bagIndex) const { static const ItemSlot EMPTY_SLOT; if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return EMPTY_SLOT; return bankBags_[bagIndex].bagItem; } void Inventory::setBankBagItem(int bagIndex, const ItemDef& item) { if (bagIndex < 0 || bagIndex >= BANK_BAG_SLOTS) return; bankBags_[bagIndex].bagItem.item = item; } void Inventory::swapBagContents(int bagA, int bagB) { if (bagA < 0 || bagA >= NUM_BAG_SLOTS || bagB < 0 || bagB >= NUM_BAG_SLOTS) return; if (bagA == bagB) return; std::swap(bags[bagA], bags[bagB]); } int Inventory::findFreeBackpackSlot() const { for (int i = 0; i < BACKPACK_SLOTS; i++) { if (backpack[i].empty()) return i; } return -1; } bool Inventory::addItem(const ItemDef& item) { // Try stacking first if (item.maxStack > 1) { for (int i = 0; i < BACKPACK_SLOTS; i++) { if (!backpack[i].empty() && backpack[i].item.itemId == item.itemId && backpack[i].item.stackCount < backpack[i].item.maxStack) { uint32_t space = backpack[i].item.maxStack - backpack[i].item.stackCount; uint32_t toAdd = std::min(space, item.stackCount); backpack[i].item.stackCount += toAdd; if (toAdd >= item.stackCount) return true; // Remaining needs a new slot — fall through } } } int slot = findFreeBackpackSlot(); if (slot < 0) return false; backpack[slot].item = item; return true; } void Inventory::sortBags() { // Collect all items from backpack and equip bags into a flat list. std::vector items; items.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE); for (int i = 0; i < BACKPACK_SLOTS; ++i) { if (!backpack[i].empty()) items.push_back(backpack[i].item); } for (int b = 0; b < NUM_BAG_SLOTS; ++b) { for (int s = 0; s < bags[b].size; ++s) { if (!bags[b].slots[s].empty()) items.push_back(bags[b].slots[s].item); } } // Sort: quality descending → itemId ascending → stackCount descending. std::stable_sort(items.begin(), items.end(), [](const ItemDef& a, const ItemDef& b) { if (a.quality != b.quality) return static_cast(a.quality) > static_cast(b.quality); if (a.itemId != b.itemId) return a.itemId < b.itemId; return a.stackCount > b.stackCount; }); // Write sorted items back, filling backpack first then equip bags. int idx = 0; int n = static_cast(items.size()); for (int i = 0; i < BACKPACK_SLOTS; ++i) backpack[i].item = (idx < n) ? items[idx++] : ItemDef{}; for (int b = 0; b < NUM_BAG_SLOTS; ++b) { for (int s = 0; s < bags[b].size; ++s) bags[b].slots[s].item = (idx < n) ? items[idx++] : ItemDef{}; } } std::vector Inventory::computeSortSwaps() const { // Build a flat list of (bag, slot, item) entries matching the same traversal // order as sortBags(): backpack first, then equip bags in order. struct Entry { uint8_t bag; // WoW bag address: 0xFF=backpack, 19+i=equip bag i uint8_t slot; // WoW slot address: 23+i for backpack, slotIndex for bags uint32_t itemId; ItemQuality quality; uint32_t stackCount; }; std::vector entries; entries.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE); for (int i = 0; i < BACKPACK_SLOTS; ++i) { entries.push_back({0xFF, static_cast(23 + i), backpack[i].item.itemId, backpack[i].item.quality, backpack[i].item.stackCount}); } for (int b = 0; b < NUM_BAG_SLOTS; ++b) { for (int s = 0; s < bags[b].size; ++s) { entries.push_back({static_cast(19 + b), static_cast(s), bags[b].slots[s].item.itemId, bags[b].slots[s].item.quality, bags[b].slots[s].item.stackCount}); } } // Build a sorted index array using the same comparator as sortBags(). int n = static_cast(entries.size()); std::vector sortedIdx(n); for (int i = 0; i < n; ++i) sortedIdx[i] = i; // Separate non-empty items and empty slots, then sort the non-empty items. // Items are sorted by quality desc -> itemId asc -> stackCount desc. // Empty slots go to the end. std::stable_sort(sortedIdx.begin(), sortedIdx.end(), [&](int a, int b) { bool aEmpty = (entries[a].itemId == 0); bool bEmpty = (entries[b].itemId == 0); if (aEmpty != bEmpty) return bEmpty; // non-empty before empty if (aEmpty) return false; // both empty: preserve order // Both non-empty: same comparator as sortBags() if (entries[a].quality != entries[b].quality) return static_cast(entries[a].quality) > static_cast(entries[b].quality); if (entries[a].itemId != entries[b].itemId) return entries[a].itemId < entries[b].itemId; return entries[a].stackCount > entries[b].stackCount; }); // sortedIdx[targetPos] = sourcePos means the item currently at sourcePos // needs to end up at targetPos. We use selection-sort-style swaps to // permute current positions into sorted order, tracking where items move. // posOf[i] = current position of the item that was originally at index i std::vector posOf(n); for (int i = 0; i < n; ++i) posOf[i] = i; // invPos[p] = which original item index is currently sitting at position p std::vector invPos(n); for (int i = 0; i < n; ++i) invPos[i] = i; std::vector swaps; for (int target = 0; target < n; ++target) { int need = sortedIdx[target]; // original index that should be at 'target' int cur = invPos[target]; // original index currently at 'target' if (cur == need) continue; // already in place // Skip swaps between two empty slots if (entries[cur].itemId == 0 && entries[need].itemId == 0) continue; int srcPos = posOf[need]; // current position of the item we need // Emit a swap between position srcPos and position target swaps.push_back({entries[srcPos].bag, entries[srcPos].slot, entries[target].bag, entries[target].slot}); // Update tracking arrays posOf[cur] = srcPos; posOf[need] = target; invPos[srcPos] = cur; invPos[target] = need; } return swaps; } void Inventory::populateTestItems() { // Equipment { ItemDef sword; sword.itemId = 25; sword.name = "Worn Shortsword"; sword.quality = ItemQuality::COMMON; sword.inventoryType = 21; // Main Hand sword.strength = 1; sword.displayInfoId = 1542; // Sword_1H_Short_A_02.m2 sword.subclassName = "Sword"; setEquipSlot(EquipSlot::MAIN_HAND, sword); } { ItemDef shield; shield.itemId = 2129; shield.name = "Large Round Shield"; shield.quality = ItemQuality::COMMON; shield.inventoryType = 14; // Off Hand (Shield) shield.armor = 18; shield.stamina = 1; shield.displayInfoId = 18662; // Shield_Round_A_01.m2 shield.subclassName = "Shield"; setEquipSlot(EquipSlot::OFF_HAND, shield); } // Shirt/pants/boots in backpack (character model renders bare geosets) { ItemDef shirt; shirt.itemId = 38; shirt.name = "Recruit's Shirt"; shirt.quality = ItemQuality::COMMON; shirt.inventoryType = 4; // Shirt shirt.displayInfoId = 2163; addItem(shirt); } { ItemDef pants; pants.itemId = 39; pants.name = "Recruit's Pants"; pants.quality = ItemQuality::COMMON; pants.inventoryType = 7; // Legs pants.armor = 4; pants.displayInfoId = 1883; addItem(pants); } { ItemDef boots; boots.itemId = 40; boots.name = "Recruit's Boots"; boots.quality = ItemQuality::COMMON; boots.inventoryType = 8; // Feet boots.armor = 3; boots.displayInfoId = 2166; addItem(boots); } // Backpack items { ItemDef potion; potion.itemId = 118; potion.name = "Minor Healing Potion"; potion.quality = ItemQuality::COMMON; potion.stackCount = 3; potion.maxStack = 5; addItem(potion); } { ItemDef hearthstone; hearthstone.itemId = 6948; hearthstone.name = "Hearthstone"; hearthstone.quality = ItemQuality::COMMON; addItem(hearthstone); } { ItemDef leather; leather.itemId = 2318; leather.name = "Light Leather"; leather.quality = ItemQuality::COMMON; leather.stackCount = 5; leather.maxStack = 20; addItem(leather); } { ItemDef cloth; cloth.itemId = 2589; cloth.name = "Linen Cloth"; cloth.quality = ItemQuality::COMMON; cloth.stackCount = 8; cloth.maxStack = 20; addItem(cloth); } { ItemDef quest; quest.itemId = 50000; quest.name = "Kobold Candle"; quest.quality = ItemQuality::COMMON; quest.stackCount = 4; quest.maxStack = 10; addItem(quest); } { ItemDef ring; ring.itemId = 11287; ring.name = "Verdant Ring"; ring.quality = ItemQuality::UNCOMMON; ring.inventoryType = 11; // Ring ring.stamina = 3; ring.spirit = 2; addItem(ring); } { ItemDef cloak; cloak.itemId = 2570; cloak.name = "Linen Cloak"; cloak.quality = ItemQuality::UNCOMMON; cloak.inventoryType = 16; // Back cloak.armor = 10; cloak.agility = 1; cloak.displayInfoId = 15055; addItem(cloak); } { ItemDef rareAxe; rareAxe.itemId = 15268; rareAxe.name = "Stoneslayer"; rareAxe.quality = ItemQuality::RARE; rareAxe.inventoryType = 17; // Two-Hand rareAxe.strength = 8; rareAxe.stamina = 7; rareAxe.subclassName = "Axe"; rareAxe.displayInfoId = 782; // Axe_2H_Battle_B_01.m2 addItem(rareAxe); } LOG_INFO("Inventory: populated test items (2 equipped, 11 backpack)"); } const char* getQualityName(ItemQuality quality) { switch (quality) { case ItemQuality::POOR: return "Poor"; case ItemQuality::COMMON: return "Common"; case ItemQuality::UNCOMMON: return "Uncommon"; case ItemQuality::RARE: return "Rare"; case ItemQuality::EPIC: return "Epic"; case ItemQuality::LEGENDARY: return "Legendary"; case ItemQuality::ARTIFACT: return "Artifact"; case ItemQuality::HEIRLOOM: return "Heirloom"; default: return "Unknown"; } } const char* getEquipSlotName(EquipSlot slot) { switch (slot) { case EquipSlot::HEAD: return "Head"; case EquipSlot::NECK: return "Neck"; case EquipSlot::SHOULDERS: return "Shoulders"; case EquipSlot::SHIRT: return "Shirt"; case EquipSlot::CHEST: return "Chest"; case EquipSlot::WAIST: return "Waist"; case EquipSlot::LEGS: return "Legs"; case EquipSlot::FEET: return "Feet"; case EquipSlot::WRISTS: return "Wrists"; case EquipSlot::HANDS: return "Hands"; case EquipSlot::RING1: return "Ring 1"; case EquipSlot::RING2: return "Ring 2"; case EquipSlot::TRINKET1: return "Trinket 1"; case EquipSlot::TRINKET2: return "Trinket 2"; case EquipSlot::BACK: return "Back"; case EquipSlot::MAIN_HAND: return "Main Hand"; case EquipSlot::OFF_HAND: return "Off Hand"; case EquipSlot::RANGED: return "Ranged"; case EquipSlot::TABARD: return "Tabard"; case EquipSlot::BAG1: return "Bag 1"; case EquipSlot::BAG2: return "Bag 2"; case EquipSlot::BAG3: return "Bag 3"; case EquipSlot::BAG4: return "Bag 4"; default: return "Unknown"; } } } // namespace game } // namespace wowee