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-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"
|
|
|
|
|
#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>
|
|
|
|
|
#include <cstdio>
|
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-06 14:24:38 -08:00
|
|
|
InventoryScreen::~InventoryScreen() {
|
|
|
|
|
// Clean up icon textures
|
|
|
|
|
for (auto& [id, tex] : iconCache_) {
|
|
|
|
|
if (tex) glDeleteTextures(1, &tex);
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
|
|
|
|
|
if (displayInfoId == 0 || !assetManager_) return 0;
|
|
|
|
|
|
|
|
|
|
auto it = iconCache_.find(displayInfoId);
|
|
|
|
|
if (it != iconCache_.end()) return it->second;
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc
|
|
|
|
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
if (!displayInfoDbc) {
|
|
|
|
|
iconCache_[displayInfoId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) {
|
|
|
|
|
iconCache_[displayInfoId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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()) {
|
|
|
|
|
iconCache_[displayInfoId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string iconPath = "Interface\\Icons\\" + iconName + ".blp";
|
|
|
|
|
auto blpData = assetManager_->readFile(iconPath);
|
|
|
|
|
if (blpData.empty()) {
|
|
|
|
|
iconCache_[displayInfoId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto image = pipeline::BLPLoader::load(blpData);
|
|
|
|
|
if (!image.isValid()) {
|
|
|
|
|
iconCache_[displayInfoId] = 0;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GLuint texId = 0;
|
|
|
|
|
glGenTextures(1, &texId);
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, texId);
|
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
|
|
|
|
|
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
|
|
|
|
|
|
iconCache_[displayInfoId] = texId;
|
|
|
|
|
return texId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
if (!charPreview_ || !charPreview_->isModelLoaded() || !assetManager_) return;
|
|
|
|
|
|
|
|
|
|
auto* charRenderer = charPreview_->getCharacterRenderer();
|
|
|
|
|
uint32_t instanceId = charPreview_->getInstanceId();
|
|
|
|
|
if (!charRenderer || instanceId == 0) return;
|
|
|
|
|
|
|
|
|
|
// --- Geosets (mirroring GameScreen::updateCharacterGeosets) ---
|
|
|
|
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
|
|
|
|
|
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
|
|
|
|
if (!displayInfoDbc || displayInfoId == 0) return 0;
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) return 0;
|
|
|
|
|
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t)
|
|
|
|
|
return slot.item.displayInfoId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::unordered_set<uint16_t> geosets;
|
|
|
|
|
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
|
|
|
|
|
|
|
|
|
// Hair geoset: group 1 = 100 + hairStyle + 1
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(100 + playerHairStyle_ + 1));
|
|
|
|
|
// Facial hair geoset: group 2 = 200 + facialHair + 1
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(200 + playerFacialHair_ + 1));
|
|
|
|
|
geosets.insert(701); // Ears
|
|
|
|
|
|
|
|
|
|
// Chest/Shirt
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : 501));
|
|
|
|
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
|
|
|
|
if (gg3 > 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legs
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({7});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Feet
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({8});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gloves
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({10});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cloak
|
|
|
|
|
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
|
|
|
|
|
|
|
|
|
// Tabard
|
|
|
|
|
if (hasEquippedType({19})) {
|
|
|
|
|
geosets.insert(1201);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charRenderer->setActiveGeosets(instanceId, geosets);
|
|
|
|
|
|
|
|
|
|
// --- Textures (mirroring GameScreen::updateCharacterTextures) ---
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
const auto& bodySkinPath = app.getBodySkinPath();
|
|
|
|
|
const auto& underwearPaths = app.getUnderwearPaths();
|
|
|
|
|
|
|
|
|
|
if (bodySkinPath.empty() || !displayInfoDbc) return;
|
|
|
|
|
|
|
|
|
|
static const char* componentDirs[] = {
|
|
|
|
|
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
|
|
|
|
"TorsoUpperTexture", "TorsoLowerTexture",
|
|
|
|
|
"LegUpperTexture", "LegLowerTexture", "FootTexture",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::vector<std::pair<int, std::string>> regionLayers;
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
|
|
|
|
if (recIdx < 0) continue;
|
|
|
|
|
|
|
|
|
|
for (int region = 0; region < 8; region++) {
|
|
|
|
|
uint32_t fieldIdx = 15 + region;
|
|
|
|
|
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
|
|
|
|
if (texName.empty()) continue;
|
|
|
|
|
|
|
|
|
|
std::string base = "Item\\TextureComponents\\" +
|
|
|
|
|
std::string(componentDirs[region]) + "\\" + texName;
|
|
|
|
|
std::string genderSuffix = (playerGender_ == game::Gender::FEMALE) ? "_F.blp" : "_M.blp";
|
|
|
|
|
std::string genderPath = base + genderSuffix;
|
|
|
|
|
std::string unisexPath = base + "_U.blp";
|
|
|
|
|
std::string fullPath;
|
|
|
|
|
if (assetManager_->fileExists(genderPath)) {
|
|
|
|
|
fullPath = genderPath;
|
|
|
|
|
} else if (assetManager_->fileExists(unisexPath)) {
|
|
|
|
|
fullPath = unisexPath;
|
|
|
|
|
} else {
|
|
|
|
|
fullPath = base + ".blp";
|
|
|
|
|
}
|
|
|
|
|
regionLayers.emplace_back(region, fullPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the skin texture slot index in the preview model
|
|
|
|
|
// The preview model uses model ID PREVIEW_MODEL_ID; find slot for type-1 (body skin)
|
|
|
|
|
const auto* modelData = charRenderer->getModelData(charPreview_->getModelId());
|
|
|
|
|
uint32_t skinSlot = 0;
|
|
|
|
|
if (modelData) {
|
|
|
|
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
|
|
|
|
if (modelData->textures[ti].type == 1) {
|
|
|
|
|
skinSlot = static_cast<uint32_t>(ti);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
|
|
|
|
if (newTex != 0) {
|
|
|
|
|
charRenderer->setModelTexture(charPreview_->getModelId(), skinSlot, newTex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
|
|
|
|
if (!holdingItem) return;
|
2026-02-06 23:52:16 -08:00
|
|
|
if (gameHandler_ && heldSource == HeldSource::EQUIPMENT) {
|
2026-02-06 18:34:45 -08:00
|
|
|
// Online mode: avoid client-side unequip; wait for server update.
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
|
|
|
|
if (!holdingItem) return;
|
2026-02-06 23:52:16 -08:00
|
|
|
if (gameHandler_) {
|
2026-02-06 18:34:45 -08:00
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
// Online mode: request server auto-equip and keep local state intact.
|
|
|
|
|
gameHandler_->autoEquipItemBySlot(heldBackpackIndex);
|
|
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (heldSource == HeldSource::EQUIPMENT) {
|
|
|
|
|
// Online mode: avoid client-side equipment swaps.
|
|
|
|
|
cancelPickup(inv);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Validate: check if the held item can go in this slot
|
|
|
|
|
if (heldItem.inventoryType > 0) {
|
|
|
|
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
2026-02-06 13:47:03 -08:00
|
|
|
if (validSlot == game::EquipSlot::NUM_SLOTS) return;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
bool valid = (slot == validSlot);
|
|
|
|
|
if (!valid) {
|
2026-02-06 13:47:03 -08:00
|
|
|
if (heldItem.inventoryType == 11)
|
2026-02-02 12:24:50 -08:00
|
|
|
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
2026-02-06 13:47:03 -08:00
|
|
|
else if (heldItem.inventoryType == 12)
|
2026-02-02 12:24:50 -08:00
|
|
|
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
|
|
|
|
}
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
} else {
|
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);
|
|
|
|
|
}
|
|
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
} 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
|
|
|
|
|
GLuint iconTex = getItemIcon(heldItem.displayInfoId);
|
|
|
|
|
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-06 13:47:03 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Bags window (B key) — bottom of screen, no equipment panel
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
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)
|
|
|
|
|
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
|
|
|
|
bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
|
|
|
|
if (bDown && !bKeyWasDown) {
|
|
|
|
|
open = !open;
|
|
|
|
|
}
|
|
|
|
|
bKeyWasDown = bDown;
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// C key toggle for character screen (edge-triggered)
|
|
|
|
|
bool cDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_C);
|
|
|
|
|
if (cDown && !cKeyWasDown) {
|
|
|
|
|
characterOpen = !characterOpen;
|
|
|
|
|
}
|
|
|
|
|
cKeyWasDown = cDown;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
if (!open) {
|
|
|
|
|
if (holdingItem) cancelPickup(inventory);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape cancels held item
|
|
|
|
|
if (holdingItem && !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_ESCAPE)) {
|
|
|
|
|
cancelPickup(inventory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Right-click anywhere while holding = cancel
|
|
|
|
|
if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
cancelPickup(inventory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
// Calculate bag window size
|
|
|
|
|
constexpr float slotSize = 40.0f;
|
|
|
|
|
constexpr int columns = 4;
|
|
|
|
|
int rows = (inventory.getBackpackSize() + columns - 1) / columns;
|
|
|
|
|
float bagContentH = rows * (slotSize + 4.0f) + 40.0f; // slots + header + money
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Check for extra bags and add space
|
|
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
|
|
|
|
int bagSize = inventory.getBagSize(bag);
|
|
|
|
|
if (bagSize <= 0) continue;
|
|
|
|
|
int bagRows = (bagSize + columns - 1) / columns;
|
|
|
|
|
bagContentH += bagRows * (slotSize + 4.0f) + 30.0f; // slots + header
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
float windowW = columns * (slotSize + 4.0f) + 30.0f;
|
|
|
|
|
float windowH = bagContentH + 50.0f; // padding
|
2026-02-06 03:24:46 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Position at bottom-right of screen
|
|
|
|
|
float posX = screenW - windowW - 10.0f;
|
|
|
|
|
float posY = screenH - windowH - 60.0f; // above action bar area
|
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
|
|
|
|
|
|
|
|
renderBackpackPanel(inventory);
|
|
|
|
|
|
2026-02-06 03:24:46 -08:00
|
|
|
// Money display
|
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-02 12:24:50 -08:00
|
|
|
ImGui::End();
|
|
|
|
|
|
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
|
|
|
// Detect held item dropped outside inventory windows → drop confirmation
|
2026-02-06 18:34:45 -08:00
|
|
|
if (holdingItem && heldItem.itemId != 6948 && ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
|
|
|
|
|
!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) &&
|
|
|
|
|
!ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) {
|
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
|
|
|
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))) {
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
heldItem = game::ItemDef{};
|
|
|
|
|
heldSource = HeldSource::NONE;
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
dropItemName_.clear();
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("No", ImVec2(80, 0))) {
|
|
|
|
|
cancelPickup(inventory);
|
|
|
|
|
dropItemName_.clear();
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Draw held item at cursor
|
2026-02-02 12:24:50 -08:00
|
|
|
renderHeldItem();
|
|
|
|
|
}
|
|
|
|
|
|
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-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();
|
|
|
|
|
renderStatsPanel(inventory, gameHandler.getPlayerLevel());
|
|
|
|
|
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(
|
|
|
|
|
(ImTextureID)(uintptr_t)charPreview_->getTextureId(),
|
|
|
|
|
pMin, pMax,
|
|
|
|
|
ImVec2(0, 1), ImVec2(1, 0)); // flip Y for GL
|
|
|
|
|
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
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel) {
|
|
|
|
|
// Sum equipment stats
|
|
|
|
|
int32_t totalArmor = 0;
|
|
|
|
|
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;
|
|
|
|
|
totalArmor += slot.item.armor;
|
|
|
|
|
totalStr += slot.item.strength;
|
|
|
|
|
totalAgi += slot.item.agility;
|
|
|
|
|
totalSta += slot.item.stamina;
|
|
|
|
|
totalInt += slot.item.intellect;
|
|
|
|
|
totalSpi += slot.item.spirit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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-02 12:24:50 -08:00
|
|
|
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
constexpr float slotSize = 40.0f;
|
|
|
|
|
constexpr int columns = 4;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
char bagLabel[32];
|
|
|
|
|
snprintf(bagLabel, sizeof(bagLabel), "Bag %d", bag + 1);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "%s", bagLabel);
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
|
|
|
|
float size, const char* label,
|
|
|
|
|
SlotKind kind, int backpackIndex,
|
|
|
|
|
game::EquipSlot equipSlot) {
|
|
|
|
|
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) {
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
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) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
|
|
|
|
} 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
|
|
|
|
|
GLuint iconTex = getItemIcon(item.displayInfoId);
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
// Left-click: pickup or place/swap
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
if (!holdingItem) {
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
pickupFromBackpack(inventory, backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
2026-02-12 14:55:27 -08:00
|
|
|
if (gameHandler_) {
|
|
|
|
|
// Online mode: don't mutate local equipment state.
|
|
|
|
|
game::MessageChatData msg{};
|
|
|
|
|
msg.type = game::ChatType::SYSTEM;
|
|
|
|
|
msg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
msg.message = "Moving equipped items not supported yet (online mode).";
|
|
|
|
|
gameHandler_->addLocalChatMessage(msg);
|
|
|
|
|
} else {
|
|
|
|
|
pickupFromEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
|
|
|
|
placeInEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Right-click: vendor sell (if vendor mode) or auto-equip/unequip
|
2026-02-02 12:24:50 -08:00
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
2026-02-06 19:50:22 -08:00
|
|
|
LOG_DEBUG("Right-click slot: kind=", (int)kind,
|
|
|
|
|
" backpackIndex=", backpackIndex,
|
|
|
|
|
" vendorMode=", vendorMode_,
|
|
|
|
|
" hasHandler=", (gameHandler_ != nullptr));
|
2026-02-06 13:47:03 -08:00
|
|
|
if (vendorMode_ && gameHandler_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
// Sell to vendor
|
|
|
|
|
gameHandler_->sellItemBySlot(backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
2026-02-12 14:55:27 -08:00
|
|
|
if (gameHandler_) {
|
|
|
|
|
// Online mode: request server-side unequip (move to first free backpack slot).
|
|
|
|
|
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
|
|
|
|
gameHandler_->unequipToBackpack(equipSlot);
|
|
|
|
|
} else {
|
|
|
|
|
// Offline mode: Unequip: move to free backpack slot
|
|
|
|
|
int freeSlot = inventory.findFreeBackpackSlot();
|
|
|
|
|
if (freeSlot >= 0) {
|
|
|
|
|
inventory.setBackpackSlot(freeSlot, item);
|
|
|
|
|
inventory.clearEquipSlot(equipSlot);
|
|
|
|
|
equipmentDirty = true;
|
|
|
|
|
inventoryDirty = true;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
2026-02-06 23:52:16 -08:00
|
|
|
if (gameHandler_) {
|
2026-02-06 19:13:38 -08:00
|
|
|
if (item.inventoryType > 0) {
|
2026-02-06 18:34:45 -08:00
|
|
|
// Auto-equip (online)
|
|
|
|
|
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
2026-02-06 18:34:45 -08:00
|
|
|
// Use consumable (online)
|
|
|
|
|
gameHandler_->useItemBySlot(backpackIndex);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !holdingItem) {
|
|
|
|
|
renderItemTooltip(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
|
|
|
|
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)) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
|
|
|
|
"Home: map %u (%.1f, %.1f, %.1f)", mapId, pos.x, pos.y, pos.z);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Home: not set");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Armor
|
|
|
|
|
if (item.armor > 0) {
|
|
|
|
|
ImGui::Text("%d Armor", item.armor);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:24:38 -08:00
|
|
|
// Stats with "Equip:" prefix style
|
|
|
|
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
|
|
|
|
ImVec4 red(1.0f, 0.2f, 0.2f, 1.0f);
|
|
|
|
|
|
|
|
|
|
auto renderStat = [&](int32_t val, const char* name) {
|
|
|
|
|
if (val > 0) {
|
|
|
|
|
ImGui::TextColored(green, "+%d %s", val, name);
|
|
|
|
|
} else if (val < 0) {
|
|
|
|
|
ImGui::TextColored(red, "%d %s", val, name);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
renderStat(item.stamina, "Stamina");
|
|
|
|
|
renderStat(item.strength, "Strength");
|
|
|
|
|
renderStat(item.agility, "Agility");
|
|
|
|
|
renderStat(item.intellect, "Intellect");
|
|
|
|
|
renderStat(item.spirit, "Spirit");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Stack info
|
|
|
|
|
if (item.maxStack > 1) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Sell price
|
|
|
|
|
if (item.sellPrice > 0) {
|
|
|
|
|
uint32_t g = item.sellPrice / 10000;
|
|
|
|
|
uint32_t s = (item.sellPrice / 100) % 100;
|
|
|
|
|
uint32_t c = item.sellPrice % 100;
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell Price: %ug %us %uc", g, s, c);
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace ui
|
|
|
|
|
} // namespace wowee
|