diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index ac338315..1881cde6 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -360,6 +360,8 @@ public: auto it = itemInfoCache_.find(itemId); return (it != itemInfoCache_.end()) ? &it->second : nullptr; } + std::string getItemTemplateName(uint32_t itemId) const; + ItemQuality getItemTemplateQuality(uint32_t itemId) const; uint64_t getBackpackItemGuid(int index) const { if (index < 0 || index >= static_cast(backpackSlotGuids_.size())) return 0; return backpackSlotGuids_[index]; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 5970a108..a37aa581 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -126,6 +126,11 @@ private: game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv); void renderHeldItem(); + // Drop confirmation + bool dropConfirmOpen_ = false; + int dropBackpackIndex_ = -1; + std::string dropItemName_; + public: static ImVec4 getQualityColor(game::ItemQuality quality); }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 59ed8838..13858eec 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -642,10 +642,8 @@ void Application::setupUICallbacks() { for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, 0); uint32_t enemyGroup = dbc->getUInt32(i, 5); - uint32_t friendGroup = dbc->getUInt32(i, 4); bool hostile = (enemyGroup & playerFriendGroup) != 0; - bool friendly = (friendGroup & playerFriendGroup) != 0; - factionMap[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0); + factionMap[id] = hostile; } gameHandler->setFactionHostileMap(std::move(factionMap)); LOG_INFO("Loaded faction hostility data (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")"); @@ -938,6 +936,19 @@ void Application::spawnPlayerCharacter() { } } } + // Override hair texture on GPU (type-6 slot) after model load + if (!hairTexturePath.empty()) { + GLuint hairTex = charRenderer->loadTexture(hairTexturePath); + if (hairTex != 0) { + for (size_t ti = 0; ti < model.textures.size(); ti++) { + if (model.textures[ti].type == 6) { + charRenderer->setModelTexture(1, static_cast(ti), hairTex); + LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", hairTexturePath); + break; + } + } + } + } } else { bodySkinPath_.clear(); underwearPaths_.clear(); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 80d3691e..46a0b19e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -46,6 +46,12 @@ struct ItemTemplateRow { uint8_t inventoryType = 0; int32_t maxStack = 1; uint32_t sellPrice = 0; + int32_t armor = 0; + int32_t stamina = 0; + int32_t strength = 0; + int32_t agility = 0; + int32_t intellect = 0; + int32_t spirit = 0; }; struct SinglePlayerLootDb { @@ -163,6 +169,7 @@ struct SinglePlayerSqlite { " spirit INTEGER," " display_info_id INTEGER," " subclass_name TEXT," + " sell_price INTEGER DEFAULT 0," " PRIMARY KEY (guid, location, slot)" ");" "CREATE TABLE IF NOT EXISTS character_spell (" @@ -208,7 +215,10 @@ struct SinglePlayerSqlite { " mouse_sensitivity REAL," " invert_mouse INTEGER" ");"; - return exec(kSchema); + if (!exec(kSchema)) return false; + // Migration: add sell_price column to existing saves + exec("ALTER TABLE character_inventory ADD COLUMN sell_price INTEGER DEFAULT 0;"); + return true; } }; @@ -502,6 +512,13 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() { int idxInvType = columnIndex(cols, "InventoryType"); int idxStack = columnIndex(cols, "stackable"); int idxSellPrice = columnIndex(cols, "SellPrice"); + int idxArmor = columnIndex(cols, "armor"); + // stat_type/stat_value pairs (up to 10) + int idxStatType[10], idxStatVal[10]; + for (int si = 0; si < 10; si++) { + idxStatType[si] = columnIndex(cols, "stat_type" + std::to_string(si + 1)); + idxStatVal[si] = columnIndex(cols, "stat_value" + std::to_string(si + 1)); + } if (idxEntry >= 0 && std::filesystem::exists(itemTemplatePath)) { std::ifstream in(itemTemplatePath); processInsertStatements(in, [&](const std::vector& row) { @@ -528,6 +545,27 @@ static SinglePlayerLootDb& getSinglePlayerLootDb() { if (idxSellPrice >= 0 && idxSellPrice < static_cast(row.size())) { ir.sellPrice = static_cast(std::stoul(row[idxSellPrice])); } + if (idxArmor >= 0 && idxArmor < static_cast(row.size())) { + ir.armor = static_cast(std::stol(row[idxArmor])); + } + // Parse stat_type/stat_value pairs (protected from parse errors) + for (int si = 0; si < 10; si++) { + try { + if (idxStatType[si] < 0 || idxStatVal[si] < 0) continue; + if (idxStatType[si] >= static_cast(row.size())) continue; + if (idxStatVal[si] >= static_cast(row.size())) continue; + int stype = std::stoi(row[idxStatType[si]]); + int sval = std::stoi(row[idxStatVal[si]]); + if (sval == 0) continue; + switch (stype) { + case 3: ir.agility += sval; break; + case 4: ir.strength += sval; break; + case 5: ir.intellect += sval; break; + case 6: ir.spirit += sval; break; + case 7: ir.stamina += sval; break; + } + } catch (...) {} + } db.itemTemplates[ir.itemId] = std::move(ir); } catch (const std::exception&) { } @@ -1671,7 +1709,8 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { inventory = Inventory(); const char* sqlInv = "SELECT location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, " - "armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name " + "armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name, " + "COALESCE(sell_price, 0) " "FROM character_inventory WHERE guid=?;"; if (sqlite3_prepare_v2(sp.db, sqlInv, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_int64(stmt, 1, static_cast(guid)); @@ -1696,6 +1735,23 @@ bool GameHandler::loadSinglePlayerCharacterState(uint64_t guid) { def.displayInfoId = static_cast(sqlite3_column_int(stmt, 15)); const unsigned char* subclassText = sqlite3_column_text(stmt, 16); def.subclassName = subclassText ? reinterpret_cast(subclassText) : ""; + def.sellPrice = static_cast(sqlite3_column_int(stmt, 17)); + + // Fill missing data from item template DB (for old saves) + if (def.itemId != 0) { + auto& itemDb = getSinglePlayerLootDb().itemTemplates; + auto itTpl = itemDb.find(def.itemId); + if (itTpl != itemDb.end()) { + if (def.sellPrice == 0) def.sellPrice = itTpl->second.sellPrice; + if (def.displayInfoId == 0) def.displayInfoId = itTpl->second.displayId; + if (def.armor == 0) def.armor = itTpl->second.armor; + if (def.stamina == 0) def.stamina = itTpl->second.stamina; + if (def.strength == 0) def.strength = itTpl->second.strength; + if (def.agility == 0) def.agility = itTpl->second.agility; + if (def.intellect == 0) def.intellect = itTpl->second.intellect; + if (def.spirit == 0) def.spirit = itTpl->second.spirit; + } + } if (location == 0) { inventory.setBackpackSlot(slot, def); @@ -1864,6 +1920,13 @@ void GameHandler::applySinglePlayerStartData(Race race, Class cls) { def.inventoryType = itTpl->second.inventoryType; def.maxStack = std::max(def.maxStack, static_cast(itTpl->second.maxStack)); def.sellPrice = itTpl->second.sellPrice; + def.displayInfoId = itTpl->second.displayId; + def.armor = itTpl->second.armor; + def.stamina = itTpl->second.stamina; + def.strength = itTpl->second.strength; + def.agility = itTpl->second.agility; + def.intellect = itTpl->second.intellect; + def.spirit = itTpl->second.spirit; } else { def.name = "Item " + std::to_string(row.itemId); } @@ -2026,8 +2089,8 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { const char* insInv = "INSERT INTO character_inventory " "(guid, location, slot, item_id, name, quality, inventory_type, stack_count, max_stack, bag_slots, " - "armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name) " - "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; + "armor, stamina, strength, agility, intellect, spirit, display_info_id, subclass_name, sell_price) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; if (sqlite3_prepare_v2(sp.db, insInv, -1, &stmt, nullptr) == SQLITE_OK) { for (int i = 0; i < Inventory::BACKPACK_SLOTS; i++) { const ItemSlot& slot = inventory.getBackpackSlot(i); @@ -2050,6 +2113,7 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { sqlite3_bind_int(stmt, 16, static_cast(slot.item.spirit)); sqlite3_bind_int(stmt, 17, static_cast(slot.item.displayInfoId)); sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 19, static_cast(slot.item.sellPrice)); sqlite3_step(stmt); sqlite3_reset(stmt); } @@ -2075,6 +2139,7 @@ void GameHandler::saveSinglePlayerCharacterState(bool force) { sqlite3_bind_int(stmt, 16, static_cast(slot.item.spirit)); sqlite3_bind_int(stmt, 17, static_cast(slot.item.displayInfoId)); sqlite3_bind_text(stmt, 18, slot.item.subclassName.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 19, static_cast(slot.item.sellPrice)); sqlite3_step(stmt); sqlite3_reset(stmt); } @@ -3636,6 +3701,13 @@ void GameHandler::lootItem(uint8_t slotIndex) { def.inventoryType = itTpl->second.inventoryType; def.maxStack = std::max(def.maxStack, static_cast(itTpl->second.maxStack)); def.sellPrice = itTpl->second.sellPrice; + def.displayInfoId = itTpl->second.displayId; + def.armor = itTpl->second.armor; + def.stamina = itTpl->second.stamina; + def.strength = itTpl->second.strength; + def.agility = itTpl->second.agility; + def.intellect = itTpl->second.intellect; + def.spirit = itTpl->second.spirit; } else { def.name = "Item " + std::to_string(it->itemId); } @@ -3819,11 +3891,15 @@ void GameHandler::handleLootResponse(network::Packet& packet) { if (!LootResponseParser::parse(packet, currentLoot)) return; lootWindowOpen = true; if (currentLoot.gold > 0) { + if (singlePlayerMode_) { + addMoneyCopper(currentLoot.gold); + } std::string msg = "You loot "; msg += std::to_string(currentLoot.getGold()) + "g "; msg += std::to_string(currentLoot.getSilver()) + "s "; msg += std::to_string(currentLoot.getCopper()) + "c."; addSystemChatMessage(msg); + currentLoot.gold = 0; // Clear gold from loot window after collecting } } @@ -4025,6 +4101,11 @@ void GameHandler::performNpcSwing(uint64_t guid) { if (!entity || entity->getType() != ObjectType::UNIT) return; auto unit = std::static_pointer_cast(entity); + // Auto-target the attacker if player has no current target + if (targetGuid == 0) { + setTarget(guid); + } + if (npcSwingCallback_) { npcSwingCallback_(guid); } @@ -4374,5 +4455,19 @@ void GameHandler::fail(const std::string& reason) { } } +std::string GameHandler::getItemTemplateName(uint32_t itemId) const { + auto& db = getSinglePlayerLootDb(); + auto it = db.itemTemplates.find(itemId); + if (it != db.itemTemplates.end()) return it->second.name; + return {}; +} + +ItemQuality GameHandler::getItemTemplateQuality(uint32_t itemId) const { + auto& db = getSinglePlayerLootDb(); + auto it = db.itemTemplates.find(itemId); + if (it != db.itemTemplates.end()) return static_cast(it->second.quality); + return ItemQuality::COMMON; +} + } // namespace game } // namespace wowee diff --git a/src/game/npc_manager.cpp b/src/game/npc_manager.cpp index 80eeebde..da79cc25 100644 --- a/src/game/npc_manager.cpp +++ b/src/game/npc_manager.cpp @@ -740,13 +740,9 @@ void NpcManager::initialize(pipeline::AssetManager* am, for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { uint32_t id = dbc->getUInt32(i, 0); uint32_t enemyGroup = dbc->getUInt32(i, 5); - uint32_t friendGroup = dbc->getUInt32(i, 4); - // Hostile if creature's enemy groups overlap player's faction/friend groups + // Hostile only if creature's enemy groups overlap player's faction/friend groups bool hostile = (enemyGroup & playerFriendGroup) != 0; - // Friendly only if creature's friendGroup explicitly includes player's groups - bool friendly = (friendGroup & playerFriendGroup) != 0; - // Hostile if explicitly hostile, or if no explicit relationship at all - factionHostile[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0); + factionHostile[id] = hostile; } LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(), " faction templates (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")"); @@ -802,7 +798,7 @@ void NpcManager::initialize(pipeline::AssetManager* am, // Determine hostility from faction template auto fIt = factionHostile.find(s.faction); - unit->setHostile(fIt != factionHostile.end() ? fIt->second : true); + unit->setHostile(fIt != factionHostile.end() ? fIt->second : false); // Store canonical WoW coordinates for targeting/server compatibility glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 84cff208..9f13c707 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -62,17 +62,13 @@ void CameraController::startIntroPan(float durationSec, float orbitDegrees) { introTimer = 0.0f; idleTimer_ = 0.0f; introDuration = std::max(0.5f, durationSec); - introStartYaw = facingYaw + orbitDegrees; - introEndYaw = facingYaw; + introStartYaw = yaw; + introEndYaw = yaw - orbitDegrees; introOrbitDegrees = orbitDegrees; - introStartPitch = -32.0f; - introEndPitch = -10.0f; - introStartDistance = 18.0f; - introEndDistance = 10.0f; - yaw = introStartYaw; - pitch = introStartPitch; - currentDistance = introStartDistance; - userTargetDistance = introEndDistance; + introStartPitch = pitch; + introEndPitch = pitch; + introStartDistance = currentDistance; + introEndDistance = currentDistance; thirdPerson = true; } @@ -105,7 +101,7 @@ void CameraController::update(float deltaTime) { idleTimer_ += deltaTime; if (idleTimer_ >= IDLE_TIMEOUT) { idleTimer_ = 0.0f; - startIntroPan(6.0f, 360.0f); // Slow full orbit + startIntroPan(30.0f, 360.0f); // Slow casual orbit over 30 seconds idleOrbit_ = true; } } @@ -117,19 +113,21 @@ void CameraController::update(float deltaTime) { idleTimer_ = 0.0f; } else { introTimer += deltaTime; - float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f; - yaw = introStartYaw + (introEndYaw - introStartYaw) * t; - pitch = introStartPitch + (introEndPitch - introStartPitch) * t; - currentDistance = introStartDistance + (introEndDistance - introStartDistance) * t; - userTargetDistance = introEndDistance; - camera->setRotation(yaw, pitch); - facingYaw = yaw; - if (t >= 1.0f) { - if (idleOrbit_) { - // Loop: restart the slow orbit continuously - startIntroPan(6.0f, 360.0f); - idleOrbit_ = true; - } else { + if (idleOrbit_) { + // Continuous smooth rotation — no lerp endpoint, just constant angular velocity + float degreesPerSec = introOrbitDegrees / introDuration; + yaw -= degreesPerSec * deltaTime; + camera->setRotation(yaw, pitch); + facingYaw = yaw; + } else { + float t = (introDuration > 0.0f) ? std::min(introTimer / introDuration, 1.0f) : 1.0f; + yaw = introStartYaw + (introEndYaw - introStartYaw) * t; + pitch = introStartPitch + (introEndPitch - introStartPitch) * t; + currentDistance = introStartDistance + (introEndDistance - introStartDistance) * t; + userTargetDistance = introEndDistance; + camera->setRotation(yaw, pitch); + facingYaw = yaw; + if (t >= 1.0f) { introActive = false; } } diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index 53b9b129..655d191a 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -33,21 +33,13 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { gameHandler.requestCharacterList(); } else if (characters.empty()) { ImGui::Text("No characters available."); - } else if (characters.size() == 1 && !characterSelected) { - // Auto-select the only available character - selectedCharacterIndex = 0; - selectedCharacterGuid = characters[0].guid; - characterSelected = true; - std::stringstream ss; - ss << "Entering world with " << characters[0].name << "..."; - setStatus(ss.str()); - if (!gameHandler.isSinglePlayerMode()) { - gameHandler.selectCharacter(characters[0].guid); - } - if (onCharacterSelected) { - onCharacterSelected(characters[0].guid); - } } else { + // Auto-highlight the first character if none selected yet + if (selectedCharacterIndex < 0 && !characters.empty()) { + selectedCharacterIndex = 0; + selectedCharacterGuid = characters[0].guid; + } + // Character table if (ImGui::BeginTable("CharactersTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0476eefd..44e59d44 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -362,10 +362,17 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImVec4 color = getChatTypeColor(msg.type); ImGui::PushStyleColor(ImGuiCol_Text, color); - if (msg.type == game::ChatType::TEXT_EMOTE) { + if (msg.type == game::ChatType::SYSTEM) { + // System messages: just yellow text, no header + ImGui::TextWrapped("%s", msg.message.c_str()); + } else if (msg.type == game::ChatType::TEXT_EMOTE) { ImGui::TextWrapped("You %s", msg.message.c_str()); } else if (!msg.senderName.empty()) { - ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str()); + if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_YELL) { + ImGui::TextWrapped("%s says: %s", msg.senderName.c_str(), msg.message.c_str()); + } else { + ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str()); + } } else { ImGui::TextWrapped("[%s] %s", getChatTypeName(msg.type), msg.message.c_str()); } @@ -521,14 +528,58 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { if (closestGuid != 0) { gameHandler.setTarget(closestGuid); + } else { + // Clicked empty space — deselect current target + gameHandler.clearTarget(); } - // Don't clear on miss — left-click is also used for camera orbit } } - // Right-click on target for NPC interaction / loot / auto-attack + // Right-click: select NPC (if needed) then interact / loot / auto-attack // Suppress when left button is held (both-button run) if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) { + // If no target or right-clicking in world, try to pick one under cursor + { + auto* renderer = core::Application::getInstance().getRenderer(); + auto* camera = renderer ? renderer->getCamera() : nullptr; + auto* window = core::Application::getInstance().getWindow(); + if (camera && window) { + glm::vec2 mousePos = input.getMousePosition(); + float screenW = static_cast(window->getWidth()); + float screenH = static_cast(window->getHeight()); + rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH); + 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; + float hitRadius = 1.5f; + float heightOffset = 1.5f; + if (t == game::ObjectType::UNIT) { + auto unit = std::static_pointer_cast(entity); + 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())); + entityGL.z += heightOffset; + float hitT; + if (raySphereIntersect(ray, entityGL, hitRadius, hitT)) { + if (hitT < closestT) { + closestT = hitT; + closestGuid = guid; + } + } + } + if (closestGuid != 0) { + gameHandler.setTarget(closestGuid); + } + } + } if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); if (target) { @@ -1734,17 +1785,27 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { // Items with icons and labels constexpr float iconSize = 32.0f; + int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation for (const auto& item : loot.items) { ImGui::PushID(item.slotIndex); // Get item info for name and quality const auto* info = gameHandler.getItemInfo(item.itemId); - std::string itemName = info && !info->name.empty() - ? info->name - : "Item #" + std::to_string(item.itemId); - game::ItemQuality quality = info - ? static_cast(info->quality) - : game::ItemQuality::COMMON; + std::string itemName; + game::ItemQuality quality = game::ItemQuality::COMMON; + if (info && !info->name.empty()) { + itemName = info->name; + quality = static_cast(info->quality); + } else { + // Fallback: look up name from item template DB (single-player) + auto tplName = gameHandler.getItemTemplateName(item.itemId); + if (!tplName.empty()) { + itemName = tplName; + quality = gameHandler.getItemTemplateQuality(item.itemId); + } else { + itemName = "Item #" + std::to_string(item.itemId); + } + } ImVec4 qColor = InventoryScreen::getQualityColor(quality); // Get item icon @@ -1757,7 +1818,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { // Invisible selectable for click handling if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) { - gameHandler.lootItem(item.slotIndex); + lootSlotClicked = item.slotIndex; } bool hovered = ImGui::IsItemHovered(); @@ -1802,6 +1863,11 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { ImGui::PopID(); } + // Process deferred loot pickup (after loop to avoid iterator invalidation) + if (lootSlotClicked >= 0) { + gameHandler.lootItem(static_cast(lootSlotClicked)); + } + if (loot.items.empty() && loot.gold == 0) { ImGui::TextDisabled("Empty"); } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index e2bd5a12..d228d53d 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -587,6 +587,43 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { static_cast(copper)); ImGui::End(); + // Detect held item dropped outside inventory windows → drop confirmation + if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) { + 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; + if (gameHandler_) { + gameHandler_->notifyInventoryChanged(); + } + dropItemName_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(80, 0))) { + cancelPickup(inventory); + dropItemName_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + // Draw held item at cursor renderHeldItem(); } @@ -617,7 +654,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { } ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(350.0f, 650.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(380.0f, 650.0f), ImGuiCond_FirstUseEver); ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; if (!ImGui::Begin("Character", &characterOpen, flags)) { @@ -640,8 +677,8 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { renderEquipmentPanel(inventory); - // Stats panel - ImGui::Spacing(); + // Stats panel — use full width and separate from equipment layout + ImGui::SetCursorPosX(ImGui::GetStyle().WindowPadding.x); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -1114,16 +1151,13 @@ 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); - } + // 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); } ImGui::EndTooltip(); diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index 0f95f030..851a6a34 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -36,13 +36,13 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { } uint32_t fieldCount = dbc->getFieldCount(); - if (fieldCount < 142) { + if (fieldCount < 154) { LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+"); return; } - // WoW 3.3.5a Spell.dbc fields: - // 0 = SpellID, 75 = Attributes, 133 = SpellIconID, 136 = SpellName, 141 = RankText + // WoW 3.3.5a Spell.dbc fields (0-based): + // 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t spellId = dbc->getUInt32(i, 0); @@ -50,10 +50,10 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { SpellInfo info; info.spellId = spellId; - info.attributes = dbc->getUInt32(i, 75); + info.attributes = dbc->getUInt32(i, 4); info.iconId = dbc->getUInt32(i, 133); info.name = dbc->getString(i, 136); - info.rank = dbc->getString(i, 141); + info.rank = dbc->getString(i, 153); if (!info.name.empty()) { spellData[spellId] = std::move(info);