2026-02-02 12:24:50 -08:00
|
|
|
#include "ui/inventory_screen.hpp"
|
2026-02-06 13:47:03 -08:00
|
|
|
#include "game/game_handler.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
#include "core/application.hpp"
|
2026-02-22 03:32:08 -08:00
|
|
|
#include "rendering/vk_context.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/input.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
#include "rendering/character_preview.hpp"
|
|
|
|
|
#include "rendering/character_renderer.hpp"
|
2026-02-22 05:58:45 -08:00
|
|
|
#include "rendering/renderer.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/dbc_loader.hpp"
|
|
|
|
|
#include "pipeline/blp_loader.hpp"
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "pipeline/dbc_layout.hpp"
|
2026-02-06 14:24:38 -08:00
|
|
|
#include "core/logger.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <SDL2/SDL.h>
|
2026-02-13 22:51:49 -08:00
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cstdio>
|
2026-02-13 22:51:49 -08:00
|
|
|
#include <cstring>
|
2026-02-06 14:24:38 -08:00
|
|
|
#include <unordered_set>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace ui {
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
namespace {
|
|
|
|
|
const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, uint8_t inventoryType) {
|
|
|
|
|
using ES = game::EquipSlot;
|
|
|
|
|
auto slotPtr = [&](ES slot) -> const game::ItemSlot* {
|
|
|
|
|
const auto& s = inventory.getEquipSlot(slot);
|
|
|
|
|
return s.empty() ? nullptr : &s;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
switch (inventoryType) {
|
|
|
|
|
case 1: return slotPtr(ES::HEAD);
|
|
|
|
|
case 2: return slotPtr(ES::NECK);
|
|
|
|
|
case 3: return slotPtr(ES::SHOULDERS);
|
|
|
|
|
case 4: return slotPtr(ES::SHIRT);
|
|
|
|
|
case 5:
|
|
|
|
|
case 20: return slotPtr(ES::CHEST);
|
|
|
|
|
case 6: return slotPtr(ES::WAIST);
|
|
|
|
|
case 7: return slotPtr(ES::LEGS);
|
|
|
|
|
case 8: return slotPtr(ES::FEET);
|
|
|
|
|
case 9: return slotPtr(ES::WRISTS);
|
|
|
|
|
case 10: return slotPtr(ES::HANDS);
|
|
|
|
|
case 11: {
|
|
|
|
|
if (auto* s = slotPtr(ES::RING1)) return s;
|
|
|
|
|
return slotPtr(ES::RING2);
|
|
|
|
|
}
|
|
|
|
|
case 12: {
|
|
|
|
|
if (auto* s = slotPtr(ES::TRINKET1)) return s;
|
|
|
|
|
return slotPtr(ES::TRINKET2);
|
|
|
|
|
}
|
|
|
|
|
case 13: // One-hand
|
|
|
|
|
if (auto* s = slotPtr(ES::MAIN_HAND)) return s;
|
|
|
|
|
return slotPtr(ES::OFF_HAND);
|
|
|
|
|
case 14:
|
|
|
|
|
case 22:
|
|
|
|
|
case 23: return slotPtr(ES::OFF_HAND);
|
|
|
|
|
case 15:
|
|
|
|
|
case 25:
|
|
|
|
|
case 26: return slotPtr(ES::RANGED);
|
|
|
|
|
case 16: return slotPtr(ES::BACK);
|
|
|
|
|
case 17:
|
|
|
|
|
case 21: return slotPtr(ES::MAIN_HAND);
|
|
|
|
|
case 18: // bag
|
|
|
|
|
for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) {
|
|
|
|
|
auto slot = static_cast<ES>(static_cast<int>(ES::BAG1) + i);
|
|
|
|
|
if (auto* s = slotPtr(slot)) return s;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
case 19: return slotPtr(ES::TABARD);
|
|
|
|
|
default: return nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
InventoryScreen::~InventoryScreen() {
|
2026-02-22 03:32:08 -08:00
|
|
|
// Vulkan textures are owned by VkContext and cleaned up on shutdown
|
2026-02-06 14:24:38 -08:00
|
|
|
iconCache_.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
|
|
|
|
switch (quality) {
|
|
|
|
|
case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey
|
|
|
|
|
case game::ItemQuality::COMMON: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
|
|
|
|
|
case game::ItemQuality::UNCOMMON: return ImVec4(0.12f, 1.0f, 0.0f, 1.0f); // Green
|
|
|
|
|
case game::ItemQuality::RARE: return ImVec4(0.0f, 0.44f, 0.87f, 1.0f); // Blue
|
|
|
|
|
case game::ItemQuality::EPIC: return ImVec4(0.64f, 0.21f, 0.93f, 1.0f); // Purple
|
|
|
|
|
case game::ItemQuality::LEGENDARY: return ImVec4(1.0f, 0.50f, 0.0f, 1.0f); // Orange
|
|
|
|
|
default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Item Icon Loading
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet InventoryScreen::getItemIcon(uint32_t displayInfoId) {
|
|
|
|
|
if (displayInfoId == 0 || !assetManager_) return VK_NULL_HANDLE;
|
2026-02-06 14:24:38 -08:00
|
|
|
|
|
|
|
|
auto it = iconCache_.find(displayInfoId);
|
|
|
|
|
if (it != iconCache_.end()) return it->second;
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc
|
|
|
|
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
if (!displayInfoDbc) {
|
2026-02-22 03:32:08 -08:00
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) {
|
2026-02-22 03:32:08 -08:00
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Field 5 = inventoryIcon_1
|
2026-02-12 22:56:36 -08:00
|
|
|
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
|
|
|
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5);
|
2026-02-06 14:24:38 -08:00
|
|
|
if (iconName.empty()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string iconPath = "Interface\\Icons\\" + iconName + ".blp";
|
|
|
|
|
auto blpData = assetManager_->readFile(iconPath);
|
|
|
|
|
if (blpData.empty()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto image = pipeline::BLPLoader::load(blpData);
|
|
|
|
|
if (!image.isValid()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload to Vulkan via VkContext
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
auto* vkCtx = window ? window->getVkContext() : nullptr;
|
|
|
|
|
if (!vkCtx) {
|
|
|
|
|
iconCache_[displayInfoId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
|
|
|
|
|
iconCache_[displayInfoId] = ds;
|
|
|
|
|
return ds;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Character Model Preview
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::setPlayerAppearance(game::Race race, game::Gender gender,
|
|
|
|
|
uint8_t skin, uint8_t face,
|
|
|
|
|
uint8_t hairStyle, uint8_t hairColor,
|
|
|
|
|
uint8_t facialHair) {
|
|
|
|
|
playerRace_ = race;
|
|
|
|
|
playerGender_ = gender;
|
|
|
|
|
playerSkin_ = skin;
|
|
|
|
|
playerFace_ = face;
|
|
|
|
|
playerHairStyle_ = hairStyle;
|
|
|
|
|
playerHairColor_ = hairColor;
|
|
|
|
|
playerFacialHair_ = facialHair;
|
|
|
|
|
// Force preview reload on next render
|
|
|
|
|
previewInitialized_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::initPreview() {
|
|
|
|
|
if (previewInitialized_ || !assetManager_) return;
|
|
|
|
|
|
|
|
|
|
if (!charPreview_) {
|
|
|
|
|
charPreview_ = std::make_unique<rendering::CharacterPreview>();
|
|
|
|
|
if (!charPreview_->initialize(assetManager_)) {
|
|
|
|
|
LOG_WARNING("InventoryScreen: failed to init CharacterPreview");
|
|
|
|
|
charPreview_.reset();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-22 05:58:45 -08:00
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (renderer) renderer->registerPreview(charPreview_.get());
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charPreview_->loadCharacter(playerRace_, playerGender_,
|
|
|
|
|
playerSkin_, playerFace_,
|
|
|
|
|
playerHairStyle_, playerHairColor_,
|
|
|
|
|
playerFacialHair_);
|
|
|
|
|
previewInitialized_ = true;
|
|
|
|
|
previewDirty_ = true; // apply equipment on first load
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::updatePreview(float deltaTime) {
|
|
|
|
|
if (charPreview_ && previewInitialized_) {
|
|
|
|
|
charPreview_->update(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::updatePreviewEquipment(game::Inventory& inventory) {
|
2026-02-20 21:50:32 -08:00
|
|
|
if (!charPreview_ || !charPreview_->isModelLoaded()) return;
|
2026-02-06 14:24:38 -08:00
|
|
|
|
2026-02-20 21:50:32 -08:00
|
|
|
std::vector<game::EquipmentItem> equipped;
|
|
|
|
|
equipped.reserve(game::Inventory::NUM_EQUIP_SLOTS);
|
2026-02-06 14:24:38 -08:00
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
2026-02-20 21:50:32 -08:00
|
|
|
game::EquipmentItem ei;
|
|
|
|
|
ei.displayModel = slot.item.displayInfoId;
|
|
|
|
|
ei.inventoryType = slot.item.inventoryType;
|
|
|
|
|
ei.enchantment = 0;
|
|
|
|
|
equipped.push_back(ei);
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
2026-02-20 21:50:32 -08:00
|
|
|
charPreview_->applyEquipment(equipped);
|
2026-02-06 14:24:38 -08:00
|
|
|
previewDirty_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Equip slot helpers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv) {
|
|
|
|
|
switch (inventoryType) {
|
|
|
|
|
case 1: return game::EquipSlot::HEAD;
|
|
|
|
|
case 2: return game::EquipSlot::NECK;
|
|
|
|
|
case 3: return game::EquipSlot::SHOULDERS;
|
|
|
|
|
case 4: return game::EquipSlot::SHIRT;
|
|
|
|
|
case 5: return game::EquipSlot::CHEST;
|
|
|
|
|
case 6: return game::EquipSlot::WAIST;
|
|
|
|
|
case 7: return game::EquipSlot::LEGS;
|
|
|
|
|
case 8: return game::EquipSlot::FEET;
|
|
|
|
|
case 9: return game::EquipSlot::WRISTS;
|
|
|
|
|
case 10: return game::EquipSlot::HANDS;
|
|
|
|
|
case 11: {
|
|
|
|
|
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
|
|
|
|
|
return game::EquipSlot::RING1;
|
|
|
|
|
return game::EquipSlot::RING2;
|
|
|
|
|
}
|
|
|
|
|
case 12: {
|
|
|
|
|
if (inv.getEquipSlot(game::EquipSlot::TRINKET1).empty())
|
|
|
|
|
return game::EquipSlot::TRINKET1;
|
|
|
|
|
return game::EquipSlot::TRINKET2;
|
|
|
|
|
}
|
|
|
|
|
case 13: // One-Hand
|
|
|
|
|
case 21: // Main Hand
|
|
|
|
|
return game::EquipSlot::MAIN_HAND;
|
|
|
|
|
case 17: // Two-Hand
|
|
|
|
|
return game::EquipSlot::MAIN_HAND;
|
|
|
|
|
case 14: // Shield
|
|
|
|
|
case 22: // Off Hand
|
|
|
|
|
case 23: // Held In Off-hand
|
|
|
|
|
return game::EquipSlot::OFF_HAND;
|
|
|
|
|
case 15: // Ranged (bow/gun)
|
|
|
|
|
case 25: // Thrown
|
|
|
|
|
case 26: // Ranged
|
|
|
|
|
return game::EquipSlot::RANGED;
|
|
|
|
|
case 16: return game::EquipSlot::BACK;
|
2026-02-20 17:41:19 -08:00
|
|
|
case 18: {
|
|
|
|
|
for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) {
|
|
|
|
|
auto slot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + i);
|
|
|
|
|
if (inv.getEquipSlot(slot).empty()) return slot;
|
|
|
|
|
}
|
|
|
|
|
return game::EquipSlot::BAG1;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
case 19: return game::EquipSlot::TABARD;
|
|
|
|
|
case 20: return game::EquipSlot::CHEST; // Robe
|
|
|
|
|
default: return game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::pickupFromBackpack(game::Inventory& inv, int index) {
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(index);
|
|
|
|
|
if (slot.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = slot.item;
|
|
|
|
|
heldSource = HeldSource::BACKPACK;
|
|
|
|
|
heldBackpackIndex = index;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
inv.clearBackpackSlot(index);
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 01:00:04 -08:00
|
|
|
void InventoryScreen::pickupFromBag(game::Inventory& inv, int bagIndex, int slotIndex) {
|
|
|
|
|
const auto& slot = inv.getBagSlot(bagIndex, slotIndex);
|
|
|
|
|
if (slot.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = slot.item;
|
|
|
|
|
heldSource = HeldSource::BAG;
|
|
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldBagIndex = bagIndex;
|
|
|
|
|
heldBagSlotIndex = slotIndex;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
inv.clearBagSlot(bagIndex, slotIndex);
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
|
|
|
|
const auto& es = inv.getEquipSlot(slot);
|
|
|
|
|
if (es.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = es.item;
|
|
|
|
|
heldSource = HeldSource::EQUIPMENT;
|
|
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldEquipSlot = slot;
|
|
|
|
|
inv.clearEquipSlot(slot);
|
|
|
|
|
equipmentDirty = true;
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
void InventoryScreen::pickupFromBank(game::Inventory& inv, int bankIndex) {
|
|
|
|
|
const auto& slot = inv.getBankSlot(bankIndex);
|
|
|
|
|
if (slot.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = slot.item;
|
|
|
|
|
heldSource = HeldSource::BANK;
|
|
|
|
|
heldBankIndex = bankIndex;
|
|
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldBagIndex = -1;
|
|
|
|
|
heldBagSlotIndex = -1;
|
|
|
|
|
heldBankBagIndex = -1;
|
|
|
|
|
heldBankBagSlotIndex = -1;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
inv.clearBankSlot(bankIndex);
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::pickupFromBankBag(game::Inventory& inv, int bagIndex, int slotIndex) {
|
|
|
|
|
const auto& slot = inv.getBankBagSlot(bagIndex, slotIndex);
|
|
|
|
|
if (slot.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = slot.item;
|
|
|
|
|
heldSource = HeldSource::BANK_BAG;
|
|
|
|
|
heldBankBagIndex = bagIndex;
|
|
|
|
|
heldBankBagSlotIndex = slotIndex;
|
|
|
|
|
heldBankIndex = -1;
|
|
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldBagIndex = -1;
|
|
|
|
|
heldBagSlotIndex = -1;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
inv.clearBankBagSlot(bagIndex, slotIndex);
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::pickupFromBankBagEquip(game::Inventory& inv, int bagIndex) {
|
|
|
|
|
const auto& slot = inv.getBankBagItem(bagIndex);
|
|
|
|
|
if (slot.empty()) return;
|
|
|
|
|
holdingItem = true;
|
|
|
|
|
heldItem = slot.item;
|
|
|
|
|
heldSource = HeldSource::BANK_BAG_EQUIP;
|
|
|
|
|
heldBankBagIndex = bagIndex;
|
|
|
|
|
heldBankBagSlotIndex = -1;
|
|
|
|
|
heldBankIndex = -1;
|
|
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldBagIndex = -1;
|
|
|
|
|
heldBagSlotIndex = -1;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
|
|
|
|
inv.setBankBagItem(bagIndex, game::ItemDef{});
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
|
|
|
|
if (!holdingItem) return;
|
2026-02-17 01:00:04 -08:00
|
|
|
if (gameHandler_) {
|
|
|
|
|
// Online mode: send server swap packet for all container moves
|
|
|
|
|
uint8_t dstBag = 0xFF;
|
|
|
|
|
uint8_t dstSlot = static_cast<uint8_t>(23 + index);
|
|
|
|
|
uint8_t srcBag = 0xFF;
|
|
|
|
|
uint8_t srcSlot = 0;
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BAG) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
2026-02-26 13:38:29 -08:00
|
|
|
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
} else {
|
|
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
2026-02-06 18:34:45 -08:00
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
const auto& target = inv.getBackpackSlot(index);
|
|
|
|
|
if (target.empty()) {
|
|
|
|
|
inv.setBackpackSlot(index, heldItem);
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
} else {
|
|
|
|
|
// Swap
|
|
|
|
|
game::ItemDef targetItem = target.item;
|
|
|
|
|
inv.setBackpackSlot(index, heldItem);
|
|
|
|
|
heldItem = targetItem;
|
|
|
|
|
heldSource = HeldSource::BACKPACK;
|
|
|
|
|
heldBackpackIndex = index;
|
|
|
|
|
}
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 01:00:04 -08:00
|
|
|
void InventoryScreen::placeInBag(game::Inventory& inv, int bagIndex, int slotIndex) {
|
|
|
|
|
if (!holdingItem) return;
|
|
|
|
|
if (gameHandler_) {
|
|
|
|
|
// Online mode: send server swap packet
|
|
|
|
|
uint8_t dstBag = static_cast<uint8_t>(19 + bagIndex);
|
|
|
|
|
uint8_t dstSlot = static_cast<uint8_t>(slotIndex);
|
|
|
|
|
uint8_t srcBag = 0xFF;
|
|
|
|
|
uint8_t srcSlot = 0;
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BAG) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
2026-02-26 13:38:29 -08:00
|
|
|
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
} else {
|
|
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
|
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const auto& target = inv.getBagSlot(bagIndex, slotIndex);
|
|
|
|
|
if (target.empty()) {
|
|
|
|
|
inv.setBagSlot(bagIndex, slotIndex, heldItem);
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
} else {
|
|
|
|
|
game::ItemDef targetItem = target.item;
|
|
|
|
|
inv.setBagSlot(bagIndex, slotIndex, heldItem);
|
|
|
|
|
heldItem = targetItem;
|
|
|
|
|
heldSource = HeldSource::BAG;
|
|
|
|
|
heldBagIndex = bagIndex;
|
|
|
|
|
heldBagSlotIndex = slotIndex;
|
|
|
|
|
}
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
|
|
|
|
if (!holdingItem) return;
|
2026-02-20 17:41:19 -08:00
|
|
|
|
|
|
|
|
// Validate: check if the held item can go in this slot
|
|
|
|
|
if (heldItem.inventoryType > 0) {
|
|
|
|
|
bool valid = false;
|
|
|
|
|
if (heldItem.inventoryType == 18) {
|
|
|
|
|
valid = (slot >= game::EquipSlot::BAG1 && slot <= game::EquipSlot::BAG4);
|
|
|
|
|
} else {
|
|
|
|
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
|
|
|
|
if (validSlot == game::EquipSlot::NUM_SLOTS) return;
|
|
|
|
|
|
|
|
|
|
valid = (slot == validSlot);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
if (heldItem.inventoryType == 11)
|
|
|
|
|
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
|
|
|
|
else if (heldItem.inventoryType == 12)
|
|
|
|
|
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 23:52:16 -08:00
|
|
|
if (gameHandler_) {
|
2026-02-20 17:41:19 -08:00
|
|
|
uint8_t dstBag = 0xFF;
|
|
|
|
|
uint8_t dstSlot = static_cast<uint8_t>(slot);
|
|
|
|
|
uint8_t srcBag = 0xFF;
|
|
|
|
|
uint8_t srcSlot = 0;
|
2026-02-06 18:34:45 -08:00
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
2026-02-20 17:41:19 -08:00
|
|
|
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BAG && heldBagIndex >= 0 && heldBagSlotIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
2026-02-26 13:38:29 -08:00
|
|
|
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
|
2026-02-20 17:41:19 -08:00
|
|
|
} else {
|
2026-02-17 01:00:04 -08:00
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 17:41:19 -08:00
|
|
|
|
|
|
|
|
if (srcBag == dstBag && srcSlot == dstSlot) {
|
2026-02-06 18:34:45 -08:00
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-20 17:41:19 -08:00
|
|
|
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
|
|
|
|
cancelPickup(inv);
|
2026-02-06 13:47:03 -08:00
|
|
|
return;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto& target = inv.getEquipSlot(slot);
|
|
|
|
|
if (target.empty()) {
|
|
|
|
|
inv.setEquipSlot(slot, heldItem);
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
} else {
|
|
|
|
|
game::ItemDef targetItem = target.item;
|
|
|
|
|
inv.setEquipSlot(slot, heldItem);
|
|
|
|
|
heldItem = targetItem;
|
|
|
|
|
heldSource = HeldSource::EQUIPMENT;
|
|
|
|
|
heldEquipSlot = slot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Two-handed weapon in main hand clears the off-hand slot
|
|
|
|
|
if (slot == game::EquipSlot::MAIN_HAND &&
|
|
|
|
|
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
|
|
|
|
const auto& offHand = inv.getEquipSlot(game::EquipSlot::OFF_HAND);
|
|
|
|
|
if (!offHand.empty()) {
|
|
|
|
|
inv.addItem(offHand.item);
|
|
|
|
|
inv.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Equipping off-hand unequips a 2H weapon from main hand
|
|
|
|
|
if (slot == game::EquipSlot::OFF_HAND &&
|
|
|
|
|
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
|
|
|
|
inv.addItem(inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
|
|
|
|
inv.clearEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
equipmentDirty = true;
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
|
|
|
|
if (!holdingItem) return;
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
|
|
|
|
|
inv.setBackpackSlot(heldBackpackIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
2026-02-17 01:00:04 -08:00
|
|
|
} else if (heldSource == HeldSource::BAG && heldBagIndex >= 0 && heldBagSlotIndex >= 0) {
|
|
|
|
|
if (inv.getBagSlot(heldBagIndex, heldBagSlotIndex).empty()) {
|
|
|
|
|
inv.setBagSlot(heldBagIndex, heldBagSlotIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
|
|
|
|
if (inv.getEquipSlot(heldEquipSlot).empty()) {
|
|
|
|
|
inv.setEquipSlot(heldEquipSlot, heldItem);
|
|
|
|
|
equipmentDirty = true;
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
2026-02-26 13:38:29 -08:00
|
|
|
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
|
|
|
|
|
if (inv.getBankSlot(heldBankIndex).empty()) {
|
|
|
|
|
inv.setBankSlot(heldBankIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0 && heldBankBagSlotIndex >= 0) {
|
|
|
|
|
if (inv.getBankBagSlot(heldBankBagIndex, heldBankBagSlotIndex).empty()) {
|
|
|
|
|
inv.setBankBagSlot(heldBankBagIndex, heldBankBagSlotIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
|
|
|
|
|
if (inv.getBankBagItem(heldBankBagIndex).empty()) {
|
|
|
|
|
inv.setBankBagItem(heldBankBagIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
|
|
|
|
inv.addItem(heldItem);
|
|
|
|
|
}
|
|
|
|
|
holdingItem = false;
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderHeldItem() {
|
|
|
|
|
if (!holdingItem) return;
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
ImVec2 mousePos = io.MousePos;
|
|
|
|
|
float size = 36.0f;
|
|
|
|
|
ImVec2 pos(mousePos.x - size * 0.5f, mousePos.y - size * 0.5f);
|
|
|
|
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
ImVec4 qColor = getQualityColor(heldItem.quality);
|
|
|
|
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Try to show icon
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet iconTex = getItemIcon(heldItem.displayInfoId);
|
2026-02-06 14:24:38 -08:00
|
|
|
if (iconTex) {
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
|
|
|
|
ImVec2(pos.x + size, pos.y + size));
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
|
|
|
|
} else {
|
|
|
|
|
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
|
|
|
|
IM_COL32(40, 35, 30, 200));
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
char abbr[4] = {};
|
|
|
|
|
if (!heldItem.name.empty()) {
|
|
|
|
|
abbr[0] = heldItem.name[0];
|
|
|
|
|
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
|
|
|
|
|
}
|
|
|
|
|
float textW = ImGui::CalcTextSize(abbr).x;
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (heldItem.stackCount > 1) {
|
|
|
|
|
char countStr[16];
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "%u", heldItem.stackCount);
|
|
|
|
|
float cw = ImGui::CalcTextSize(countStr).x;
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
|
|
|
|
|
IM_COL32(255, 255, 255, 220), countStr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 17:41:19 -08:00
|
|
|
bool InventoryScreen::dropHeldItemToEquipSlot(game::Inventory& inv, game::EquipSlot slot) {
|
|
|
|
|
if (!holdingItem) return false;
|
|
|
|
|
placeInEquipment(inv, slot);
|
|
|
|
|
return !holdingItem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 13:54:47 -08:00
|
|
|
void InventoryScreen::dropIntoBankSlot(game::GameHandler& /*gh*/, uint8_t dstBag, uint8_t dstSlot) {
|
|
|
|
|
if (!holdingItem || !gameHandler_) return;
|
|
|
|
|
uint8_t srcBag = 0xFF;
|
|
|
|
|
uint8_t srcSlot = 0;
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BAG) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
2026-02-26 13:38:29 -08:00
|
|
|
} else if (heldSource == HeldSource::BANK && heldBankIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(39 + heldBankIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG && heldBankBagIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(67 + heldBankBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBankBagSlotIndex);
|
|
|
|
|
} else if (heldSource == HeldSource::BANK_BAG_EQUIP && heldBankBagIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(67 + heldBankBagIndex);
|
2026-02-25 13:54:47 -08:00
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-26 13:38:29 -08:00
|
|
|
// Same source and dest — just cancel pickup (restore item locally).
|
|
|
|
|
// Server ignores same-slot swaps so no rebuild would run, losing the item data.
|
|
|
|
|
if (srcBag == dstBag && srcSlot == dstSlot) {
|
|
|
|
|
cancelPickup(gameHandler_->getInventory());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-25 13:54:47 -08:00
|
|
|
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 17:41:19 -08:00
|
|
|
bool InventoryScreen::beginPickupFromEquipSlot(game::Inventory& inv, game::EquipSlot slot) {
|
|
|
|
|
if (holdingItem) return false;
|
|
|
|
|
const auto& eq = inv.getEquipSlot(slot);
|
|
|
|
|
if (eq.empty()) return false;
|
|
|
|
|
pickupFromEquipment(inv, slot);
|
|
|
|
|
return holdingItem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Bags window (B key) — bottom of screen, no equipment panel
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
void InventoryScreen::toggleBackpack() {
|
|
|
|
|
backpackOpen_ = !backpackOpen_;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::toggleBag(int idx) {
|
2026-02-19 01:50:50 -08:00
|
|
|
if (idx >= 0 && idx < 4) {
|
2026-02-13 22:51:49 -08:00
|
|
|
bagOpen_[idx] = !bagOpen_[idx];
|
2026-02-19 01:50:50 -08:00
|
|
|
if (bagOpen_[idx]) {
|
|
|
|
|
// Keep backpack as the anchor window at the bottom of the stack.
|
|
|
|
|
backpackOpen_ = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::openAllBags() {
|
|
|
|
|
backpackOpen_ = true;
|
|
|
|
|
for (auto& b : bagOpen_) b = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::closeAllBags() {
|
|
|
|
|
backpackOpen_ = false;
|
|
|
|
|
for (auto& b : bagOpen_) b = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
bool InventoryScreen::bagHasAnyItems(const game::Inventory& inventory, int bagIndex) const {
|
|
|
|
|
int bagSize = inventory.getBagSize(bagIndex);
|
|
|
|
|
if (bagSize <= 0) return false;
|
|
|
|
|
for (int i = 0; i < bagSize; ++i) {
|
|
|
|
|
if (!inventory.getBagSlot(bagIndex, i).empty()) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:01:26 -08:00
|
|
|
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
2026-02-02 12:24:50 -08:00
|
|
|
// B key toggle (edge-triggered)
|
2026-02-14 21:56:38 -08:00
|
|
|
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
|
|
|
|
bool bDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
2026-02-13 22:51:49 -08:00
|
|
|
bool bToggled = bDown && !bKeyWasDown;
|
2026-02-02 12:24:50 -08:00
|
|
|
bKeyWasDown = bDown;
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// C key toggle for character screen (edge-triggered)
|
2026-02-14 21:56:38 -08:00
|
|
|
bool cDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_C);
|
2026-02-06 13:47:03 -08:00
|
|
|
if (cDown && !cKeyWasDown) {
|
|
|
|
|
characterOpen = !characterOpen;
|
|
|
|
|
}
|
|
|
|
|
cKeyWasDown = cDown;
|
|
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
if (separateBags_) {
|
|
|
|
|
if (bToggled) {
|
|
|
|
|
bool anyOpen = backpackOpen_;
|
|
|
|
|
for (auto b : bagOpen_) anyOpen |= b;
|
|
|
|
|
if (anyOpen) closeAllBags();
|
|
|
|
|
else openAllBags();
|
|
|
|
|
}
|
|
|
|
|
open = backpackOpen_ || std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b){ return b; });
|
|
|
|
|
} else {
|
|
|
|
|
if (bToggled) open = !open;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
if (!open) {
|
|
|
|
|
if (holdingItem) cancelPickup(inventory);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape cancels held item
|
2026-02-14 21:56:38 -08:00
|
|
|
if (holdingItem && !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_ESCAPE)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
cancelPickup(inventory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Right-click anywhere while holding = cancel
|
|
|
|
|
if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
cancelPickup(inventory);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
// Cancel pending pickup if mouse released before threshold
|
|
|
|
|
if (pickupPending_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
pickupPending_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
if (separateBags_) {
|
|
|
|
|
renderSeparateBags(inventory, moneyCopper);
|
|
|
|
|
} else {
|
|
|
|
|
renderAggregateBags(inventory, moneyCopper);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect held item dropped outside inventory windows → drop confirmation
|
|
|
|
|
if (holdingItem && heldItem.itemId != 6948 && ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
|
|
|
|
|
!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) &&
|
|
|
|
|
!ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) {
|
|
|
|
|
dropConfirmOpen_ = true;
|
|
|
|
|
dropItemName_ = heldItem.name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop item confirmation popup — positioned near cursor
|
|
|
|
|
if (dropConfirmOpen_) {
|
|
|
|
|
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::OpenPopup("##DropItem");
|
|
|
|
|
dropConfirmOpen_ = false;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::BeginPopup("##DropItem", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
|
|
|
|
ImGui::Text("Destroy \"%s\"?", dropItemName_.c_str());
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
if (ImGui::Button("Yes", ImVec2(80, 0))) {
|
2026-02-19 06:34:06 -08:00
|
|
|
if (gameHandler_) {
|
|
|
|
|
uint8_t srcBag = 0xFF;
|
|
|
|
|
uint8_t srcSlot = 0;
|
|
|
|
|
bool haveSource = false;
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
|
|
|
|
haveSource = true;
|
|
|
|
|
} else if (heldSource == HeldSource::BAG && heldBagIndex >= 0 && heldBagSlotIndex >= 0) {
|
|
|
|
|
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
|
|
|
|
haveSource = true;
|
|
|
|
|
} else if (heldSource == HeldSource::EQUIPMENT &&
|
|
|
|
|
heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
|
|
|
|
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
|
|
|
|
haveSource = true;
|
|
|
|
|
}
|
|
|
|
|
if (haveSource) {
|
|
|
|
|
uint8_t destroyCount = static_cast<uint8_t>(std::clamp<uint32_t>(
|
|
|
|
|
std::max<uint32_t>(1u, heldItem.stackCount), 1u, 255u));
|
|
|
|
|
gameHandler_->destroyItem(srcBag, srcSlot, destroyCount);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
holdingItem = false;
|
|
|
|
|
heldItem = game::ItemDef{};
|
|
|
|
|
heldSource = HeldSource::NONE;
|
2026-02-19 06:34:06 -08:00
|
|
|
heldBackpackIndex = -1;
|
|
|
|
|
heldBagIndex = -1;
|
|
|
|
|
heldBagSlotIndex = -1;
|
|
|
|
|
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
2026-02-13 22:51:49 -08:00
|
|
|
inventoryDirty = true;
|
|
|
|
|
dropItemName_.clear();
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("No", ImVec2(80, 0))) {
|
|
|
|
|
cancelPickup(inventory);
|
|
|
|
|
dropItemName_.clear();
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw held item at cursor
|
|
|
|
|
renderHeldItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Aggregate mode — original single-window bags
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper) {
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
2026-02-06 13:47:03 -08:00
|
|
|
float screenH = io.DisplaySize.y;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
constexpr float slotSize = 40.0f;
|
2026-02-19 01:50:50 -08:00
|
|
|
constexpr int columns = 6;
|
2026-02-06 13:47:03 -08:00
|
|
|
int rows = (inventory.getBackpackSize() + columns - 1) / columns;
|
2026-02-13 22:51:49 -08:00
|
|
|
float bagContentH = rows * (slotSize + 4.0f) + 40.0f;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
|
|
|
|
int bagSize = inventory.getBagSize(bag);
|
|
|
|
|
if (bagSize <= 0) continue;
|
2026-02-19 01:50:50 -08:00
|
|
|
if (compactBags_ && !bagHasAnyItems(inventory, bag)) continue;
|
2026-02-06 13:47:03 -08:00
|
|
|
int bagRows = (bagSize + columns - 1) / columns;
|
2026-02-13 22:51:49 -08:00
|
|
|
bagContentH += bagRows * (slotSize + 4.0f) + 30.0f;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
float windowW = columns * (slotSize + 4.0f) + 30.0f;
|
2026-02-13 22:51:49 -08:00
|
|
|
float windowH = bagContentH + 50.0f;
|
2026-02-06 03:24:46 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
float posX = screenW - windowW - 10.0f;
|
2026-02-13 22:51:49 -08:00
|
|
|
float posY = screenH - windowH - 60.0f;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
|
|
|
|
|
if (!ImGui::Begin("Bags", &open, flags)) {
|
|
|
|
|
ImGui::End();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
renderBackpackPanel(inventory, compactBags_);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
ImGui::Spacing();
|
2026-02-05 14:01:26 -08:00
|
|
|
uint64_t gold = moneyCopper / 10000;
|
|
|
|
|
uint64_t silver = (moneyCopper / 100) % 100;
|
|
|
|
|
uint64_t copper = moneyCopper % 100;
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc",
|
|
|
|
|
static_cast<unsigned long long>(gold),
|
|
|
|
|
static_cast<unsigned long long>(silver),
|
|
|
|
|
static_cast<unsigned long long>(copper));
|
2026-02-19 01:50:50 -08:00
|
|
|
ImGui::SameLine();
|
|
|
|
|
const char* collapseLabel = compactBags_ ? "Expand Empty" : "Collapse Empty";
|
|
|
|
|
const float btnW = 92.0f;
|
|
|
|
|
const float rightMargin = 8.0f;
|
|
|
|
|
float rightX = ImGui::GetWindowContentRegionMax().x - btnW - rightMargin;
|
|
|
|
|
if (rightX > ImGui::GetCursorPosX()) ImGui::SetCursorPosX(rightX);
|
|
|
|
|
if (ImGui::SmallButton(collapseLabel)) {
|
|
|
|
|
compactBags_ = !compactBags_;
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::SetTooltip("Toggle empty bag section visibility");
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::End();
|
2026-02-13 22:51:49 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Separate mode — individual draggable bag windows
|
|
|
|
|
// ============================================================
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
void InventoryScreen::renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper) {
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
|
|
|
|
|
|
|
|
|
constexpr float slotSize = 40.0f;
|
2026-02-19 01:50:50 -08:00
|
|
|
constexpr int columns = 6;
|
2026-02-13 22:51:49 -08:00
|
|
|
constexpr float baseWindowW = columns * (slotSize + 4.0f) + 30.0f;
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
bool anyBagOpen = std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b) { return b; });
|
|
|
|
|
if (anyBagOpen && !backpackOpen_) {
|
|
|
|
|
// Enforce backpack as the bottom-most stack window when any bag is open.
|
|
|
|
|
backpackOpen_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Anchor stack to the bag bar (bottom-right), opening upward.
|
|
|
|
|
const float bagBarTop = screenH - (42.0f + 12.0f) - 10.0f;
|
|
|
|
|
const float stackGap = 8.0f;
|
|
|
|
|
float stackBottom = bagBarTop - stackGap;
|
|
|
|
|
float stackX = screenW - baseWindowW - 10.0f;
|
|
|
|
|
|
|
|
|
|
// Backpack window (bottom of stack)
|
2026-02-13 22:51:49 -08:00
|
|
|
if (backpackOpen_) {
|
|
|
|
|
int bpRows = (inventory.getBackpackSize() + columns - 1) / columns;
|
|
|
|
|
float bpH = bpRows * (slotSize + 4.0f) + 80.0f; // header + money + padding
|
2026-02-19 01:50:50 -08:00
|
|
|
float defaultY = stackBottom - bpH;
|
|
|
|
|
renderBagWindow("Backpack", backpackOpen_, inventory, -1, stackX, defaultY, moneyCopper);
|
|
|
|
|
stackBottom = defaultY - stackGap;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
// Extra bag windows in right-to-left bag-bar order (closest to backpack first).
|
|
|
|
|
constexpr int kBagOrder[game::Inventory::NUM_BAG_SLOTS] = {3, 2, 1, 0};
|
|
|
|
|
for (int ord = 0; ord < game::Inventory::NUM_BAG_SLOTS; ++ord) {
|
|
|
|
|
int bag = kBagOrder[ord];
|
2026-02-13 22:51:49 -08:00
|
|
|
if (!bagOpen_[bag]) continue;
|
|
|
|
|
int bagSize = inventory.getBagSize(bag);
|
|
|
|
|
if (bagSize <= 0) {
|
|
|
|
|
bagOpen_[bag] = false;
|
|
|
|
|
continue;
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
// In separate-bag mode, never auto-hide empty bags. Players still need
|
|
|
|
|
// to open empty bags to move items into them.
|
2026-02-13 22:51:49 -08:00
|
|
|
|
|
|
|
|
int bagRows = (bagSize + columns - 1) / columns;
|
|
|
|
|
float bagH = bagRows * (slotSize + 4.0f) + 60.0f;
|
2026-02-19 01:50:50 -08:00
|
|
|
float defaultY = stackBottom - bagH;
|
|
|
|
|
stackBottom = defaultY - stackGap;
|
2026-02-13 22:51:49 -08:00
|
|
|
|
|
|
|
|
// Build title from equipped bag item name
|
|
|
|
|
char title[64];
|
|
|
|
|
game::EquipSlot bagSlot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + bag);
|
|
|
|
|
const auto& bagItem = inventory.getEquipSlot(bagSlot);
|
|
|
|
|
if (!bagItem.empty() && !bagItem.item.name.empty()) {
|
|
|
|
|
snprintf(title, sizeof(title), "%s##bag%d", bagItem.item.name.c_str(), bag);
|
|
|
|
|
} else {
|
2026-02-19 01:50:50 -08:00
|
|
|
snprintf(title, sizeof(title), "Bag Slot %d##bag%d", bag + 1, bag);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
renderBagWindow(title, bagOpen_[bag], inventory, bag, stackX, defaultY, 0);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 22:51:49 -08:00
|
|
|
// Update open state based on individual windows
|
|
|
|
|
open = backpackOpen_ || std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b){ return b; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
|
|
|
|
game::Inventory& inventory, int bagIndex,
|
|
|
|
|
float defaultX, float defaultY, uint64_t moneyCopper) {
|
|
|
|
|
constexpr float slotSize = 40.0f;
|
2026-02-19 01:50:50 -08:00
|
|
|
constexpr int columns = 6;
|
2026-02-13 22:51:49 -08:00
|
|
|
|
|
|
|
|
int numSlots = (bagIndex < 0) ? inventory.getBackpackSize() : inventory.getBagSize(bagIndex);
|
|
|
|
|
if (numSlots <= 0) return;
|
|
|
|
|
|
|
|
|
|
int rows = (numSlots + columns - 1) / columns;
|
|
|
|
|
float contentH = rows * (slotSize + 4.0f) + 10.0f;
|
|
|
|
|
if (bagIndex < 0) contentH += 25.0f; // money display for backpack
|
|
|
|
|
float gridW = columns * (slotSize + 4.0f) + 30.0f;
|
|
|
|
|
// Ensure window is wide enough for the title + close button
|
|
|
|
|
const char* displayTitle = title;
|
|
|
|
|
const char* hashPos = strstr(title, "##");
|
|
|
|
|
float titleW = ImGui::CalcTextSize(displayTitle, hashPos).x + 50.0f; // close button + padding
|
|
|
|
|
float windowW = std::max(gridW, titleW);
|
|
|
|
|
float windowH = contentH + 40.0f; // title bar + padding
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
// Keep separate bag windows anchored to the bag-bar stack.
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_Always);
|
2026-02-13 22:51:49 -08:00
|
|
|
ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
|
|
|
|
if (!ImGui::Begin(title, &isOpen, flags)) {
|
|
|
|
|
ImGui::End();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render item slots in 4-column grid
|
|
|
|
|
for (int i = 0; i < numSlots; i++) {
|
|
|
|
|
if (i % columns != 0) ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
const game::ItemSlot& slot = (bagIndex < 0)
|
|
|
|
|
? inventory.getBackpackSlot(i)
|
|
|
|
|
: inventory.getBagSlot(bagIndex, i);
|
|
|
|
|
|
|
|
|
|
char id[32];
|
|
|
|
|
if (bagIndex < 0) {
|
|
|
|
|
snprintf(id, sizeof(id), "##sbp_%d", i);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(id, sizeof(id), "##sb%d_%d", bagIndex, i);
|
|
|
|
|
}
|
|
|
|
|
ImGui::PushID(id);
|
|
|
|
|
|
2026-02-17 01:00:04 -08:00
|
|
|
if (bagIndex < 0) {
|
|
|
|
|
// Backpack slot
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
|
|
|
|
SlotKind::BACKPACK, i, game::EquipSlot::NUM_SLOTS);
|
|
|
|
|
} else {
|
|
|
|
|
// Bag slot - pass bag index info for interactions
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
|
|
|
|
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS,
|
|
|
|
|
bagIndex, i);
|
|
|
|
|
}
|
2026-02-13 22:51:49 -08:00
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Money display at bottom of backpack
|
|
|
|
|
if (bagIndex < 0 && moneyCopper > 0) {
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
uint64_t gold = moneyCopper / 10000;
|
|
|
|
|
uint64_t silver = (moneyCopper / 100) % 100;
|
|
|
|
|
uint64_t copper = moneyCopper % 100;
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc",
|
|
|
|
|
static_cast<unsigned long long>(gold),
|
|
|
|
|
static_cast<unsigned long long>(silver),
|
|
|
|
|
static_cast<unsigned long long>(copper));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// ============================================================
|
2026-02-06 14:24:38 -08:00
|
|
|
// Character screen (C key) — equipment + model preview + stats
|
2026-02-06 13:47:03 -08:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
2026-02-06 13:47:03 -08:00
|
|
|
if (!characterOpen) return;
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
auto& inventory = gameHandler.getInventory();
|
|
|
|
|
|
|
|
|
|
// Lazy-init the preview
|
|
|
|
|
if (!previewInitialized_ && assetManager_) {
|
|
|
|
|
initPreview();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update preview equipment if dirty
|
|
|
|
|
if (previewDirty_ && charPreview_ && previewInitialized_) {
|
|
|
|
|
updatePreviewEquipment(inventory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update and render the preview FBO
|
|
|
|
|
if (charPreview_ && previewInitialized_) {
|
|
|
|
|
charPreview_->update(ImGui::GetIO().DeltaTime);
|
|
|
|
|
charPreview_->render();
|
2026-02-22 05:58:45 -08:00
|
|
|
charPreview_->requestComposite();
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
ImGui::SetNextWindowSize(ImVec2(380.0f, 650.0f), ImGuiCond_FirstUseEver);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
|
|
|
|
ImGui::End();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Clamp window position within screen after resize
|
|
|
|
|
{
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
ImVec2 pos = ImGui::GetWindowPos();
|
|
|
|
|
ImVec2 sz = ImGui::GetWindowSize();
|
|
|
|
|
bool clamped = false;
|
|
|
|
|
if (pos.x + sz.x > io.DisplaySize.x) { pos.x = std::max(0.0f, io.DisplaySize.x - sz.x); clamped = true; }
|
|
|
|
|
if (pos.y + sz.y > io.DisplaySize.y) { pos.y = std::max(0.0f, io.DisplaySize.y - sz.y); clamped = true; }
|
|
|
|
|
if (pos.x < 0.0f) { pos.x = 0.0f; clamped = true; }
|
|
|
|
|
if (pos.y < 0.0f) { pos.y = 0.0f; clamped = true; }
|
|
|
|
|
if (clamped) ImGui::SetWindowPos(pos);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
if (ImGui::BeginTabBar("##CharacterTabs")) {
|
|
|
|
|
if (ImGui::BeginTabItem("Equipment")) {
|
|
|
|
|
renderEquipmentPanel(inventory);
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
2026-02-06 13:47:03 -08:00
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
if (ImGui::BeginTabItem("Stats")) {
|
|
|
|
|
ImGui::Spacing();
|
2026-02-19 17:45:09 -08:00
|
|
|
renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating());
|
2026-02-06 18:34:45 -08:00
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginTabItem("Skills")) {
|
2026-02-07 14:21:50 -08:00
|
|
|
const auto& skills = gameHandler.getPlayerSkills();
|
|
|
|
|
if (skills.empty()) {
|
|
|
|
|
ImGui::TextDisabled("No skill data received yet.");
|
|
|
|
|
} else {
|
|
|
|
|
// Group skills by SkillLine.dbc category
|
|
|
|
|
struct CategoryGroup {
|
|
|
|
|
const char* label;
|
|
|
|
|
uint32_t categoryId;
|
|
|
|
|
};
|
|
|
|
|
static const CategoryGroup groups[] = {
|
|
|
|
|
{ "Weapon Skills", 6 },
|
|
|
|
|
{ "Armor Skills", 8 },
|
|
|
|
|
{ "Secondary Skills", 10 },
|
|
|
|
|
{ "Professions", 11 },
|
|
|
|
|
{ "Languages", 9 },
|
|
|
|
|
{ "Other", 0 },
|
2026-02-06 18:34:45 -08:00
|
|
|
};
|
2026-02-07 14:21:50 -08:00
|
|
|
|
|
|
|
|
ImGui::BeginChild("##SkillsList", ImVec2(0, 0), true);
|
|
|
|
|
|
|
|
|
|
for (const auto& group : groups) {
|
|
|
|
|
// Collect skills for this category
|
|
|
|
|
std::vector<const game::PlayerSkill*> groupSkills;
|
|
|
|
|
for (const auto& [id, skill] : skills) {
|
|
|
|
|
if (skill.value == 0 && skill.maxValue == 0) continue;
|
|
|
|
|
uint32_t cat = gameHandler.getSkillCategory(id);
|
|
|
|
|
if (group.categoryId == 0) {
|
|
|
|
|
// "Other" catches everything not in the named categories
|
|
|
|
|
if (cat != 6 && cat != 8 && cat != 9 && cat != 10 && cat != 11) {
|
|
|
|
|
groupSkills.push_back(&skill);
|
|
|
|
|
}
|
|
|
|
|
} else if (cat == group.categoryId) {
|
|
|
|
|
groupSkills.push_back(&skill);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (groupSkills.empty()) continue;
|
|
|
|
|
|
|
|
|
|
if (ImGui::CollapsingHeader(group.label, ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
|
|
|
for (const game::PlayerSkill* skill : groupSkills) {
|
|
|
|
|
const std::string& name = gameHandler.getSkillName(skill->skillId);
|
|
|
|
|
char label[128];
|
|
|
|
|
if (name.empty()) {
|
|
|
|
|
snprintf(label, sizeof(label), "Skill #%u", skill->skillId);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(label, sizeof(label), "%s", name.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show progress bar with value/max overlay
|
|
|
|
|
float ratio = (skill->maxValue > 0)
|
|
|
|
|
? static_cast<float>(skill->value) / static_cast<float>(skill->maxValue)
|
|
|
|
|
: 0.0f;
|
|
|
|
|
|
|
|
|
|
char overlay[64];
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", skill->value, skill->maxValue);
|
|
|
|
|
|
|
|
|
|
ImGui::Text("%s", label);
|
|
|
|
|
ImGui::SameLine(180.0f);
|
|
|
|
|
ImGui::SetNextItemWidth(-1.0f);
|
|
|
|
|
ImGui::ProgressBar(ratio, ImVec2(0, 14.0f), overlay);
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 14:21:50 -08:00
|
|
|
ImGui::EndChild();
|
2026-02-06 18:34:45 -08:00
|
|
|
}
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabBar();
|
|
|
|
|
}
|
2026-02-06 14:24:38 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
// If both bags and character are open, allow drag-and-drop between them
|
|
|
|
|
// (held item rendering is handled in render())
|
|
|
|
|
if (open) {
|
|
|
|
|
renderHeldItem();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
static const game::EquipSlot leftSlots[] = {
|
|
|
|
|
game::EquipSlot::HEAD, game::EquipSlot::NECK,
|
|
|
|
|
game::EquipSlot::SHOULDERS, game::EquipSlot::BACK,
|
|
|
|
|
game::EquipSlot::CHEST, game::EquipSlot::SHIRT,
|
|
|
|
|
game::EquipSlot::TABARD, game::EquipSlot::WRISTS,
|
|
|
|
|
};
|
|
|
|
|
static const game::EquipSlot rightSlots[] = {
|
|
|
|
|
game::EquipSlot::HANDS, game::EquipSlot::WAIST,
|
|
|
|
|
game::EquipSlot::LEGS, game::EquipSlot::FEET,
|
|
|
|
|
game::EquipSlot::RING1, game::EquipSlot::RING2,
|
|
|
|
|
game::EquipSlot::TRINKET1, game::EquipSlot::TRINKET2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
constexpr float slotSize = 36.0f;
|
2026-02-06 14:24:38 -08:00
|
|
|
constexpr float previewW = 140.0f;
|
|
|
|
|
|
|
|
|
|
// Calculate column positions for the 3-column layout
|
|
|
|
|
float contentStartX = ImGui::GetCursorPosX();
|
|
|
|
|
float rightColX = contentStartX + slotSize + 8.0f + previewW + 8.0f;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
int rows = 8;
|
2026-02-06 14:24:38 -08:00
|
|
|
float previewStartY = ImGui::GetCursorScreenPos().y;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
for (int r = 0; r < rows; r++) {
|
2026-02-06 14:24:38 -08:00
|
|
|
// Left column
|
2026-02-02 12:24:50 -08:00
|
|
|
{
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
|
|
|
|
const char* label = game::getEquipSlotName(leftSlots[r]);
|
|
|
|
|
char id[64];
|
|
|
|
|
snprintf(id, sizeof(id), "##eq_l_%d", r);
|
|
|
|
|
ImGui::PushID(id);
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, label,
|
|
|
|
|
SlotKind::EQUIPMENT, -1, leftSlots[r]);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Right column
|
|
|
|
|
ImGui::SameLine(rightColX);
|
2026-02-02 12:24:50 -08:00
|
|
|
{
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
|
|
|
|
const char* label = game::getEquipSlotName(rightSlots[r]);
|
|
|
|
|
char id[64];
|
|
|
|
|
snprintf(id, sizeof(id), "##eq_r_%d", r);
|
|
|
|
|
ImGui::PushID(id);
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, label,
|
|
|
|
|
SlotKind::EQUIPMENT, -1, rightSlots[r]);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
float previewEndY = ImGui::GetCursorScreenPos().y;
|
|
|
|
|
|
|
|
|
|
// Draw the 3D character preview in the center column
|
|
|
|
|
if (charPreview_ && previewInitialized_ && charPreview_->getTextureId()) {
|
|
|
|
|
float previewX = ImGui::GetWindowPos().x + contentStartX + slotSize + 8.0f;
|
|
|
|
|
float previewH = previewEndY - previewStartY;
|
|
|
|
|
// Maintain aspect ratio
|
|
|
|
|
float texAspect = static_cast<float>(charPreview_->getWidth()) / static_cast<float>(charPreview_->getHeight());
|
|
|
|
|
float displayW = previewW;
|
|
|
|
|
float displayH = displayW / texAspect;
|
|
|
|
|
if (displayH > previewH) {
|
|
|
|
|
displayH = previewH;
|
|
|
|
|
displayW = displayH * texAspect;
|
|
|
|
|
}
|
|
|
|
|
float offsetX = previewX + (previewW - displayW) * 0.5f;
|
|
|
|
|
float offsetY = previewStartY + (previewH - displayH) * 0.5f;
|
|
|
|
|
|
|
|
|
|
ImVec2 pMin(offsetX, offsetY);
|
|
|
|
|
ImVec2 pMax(offsetX + displayW, offsetY + displayH);
|
|
|
|
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
// Background for preview area
|
|
|
|
|
drawList->AddRectFilled(pMin, pMax, IM_COL32(13, 13, 25, 255));
|
|
|
|
|
drawList->AddImage(
|
2026-02-22 05:58:45 -08:00
|
|
|
reinterpret_cast<ImTextureID>(charPreview_->getTextureId()),
|
|
|
|
|
pMin, pMax);
|
2026-02-06 14:24:38 -08:00
|
|
|
drawList->AddRect(pMin, pMax, IM_COL32(60, 60, 80, 200));
|
|
|
|
|
|
|
|
|
|
// Drag-to-rotate: detect mouse drag over the preview image
|
|
|
|
|
ImGui::SetCursorScreenPos(pMin);
|
|
|
|
|
ImGui::InvisibleButton("##charPreviewDrag", ImVec2(displayW, displayH));
|
|
|
|
|
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
|
|
|
|
float dx = ImGui::GetIO().MouseDelta.x;
|
|
|
|
|
charPreview_->rotate(dx * 1.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Weapon row
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
static const game::EquipSlot weaponSlots[] = {
|
|
|
|
|
game::EquipSlot::MAIN_HAND,
|
|
|
|
|
game::EquipSlot::OFF_HAND,
|
|
|
|
|
game::EquipSlot::RANGED,
|
|
|
|
|
};
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
|
if (i > 0) ImGui::SameLine();
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(weaponSlots[i]);
|
|
|
|
|
const char* label = game::getEquipSlotName(weaponSlots[i]);
|
|
|
|
|
char id[64];
|
|
|
|
|
snprintf(id, sizeof(id), "##eq_w_%d", i);
|
|
|
|
|
ImGui::PushID(id);
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, label,
|
|
|
|
|
SlotKind::EQUIPMENT, -1, weaponSlots[i]);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Stats Panel
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-19 17:45:09 -08:00
|
|
|
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor) {
|
2026-02-06 14:24:38 -08:00
|
|
|
// Sum equipment stats
|
|
|
|
|
int32_t totalStr = 0, totalAgi = 0, totalSta = 0, totalInt = 0, totalSpi = 0;
|
|
|
|
|
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
totalStr += slot.item.strength;
|
|
|
|
|
totalAgi += slot.item.agility;
|
|
|
|
|
totalSta += slot.item.stamina;
|
|
|
|
|
totalInt += slot.item.intellect;
|
|
|
|
|
totalSpi += slot.item.spirit;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 17:45:09 -08:00
|
|
|
// Use server-authoritative armor from UNIT_FIELD_RESISTANCES when available.
|
|
|
|
|
// Falls back to summing item query armors if server armor wasn't received yet.
|
|
|
|
|
int32_t itemQueryArmor = 0;
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) itemQueryArmor += slot.item.armor;
|
|
|
|
|
}
|
|
|
|
|
int32_t totalArmor = (serverArmor > 0) ? serverArmor : itemQueryArmor;
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Base stats: 20 + level
|
|
|
|
|
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
|
|
|
|
|
|
|
|
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
|
|
|
|
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
|
|
|
|
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
|
|
|
|
ImVec4 gray(0.6f, 0.6f, 0.6f, 1.0f);
|
|
|
|
|
|
|
|
|
|
// Armor (no base)
|
|
|
|
|
if (totalArmor > 0) {
|
|
|
|
|
ImGui::TextColored(gold, "Armor: %d", totalArmor);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(gray, "Armor: 0");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to render a stat line
|
|
|
|
|
auto renderStat = [&](const char* name, int32_t equipBonus) {
|
|
|
|
|
int32_t total = baseStat + equipBonus;
|
|
|
|
|
if (equipBonus > 0) {
|
|
|
|
|
ImGui::TextColored(white, "%s: %d", name, total);
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextColored(green, "(+%d)", equipBonus);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(gray, "%s: %d", name, total);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderStat("Strength", totalStr);
|
|
|
|
|
renderStat("Agility", totalAgi);
|
|
|
|
|
renderStat("Stamina", totalSta);
|
|
|
|
|
renderStat("Intellect", totalInt);
|
|
|
|
|
renderStat("Spirit", totalSpi);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections) {
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
constexpr float slotSize = 40.0f;
|
2026-02-19 01:50:50 -08:00
|
|
|
constexpr int columns = 6;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
for (int i = 0; i < inventory.getBackpackSize(); i++) {
|
|
|
|
|
if (i % columns != 0) ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
const auto& slot = inventory.getBackpackSlot(i);
|
|
|
|
|
char id[32];
|
|
|
|
|
snprintf(id, sizeof(id), "##bp_%d", i);
|
|
|
|
|
ImGui::PushID(id);
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
|
|
|
|
SlotKind::BACKPACK, i, game::EquipSlot::NUM_SLOTS);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show extra bags if equipped
|
|
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
|
|
|
|
int bagSize = inventory.getBagSize(bag);
|
|
|
|
|
if (bagSize <= 0) continue;
|
2026-02-19 01:50:50 -08:00
|
|
|
if (collapseEmptySections && !bagHasAnyItems(inventory, bag)) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
2026-02-19 01:50:50 -08:00
|
|
|
game::EquipSlot bagSlot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + bag);
|
|
|
|
|
const auto& bagItem = inventory.getEquipSlot(bagSlot);
|
|
|
|
|
std::string bagLabel = (!bagItem.empty() && !bagItem.item.name.empty())
|
|
|
|
|
? bagItem.item.name
|
|
|
|
|
: ("Bag Slot " + std::to_string(bag + 1));
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "%s", bagLabel.c_str());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
for (int s = 0; s < bagSize; s++) {
|
|
|
|
|
if (s % columns != 0) ImGui::SameLine();
|
|
|
|
|
const auto& slot = inventory.getBagSlot(bag, s);
|
|
|
|
|
char sid[32];
|
|
|
|
|
snprintf(sid, sizeof(sid), "##bag%d_%d", bag, s);
|
|
|
|
|
ImGui::PushID(sid);
|
|
|
|
|
renderItemSlot(inventory, slot, slotSize, nullptr,
|
2026-02-17 01:00:04 -08:00
|
|
|
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS,
|
|
|
|
|
bag, s);
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
|
|
|
|
float size, const char* label,
|
|
|
|
|
SlotKind kind, int backpackIndex,
|
2026-02-17 01:00:04 -08:00
|
|
|
game::EquipSlot equipSlot,
|
|
|
|
|
int bagIndex, int bagSlotIndex) {
|
|
|
|
|
// Bag items are valid inventory slots even though backpackIndex is -1
|
|
|
|
|
bool isBagSlot = (bagIndex >= 0 && bagSlotIndex >= 0);
|
2026-02-02 12:24:50 -08:00
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
|
|
|
|
bool isEmpty = slot.empty();
|
|
|
|
|
|
|
|
|
|
// Determine if this is a valid drop target for held item
|
|
|
|
|
bool validDrop = false;
|
|
|
|
|
if (holdingItem) {
|
2026-02-17 01:00:04 -08:00
|
|
|
if (kind == SlotKind::BACKPACK && (backpackIndex >= 0 || isBagSlot)) {
|
2026-02-06 13:47:03 -08:00
|
|
|
validDrop = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
2026-02-20 17:41:19 -08:00
|
|
|
if (heldItem.inventoryType == 18) {
|
|
|
|
|
validDrop = (equipSlot >= game::EquipSlot::BAG1 && equipSlot <= game::EquipSlot::BAG4);
|
|
|
|
|
} else {
|
|
|
|
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
|
|
|
|
validDrop = (equipSlot == validSlot);
|
|
|
|
|
if (!validDrop && heldItem.inventoryType == 11)
|
|
|
|
|
validDrop = (equipSlot == game::EquipSlot::RING1 || equipSlot == game::EquipSlot::RING2);
|
|
|
|
|
if (!validDrop && heldItem.inventoryType == 12)
|
|
|
|
|
validDrop = (equipSlot == game::EquipSlot::TRINKET1 || equipSlot == game::EquipSlot::TRINKET2);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isEmpty) {
|
|
|
|
|
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
|
|
|
|
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
|
|
|
|
|
|
|
|
|
if (validDrop) {
|
|
|
|
|
bgCol = IM_COL32(20, 50, 20, 200);
|
|
|
|
|
borderCol = IM_COL32(0, 180, 0, 200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size), borderCol);
|
|
|
|
|
|
|
|
|
|
if (label) {
|
|
|
|
|
char abbr[4] = {};
|
|
|
|
|
abbr[0] = label[0];
|
|
|
|
|
if (label[1]) abbr[1] = label[1];
|
|
|
|
|
float textW = ImGui::CalcTextSize(abbr).x;
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + size * 0.3f),
|
|
|
|
|
IM_COL32(80, 80, 80, 180), abbr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
// Drop held item on mouse release over empty slot
|
|
|
|
|
if (ImGui::IsItemHovered() && holdingItem && validDrop &&
|
|
|
|
|
ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
|
|
|
|
placeInBag(inventory, bagIndex, bagSlotIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
|
|
|
|
placeInEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (label && ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.4f, 0.4f, 1.0f), "Empty");
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const auto& item = slot.item;
|
|
|
|
|
ImVec4 qColor = getQualityColor(item.quality);
|
|
|
|
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
|
|
|
|
|
|
|
|
|
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
|
|
|
|
|
if (holdingItem && validDrop) {
|
|
|
|
|
bgCol = IM_COL32(30, 55, 30, 220);
|
|
|
|
|
borderCol = IM_COL32(0, 200, 0, 220);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Try to show icon
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet iconTex = getItemIcon(item.displayInfoId);
|
2026-02-06 14:24:38 -08:00
|
|
|
if (iconTex) {
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
|
|
|
|
ImVec2(pos.x + size, pos.y + size));
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
|
|
|
|
} else {
|
|
|
|
|
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
char abbr[4] = {};
|
|
|
|
|
if (!item.name.empty()) {
|
|
|
|
|
abbr[0] = item.name[0];
|
|
|
|
|
if (item.name.size() > 1) abbr[1] = item.name[1];
|
|
|
|
|
}
|
|
|
|
|
float textW = ImGui::CalcTextSize(abbr).x;
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (item.stackCount > 1) {
|
|
|
|
|
char countStr[16];
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
|
|
|
|
|
float cw = ImGui::CalcTextSize(countStr).x;
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
|
|
|
|
|
IM_COL32(255, 255, 255, 220), countStr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
|
|
|
|
|
2026-02-26 00:59:07 -08:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-26 00:59:07 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Drop/swap on mouse release over a filled slot
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
|
|
|
|
placeInBag(inventory, bagIndex, bagSlotIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
|
|
|
|
placeInEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 13:47:42 -08:00
|
|
|
// Right-click: bank deposit (if bank open), vendor sell (if vendor mode), or auto-equip/use
|
2026-02-26 00:59:07 -08:00
|
|
|
// 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,
|
2026-02-17 01:00:04 -08:00
|
|
|
" backpackIndex=", backpackIndex,
|
|
|
|
|
" bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex,
|
2026-02-25 13:47:42 -08:00
|
|
|
" vendorMode=", vendorMode_,
|
2026-02-26 00:59:07 -08:00
|
|
|
" bankOpen=", gameHandler_->isBankOpen(),
|
|
|
|
|
" item='", item.name, "' invType=", (int)item.inventoryType);
|
2026-02-25 14:11:09 -08:00
|
|
|
if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
gameHandler_->attachItemFromBackpack(backpackIndex);
|
|
|
|
|
} else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
|
|
|
|
|
gameHandler_->attachItemFromBag(bagIndex, bagSlotIndex);
|
|
|
|
|
} else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
2026-02-25 13:47:42 -08:00
|
|
|
gameHandler_->depositItem(0xFF, static_cast<uint8_t>(23 + backpackIndex));
|
2026-02-25 14:00:54 -08:00
|
|
|
} else if (gameHandler_->isBankOpen() && kind == SlotKind::BACKPACK && isBagSlot) {
|
2026-02-25 13:47:42 -08:00
|
|
|
gameHandler_->depositItem(static_cast<uint8_t>(19 + bagIndex), static_cast<uint8_t>(bagSlotIndex));
|
|
|
|
|
} else if (vendorMode_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
2026-02-06 13:47:03 -08:00
|
|
|
gameHandler_->sellItemBySlot(backpackIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
} else if (vendorMode_ && kind == SlotKind::BACKPACK && isBagSlot) {
|
|
|
|
|
gameHandler_->sellItemInBag(bagIndex, bagSlotIndex);
|
2026-02-06 13:47:03 -08:00
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
2026-02-17 01:00:04 -08:00
|
|
|
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
|
|
|
|
gameHandler_->unequipToBackpack(equipSlot);
|
|
|
|
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
2026-02-26 00:59:07 -08:00
|
|
|
LOG_INFO("Right-click backpack item: name='", item.name,
|
|
|
|
|
"' inventoryType=", (int)item.inventoryType,
|
|
|
|
|
" itemId=", item.itemId);
|
2026-02-17 01:00:04 -08:00
|
|
|
if (item.inventoryType > 0) {
|
|
|
|
|
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
2026-02-12 14:55:27 -08:00
|
|
|
} else {
|
2026-02-17 01:00:04 -08:00
|
|
|
gameHandler_->useItemBySlot(backpackIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-17 01:00:04 -08:00
|
|
|
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
2026-02-26 00:59:07 -08:00
|
|
|
LOG_INFO("Right-click bag item: name='", item.name,
|
|
|
|
|
"' inventoryType=", (int)item.inventoryType,
|
|
|
|
|
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex);
|
2026-02-17 01:00:04 -08:00
|
|
|
if (item.inventoryType > 0) {
|
|
|
|
|
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
|
|
|
|
|
} else {
|
|
|
|
|
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !holdingItem) {
|
2026-02-19 01:50:50 -08:00
|
|
|
renderItemTooltip(item, &inventory);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory) {
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
|
|
|
|
ImVec4 qColor = getQualityColor(item.quality);
|
|
|
|
|
ImGui::TextColored(qColor, "%s", item.name.c_str());
|
|
|
|
|
|
2026-02-08 03:39:02 -08:00
|
|
|
if (item.itemId == 6948 && gameHandler_) {
|
|
|
|
|
uint32_t mapId = 0;
|
|
|
|
|
glm::vec3 pos;
|
|
|
|
|
if (gameHandler_->getHomeBind(mapId, pos)) {
|
2026-02-17 03:50:36 -08:00
|
|
|
const char* mapName = "Unknown";
|
|
|
|
|
switch (mapId) {
|
|
|
|
|
case 0: mapName = "Eastern Kingdoms"; break;
|
|
|
|
|
case 1: mapName = "Kalimdor"; break;
|
|
|
|
|
case 530: mapName = "Outland"; break;
|
|
|
|
|
case 571: mapName = "Northrend"; break;
|
|
|
|
|
case 13: mapName = "Test"; break;
|
|
|
|
|
case 169: mapName = "Emerald Dream"; break;
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName);
|
2026-02-08 03:39:02 -08:00
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Home: not set");
|
|
|
|
|
}
|
2026-02-17 03:50:36 -08:00
|
|
|
ImGui::TextDisabled("Use: Teleport home");
|
2026-02-08 03:39:02 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Slot type
|
|
|
|
|
if (item.inventoryType > 0) {
|
|
|
|
|
const char* slotName = "";
|
|
|
|
|
switch (item.inventoryType) {
|
|
|
|
|
case 1: slotName = "Head"; break;
|
|
|
|
|
case 2: slotName = "Neck"; break;
|
|
|
|
|
case 3: slotName = "Shoulder"; break;
|
|
|
|
|
case 4: slotName = "Shirt"; break;
|
|
|
|
|
case 5: slotName = "Chest"; break;
|
|
|
|
|
case 6: slotName = "Waist"; break;
|
|
|
|
|
case 7: slotName = "Legs"; break;
|
|
|
|
|
case 8: slotName = "Feet"; break;
|
|
|
|
|
case 9: slotName = "Wrist"; break;
|
|
|
|
|
case 10: slotName = "Hands"; break;
|
|
|
|
|
case 11: slotName = "Finger"; break;
|
|
|
|
|
case 12: slotName = "Trinket"; break;
|
|
|
|
|
case 13: slotName = "One-Hand"; break;
|
|
|
|
|
case 14: slotName = "Shield"; break;
|
|
|
|
|
case 15: slotName = "Ranged"; break;
|
|
|
|
|
case 16: slotName = "Back"; break;
|
|
|
|
|
case 17: slotName = "Two-Hand"; break;
|
|
|
|
|
case 18: slotName = "Bag"; break;
|
|
|
|
|
case 19: slotName = "Tabard"; break;
|
|
|
|
|
case 20: slotName = "Robe"; break;
|
|
|
|
|
case 21: slotName = "Main Hand"; break;
|
|
|
|
|
case 22: slotName = "Off Hand"; break;
|
|
|
|
|
case 23: slotName = "Held In Off-hand"; break;
|
|
|
|
|
case 25: slotName = "Thrown"; break;
|
|
|
|
|
case 26: slotName = "Ranged"; break;
|
|
|
|
|
default: slotName = ""; break;
|
|
|
|
|
}
|
|
|
|
|
if (slotName[0]) {
|
|
|
|
|
if (!item.subclassName.empty()) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, item.subclassName.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 02:27:01 -08:00
|
|
|
auto isWeaponInventoryType = [](uint32_t invType) {
|
|
|
|
|
switch (invType) {
|
|
|
|
|
case 13: // One-Hand
|
|
|
|
|
case 15: // Ranged
|
|
|
|
|
case 17: // Two-Hand
|
|
|
|
|
case 21: // Main Hand
|
|
|
|
|
case 25: // Thrown
|
|
|
|
|
case 26: // Ranged Right
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
2026-02-18 03:50:47 -08:00
|
|
|
}
|
2026-02-19 02:27:01 -08:00
|
|
|
};
|
|
|
|
|
const bool isWeapon = isWeaponInventoryType(item.inventoryType);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-19 02:27:01 -08:00
|
|
|
// Compact stats view for weapons: DPS + condensed stat bonuses.
|
|
|
|
|
// Non-weapons keep armor/sell info visible.
|
2026-02-06 14:24:38 -08:00
|
|
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
2026-02-19 02:27:01 -08:00
|
|
|
if (isWeapon && item.damageMax > 0.0f && item.delayMs > 0) {
|
|
|
|
|
float speed = static_cast<float>(item.delayMs) / 1000.0f;
|
|
|
|
|
float dps = ((item.damageMin + item.damageMax) * 0.5f) / speed;
|
|
|
|
|
ImGui::Text("%.1f DPS", dps);
|
|
|
|
|
}
|
2026-02-19 16:52:04 -08:00
|
|
|
|
|
|
|
|
// Armor appears before stat bonuses — matches WoW tooltip order
|
|
|
|
|
if (item.armor > 0) {
|
|
|
|
|
ImGui::Text("%d Armor", item.armor);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 02:27:01 -08:00
|
|
|
auto appendBonus = [](std::string& out, int32_t val, const char* shortName) {
|
|
|
|
|
if (val <= 0) return;
|
|
|
|
|
if (!out.empty()) out += " ";
|
|
|
|
|
out += "+" + std::to_string(val) + " ";
|
|
|
|
|
out += shortName;
|
2026-02-06 14:24:38 -08:00
|
|
|
};
|
2026-02-19 02:27:01 -08:00
|
|
|
std::string bonusLine;
|
|
|
|
|
appendBonus(bonusLine, item.strength, "Str");
|
|
|
|
|
appendBonus(bonusLine, item.agility, "Agi");
|
|
|
|
|
appendBonus(bonusLine, item.stamina, "Sta");
|
|
|
|
|
appendBonus(bonusLine, item.intellect, "Int");
|
|
|
|
|
appendBonus(bonusLine, item.spirit, "Spi");
|
|
|
|
|
if (!bonusLine.empty()) {
|
|
|
|
|
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
if (item.sellPrice > 0) {
|
|
|
|
|
uint32_t g = item.sellPrice / 10000;
|
|
|
|
|
uint32_t s = (item.sellPrice / 100) % 100;
|
|
|
|
|
uint32_t c = item.sellPrice % 100;
|
2026-02-19 02:27:01 -08:00
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c);
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 01:50:50 -08:00
|
|
|
// Shift-hover comparison with currently equipped equivalent.
|
|
|
|
|
if (inventory && ImGui::GetIO().KeyShift && item.inventoryType > 0) {
|
|
|
|
|
if (const game::ItemSlot* eq = findComparableEquipped(*inventory, item.inventoryType)) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextDisabled("Equipped:");
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet eqIcon = getItemIcon(eq->item.displayInfoId);
|
2026-02-19 01:50:50 -08:00
|
|
|
if (eqIcon) {
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
}
|
|
|
|
|
ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str());
|
|
|
|
|
|
2026-02-19 02:27:01 -08:00
|
|
|
if (isWeaponInventoryType(eq->item.inventoryType) &&
|
|
|
|
|
eq->item.damageMax > 0.0f && eq->item.delayMs > 0) {
|
|
|
|
|
float speed = static_cast<float>(eq->item.delayMs) / 1000.0f;
|
|
|
|
|
float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed;
|
|
|
|
|
ImGui::Text("%.1f DPS", dps);
|
|
|
|
|
}
|
2026-02-19 06:34:06 -08:00
|
|
|
if (eq->item.armor > 0) {
|
2026-02-19 02:27:01 -08:00
|
|
|
ImGui::Text("%d Armor", eq->item.armor);
|
|
|
|
|
}
|
|
|
|
|
std::string eqBonusLine;
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.strength, "Str");
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.agility, "Agi");
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.stamina, "Sta");
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.intellect, "Int");
|
|
|
|
|
appendBonus(eqBonusLine, eq->item.spirit, "Spi");
|
|
|
|
|
if (!eqBonusLine.empty()) {
|
|
|
|
|
ImGui::TextColored(green, "%s", eqBonusLine.c_str());
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace ui
|
|
|
|
|
} // namespace wowee
|