mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +00:00
Restructure inventory UI, add vendor selling, camera intro on all spawns, and quest log
Split inventory into bags-only (B key) and character screen (C key). Vendor window auto-opens bags with sell prices on hover and right-click to sell. Add camera intro pan on all login/spawn/teleport/hearthstone events and idle orbit after 2 minutes. Add quest log UI, SMSG_MONSTER_MOVE handling, deferred creature spawn queue, and creature fade-in/movement interpolation for online mode.
This commit is contained in:
parent
bb4c2c25f7
commit
71c3d2ea77
21 changed files with 1092 additions and 149 deletions
|
|
@ -95,12 +95,26 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
// Teleporter panel (T key toggle handled in Application event loop)
|
||||
renderTeleporterPanel();
|
||||
|
||||
// Quest Log (L key toggle handled inside)
|
||||
questLogScreen.render(gameHandler);
|
||||
|
||||
// Spellbook (P key toggle handled inside)
|
||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
||||
|
||||
// Inventory (B key toggle handled inside)
|
||||
// Set vendor mode before rendering inventory
|
||||
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
||||
|
||||
// Auto-open bags when vendor window opens
|
||||
if (gameHandler.isVendorWindowOpen() && !inventoryScreen.isOpen()) {
|
||||
inventoryScreen.setOpen(true);
|
||||
}
|
||||
|
||||
// Bags (B key toggle handled inside)
|
||||
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||
|
||||
// Character screen (C key toggle handled inside render())
|
||||
inventoryScreen.renderCharacterScreen(gameHandler.getInventory());
|
||||
|
||||
if (inventoryScreen.consumeInventoryDirty()) {
|
||||
gameHandler.notifyInventoryChanged();
|
||||
}
|
||||
|
|
@ -112,7 +126,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
gameHandler.notifyEquipmentChanged();
|
||||
}
|
||||
|
||||
// Update renderer face-target position
|
||||
// Update renderer face-target position and selection circle
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
static glm::vec3 targetGLPos;
|
||||
|
|
@ -121,11 +135,30 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
if (target) {
|
||||
targetGLPos = core::coords::canonicalToRender(glm::vec3(target->getX(), target->getY(), target->getZ()));
|
||||
renderer->setTargetPosition(&targetGLPos);
|
||||
|
||||
// Selection circle color: red=hostile, green=friendly, gray=dead
|
||||
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
||||
float circleRadius = 1.5f;
|
||||
if (target->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
||||
} else if (unit->isInteractable()) {
|
||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
||||
} else {
|
||||
circleColor = glm::vec3(1.0f, 0.2f, 0.2f); // red (hostile)
|
||||
}
|
||||
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
||||
}
|
||||
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
||||
} else {
|
||||
renderer->setTargetPosition(nullptr);
|
||||
renderer->clearSelectionCircle();
|
||||
}
|
||||
} else {
|
||||
renderer->setTargetPosition(nullptr);
|
||||
renderer->clearSelectionCircle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -422,16 +455,29 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
float closestT = 1e30f;
|
||||
uint64_t closestGuid = 0;
|
||||
|
||||
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
|
||||
if (guid == myGuid) continue; // Don't target self
|
||||
|
||||
// Scale hitbox based on entity type
|
||||
float hitRadius = 1.5f;
|
||||
float heightOffset = 1.5f;
|
||||
if (t == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||
// Critters have very low max health (< 100)
|
||||
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
||||
hitRadius = 0.5f;
|
||||
heightOffset = 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 entityGL = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
// Add half-height offset so we target the body center, not feet
|
||||
entityGL.z += 3.0f;
|
||||
entityGL.z += heightOffset;
|
||||
|
||||
float hitT;
|
||||
if (raySphereIntersect(ray, entityGL, 3.0f, hitT)) {
|
||||
if (raySphereIntersect(ray, entityGL, hitRadius, hitT)) {
|
||||
if (hitT < closestT) {
|
||||
closestT = hitT;
|
||||
closestGuid = guid;
|
||||
|
|
@ -505,11 +551,11 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
|||
|
||||
const auto& characters = gameHandler.getCharacters();
|
||||
if (!characters.empty()) {
|
||||
// Use the first (or most recently selected) character
|
||||
const auto& ch = characters[0];
|
||||
playerName = ch.name;
|
||||
playerLevel = ch.level;
|
||||
// Characters don't store HP; use level-scaled estimate
|
||||
// Use live server level if available, otherwise character struct
|
||||
playerLevel = gameHandler.getPlayerLevel();
|
||||
if (playerLevel == 0) playerLevel = ch.level;
|
||||
playerMaxHp = 20 + playerLevel * 10;
|
||||
playerHp = playerMaxHp;
|
||||
}
|
||||
|
|
@ -589,26 +635,30 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
// Determine hostility color for border and name
|
||||
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
if (target->getType() == game::ObjectType::PLAYER) {
|
||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else if (target->getType() == game::ObjectType::UNIT) {
|
||||
auto u = std::static_pointer_cast<game::Unit>(target);
|
||||
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
} else if (u->isInteractable()) {
|
||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else {
|
||||
hostileColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f));
|
||||
|
||||
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
||||
// Entity name and type
|
||||
std::string name = getEntityName(target);
|
||||
|
||||
ImVec4 nameColor;
|
||||
switch (target->getType()) {
|
||||
case game::ObjectType::PLAYER:
|
||||
nameColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
|
||||
break;
|
||||
case game::ObjectType::UNIT:
|
||||
nameColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
|
||||
break;
|
||||
default:
|
||||
nameColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
break;
|
||||
}
|
||||
ImVec4 nameColor = hostileColor;
|
||||
|
||||
ImGui::TextColored(nameColor, "%s", name.c_str());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "ui/inventory_screen.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
|
@ -32,13 +33,11 @@ game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game
|
|||
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;
|
||||
|
|
@ -99,7 +98,6 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
|||
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;
|
||||
}
|
||||
|
|
@ -112,19 +110,18 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
|||
// 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
|
||||
if (validSlot == game::EquipSlot::NUM_SLOTS) return;
|
||||
|
||||
// For rings/trinkets, allow either slot
|
||||
bool valid = (slot == validSlot);
|
||||
if (!valid) {
|
||||
if (heldItem.inventoryType == 11) // Ring
|
||||
if (heldItem.inventoryType == 11)
|
||||
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
||||
else if (heldItem.inventoryType == 12) // Trinket
|
||||
else if (heldItem.inventoryType == 12)
|
||||
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
||||
}
|
||||
if (!valid) return;
|
||||
} else {
|
||||
return; // No inventoryType means not equippable
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& target = inv.getEquipSlot(slot);
|
||||
|
|
@ -132,7 +129,6 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
|||
inv.setEquipSlot(slot, heldItem);
|
||||
holdingItem = false;
|
||||
} else {
|
||||
// Swap
|
||||
game::ItemDef targetItem = target.item;
|
||||
inv.setEquipSlot(slot, heldItem);
|
||||
heldItem = targetItem;
|
||||
|
|
@ -163,13 +159,10 @@ void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slo
|
|||
|
||||
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) {
|
||||
|
|
@ -180,7 +173,6 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
|||
inv.addItem(heldItem);
|
||||
}
|
||||
} else {
|
||||
// Fallback: just add to inventory
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
holdingItem = false;
|
||||
|
|
@ -199,13 +191,11 @@ void InventoryScreen::renderHeldItem() {
|
|||
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];
|
||||
|
|
@ -215,7 +205,6 @@ void InventoryScreen::renderHeldItem() {
|
|||
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);
|
||||
|
|
@ -225,6 +214,10 @@ void InventoryScreen::renderHeldItem() {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Bags window (B key) — bottom of screen, no equipment panel
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||
// B key toggle (edge-triggered)
|
||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||
|
|
@ -234,8 +227,14 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
}
|
||||
bKeyWasDown = bDown;
|
||||
|
||||
// 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;
|
||||
|
||||
if (!open) {
|
||||
// Cancel held item if inventory closes
|
||||
if (holdingItem) cancelPickup(inventory);
|
||||
return;
|
||||
}
|
||||
|
|
@ -252,33 +251,42 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float screenW = io.DisplaySize.x;
|
||||
float screenH = io.DisplaySize.y;
|
||||
|
||||
// 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);
|
||||
// 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
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Inventory", &open, flags)) {
|
||||
// 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
|
||||
}
|
||||
|
||||
float windowW = columns * (slotSize + 4.0f) + 30.0f;
|
||||
float windowH = bagContentH + 50.0f; // padding
|
||||
|
||||
// Position at bottom-right of screen
|
||||
float posX = screenW - windowW - 10.0f;
|
||||
float posY = screenH - windowH - 60.0f; // above action bar area
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Reserve space for money display at bottom
|
||||
float moneyHeight = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||
float panelHeight = ImGui::GetContentRegionAvail().y - moneyHeight;
|
||||
|
||||
// Two-column layout: Equipment (left) | Backpack (right)
|
||||
ImGui::BeginChild("EquipPanel", ImVec2(200.0f, panelHeight), true);
|
||||
renderEquipmentPanel(inventory);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, panelHeight), true);
|
||||
renderBackpackPanel(inventory);
|
||||
ImGui::EndChild();
|
||||
|
||||
// Money display
|
||||
ImGui::Spacing();
|
||||
uint64_t gold = moneyCopper / 10000;
|
||||
uint64_t silver = (moneyCopper / 100) % 100;
|
||||
uint64_t copper = moneyCopper % 100;
|
||||
|
|
@ -288,10 +296,37 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
static_cast<unsigned long long>(copper));
|
||||
ImGui::End();
|
||||
|
||||
// Draw held item at cursor (on top of everything)
|
||||
// Draw held item at cursor
|
||||
renderHeldItem();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Character screen (C key) — standalone equipment window
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::renderCharacterScreen(game::Inventory& inventory) {
|
||||
if (!characterOpen) return;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(220.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
renderEquipmentPanel(inventory);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
|
||||
ImGui::Separator();
|
||||
|
|
@ -312,10 +347,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
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]);
|
||||
|
|
@ -329,7 +362,6 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
|
||||
ImGui::SameLine(slotSize + spacing + 60.0f);
|
||||
|
||||
// Right slot
|
||||
{
|
||||
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
||||
const char* label = game::getEquipSlotName(rightSlots[r]);
|
||||
|
|
@ -420,7 +452,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
bool validDrop = false;
|
||||
if (holdingItem) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
validDrop = true; // Can always drop in backpack
|
||||
validDrop = true;
|
||||
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
||||
validDrop = (equipSlot == validSlot);
|
||||
|
|
@ -432,11 +464,9 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -445,7 +475,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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];
|
||||
|
|
@ -457,7 +486,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
|
||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||
|
||||
// Click interactions
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
|
|
@ -466,7 +494,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
}
|
||||
|
||||
// Tooltip for empty equip slots
|
||||
if (label && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
|
||||
|
|
@ -478,7 +505,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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);
|
||||
|
|
@ -489,7 +515,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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];
|
||||
|
|
@ -499,7 +524,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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);
|
||||
|
|
@ -513,14 +537,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
// 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) {
|
||||
|
|
@ -529,9 +551,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
}
|
||||
}
|
||||
|
||||
// Right-click: auto-equip from backpack, or unequip from equipment
|
||||
// Right-click: vendor sell (if vendor mode) or auto-equip/unequip
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
||||
if (kind == SlotKind::EQUIPMENT) {
|
||||
if (vendorMode_ && gameHandler_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
// Sell to vendor
|
||||
gameHandler_->sellItemBySlot(backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
// Unequip: move to free backpack slot
|
||||
int freeSlot = inventory.findFreeBackpackSlot();
|
||||
if (freeSlot >= 0) {
|
||||
|
|
@ -541,8 +566,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
inventoryDirty = true;
|
||||
}
|
||||
} 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)
|
||||
// Auto-equip
|
||||
uint8_t equippingType = item.inventoryType;
|
||||
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
||||
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
||||
|
|
@ -551,12 +575,10 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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()) {
|
||||
|
|
@ -564,7 +586,6 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
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);
|
||||
|
|
@ -645,6 +666,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
|||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
||||
}
|
||||
|
||||
// Sell price (when vendor is open)
|
||||
if (vendorMode_ && gameHandler_) {
|
||||
const auto* info = gameHandler_->getItemInfo(item.itemId);
|
||||
if (info && info->sellPrice > 0) {
|
||||
uint32_t g = info->sellPrice / 10000;
|
||||
uint32_t s = (info->sellPrice / 100) % 100;
|
||||
uint32_t c = info->sellPrice % 100;
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell Price: %ug %us %uc", g, s, c);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
|
|
|
|||
90
src/ui/quest_log_screen.cpp
Normal file
90
src/ui/quest_log_screen.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#include "ui/quest_log_screen.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||
// L key toggle (edge-triggered)
|
||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||
bool lDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_L);
|
||||
if (lDown && !lKeyWasDown) {
|
||||
open = !open;
|
||||
}
|
||||
lKeyWasDown = lDown;
|
||||
|
||||
if (!open) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
float logW = 380.0f;
|
||||
float logH = std::min(450.0f, screenH - 120.0f);
|
||||
float logX = (screenW - logW) * 0.5f;
|
||||
float logY = 80.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(logX, logY), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(logW, logH), ImGuiCond_FirstUseEver);
|
||||
|
||||
bool stillOpen = true;
|
||||
if (ImGui::Begin("Quest Log", &stillOpen)) {
|
||||
const auto& quests = gameHandler.getQuestLog();
|
||||
|
||||
if (quests.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active quests.");
|
||||
} else {
|
||||
// Left panel: quest list
|
||||
ImGui::BeginChild("QuestList", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - 4), true);
|
||||
for (size_t i = 0; i < quests.size(); i++) {
|
||||
const auto& q = quests[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
ImVec4 color = q.complete
|
||||
? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) // Green for complete
|
||||
: ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // Gold for active
|
||||
|
||||
bool selected = (selectedIndex == static_cast<int>(i));
|
||||
if (ImGui::Selectable("##quest", selected, 0, ImVec2(0, 20))) {
|
||||
selectedIndex = static_cast<int>(i);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(color, "%s%s",
|
||||
q.title.c_str(),
|
||||
q.complete ? " (Complete)" : "");
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// Details panel for selected quest
|
||||
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(quests.size())) {
|
||||
const auto& sel = quests[static_cast<size_t>(selectedIndex)];
|
||||
|
||||
if (!sel.objectives.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", sel.objectives.c_str());
|
||||
}
|
||||
|
||||
// Abandon button
|
||||
if (!sel.complete) {
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Abandon Quest")) {
|
||||
gameHandler.abandonQuest(sel.questId);
|
||||
if (selectedIndex >= static_cast<int>(quests.size())) {
|
||||
selectedIndex = static_cast<int>(quests.size()) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (!stillOpen) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
Loading…
Add table
Add a link
Reference in a new issue