2026-02-02 12:24:50 -08:00
|
|
|
#include "ui/inventory_screen.hpp"
|
|
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace ui {
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: {
|
|
|
|
|
// Ring: prefer empty slot, else RING1
|
|
|
|
|
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
|
|
|
|
|
return game::EquipSlot::RING1;
|
|
|
|
|
return game::EquipSlot::RING2;
|
|
|
|
|
}
|
|
|
|
|
case 12: {
|
|
|
|
|
// Trinket: prefer empty slot, else TRINKET1
|
|
|
|
|
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;
|
|
|
|
|
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;
|
|
|
|
|
// Keep holding the swapped item - update source to this backpack slot
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Validate: check if the held item can go in this slot
|
|
|
|
|
if (heldItem.inventoryType > 0) {
|
|
|
|
|
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
|
|
|
|
if (validSlot == game::EquipSlot::NUM_SLOTS) return; // Not equippable
|
|
|
|
|
|
|
|
|
|
// For rings/trinkets, allow either slot
|
|
|
|
|
bool valid = (slot == validSlot);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
if (heldItem.inventoryType == 11) // Ring
|
|
|
|
|
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
|
|
|
|
else if (heldItem.inventoryType == 12) // Trinket
|
|
|
|
|
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
|
|
|
|
}
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
} else {
|
|
|
|
|
return; // No inventoryType means not equippable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto& target = inv.getEquipSlot(slot);
|
|
|
|
|
if (target.empty()) {
|
|
|
|
|
inv.setEquipSlot(slot, heldItem);
|
|
|
|
|
holdingItem = false;
|
|
|
|
|
} else {
|
|
|
|
|
// Swap
|
|
|
|
|
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;
|
|
|
|
|
// Return item to source
|
|
|
|
|
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
|
|
|
|
// If source slot is still empty, put it back
|
|
|
|
|
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
|
|
|
|
|
inv.setBackpackSlot(heldBackpackIndex, heldItem);
|
|
|
|
|
} else {
|
|
|
|
|
// Source was swapped into; find free slot
|
|
|
|
|
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 {
|
|
|
|
|
// Fallback: just add to inventory
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Background
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Item abbreviation
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Stack count
|
|
|
|
|
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-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;
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
// Cancel held item if inventory closes
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Position inventory window on the right side of the screen
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - 520.0f, 80.0f), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500.0f, 560.0f), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
if (!ImGui::Begin("Inventory", &open, flags)) {
|
|
|
|
|
ImGui::End();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:24:46 -08:00
|
|
|
// Reserve space for money display at bottom
|
|
|
|
|
float moneyHeight = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
|
|
|
|
float panelHeight = ImGui::GetContentRegionAvail().y - moneyHeight;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Two-column layout: Equipment (left) | Backpack (right)
|
2026-02-06 03:24:46 -08:00
|
|
|
ImGui::BeginChild("EquipPanel", ImVec2(200.0f, panelHeight), true);
|
2026-02-02 12:24:50 -08:00
|
|
|
renderEquipmentPanel(inventory);
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
2026-02-06 03:24:46 -08:00
|
|
|
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, panelHeight), true);
|
2026-02-02 12:24:50 -08:00
|
|
|
renderBackpackPanel(inventory);
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
2026-02-06 03:24:46 -08:00
|
|
|
// Money display
|
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();
|
|
|
|
|
|
|
|
|
|
// Draw held item at cursor (on top of everything)
|
|
|
|
|
renderHeldItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
constexpr float spacing = 4.0f;
|
|
|
|
|
|
|
|
|
|
// Two columns of equipment
|
|
|
|
|
int rows = 8;
|
|
|
|
|
for (int r = 0; r < rows; r++) {
|
|
|
|
|
// Left slot
|
|
|
|
|
{
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(slotSize + spacing + 60.0f);
|
|
|
|
|
|
|
|
|
|
// Right slot
|
|
|
|
|
{
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
validDrop = true; // Can always drop in backpack
|
|
|
|
|
} 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) {
|
|
|
|
|
// Empty slot: dark grey background
|
|
|
|
|
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
|
|
|
|
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
|
|
|
|
|
|
|
|
|
// Highlight valid drop targets
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Slot label for equipment slots
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
// Click interactions
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
|
|
|
|
placeInEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tooltip for empty equip slots
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Highlight valid drop targets with green tint
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Item abbreviation (first 2 letters)
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Stack count (bottom-right)
|
|
|
|
|
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) {
|
|
|
|
|
// Pick up this item
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
pickupFromBackpack(inventory, backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT) {
|
|
|
|
|
pickupFromEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Holding an item - place or swap
|
|
|
|
|
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
|
|
|
|
placeInBackpack(inventory, backpackIndex);
|
|
|
|
|
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
|
|
|
|
placeInEquipment(inventory, equipSlot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Right-click: auto-equip from backpack, or unequip from equipment
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
|
|
|
|
if (kind == SlotKind::EQUIPMENT) {
|
|
|
|
|
// Unequip: move to free backpack slot
|
|
|
|
|
int freeSlot = inventory.findFreeBackpackSlot();
|
|
|
|
|
if (freeSlot >= 0) {
|
|
|
|
|
inventory.setBackpackSlot(freeSlot, item);
|
|
|
|
|
inventory.clearEquipSlot(equipSlot);
|
|
|
|
|
equipmentDirty = true;
|
2026-02-05 14:55:42 -08:00
|
|
|
inventoryDirty = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
|
|
|
|
|
// Auto-equip: find the right slot
|
|
|
|
|
// Capture type before swap (item ref may become stale)
|
|
|
|
|
uint8_t equippingType = item.inventoryType;
|
|
|
|
|
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
|
|
|
|
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
|
|
|
|
const auto& eqSlot = inventory.getEquipSlot(targetSlot);
|
|
|
|
|
if (eqSlot.empty()) {
|
|
|
|
|
inventory.setEquipSlot(targetSlot, item);
|
|
|
|
|
inventory.clearBackpackSlot(backpackIndex);
|
|
|
|
|
} else {
|
|
|
|
|
// Swap with equipped item
|
|
|
|
|
game::ItemDef equippedItem = eqSlot.item;
|
|
|
|
|
inventory.setEquipSlot(targetSlot, item);
|
|
|
|
|
inventory.setBackpackSlot(backpackIndex, equippedItem);
|
|
|
|
|
}
|
|
|
|
|
// Two-handed weapon in main hand clears the off-hand
|
|
|
|
|
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
|
|
|
|
|
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
|
|
|
|
|
if (!offHand.empty()) {
|
|
|
|
|
inventory.addItem(offHand.item);
|
|
|
|
|
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Equipping off-hand unequips a 2H weapon from main hand
|
|
|
|
|
if (targetSlot == game::EquipSlot::OFF_HAND &&
|
|
|
|
|
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
|
|
|
|
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
|
|
|
|
inventory.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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stats
|
|
|
|
|
if (item.stamina != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Stamina", item.stamina);
|
|
|
|
|
if (item.strength != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Strength", item.strength);
|
|
|
|
|
if (item.agility != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Agility", item.agility);
|
|
|
|
|
if (item.intellect != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Intellect", item.intellect);
|
|
|
|
|
if (item.spirit != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Spirit", item.spirit);
|
|
|
|
|
|
|
|
|
|
// Stack info
|
|
|
|
|
if (item.maxStack > 1) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace ui
|
|
|
|
|
} // namespace wowee
|