#include "ui/game_screen.hpp" #include "rendering/character_preview.hpp" #include "core/application.hpp" #include "core/coordinates.hpp" #include "core/spawn_presets.hpp" #include "core/input.hpp" #include "rendering/renderer.hpp" #include "rendering/minimap.hpp" #include "rendering/character_renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "game/zone_manager.hpp" #include "audio/footstep_manager.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/mount_sound_manager.hpp" #include "audio/npc_voice_manager.hpp" #include "audio/ambient_sound_manager.hpp" #include "audio/ui_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/spell_sound_manager.hpp" #include "audio/movement_sound_manager.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/blp_loader.hpp" #include "pipeline/dbc_layout.hpp" #include "game/expansion_profile.hpp" #include "core/logger.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::string trim(const std::string& s) { size_t first = s.find_first_not_of(" \t\r\n"); if (first == std::string::npos) return ""; size_t last = s.find_last_not_of(" \t\r\n"); return s.substr(first, last - first + 1); } std::string toLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s; } bool isPortBotTarget(const std::string& target) { std::string t = toLower(trim(target)); return t == "portbot" || t == "gmbot" || t == "telebot"; } std::string buildPortBotCommand(const std::string& rawInput) { std::string input = trim(rawInput); if (input.empty()) return ""; std::string lower = toLower(input); if (lower == "help" || lower == "?") { return "__help__"; } if (lower.rfind(".tele ", 0) == 0 || lower.rfind(".go ", 0) == 0) { return input; } if (lower.rfind("xyz ", 0) == 0) { return ".go " + input; } if (lower == "sw" || lower == "stormwind") return ".tele stormwind"; if (lower == "if" || lower == "ironforge") return ".tele ironforge"; if (lower == "darn" || lower == "darnassus") return ".tele darnassus"; if (lower == "org" || lower == "orgrimmar") return ".tele orgrimmar"; if (lower == "tb" || lower == "thunderbluff") return ".tele thunderbluff"; if (lower == "uc" || lower == "undercity") return ".tele undercity"; if (lower == "shatt" || lower == "shattrath") return ".tele shattrath"; if (lower == "dal" || lower == "dalaran") return ".tele dalaran"; return ".tele " + input; } bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) { glm::vec3 oc = ray.origin - center; float b = glm::dot(oc, ray.direction); float c = glm::dot(oc, oc) - radius * radius; float discriminant = b * b - c; if (discriminant < 0.0f) return false; float t = -b - std::sqrt(discriminant); if (t < 0.0f) t = -b + std::sqrt(discriminant); if (t < 0.0f) return false; tOut = t; return true; } std::string getEntityName(const std::shared_ptr& entity) { if (entity->getType() == wowee::game::ObjectType::PLAYER) { auto player = std::static_pointer_cast(entity); if (!player->getName().empty()) return player->getName(); } else if (entity->getType() == wowee::game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(entity); if (!unit->getName().empty()) return unit->getName(); } else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); if (!go->getName().empty()) return go->getName(); } return "Unknown"; } } namespace wowee { namespace ui { GameScreen::GameScreen() { loadSettings(); initChatTabs(); } void GameScreen::initChatTabs() { chatTabs_.clear(); // General tab: shows everything chatTabs_.push_back({"General", 0xFFFFFFFF}); // Combat tab: system + loot messages chatTabs_.push_back({"Combat", (1u << static_cast(game::ChatType::SYSTEM)) | (1u << static_cast(game::ChatType::LOOT))}); // Whispers tab chatTabs_.push_back({"Whispers", (1u << static_cast(game::ChatType::WHISPER)) | (1u << static_cast(game::ChatType::WHISPER_INFORM))}); // Trade/LFG tab: channel messages chatTabs_.push_back({"Trade/LFG", (1u << static_cast(game::ChatType::CHANNEL))}); } bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const { if (tabIndex < 0 || tabIndex >= static_cast(chatTabs_.size())) return true; const auto& tab = chatTabs_[tabIndex]; if (tab.typeMask == 0xFFFFFFFF) return true; // General tab shows all uint32_t typeBit = 1u << static_cast(msg.type); // For Trade/LFG tab, also filter by channel name if (tabIndex == 3 && msg.type == game::ChatType::CHANNEL) { const std::string& ch = msg.channelName; if (ch.find("Trade") == std::string::npos && ch.find("General") == std::string::npos && ch.find("LookingForGroup") == std::string::npos && ch.find("Local") == std::string::npos) { return false; } return true; } return (tab.typeMask & typeBit) != 0; } void GameScreen::render(game::GameHandler& gameHandler) { // Set up chat bubble callback (once) if (!chatBubbleCallbackSet_) { gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) { float duration = 8.0f + static_cast(msg.size()) * 0.06f; if (isYell) duration += 2.0f; if (duration > 15.0f) duration = 15.0f; // Replace existing bubble for same sender for (auto& b : chatBubbles_) { if (b.senderGuid == guid) { b.message = msg; b.timeRemaining = duration; b.totalDuration = duration; b.isYell = isYell; return; } } // Evict oldest if too many if (chatBubbles_.size() >= 10) { chatBubbles_.erase(chatBubbles_.begin()); } chatBubbles_.push_back({guid, msg, duration, duration, isYell}); }); chatBubbleCallbackSet_ = true; } // Apply UI transparency setting float prevAlpha = ImGui::GetStyle().Alpha; ImGui::GetStyle().Alpha = uiOpacity_; // Apply initial settings when renderer becomes available if (!minimapSettingsApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimapRotate_ = false; pendingMinimapRotate = false; minimap->setRotateWithCamera(false); minimap->setSquareShape(minimapSquare_); minimapSettingsApplied_ = true; } if (auto* zm = renderer->getZoneManager()) { zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack); } // Restore mute state: save actual master volume first, then apply mute if (soundMuted_) { float actual = audio::AudioEngine::instance().getMasterVolume(); preMuteVolume_ = (actual > 0.0f) ? actual : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(0.0f); } } } // Apply saved volume settings once when audio managers first become available if (!volumeSettingsApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); if (renderer && renderer->getUiSoundManager()) { float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(soundMuted_ ? 0.0f : masterScale); if (auto* music = renderer->getMusicManager()) { music->setVolume(static_cast(pendingMusicVolume * masterScale)); } if (auto* ambient = renderer->getAmbientSoundManager()) { ambient->setVolumeScale(pendingAmbientVolume / 100.0f * masterScale); } if (auto* ui = renderer->getUiSoundManager()) { ui->setVolumeScale(pendingUiVolume / 100.0f * masterScale); } if (auto* combat = renderer->getCombatSoundManager()) { combat->setVolumeScale(pendingCombatVolume / 100.0f * masterScale); } if (auto* spell = renderer->getSpellSoundManager()) { spell->setVolumeScale(pendingSpellVolume / 100.0f * masterScale); } if (auto* movement = renderer->getMovementSoundManager()) { movement->setVolumeScale(pendingMovementVolume / 100.0f * masterScale); } if (auto* footstep = renderer->getFootstepManager()) { footstep->setVolumeScale(pendingFootstepVolume / 100.0f * masterScale); } if (auto* npcVoice = renderer->getNpcVoiceManager()) { npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f * masterScale); } if (auto* mount = renderer->getMountSoundManager()) { mount->setVolumeScale(pendingMountVolume / 100.0f * masterScale); } if (auto* activity = renderer->getActivitySoundManager()) { activity->setVolumeScale(pendingActivityVolume / 100.0f * masterScale); } volumeSettingsApplied_ = true; } } // Apply auto-loot setting to GameHandler every frame (cheap bool sync) gameHandler.setAutoLoot(pendingAutoLoot); // Sync chat auto-join settings to GameHandler gameHandler.chatAutoJoin.general = chatAutoJoinGeneral_; gameHandler.chatAutoJoin.trade = chatAutoJoinTrade_; gameHandler.chatAutoJoin.localDefense = chatAutoJoinLocalDefense_; gameHandler.chatAutoJoin.lfg = chatAutoJoinLFG_; gameHandler.chatAutoJoin.local = chatAutoJoinLocal_; // Process targeting input before UI windows processTargetInput(gameHandler); // Player unit frame (top-left) renderPlayerFrame(gameHandler); // Target frame (only when we have a target) if (gameHandler.hasTarget()) { renderTargetFrame(gameHandler); } // Render windows if (showPlayerInfo) { renderPlayerInfo(gameHandler); } if (showEntityWindow) { renderEntityList(gameHandler); } if (showChatWindow) { renderChatWindow(gameHandler); } // ---- New UI elements ---- renderActionBar(gameHandler); renderBagBar(gameHandler); renderXpBar(gameHandler); renderCastBar(gameHandler); renderCombatText(gameHandler); renderPartyFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderGuildInvitePopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); renderLootWindow(gameHandler); renderGossipWindow(gameHandler); renderQuestDetailsWindow(gameHandler); renderQuestRequestItemsWindow(gameHandler); renderQuestOfferRewardWindow(gameHandler); renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); renderMailComposeWindow(gameHandler); renderBankWindow(gameHandler); renderGuildBankWindow(gameHandler); renderAuctionHouseWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); renderResurrectDialog(gameHandler); renderChatBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); renderDingEffect(); // World map (M key toggle handled inside) renderWorldMap(gameHandler); // Quest Log (L key toggle handled inside) questLogScreen.render(gameHandler); // Spellbook (P key toggle handled inside) spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager()); // Talents (N key toggle handled inside) talentScreen.render(gameHandler); // Set up inventory screen asset manager + player appearance (re-init on character switch) { uint64_t activeGuid = gameHandler.getActiveCharacterGuid(); if (activeGuid != 0 && activeGuid != inventoryScreenCharGuid_) { auto* am = core::Application::getInstance().getAssetManager(); if (am) { inventoryScreen.setAssetManager(am); const auto* ch = gameHandler.getActiveCharacter(); if (ch) { uint8_t skin = ch->appearanceBytes & 0xFF; uint8_t face = (ch->appearanceBytes >> 8) & 0xFF; uint8_t hairStyle = (ch->appearanceBytes >> 16) & 0xFF; uint8_t hairColor = (ch->appearanceBytes >> 24) & 0xFF; inventoryScreen.setPlayerAppearance( ch->race, ch->gender, skin, face, hairStyle, hairColor, ch->facialFeatures); inventoryScreenCharGuid_ = activeGuid; } } } } // Set vendor mode before rendering inventory inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler); // Auto-open bags once when vendor window first opens if (gameHandler.isVendorWindowOpen()) { if (!vendorBagsOpened_) { vendorBagsOpened_ = true; if (inventoryScreen.isSeparateBags()) { inventoryScreen.openAllBags(); } else if (!inventoryScreen.isOpen()) { inventoryScreen.setOpen(true); } } } else { vendorBagsOpened_ = false; } // Bags (B key toggle handled inside) inventoryScreen.setGameHandler(&gameHandler); inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper()); // Character screen (C key toggle handled inside render()) inventoryScreen.renderCharacterScreen(gameHandler); if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) { updateCharacterGeosets(gameHandler.getInventory()); updateCharacterTextures(gameHandler.getInventory()); core::Application::getInstance().loadEquippedWeapons(); inventoryScreen.markPreviewDirty(); // Update renderer weapon type for animation selection auto* r = core::Application::getInstance().getRenderer(); if (r) { const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND); r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType); } } // Update renderer face-target position and selection circle auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { renderer->setInCombat(gameHandler.isAutoAttacking()); static glm::vec3 targetGLPos; if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); if (target) { targetGLPos = core::coords::canonicalToRender( glm::vec3(target->getX(), target->getY(), target->getZ())); renderer->setTargetPosition(&targetGLPos); // Selection circle color: WoW-canonical level-based colors glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow float circleRadius = 1.5f; { glm::vec3 boundsCenter; float boundsRadius = 0.0f; if (core::Application::getInstance().getRenderBoundsForGuid(target->getGuid(), boundsCenter, boundsRadius)) { float r = boundsRadius * 1.1f; circleRadius = std::min(std::max(r, 0.8f), 8.0f); } } if (target->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(target); if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) { circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead) } else if (unit->isHostile() || gameHandler.isAggressiveTowardPlayer(target->getGuid())) { uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = unit->getLevel(); int32_t diff = static_cast(mobLv) - static_cast(playerLv); if (game::GameHandler::killXp(playerLv, mobLv) == 0) { circleColor = glm::vec3(0.6f, 0.6f, 0.6f); // grey } else if (diff >= 10) { circleColor = glm::vec3(1.0f, 0.1f, 0.1f); // red } else if (diff >= 5) { circleColor = glm::vec3(1.0f, 0.5f, 0.1f); // orange } else if (diff >= -2) { circleColor = glm::vec3(1.0f, 1.0f, 0.1f); // yellow } else { circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green } } else { circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly) } } 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(); } } // Restore previous alpha ImGui::GetStyle().Alpha = prevAlpha; } void GameScreen::renderPlayerInfo(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(350, 250), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(10, 30), ImGuiCond_FirstUseEver); ImGui::Begin("Player Info", &showPlayerInfo); const auto& movement = gameHandler.getMovementInfo(); ImGui::Text("Position & Movement"); ImGui::Separator(); ImGui::Spacing(); // Position ImGui::Text("Position:"); ImGui::Indent(); ImGui::Text("X: %.2f", movement.x); ImGui::Text("Y: %.2f", movement.y); ImGui::Text("Z: %.2f", movement.z); ImGui::Text("Orientation: %.2f rad (%.1f deg)", movement.orientation, movement.orientation * 180.0f / 3.14159f); ImGui::Unindent(); ImGui::Spacing(); // Movement flags ImGui::Text("Movement Flags: 0x%08X", movement.flags); ImGui::Text("Time: %u ms", movement.time); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Connection state ImGui::Text("Connection State:"); ImGui::Indent(); auto state = gameHandler.getState(); switch (state) { case game::WorldState::IN_WORLD: ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "In World"); break; case game::WorldState::AUTHENTICATED: ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Authenticated"); break; case game::WorldState::ENTERING_WORLD: ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Entering World..."); break; default: ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "State: %d", static_cast(state)); break; } ImGui::Unindent(); ImGui::End(); } void GameScreen::renderEntityList(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(10, 290), ImGuiCond_FirstUseEver); ImGui::Begin("Entities", &showEntityWindow); const auto& entityManager = gameHandler.getEntityManager(); const auto& entities = entityManager.getEntities(); ImGui::Text("Entities in View: %zu", entities.size()); ImGui::Separator(); ImGui::Spacing(); if (entities.empty()) { ImGui::TextDisabled("No entities in view"); } else { // Entity table if (ImGui::BeginTable("EntitiesTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("GUID", ImGuiTableColumnFlags_WidthFixed, 140.0f); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Position", ImGuiTableColumnFlags_WidthFixed, 150.0f); ImGui::TableSetupColumn("Distance", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableHeadersRow(); const auto& playerMovement = gameHandler.getMovementInfo(); float playerX = playerMovement.x; float playerY = playerMovement.y; float playerZ = playerMovement.z; for (const auto& [guid, entity] : entities) { ImGui::TableNextRow(); // GUID ImGui::TableSetColumnIndex(0); char guidStr[24]; snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid); ImGui::Text("%s", guidStr); // Type ImGui::TableSetColumnIndex(1); switch (entity->getType()) { case game::ObjectType::PLAYER: ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Player"); break; case game::ObjectType::UNIT: ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Unit"); break; case game::ObjectType::GAMEOBJECT: ImGui::TextColored(ImVec4(0.3f, 0.8f, 1.0f, 1.0f), "GameObject"); break; default: ImGui::Text("Object"); break; } // Name (for players and units) ImGui::TableSetColumnIndex(2); if (entity->getType() == game::ObjectType::PLAYER) { auto player = std::static_pointer_cast(entity); ImGui::Text("%s", player->getName().c_str()); } else if (entity->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(entity); if (!unit->getName().empty()) { ImGui::Text("%s", unit->getName().c_str()); } else { ImGui::TextDisabled("--"); } } else { ImGui::TextDisabled("--"); } // Position ImGui::TableSetColumnIndex(3); ImGui::Text("%.1f, %.1f, %.1f", entity->getX(), entity->getY(), entity->getZ()); // Distance from player ImGui::TableSetColumnIndex(4); float dx = entity->getX() - playerX; float dy = entity->getY() - playerY; float dz = entity->getZ() - playerZ; float distance = std::sqrt(dx*dx + dy*dy + dz*dz); ImGui::Text("%.1f", distance); } ImGui::EndTable(); } } ImGui::End(); } void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float chatW = std::min(500.0f, screenW * 0.4f); float chatH = 220.0f; float chatX = 8.0f; float chatY = screenH - chatH - 80.0f; // Above action bar if (chatWindowLocked) { // Always recompute position from current window size when locked chatWindowPos_ = ImVec2(chatX, chatY); ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_Always); ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_Always); } else { if (!chatWindowPosInit_) { chatWindowPos_ = ImVec2(chatX, chatY); chatWindowPosInit_ = true; } ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_FirstUseEver); } ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; if (chatWindowLocked) { flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar; } ImGui::Begin("Chat", nullptr, flags); if (!chatWindowLocked) { chatWindowPos_ = ImGui::GetWindowPos(); } // Chat tabs if (ImGui::BeginTabBar("ChatTabs")) { for (int i = 0; i < static_cast(chatTabs_.size()); ++i) { if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) { activeChatTab_ = i; ImGui::EndTabItem(); } } ImGui::EndTabBar(); } // Chat history const auto& chatHistory = gameHandler.getChatHistory(); // Apply chat font size scaling float chatScale = chatFontSize_ == 0 ? 0.85f : (chatFontSize_ == 2 ? 1.2f : 1.0f); ImGui::SetWindowFontScale(chatScale); ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar); bool chatHistoryHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); // Helper: parse WoW color code |cAARRGGBB → ImVec4 auto parseWowColor = [](const std::string& text, size_t pos) -> ImVec4 { // |cAARRGGBB (10 chars total: |c + 8 hex) if (pos + 10 > text.size()) return ImVec4(1, 1, 1, 1); auto hexByte = [&](size_t offset) -> float { const char* s = text.c_str() + pos + offset; char buf[3] = {s[0], s[1], '\0'}; return static_cast(strtol(buf, nullptr, 16)) / 255.0f; }; float a = hexByte(2); float r = hexByte(4); float g = hexByte(6); float b = hexByte(8); return ImVec4(r, g, b, a); }; // Helper: render an item tooltip from ItemQueryResponseData auto renderItemLinkTooltip = [&](uint32_t itemEntry) { const auto* info = gameHandler.getItemInfo(itemEntry); if (!info || !info->valid) return; auto findComparableEquipped = [&](uint8_t inventoryType) -> const game::ItemSlot* { using ES = game::EquipSlot; const auto& inv = gameHandler.getInventory(); auto slotPtr = [&](ES slot) -> const game::ItemSlot* { const auto& s = inv.getEquipSlot(slot); return s.empty() ? nullptr : &s; }; switch (inventoryType) { case 1: return slotPtr(ES::HEAD); case 2: return slotPtr(ES::NECK); case 3: return slotPtr(ES::SHOULDERS); case 4: return slotPtr(ES::SHIRT); case 5: case 20: return slotPtr(ES::CHEST); case 6: return slotPtr(ES::WAIST); case 7: return slotPtr(ES::LEGS); case 8: return slotPtr(ES::FEET); case 9: return slotPtr(ES::WRISTS); case 10: return slotPtr(ES::HANDS); case 11: { if (auto* s = slotPtr(ES::RING1)) return s; return slotPtr(ES::RING2); } case 12: { if (auto* s = slotPtr(ES::TRINKET1)) return s; return slotPtr(ES::TRINKET2); } case 13: if (auto* s = slotPtr(ES::MAIN_HAND)) return s; return slotPtr(ES::OFF_HAND); case 14: case 22: case 23: return slotPtr(ES::OFF_HAND); case 15: case 25: case 26: return slotPtr(ES::RANGED); case 16: return slotPtr(ES::BACK); case 17: case 21: return slotPtr(ES::MAIN_HAND); case 18: for (int i = 0; i < game::Inventory::NUM_BAG_SLOTS; ++i) { auto slot = static_cast(static_cast(ES::BAG1) + i); if (auto* s = slotPtr(slot)) return s; } return nullptr; case 19: return slotPtr(ES::TABARD); default: return nullptr; } }; ImGui::BeginTooltip(); // Quality color for name ImVec4 qColor(1, 1, 1, 1); switch (info->quality) { case 0: qColor = ImVec4(0.62f, 0.62f, 0.62f, 1.0f); break; // Poor case 1: qColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common case 2: qColor = ImVec4(0.12f, 1.0f, 0.0f, 1.0f); break; // Uncommon case 3: qColor = ImVec4(0.0f, 0.44f, 0.87f, 1.0f); break; // Rare case 4: qColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic case 5: qColor = ImVec4(1.0f, 0.50f, 0.0f, 1.0f); break; // Legendary } ImGui::TextColored(qColor, "%s", info->name.c_str()); // Slot type if (info->inventoryType > 0) { const char* slotName = ""; switch (info->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; } if (slotName[0]) { if (!info->subclassName.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info->subclassName.c_str()); else ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName); } } auto isWeaponInventoryType = [](uint32_t invType) { switch (invType) { case 13: // One-Hand case 15: // Ranged case 17: // Two-Hand case 21: // Main Hand case 25: // Thrown case 26: // Ranged Right return true; default: return false; } }; const bool isWeapon = isWeaponInventoryType(info->inventoryType); if (isWeapon && info->damageMax > 0.0f && info->delayMs > 0) { float speed = static_cast(info->delayMs) / 1000.0f; float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed; ImGui::Text("%.1f DPS", dps); } ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); auto appendBonus = [](std::string& out, int32_t val, const char* shortName) { if (val <= 0) return; if (!out.empty()) out += " "; out += "+" + std::to_string(val) + " "; out += shortName; }; std::string bonusLine; appendBonus(bonusLine, info->strength, "Str"); appendBonus(bonusLine, info->agility, "Agi"); appendBonus(bonusLine, info->stamina, "Sta"); appendBonus(bonusLine, info->intellect, "Int"); appendBonus(bonusLine, info->spirit, "Spi"); if (!bonusLine.empty()) { ImGui::TextColored(green, "%s", bonusLine.c_str()); } if (info->armor > 0) { ImGui::Text("%d Armor", info->armor); } if (info->sellPrice > 0) { uint32_t g = info->sellPrice / 10000; uint32_t s = (info->sellPrice / 100) % 100; uint32_t c = info->sellPrice % 100; ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); } if (ImGui::GetIO().KeyShift && info->inventoryType > 0) { if (const auto* eq = findComparableEquipped(static_cast(info->inventoryType))) { ImGui::Separator(); ImGui::TextDisabled("Equipped:"); GLuint eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId); if (eqIcon) { ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f)); ImGui::SameLine(); } ImGui::TextColored(InventoryScreen::getQualityColor(eq->item.quality), "%s", eq->item.name.c_str()); if (isWeaponInventoryType(eq->item.inventoryType) && eq->item.damageMax > 0.0f && eq->item.delayMs > 0) { float speed = static_cast(eq->item.delayMs) / 1000.0f; float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed; ImGui::Text("%.1f DPS", dps); } if (eq->item.armor > 0) { ImGui::Text("%d Armor", eq->item.armor); } std::string eqBonusLine; appendBonus(eqBonusLine, eq->item.strength, "Str"); appendBonus(eqBonusLine, eq->item.agility, "Agi"); appendBonus(eqBonusLine, eq->item.stamina, "Sta"); appendBonus(eqBonusLine, eq->item.intellect, "Int"); appendBonus(eqBonusLine, eq->item.spirit, "Spi"); if (!eqBonusLine.empty()) { ImGui::TextColored(green, "%s", eqBonusLine.c_str()); } } } ImGui::EndTooltip(); }; // Helper: render text with clickable URLs and WoW item links auto renderTextWithLinks = [&](const std::string& text, const ImVec4& color) { size_t pos = 0; while (pos < text.size()) { // Find next special element: URL or WoW link size_t urlStart = text.find("https://", pos); // Find next WoW item link: |cXXXXXXXX|Hitem:ENTRY:...|h[Name]|h|r size_t linkStart = text.find("|c", pos); // Also handle bare |Hitem: without color prefix size_t bareLinkStart = text.find("|Hitem:", pos); // Determine which comes first size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart}); if (nextSpecial == std::string::npos) { // No more special elements, render remaining text std::string remaining = text.substr(pos); if (!remaining.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", remaining.c_str()); ImGui::PopStyleColor(); } break; } // Render plain text before special element if (nextSpecial > pos) { std::string before = text.substr(pos, nextSpecial - pos); ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", before.c_str()); ImGui::PopStyleColor(); ImGui::SameLine(0, 0); } // Handle WoW item link if (nextSpecial == linkStart || nextSpecial == bareLinkStart) { ImVec4 linkColor = color; size_t hStart = std::string::npos; if (nextSpecial == linkStart && text.size() > linkStart + 10) { // Parse |cAARRGGBB color linkColor = parseWowColor(text, linkStart); hStart = text.find("|Hitem:", linkStart + 10); } else if (nextSpecial == bareLinkStart) { hStart = bareLinkStart; } if (hStart != std::string::npos) { // Parse item entry: |Hitem:ENTRY:... size_t entryStart = hStart + 7; // skip "|Hitem:" size_t entryEnd = text.find(':', entryStart); uint32_t itemEntry = 0; if (entryEnd != std::string::npos) { itemEntry = static_cast(strtoul( text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10)); } // Find display name: |h[Name]|h size_t nameTagStart = text.find("|h[", hStart); size_t nameTagEnd = (nameTagStart != std::string::npos) ? text.find("]|h", nameTagStart + 3) : std::string::npos; std::string itemName = "Unknown Item"; if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) { itemName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3); } // Find end of entire link sequence (|r or after ]|h) size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + 7; size_t resetPos = text.find("|r", linkEnd); if (resetPos != std::string::npos && resetPos <= linkEnd + 2) { linkEnd = resetPos + 2; } // Ensure item info is cached (trigger query if needed) if (itemEntry > 0) { gameHandler.ensureItemInfo(itemEntry); } // Render bracketed item name in quality color std::string display = "[" + itemName + "]"; ImGui::PushStyleColor(ImGuiCol_Text, linkColor); ImGui::TextWrapped("%s", display.c_str()); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (itemEntry > 0) { renderItemLinkTooltip(itemEntry); } } // Shift-click: insert item link into chat input if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) { std::string linkText = text.substr(nextSpecial, linkEnd - nextSpecial); size_t curLen = strlen(chatInputBuffer); if (curLen + linkText.size() + 1 < sizeof(chatInputBuffer)) { strncat(chatInputBuffer, linkText.c_str(), sizeof(chatInputBuffer) - curLen - 1); chatInputMoveCursorToEnd = true; } } pos = linkEnd; continue; } // Not an item link — treat as colored text: |cAARRGGBB...text...|r if (nextSpecial == linkStart && text.size() > linkStart + 10) { ImVec4 cColor = parseWowColor(text, linkStart); size_t textStart = linkStart + 10; // after |cAARRGGBB size_t resetPos2 = text.find("|r", textStart); std::string coloredText; if (resetPos2 != std::string::npos) { coloredText = text.substr(textStart, resetPos2 - textStart); pos = resetPos2 + 2; // skip |r } else { coloredText = text.substr(textStart); pos = text.size(); } // Strip any remaining WoW markup from the colored segment // (e.g. |H...|h pairs that aren't item links) std::string clean; for (size_t i = 0; i < coloredText.size(); i++) { if (coloredText[i] == '|' && i + 1 < coloredText.size()) { char next = coloredText[i + 1]; if (next == 'H') { // Skip |H...|h size_t hEnd = coloredText.find("|h", i + 2); if (hEnd != std::string::npos) { i = hEnd + 1; continue; } } else if (next == 'h') { i += 1; continue; // skip |h } else if (next == 'r') { i += 1; continue; // skip |r } } clean += coloredText[i]; } if (!clean.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, cColor); ImGui::TextWrapped("%s", clean.c_str()); ImGui::PopStyleColor(); ImGui::SameLine(0, 0); } } else { // Bare |c without enough chars for color — render literally ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("|c"); ImGui::PopStyleColor(); ImGui::SameLine(0, 0); pos = nextSpecial + 2; } continue; } // Handle URL if (nextSpecial == urlStart) { size_t urlEnd = text.find_first_of(" \t\n\r", urlStart); if (urlEnd == std::string::npos) urlEnd = text.size(); std::string url = text.substr(urlStart, urlEnd - urlStart); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.7f, 1.0f, 1.0f)); ImGui::TextWrapped("%s", url.c_str()); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("Open: %s", url.c_str()); } if (ImGui::IsItemClicked()) { std::string cmd = "xdg-open '" + url + "' &"; [[maybe_unused]] int result = system(cmd.c_str()); } ImGui::PopStyleColor(); pos = urlEnd; continue; } } }; for (const auto& msg : chatHistory) { if (!shouldShowMessage(msg, activeChatTab_)) continue; std::string processedMessage = replaceGenderPlaceholders(msg.message, gameHandler); ImVec4 color = getChatTypeColor(msg.type); // Optional timestamp prefix std::string tsPrefix; if (chatShowTimestamps_) { auto tt = std::chrono::system_clock::to_time_t(msg.timestamp); std::tm tm{}; #ifdef _WIN32 localtime_s(&tm, &tt); #else localtime_r(&tt, &tm); #endif char tsBuf[16]; snprintf(tsBuf, sizeof(tsBuf), "[%02d:%02d] ", tm.tm_hour, tm.tm_min); tsPrefix = tsBuf; } if (msg.type == game::ChatType::SYSTEM) { renderTextWithLinks(tsPrefix + processedMessage, color); } else if (msg.type == game::ChatType::TEXT_EMOTE) { renderTextWithLinks(tsPrefix + processedMessage, color); } else if (!msg.senderName.empty()) { if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_YELL) { std::string fullMsg = tsPrefix + msg.senderName + " says: " + processedMessage; renderTextWithLinks(fullMsg, color); } else if (msg.type == game::ChatType::CHANNEL && !msg.channelName.empty()) { int chIdx = gameHandler.getChannelIndex(msg.channelName); std::string chDisplay = chIdx > 0 ? "[" + std::to_string(chIdx) + ". " + msg.channelName + "]" : "[" + msg.channelName + "]"; std::string fullMsg = tsPrefix + chDisplay + " [" + msg.senderName + "]: " + processedMessage; renderTextWithLinks(fullMsg, color); } else { std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + msg.senderName + ": " + processedMessage; renderTextWithLinks(fullMsg, color); } } else { std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + processedMessage; renderTextWithLinks(fullMsg, color); } } // Auto-scroll to bottom if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.0f); } ImGui::EndChild(); // Reset font scale after chat history ImGui::SetWindowFontScale(1.0f); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Lock toggle ImGui::Checkbox("Lock", &chatWindowLocked); ImGui::SameLine(); ImGui::TextDisabled(chatWindowLocked ? "(locked)" : "(movable)"); // Chat input ImGui::Text("Type:"); ImGui::SameLine(); ImGui::SetNextItemWidth(100); const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE" }; ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 10); // Auto-fill whisper target when switching to WHISPER mode if (selectedChatType == 4 && lastChatType != 4) { // Just switched to WHISPER mode if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); if (target && target->getType() == game::ObjectType::PLAYER) { auto player = std::static_pointer_cast(target); if (!player->getName().empty()) { strncpy(whisperTargetBuffer, player->getName().c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; } } } } lastChatType = selectedChatType; // Show whisper target field if WHISPER is selected if (selectedChatType == 4) { ImGui::SameLine(); ImGui::Text("To:"); ImGui::SameLine(); ImGui::SetNextItemWidth(120); ImGui::InputText("##WhisperTarget", whisperTargetBuffer, sizeof(whisperTargetBuffer)); } ImGui::SameLine(); ImGui::Text("Message:"); ImGui::SameLine(); ImGui::SetNextItemWidth(-1); if (refocusChatInput) { ImGui::SetKeyboardFocusHere(); refocusChatInput = false; } // Detect chat channel prefix as user types and switch the dropdown { std::string buf(chatInputBuffer); if (buf.size() >= 2 && buf[0] == '/') { // Find the command and check if there's a space after it size_t sp = buf.find(' ', 1); if (sp != std::string::npos) { std::string cmd = buf.substr(1, sp - 1); for (char& c : cmd) c = std::tolower(c); int detected = -1; if (cmd == "s" || cmd == "say") detected = 0; else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1; else if (cmd == "p" || cmd == "party") detected = 2; else if (cmd == "g" || cmd == "guild") detected = 3; else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4; else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5; else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6; else if (cmd == "bg" || cmd == "battleground") detected = 7; else if (cmd == "rw" || cmd == "raidwarning") detected = 8; else if (cmd == "i" || cmd == "instance") detected = 9; if (detected >= 0 && selectedChatType != detected) { selectedChatType = detected; // Strip the prefix, keep only the message part std::string remaining = buf.substr(sp + 1); // For whisper, first word after /w is the target if (detected == 4) { size_t msgStart = remaining.find(' '); if (msgStart != std::string::npos) { std::string wTarget = remaining.substr(0, msgStart); strncpy(whisperTargetBuffer, wTarget.c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; remaining = remaining.substr(msgStart + 1); } else { // Just the target name so far, no message yet strncpy(whisperTargetBuffer, remaining.c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; remaining = ""; } } strncpy(chatInputBuffer, remaining.c_str(), sizeof(chatInputBuffer) - 1); chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; chatInputMoveCursorToEnd = true; } } } } // Color the input text based on current chat type ImVec4 inputColor; switch (selectedChatType) { case 1: inputColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); break; // YELL - red case 2: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // PARTY - blue case 3: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // GUILD - green case 4: inputColor = ImVec4(1.0f, 0.5f, 1.0f, 1.0f); break; // WHISPER - pink case 5: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // RAID - orange case 6: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // OFFICER - green case 7: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // BG - orange case 8: inputColor = ImVec4(1.0f, 0.3f, 0.0f, 1.0f); break; // RAID WARNING - red-orange case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue default: inputColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // SAY - white } ImGui::PushStyleColor(ImGuiCol_Text, inputColor); auto inputCallback = [](ImGuiInputTextCallbackData* data) -> int { auto* self = static_cast(data->UserData); if (self && self->chatInputMoveCursorToEnd) { int len = static_cast(std::strlen(data->Buf)); data->CursorPos = len; data->SelectionStart = len; data->SelectionEnd = len; self->chatInputMoveCursorToEnd = false; } return 0; }; ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways; if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) { sendChatMessage(gameHandler); // Close chat input on send so movement keys work immediately. refocusChatInput = false; ImGui::ClearActiveID(); } ImGui::PopStyleColor(); if (ImGui::IsItemActive()) { chatInputActive = true; } else { chatInputActive = false; } // Click in chat history area (received messages) → focus input. { if (chatHistoryHovered && ImGui::IsMouseClicked(0)) { refocusChatInput = true; } } ImGui::End(); } void GameScreen::processTargetInput(game::GameHandler& gameHandler) { auto& io = ImGui::GetIO(); auto& input = core::Input::getInstance(); // Tab targeting (when keyboard not captured by UI) if (!io.WantCaptureKeyboard) { if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) { const auto& movement = gameHandler.getMovementInfo(); gameHandler.tabTarget(movement.x, movement.y, movement.z); } if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { if (showSettingsWindow) { // Close settings window if open showSettingsWindow = false; } else if (showEscapeMenu) { showEscapeMenu = false; showEscapeSettingsNotice = false; } else if (gameHandler.isCasting()) { gameHandler.cancelCast(); } else if (gameHandler.isLootWindowOpen()) { gameHandler.closeLoot(); } else if (gameHandler.isGossipWindowOpen()) { gameHandler.closeGossip(); } else { showEscapeMenu = true; } } // Action bar keys (1-9, 0, -, =) static const SDL_Scancode actionBarKeys[] = { SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8, SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS }; for (int i = 0; i < 12; ++i) { if (input.isKeyJustPressed(actionBarKeys[i])) { const auto& bar = gameHandler.getActionBar(); if (bar[i].type == game::ActionBarSlot::SPELL && bar[i].isReady()) { uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; gameHandler.castSpell(bar[i].id, target); } else if (bar[i].type == game::ActionBarSlot::ITEM && bar[i].id != 0) { gameHandler.useItemById(bar[i].id); } } } } // Slash key: focus chat input if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { refocusChatInput = true; chatInputBuffer[0] = '/'; chatInputBuffer[1] = '\0'; chatInputMoveCursorToEnd = true; } // Enter key: focus chat input (empty) if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) { refocusChatInput = true; } // Cursor affordance: show hand cursor over interactable game objects. if (!io.WantCaptureMouse) { 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; bool hoverInteractableGo = false; for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { if (entity->getType() != game::ObjectType::GAMEOBJECT) continue; auto go = std::static_pointer_cast(entity); auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry()); uint32_t goType = goInfo ? goInfo->type : 0; if (goType == 5) continue; // decoration/non-interactable generic glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); if (!hasBounds) { hitRadius = 2.5f; hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); hitCenter.z += 1.2f; } else { hitRadius = std::max(hitRadius * 1.1f, 0.8f); } float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) { closestT = hitT; hoverInteractableGo = true; } } if (hoverInteractableGo) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } } } // Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate) // Record press position on mouse-down if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) { leftClickPressPos_ = input.getMousePosition(); leftClickWasPress_ = true; } // On mouse-up, check if it was a click (not a drag) if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) { leftClickWasPress_ = false; glm::vec2 releasePos = input.getMousePosition(); float dragDist = glm::length(releasePos - leftClickPressPos_); constexpr float CLICK_THRESHOLD = 5.0f; // pixels if (dragDist < CLICK_THRESHOLD) { auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; auto* window = core::Application::getInstance().getWindow(); if (camera && window) { float screenW = static_cast(window->getWidth()); float screenH = static_cast(window->getHeight()); rendering::Ray ray = camera->screenToWorldRay(leftClickPressPos_.x, leftClickPressPos_.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; // Don't target self glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); if (!hasBounds) { // Fallback hitbox based on entity type float heightOffset = 1.5f; hitRadius = 1.5f; if (t == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(entity); // Critters have very low max health (< 100) if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) { hitRadius = 0.5f; heightOffset = 0.3f; } } hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); hitCenter.z += heightOffset; } else { hitRadius = std::max(hitRadius * 1.1f, 0.6f); } float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) { if (hitT < closestT) { closestT = hitT; closestGuid = guid; } } } if (closestGuid != 0) { gameHandler.setTarget(closestGuid); } else { // Clicked empty space — deselect current target gameHandler.clearTarget(); } } } } // 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; game::ObjectType closestType = game::ObjectType::OBJECT; 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 && t != game::ObjectType::GAMEOBJECT) continue; if (guid == myGuid) continue; glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); if (!hasBounds) { float heightOffset = 1.5f; hitRadius = 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; } } else if (t == game::ObjectType::GAMEOBJECT) { // Check GO type — skip non-interactable decorations auto go = std::static_pointer_cast(entity); auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry()); uint32_t goType = goInfo ? goInfo->type : 0; // Type 5 = GENERIC (decorations), skip if (goType == 5) continue; hitRadius = 2.5f; heightOffset = 1.2f; } hitCenter = core::coords::canonicalToRender( glm::vec3(entity->getX(), entity->getY(), entity->getZ())); hitCenter.z += heightOffset; } else { hitRadius = std::max(hitRadius * 1.1f, 0.6f); } float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) { if (hitT < closestT) { closestT = hitT; closestGuid = guid; closestType = t; } } } if (closestGuid != 0) { if (closestType == game::ObjectType::GAMEOBJECT) { gameHandler.interactWithGameObject(closestGuid); return; } gameHandler.setTarget(closestGuid); } } } if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); if (target) { if (target->getType() == game::ObjectType::UNIT) { // Check if unit is dead (health == 0) → loot, otherwise interact/attack auto unit = std::static_pointer_cast(target); if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) { gameHandler.lootTarget(target->getGuid()); } else { // Interact with friendly NPCs; hostile units just get targeted auto isSpiritNpc = [&]() -> bool { constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000; constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000; if (unit->getNpcFlags() & (NPC_FLAG_SPIRIT_GUIDE | NPC_FLAG_SPIRIT_HEALER)) { return true; } std::string name = unit->getName(); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); return (name.find("spirit healer") != std::string::npos) || (name.find("spirit guide") != std::string::npos); }; bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc(); if (!unit->isHostile() && (unit->isInteractable() || allowSpiritInteract)) { gameHandler.interactWithNpc(target->getGuid()); } else if (unit->isHostile()) { gameHandler.startAutoAttack(target->getGuid()); } } } else if (target->getType() == game::ObjectType::GAMEOBJECT) { gameHandler.interactWithGameObject(target->getGuid()); } else if (target->getType() == game::ObjectType::PLAYER) { // Right-click another player could start attack in PvP context } } } } } void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { bool isDead = gameHandler.isPlayerDead(); ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(250.0f, 0.0f), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f)); ImVec4 playerBorder = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : (gameHandler.isAutoAttacking() ? ImVec4(1.0f, 0.2f, 0.2f, 1.0f) : ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, playerBorder); if (ImGui::Begin("##PlayerFrame", nullptr, flags)) { // Use selected character info if available, otherwise defaults std::string playerName = "Adventurer"; uint32_t playerLevel = 1; uint32_t playerHp = 100; uint32_t playerMaxHp = 100; const auto& characters = gameHandler.getCharacters(); uint64_t activeGuid = gameHandler.getActiveCharacterGuid(); const game::Character* activeChar = nullptr; for (const auto& c : characters) { if (c.guid == activeGuid) { activeChar = &c; break; } } if (!activeChar && !characters.empty()) activeChar = &characters[0]; if (activeChar) { const auto& ch = *activeChar; playerName = ch.name; // Use live server level if available, otherwise character struct playerLevel = gameHandler.getPlayerLevel(); if (playerLevel == 0) playerLevel = ch.level; playerMaxHp = 20 + playerLevel * 10; playerHp = playerMaxHp; } // Name in green (friendly player color) — clickable for self-target ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) { gameHandler.setTarget(gameHandler.getPlayerGuid()); } ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("Lv %u", playerLevel); if (isDead) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.2f, 1.0f), "DEAD"); } // Try to get real HP/mana from the player entity auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) { auto unit = std::static_pointer_cast(playerEntity); if (unit->getMaxHealth() > 0) { playerHp = unit->getHealth(); playerMaxHp = unit->getMaxHealth(); } } // Health bar float pct = static_cast(playerHp) / static_cast(playerMaxHp); ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpColor); char overlay[64]; snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp); ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay); ImGui::PopStyleColor(); // Mana/Power bar if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) { auto unit = std::static_pointer_cast(playerEntity); uint8_t powerType = unit->getPowerType(); uint32_t power = unit->getPower(); uint32_t maxPower = unit->getMaxPower(); // Rage (1) and Energy (3) always cap at 100 — show bar even if server // hasn't sent UNIT_FIELD_MAXPOWER1 yet (warriors start combat at 0 rage). if (maxPower == 0 && (powerType == 1 || powerType == 3)) maxPower = 100; if (maxPower > 0) { float mpPct = static_cast(power) / static_cast(maxPower); ImVec4 powerColor; switch (powerType) { case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue) case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red) case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow) default: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor); char mpOverlay[64]; snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", power, maxPower); ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay); ImGui::PopStyleColor(); } } } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { auto target = gameHandler.getTarget(); if (!target) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float frameW = 250.0f; float frameX = (screenW - frameW) / 2.0f; ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; // Determine hostility/level color for border and name (WoW-canonical) 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(target); if (u->getHealth() == 0 && u->getMaxHealth() > 0) { hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); } else if (u->isHostile()) { // WoW level-based color for hostile mobs uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); int32_t diff = static_cast(mobLv) - static_cast(playerLv); if (game::GameHandler::killXp(playerLv, mobLv) == 0) { hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP } else if (diff >= 10) { hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard } else if (diff >= 5) { hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard } else if (diff >= -2) { hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even } else { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy } } else { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly } } ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f)); bool isHostileTarget = gameHandler.isHostileAttacker(target->getGuid()); if (!isHostileTarget && target->getType() == game::ObjectType::UNIT) { auto u = std::static_pointer_cast(target); isHostileTarget = u->isHostile(); } ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f); if (isHostileTarget) { float t = ImGui::GetTime(); float pulse = (std::fmod(t, 0.6f) < 0.3f) ? 1.0f : 0.0f; borderColor = ImVec4(1.0f, 0.1f, 0.1f, pulse); } else if (gameHandler.isAutoAttacking()) { borderColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); } ImGui::PushStyleColor(ImGuiCol_Border, borderColor); if (ImGui::Begin("##TargetFrame", nullptr, flags)) { // Entity name and type std::string name = getEntityName(target); ImVec4 nameColor = hostileColor; ImGui::TextColored(nameColor, "%s", name.c_str()); // Level (for units/players) — colored by difficulty if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) { auto unit = std::static_pointer_cast(target); ImGui::SameLine(); // Level color matches the hostility/difficulty color ImVec4 levelColor = hostileColor; if (target->getType() == game::ObjectType::PLAYER) { levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); } ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); // Health bar uint32_t hp = unit->getHealth(); uint32_t maxHp = unit->getMaxHealth(); if (maxHp > 0) { float pct = static_cast(hp) / static_cast(maxHp); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); char overlay[64]; snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp); ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay); ImGui::PopStyleColor(); // Target power bar (mana/rage/energy) uint8_t targetPowerType = unit->getPowerType(); uint32_t targetPower = unit->getPower(); uint32_t targetMaxPower = unit->getMaxPower(); if (targetMaxPower == 0 && (targetPowerType == 1 || targetPowerType == 3)) targetMaxPower = 100; if (targetMaxPower > 0) { float mpPct = static_cast(targetPower) / static_cast(targetMaxPower); ImVec4 targetPowerColor; switch (targetPowerType) { case 0: targetPowerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue) case 1: targetPowerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red) case 3: targetPowerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow) default: targetPowerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, targetPowerColor); char mpOverlay[64]; snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", targetPower, targetMaxPower); ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay); ImGui::PopStyleColor(); } } else { ImGui::TextDisabled("No health data"); } } // Distance const auto& movement = gameHandler.getMovementInfo(); float dx = target->getX() - movement.x; float dy = target->getY() - movement.y; float dz = target->getZ() - movement.z; float distance = std::sqrt(dx*dx + dy*dy + dz*dz); ImGui::TextDisabled("%.1f yd", distance); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { if (strlen(chatInputBuffer) > 0) { std::string input(chatInputBuffer); game::ChatType type; std::string message = input; std::string target; // Track if a channel shortcut should change the chat type dropdown int switchChatType = -1; // Check for slash commands if (input.size() > 1 && input[0] == '/') { std::string command = input.substr(1); size_t spacePos = command.find(' '); std::string cmd = (spacePos != std::string::npos) ? command.substr(0, spacePos) : command; // Convert command to lowercase for comparison std::string cmdLower = cmd; for (char& c : cmdLower) c = std::tolower(c); // Special commands if (cmdLower == "logout") { core::Application::getInstance().logoutToLogin(); chatInputBuffer[0] = '\0'; return; } // /invite command if (cmdLower == "invite" && spacePos != std::string::npos) { std::string targetName = command.substr(spacePos + 1); gameHandler.inviteToGroup(targetName); chatInputBuffer[0] = '\0'; return; } // /inspect command if (cmdLower == "inspect") { gameHandler.inspectTarget(); chatInputBuffer[0] = '\0'; return; } // /time command if (cmdLower == "time") { gameHandler.queryServerTime(); chatInputBuffer[0] = '\0'; return; } // /played command if (cmdLower == "played") { gameHandler.requestPlayedTime(); chatInputBuffer[0] = '\0'; return; } // /who commands if (cmdLower == "who" || cmdLower == "whois" || cmdLower == "online" || cmdLower == "players") { std::string query; if (spacePos != std::string::npos) { query = command.substr(spacePos + 1); // Trim leading/trailing whitespace size_t first = query.find_first_not_of(" \t\r\n"); if (first == std::string::npos) { query.clear(); } else { size_t last = query.find_last_not_of(" \t\r\n"); query = query.substr(first, last - first + 1); } } if ((cmdLower == "whois") && query.empty()) { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /whois "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "who" && (query == "help" || query == "?")) { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Who commands: /who [name/filter], /whois , /online"; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } gameHandler.queryWho(query); chatInputBuffer[0] = '\0'; return; } // /roll command if (cmdLower == "roll" || cmdLower == "random" || cmdLower == "rnd") { uint32_t minRoll = 1; uint32_t maxRoll = 100; if (spacePos != std::string::npos) { std::string args = command.substr(spacePos + 1); size_t dashPos = args.find('-'); size_t spacePos2 = args.find(' '); if (dashPos != std::string::npos) { // Format: /roll 1-100 try { minRoll = std::stoul(args.substr(0, dashPos)); maxRoll = std::stoul(args.substr(dashPos + 1)); } catch (...) {} } else if (spacePos2 != std::string::npos) { // Format: /roll 1 100 try { minRoll = std::stoul(args.substr(0, spacePos2)); maxRoll = std::stoul(args.substr(spacePos2 + 1)); } catch (...) {} } else { // Format: /roll 100 (means 1-100) try { maxRoll = std::stoul(args); } catch (...) {} } } gameHandler.randomRoll(minRoll, maxRoll); chatInputBuffer[0] = '\0'; return; } // /friend or /addfriend command if (cmdLower == "friend" || cmdLower == "addfriend") { if (spacePos != std::string::npos) { std::string args = command.substr(spacePos + 1); size_t subCmdSpace = args.find(' '); if (cmdLower == "friend" && subCmdSpace != std::string::npos) { std::string subCmd = args.substr(0, subCmdSpace); std::transform(subCmd.begin(), subCmd.end(), subCmd.begin(), ::tolower); if (subCmd == "add") { std::string playerName = args.substr(subCmdSpace + 1); gameHandler.addFriend(playerName); chatInputBuffer[0] = '\0'; return; } else if (subCmd == "remove" || subCmd == "delete" || subCmd == "rem") { std::string playerName = args.substr(subCmdSpace + 1); gameHandler.removeFriend(playerName); chatInputBuffer[0] = '\0'; return; } } else { // /addfriend name or /friend name (assume add) gameHandler.addFriend(args); chatInputBuffer[0] = '\0'; return; } } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /friend add or /friend remove "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /removefriend or /delfriend command if (cmdLower == "removefriend" || cmdLower == "delfriend" || cmdLower == "remfriend") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.removeFriend(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /removefriend "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /ignore command if (cmdLower == "ignore") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.addIgnore(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /ignore "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /unignore command if (cmdLower == "unignore") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.removeIgnore(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /unignore "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /dismount command if (cmdLower == "dismount") { gameHandler.dismount(); chatInputBuffer[0] = '\0'; return; } // /sit command if (cmdLower == "sit") { gameHandler.setStandState(1); // 1 = sit chatInputBuffer[0] = '\0'; return; } // /stand command if (cmdLower == "stand") { gameHandler.setStandState(0); // 0 = stand chatInputBuffer[0] = '\0'; return; } // /kneel command if (cmdLower == "kneel") { gameHandler.setStandState(8); // 8 = kneel chatInputBuffer[0] = '\0'; return; } // /logout command (already exists but using /logout instead of going to login) if (cmdLower == "logout" || cmdLower == "camp") { gameHandler.requestLogout(); chatInputBuffer[0] = '\0'; return; } // /cancellogout command if (cmdLower == "cancellogout") { gameHandler.cancelLogout(); chatInputBuffer[0] = '\0'; return; } // /helm command if (cmdLower == "helm" || cmdLower == "helmet" || cmdLower == "showhelm") { gameHandler.toggleHelm(); chatInputBuffer[0] = '\0'; return; } // /cloak command if (cmdLower == "cloak" || cmdLower == "showcloak") { gameHandler.toggleCloak(); chatInputBuffer[0] = '\0'; return; } // /follow command if (cmdLower == "follow" || cmdLower == "f") { gameHandler.followTarget(); chatInputBuffer[0] = '\0'; return; } // /assist command if (cmdLower == "assist") { gameHandler.assistTarget(); chatInputBuffer[0] = '\0'; return; } // /pvp command if (cmdLower == "pvp") { gameHandler.togglePvp(); chatInputBuffer[0] = '\0'; return; } // /ginfo command if (cmdLower == "ginfo" || cmdLower == "guildinfo") { gameHandler.requestGuildInfo(); chatInputBuffer[0] = '\0'; return; } // /groster command if (cmdLower == "groster" || cmdLower == "guildroster") { gameHandler.requestGuildRoster(); chatInputBuffer[0] = '\0'; return; } // /gmotd command if (cmdLower == "gmotd" || cmdLower == "guildmotd") { if (spacePos != std::string::npos) { std::string motd = command.substr(spacePos + 1); gameHandler.setGuildMotd(motd); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /gmotd "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /gpromote command if (cmdLower == "gpromote" || cmdLower == "guildpromote") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.promoteGuildMember(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /gpromote "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /gdemote command if (cmdLower == "gdemote" || cmdLower == "guilddemote") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.demoteGuildMember(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /gdemote "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /gquit command if (cmdLower == "gquit" || cmdLower == "guildquit" || cmdLower == "leaveguild") { gameHandler.leaveGuild(); chatInputBuffer[0] = '\0'; return; } // /ginvite command if (cmdLower == "ginvite" || cmdLower == "guildinvite") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.inviteToGuild(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /ginvite "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /gkick command if (cmdLower == "gkick" || cmdLower == "guildkick") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.kickGuildMember(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /gkick "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /gdisband command if (cmdLower == "gdisband" || cmdLower == "guilddisband") { gameHandler.disbandGuild(); chatInputBuffer[0] = '\0'; return; } // /gleader command if (cmdLower == "gleader" || cmdLower == "guildleader") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.setGuildLeader(playerName); chatInputBuffer[0] = '\0'; return; } game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /gleader "; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // /readycheck command if (cmdLower == "readycheck" || cmdLower == "rc") { gameHandler.initiateReadyCheck(); chatInputBuffer[0] = '\0'; return; } // /ready command (respond yes to ready check) if (cmdLower == "ready") { gameHandler.respondToReadyCheck(true); chatInputBuffer[0] = '\0'; return; } // /notready command (respond no to ready check) if (cmdLower == "notready" || cmdLower == "nr") { gameHandler.respondToReadyCheck(false); chatInputBuffer[0] = '\0'; return; } // /yield or /forfeit command if (cmdLower == "yield" || cmdLower == "forfeit" || cmdLower == "surrender") { gameHandler.forfeitDuel(); chatInputBuffer[0] = '\0'; return; } // AFK command if (cmdLower == "afk" || cmdLower == "away") { std::string afkMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; gameHandler.toggleAfk(afkMsg); chatInputBuffer[0] = '\0'; return; } // DND command if (cmdLower == "dnd" || cmdLower == "busy") { std::string dndMsg = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; gameHandler.toggleDnd(dndMsg); chatInputBuffer[0] = '\0'; return; } // Reply command if (cmdLower == "r" || cmdLower == "reply") { std::string lastSender = gameHandler.getLastWhisperSender(); if (lastSender.empty()) { game::MessageChatData errMsg; errMsg.type = game::ChatType::SYSTEM; errMsg.language = game::ChatLanguage::UNIVERSAL; errMsg.message = "No one has whispered you yet."; gameHandler.addLocalChatMessage(errMsg); chatInputBuffer[0] = '\0'; return; } // Set whisper target to last whisper sender strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; if (spacePos != std::string::npos) { // /r message — send reply immediately std::string replyMsg = command.substr(spacePos + 1); gameHandler.sendChatMessage(game::ChatType::WHISPER, replyMsg, lastSender); } // Switch to whisper tab selectedChatType = 4; chatInputBuffer[0] = '\0'; return; } // Party/Raid management commands if (cmdLower == "uninvite" || cmdLower == "kick") { if (spacePos != std::string::npos) { std::string playerName = command.substr(spacePos + 1); gameHandler.uninvitePlayer(playerName); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Usage: /uninvite "; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "leave" || cmdLower == "leaveparty") { gameHandler.leaveParty(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "maintank" || cmdLower == "mt") { if (gameHandler.hasTarget()) { gameHandler.setMainTank(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a player to set as main tank."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "mainassist" || cmdLower == "ma") { if (gameHandler.hasTarget()) { gameHandler.setMainAssist(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a player to set as main assist."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "clearmaintank") { gameHandler.clearMainTank(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "clearmainassist") { gameHandler.clearMainAssist(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "raidinfo") { gameHandler.requestRaidInfo(); chatInputBuffer[0] = '\0'; return; } // Combat and Trade commands if (cmdLower == "duel") { if (gameHandler.hasTarget()) { gameHandler.proposeDuel(gameHandler.getTargetGuid()); } else if (spacePos != std::string::npos) { // Target player by name (would need name-to-GUID lookup) game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a player to challenge to a duel."; gameHandler.addLocalChatMessage(msg); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a player to challenge to a duel."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "trade") { if (gameHandler.hasTarget()) { gameHandler.initiateTrade(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a player to trade with."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "startattack") { if (gameHandler.hasTarget()) { gameHandler.startAutoAttack(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You have no target."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "stopattack") { gameHandler.stopAutoAttack(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "stopcasting") { gameHandler.stopCasting(); chatInputBuffer[0] = '\0'; return; } // Targeting commands if (cmdLower == "cleartarget") { gameHandler.clearTarget(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "targetenemy") { gameHandler.targetEnemy(false); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "targetfriend") { gameHandler.targetFriend(false); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "targetlasttarget" || cmdLower == "targetlast") { gameHandler.targetLastTarget(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "targetlastenemy") { gameHandler.targetEnemy(true); // Reverse direction chatInputBuffer[0] = '\0'; return; } if (cmdLower == "targetlastfriend") { gameHandler.targetFriend(true); // Reverse direction chatInputBuffer[0] = '\0'; return; } if (cmdLower == "focus") { if (gameHandler.hasTarget()) { gameHandler.setFocus(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must target a unit to set as focus."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } if (cmdLower == "clearfocus") { gameHandler.clearFocus(); chatInputBuffer[0] = '\0'; return; } // /unstuck command — resets player position to floor height if (cmdLower == "unstuck") { gameHandler.unstuck(); chatInputBuffer[0] = '\0'; return; } // /unstuckgy command — move to nearest graveyard if (cmdLower == "unstuckgy") { gameHandler.unstuckGy(); chatInputBuffer[0] = '\0'; return; } // /transport board — board test transport if (cmdLower == "transport board") { auto* tm = gameHandler.getTransportManager(); if (tm) { // Test transport GUID uint64_t testTransportGuid = 0x1000000000000001ULL; // Place player at center of deck (rough estimate) glm::vec3 deckCenter(0.0f, 0.0f, 5.0f); gameHandler.setPlayerOnTransport(testTransportGuid, deckCenter); game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Boarded test transport. Use '/transport leave' to disembark."; gameHandler.addLocalChatMessage(msg); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Transport system not available."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } // /transport leave — disembark from transport if (cmdLower == "transport leave") { if (gameHandler.isOnTransport()) { gameHandler.clearPlayerTransport(); game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "Disembarked from transport."; gameHandler.addLocalChatMessage(msg); } else { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You are not on a transport."; gameHandler.addLocalChatMessage(msg); } chatInputBuffer[0] = '\0'; return; } // Chat channel slash commands // If used without a message (e.g. just "/s"), switch the chat type dropdown bool isChannelCommand = false; if (cmdLower == "s" || cmdLower == "say") { type = game::ChatType::SAY; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 0; } else if (cmdLower == "y" || cmdLower == "yell" || cmdLower == "shout") { type = game::ChatType::YELL; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 1; } else if (cmdLower == "p" || cmdLower == "party") { type = game::ChatType::PARTY; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 2; } else if (cmdLower == "g" || cmdLower == "guild") { type = game::ChatType::GUILD; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 3; } else if (cmdLower == "raid" || cmdLower == "rsay" || cmdLower == "ra") { type = game::ChatType::RAID; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 5; } else if (cmdLower == "raidwarning" || cmdLower == "rw") { type = game::ChatType::RAID_WARNING; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 8; } else if (cmdLower == "officer" || cmdLower == "o" || cmdLower == "osay") { type = game::ChatType::OFFICER; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 6; } else if (cmdLower == "battleground" || cmdLower == "bg") { type = game::ChatType::BATTLEGROUND; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 7; } else if (cmdLower == "instance" || cmdLower == "i") { // Instance chat uses PARTY chat type type = game::ChatType::PARTY; message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : ""; isChannelCommand = true; switchChatType = 9; } else if (cmdLower == "join") { // /join ChannelName [password] if (spacePos != std::string::npos) { std::string rest = command.substr(spacePos + 1); size_t pwStart = rest.find(' '); std::string channelName = (pwStart != std::string::npos) ? rest.substr(0, pwStart) : rest; std::string password = (pwStart != std::string::npos) ? rest.substr(pwStart + 1) : ""; gameHandler.joinChannel(channelName, password); } chatInputBuffer[0] = '\0'; return; } else if (cmdLower == "leave") { // /leave ChannelName if (spacePos != std::string::npos) { std::string channelName = command.substr(spacePos + 1); gameHandler.leaveChannel(channelName); } chatInputBuffer[0] = '\0'; return; } else if (cmdLower.size() == 1 && cmdLower[0] >= '1' && cmdLower[0] <= '9') { // /1 msg, /2 msg — channel shortcuts int channelIdx = cmdLower[0] - '0'; std::string channelName = gameHandler.getChannelByIndex(channelIdx); if (!channelName.empty() && spacePos != std::string::npos) { message = command.substr(spacePos + 1); type = game::ChatType::CHANNEL; target = channelName; isChannelCommand = true; } else if (channelName.empty()) { game::MessageChatData errMsg; errMsg.type = game::ChatType::SYSTEM; errMsg.message = "You are not in channel " + std::to_string(channelIdx) + "."; gameHandler.addLocalChatMessage(errMsg); chatInputBuffer[0] = '\0'; return; } else { chatInputBuffer[0] = '\0'; return; } } else if (cmdLower == "w" || cmdLower == "whisper" || cmdLower == "tell" || cmdLower == "t") { switchChatType = 4; if (spacePos != std::string::npos) { std::string rest = command.substr(spacePos + 1); size_t msgStart = rest.find(' '); if (msgStart != std::string::npos) { // /w PlayerName message — send whisper immediately target = rest.substr(0, msgStart); message = rest.substr(msgStart + 1); type = game::ChatType::WHISPER; isChannelCommand = true; // Set whisper target for future messages strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; } else { // /w PlayerName — switch to whisper mode with target set strncpy(whisperTargetBuffer, rest.c_str(), sizeof(whisperTargetBuffer) - 1); whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; message = ""; isChannelCommand = true; } } else { // Just "/w" — switch to whisper mode message = ""; isChannelCommand = true; } } // Check for emote commands if (!isChannelCommand) { std::string targetName; const std::string* targetNamePtr = nullptr; if (gameHandler.hasTarget()) { auto targetEntity = gameHandler.getTarget(); if (targetEntity) { targetName = getEntityName(targetEntity); if (!targetName.empty()) targetNamePtr = &targetName; } } std::string emoteText = rendering::Renderer::getEmoteText(cmdLower, targetNamePtr); if (!emoteText.empty()) { // Play the emote animation auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { renderer->playEmote(cmdLower); } // Send CMSG_TEXT_EMOTE to server uint32_t dbcId = rendering::Renderer::getEmoteDbcId(cmdLower); if (dbcId != 0) { uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; gameHandler.sendTextEmote(dbcId, targetGuid); } // Add local chat message game::MessageChatData msg; msg.type = game::ChatType::TEXT_EMOTE; msg.language = game::ChatLanguage::COMMON; msg.message = emoteText; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // Not a recognized command — fall through and send as normal chat if (!isChannelCommand) { message = input; } } // If no valid command found and starts with /, just send as-is if (!isChannelCommand && message == input) { // Use the selected chat type from dropdown switch (selectedChatType) { case 0: type = game::ChatType::SAY; break; case 1: type = game::ChatType::YELL; break; case 2: type = game::ChatType::PARTY; break; case 3: type = game::ChatType::GUILD; break; case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break; case 5: type = game::ChatType::RAID; break; case 6: type = game::ChatType::OFFICER; break; case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY default: type = game::ChatType::SAY; break; } } } else { // No slash command, use the selected chat type from dropdown switch (selectedChatType) { case 0: type = game::ChatType::SAY; break; case 1: type = game::ChatType::YELL; break; case 2: type = game::ChatType::PARTY; break; case 3: type = game::ChatType::GUILD; break; case 4: type = game::ChatType::WHISPER; target = whisperTargetBuffer; break; case 5: type = game::ChatType::RAID; break; case 6: type = game::ChatType::OFFICER; break; case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY default: type = game::ChatType::SAY; break; } } // Whisper shortcuts to PortBot/GMBot: translate to GM teleport commands. if (type == game::ChatType::WHISPER && isPortBotTarget(target)) { std::string cmd = buildPortBotCommand(message); game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; if (cmd.empty() || cmd == "__help__") { msg.message = "PortBot: /w PortBot . Aliases: sw if darn org tb uc shatt dal. Also supports '.tele ...' or 'xyz x y z [map [o]]'."; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } gameHandler.sendChatMessage(game::ChatType::SAY, cmd, ""); msg.message = "PortBot executed: " + cmd; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // Validate whisper has a target if (type == game::ChatType::WHISPER && target.empty()) { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = "You must specify a player name for whisper."; gameHandler.addLocalChatMessage(msg); chatInputBuffer[0] = '\0'; return; } // Don't send empty messages — but switch chat type if a channel shortcut was used if (!message.empty()) { gameHandler.sendChatMessage(type, message, target); } // Switch chat type dropdown when channel shortcut used (with or without message) if (switchChatType >= 0) { selectedChatType = switchChatType; } // Clear input chatInputBuffer[0] = '\0'; } } const char* GameScreen::getChatTypeName(game::ChatType type) const { switch (type) { case game::ChatType::SAY: return "SAY"; case game::ChatType::YELL: return "YELL"; case game::ChatType::EMOTE: return "EMOTE"; case game::ChatType::TEXT_EMOTE: return "EMOTE"; case game::ChatType::PARTY: return "PARTY"; case game::ChatType::GUILD: return "GUILD"; case game::ChatType::OFFICER: return "OFFICER"; case game::ChatType::RAID: return "RAID"; case game::ChatType::RAID_LEADER: return "RAID LEADER"; case game::ChatType::RAID_WARNING: return "RAID WARNING"; case game::ChatType::BATTLEGROUND: return "BATTLEGROUND"; case game::ChatType::BATTLEGROUND_LEADER: return "BG LEADER"; case game::ChatType::WHISPER: return "WHISPER"; case game::ChatType::WHISPER_INFORM: return "TO"; case game::ChatType::SYSTEM: return "SYSTEM"; case game::ChatType::MONSTER_SAY: return "SAY"; case game::ChatType::MONSTER_YELL: return "YELL"; case game::ChatType::MONSTER_EMOTE: return "EMOTE"; case game::ChatType::CHANNEL: return "CHANNEL"; case game::ChatType::ACHIEVEMENT: return "ACHIEVEMENT"; case game::ChatType::DND: return "DND"; case game::ChatType::AFK: return "AFK"; default: return "UNKNOWN"; } } ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { switch (type) { case game::ChatType::SAY: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White case game::ChatType::YELL: return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red case game::ChatType::EMOTE: return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange case game::ChatType::TEXT_EMOTE: return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange case game::ChatType::PARTY: return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue case game::ChatType::GUILD: return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green case game::ChatType::OFFICER: return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green case game::ChatType::RAID: return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange case game::ChatType::RAID_LEADER: return ImVec4(1.0f, 0.4f, 0.0f, 1.0f); // Darker orange case game::ChatType::RAID_WARNING: return ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red case game::ChatType::BATTLEGROUND: return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange-gold case game::ChatType::BATTLEGROUND_LEADER: return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange case game::ChatType::WHISPER: return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink case game::ChatType::WHISPER_INFORM: return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink case game::ChatType::SYSTEM: return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow case game::ChatType::MONSTER_SAY: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (same as SAY) case game::ChatType::MONSTER_YELL: return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red (same as YELL) case game::ChatType::MONSTER_EMOTE: return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE) case game::ChatType::CHANNEL: return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink case game::ChatType::ACHIEVEMENT: return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow default: return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray } } void GameScreen::updateCharacterGeosets(game::Inventory& inventory) { auto& app = core::Application::getInstance(); auto* renderer = app.getRenderer(); if (!renderer) return; uint32_t instanceId = renderer->getCharacterInstanceId(); if (instanceId == 0) return; auto* charRenderer = renderer->getCharacterRenderer(); if (!charRenderer) return; auto* assetManager = app.getAssetManager(); // Load ItemDisplayInfo.dbc for geosetGroup lookup std::shared_ptr displayInfoDbc; if (assetManager) { displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); } // Helper: get geosetGroup field for an equipped item's displayInfoId // DBC binary fields: 7=geosetGroup_1, 8=geosetGroup_2, 9=geosetGroup_3 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(recIdx), 7 + groupField); }; // Helper: find first equipped item matching inventoryType, return its displayInfoId auto findEquippedDisplayId = [&](std::initializer_list types) -> uint32_t { for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(s)); if (!slot.empty()) { for (uint8_t t : types) { if (slot.item.inventoryType == t) return slot.item.displayInfoId; } } } return 0; }; // Helper: check if any equipment slot has the given inventoryType auto hasEquippedType = [&](std::initializer_list types) -> bool { for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(s)); if (!slot.empty()) { for (uint8_t t : types) { if (slot.item.inventoryType == t) return true; } } } return false; }; // Base geosets always present (group 0: IDs 0-99, some models use up to 27) std::unordered_set geosets; for (uint16_t i = 0; i <= 99; i++) { geosets.insert(i); } // Hair/facial geosets must match the active character's appearance, otherwise // we end up forcing a default hair mesh (often perceived as "wrong hair"). { uint8_t hairStyleId = 0; uint8_t facialId = 0; if (auto* gh = app.getGameHandler()) { if (const auto* ch = gh->getActiveCharacter()) { hairStyleId = static_cast((ch->appearanceBytes >> 16) & 0xFF); facialId = ch->facialFeatures; } } geosets.insert(static_cast(100 + hairStyleId + 1)); // Group 1 hair geosets.insert(static_cast(200 + facialId + 1)); // Group 2 facial } geosets.insert(702); // Ears: visible (default) geosets.insert(2002); // Bare feet mesh (group 20 = CG_FEET, always on) // CharGeosets mapping (verified via vertex bounding boxes): // Group 4 (401+) = GLOVES (forearm area, Z~1.1-1.4) // Group 5 (501+) = BOOTS (shin area, Z~0.1-0.6) // Group 8 (801+) = WRISTBANDS/SLEEVES (controlled by chest armor) // Group 9 (901+) = KNEEPADS // Group 13 (1301+) = TROUSERS/PANTS // Group 15 (1501+) = CAPE/CLOAK // Group 20 (2002) = FEET // Gloves: inventoryType 10 → group 4 (forearms) // 401=bare forearms, 402+=glove styles covering forearm { uint32_t did = findEquippedDisplayId({10}); uint32_t gg = getGeosetGroup(did, 0); geosets.insert(static_cast(gg > 0 ? 401 + gg : 401)); } // Boots: inventoryType 8 → group 5 (shins/lower legs) // 501=narrow bare shin, 502=wider (matches thigh width better). Use 502 as bare default. // When boots equipped, gg selects boot style: 501+gg (gg=1→502, gg=2→503, etc.) { uint32_t did = findEquippedDisplayId({8}); uint32_t gg = getGeosetGroup(did, 0); geosets.insert(static_cast(gg > 0 ? 501 + gg : 502)); } // Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe) // Controls group 8 (wristbands/sleeve length): 801=bare wrists, 802+=sleeve styles // Also controls group 13 (trousers) via GeosetGroup[2] for robes { uint32_t did = findEquippedDisplayId({4, 5, 20}); uint32_t gg = getGeosetGroup(did, 0); geosets.insert(static_cast(gg > 0 ? 801 + gg : 801)); // Robe kilt: GeosetGroup[2] > 0 → show kilt legs (1302+) uint32_t gg3 = getGeosetGroup(did, 2); if (gg3 > 0) { geosets.insert(static_cast(1301 + gg3)); } } // Kneepads: group 9 (always default 902) geosets.insert(902); // Legs/Pants: inventoryType 7 → group 13 (trousers/thighs) // 1301=bare legs, 1302+=pant/kilt styles { uint32_t did = findEquippedDisplayId({7}); uint32_t gg = getGeosetGroup(did, 0); // Only add if robe hasn't already set a kilt geoset if (geosets.count(1302) == 0 && geosets.count(1303) == 0) { geosets.insert(static_cast(gg > 0 ? 1301 + gg : 1301)); } } // Back/Cloak: inventoryType 16 → group 15 geosets.insert(hasEquippedType({16}) ? 1502 : 1501); // Tabard: inventoryType 19 → group 12 if (hasEquippedType({19})) { geosets.insert(1201); } charRenderer->setActiveGeosets(instanceId, geosets); } void GameScreen::updateCharacterTextures(game::Inventory& inventory) { auto& app = core::Application::getInstance(); auto* renderer = app.getRenderer(); if (!renderer) return; auto* charRenderer = renderer->getCharacterRenderer(); if (!charRenderer) return; auto* assetManager = app.getAssetManager(); if (!assetManager) return; const auto& bodySkinPath = app.getBodySkinPath(); const auto& underwearPaths = app.getUnderwearPaths(); uint32_t skinSlot = app.getSkinTextureSlotIndex(); if (bodySkinPath.empty()) return; // Component directory names indexed by region static const char* componentDirs[] = { "ArmUpperTexture", // 0 "ArmLowerTexture", // 1 "HandTexture", // 2 "TorsoUpperTexture", // 3 "TorsoLowerTexture", // 4 "LegUpperTexture", // 5 "LegLowerTexture", // 6 "FootTexture", // 7 }; // Load ItemDisplayInfo.dbc auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); if (!displayInfoDbc) return; const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; // Texture component region fields (8 regions: ArmUpper..Foot) // Binary DBC (23 fields) has textures at 14+ const uint32_t texRegionFields[8] = { idiL ? (*idiL)["TextureArmUpper"] : 14u, idiL ? (*idiL)["TextureArmLower"] : 15u, idiL ? (*idiL)["TextureHand"] : 16u, idiL ? (*idiL)["TextureTorsoUpper"]: 17u, idiL ? (*idiL)["TextureTorsoLower"]: 18u, idiL ? (*idiL)["TextureLegUpper"] : 19u, idiL ? (*idiL)["TextureLegLower"] : 20u, idiL ? (*idiL)["TextureFoot"] : 21u, }; // Collect equipment texture regions from all equipped items std::vector> regionLayers; for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(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++) { std::string texName = displayInfoDbc->getString( static_cast(recIdx), texRegionFields[region]); if (texName.empty()) continue; // Actual MPQ files have a gender suffix: _M (male), _F (female), _U (unisex) // Try gender-specific first, then unisex fallback std::string base = "Item\\TextureComponents\\" + std::string(componentDirs[region]) + "\\" + texName; // Determine gender suffix from active character bool isFemale = false; if (auto* gh = app.getGameHandler()) { if (auto* ch = gh->getActiveCharacter()) { isFemale = (ch->gender == game::Gender::FEMALE) || (ch->gender == game::Gender::NONBINARY && ch->useFemaleModel); } } std::string genderPath = base + (isFemale ? "_F.blp" : "_M.blp"); std::string unisexPath = base + "_U.blp"; std::string fullPath; if (assetManager->fileExists(genderPath)) { fullPath = genderPath; } else if (assetManager->fileExists(unisexPath)) { fullPath = unisexPath; } else { // Last resort: try without suffix fullPath = base + ".blp"; } regionLayers.emplace_back(region, fullPath); } } // Re-composite: base skin + underwear + equipment regions // Clear composite cache first to prevent stale textures from being reused charRenderer->clearCompositeCache(); // Use per-instance texture override (not model-level) to avoid deleting cached composites. uint32_t instanceId = renderer->getCharacterInstanceId(); GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers); if (newTex != 0 && instanceId != 0) { charRenderer->setTextureSlotOverride(instanceId, static_cast(skinSlot), newTex); } // Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin) uint32_t cloakSlot = app.getCloakTextureSlotIndex(); if (cloakSlot > 0 && instanceId != 0) { // Find equipped cloak (inventoryType 16) uint32_t cloakDisplayId = 0; for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(s)); if (!slot.empty() && slot.item.inventoryType == 16 && slot.item.displayInfoId != 0) { cloakDisplayId = slot.item.displayInfoId; break; } } if (cloakDisplayId > 0) { int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId); if (recIdx >= 0) { // DBC field 3 = modelTexture_1 (cape texture name) const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; std::string capeName = displayInfoDbc->getString(static_cast(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3); if (!capeName.empty()) { std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp"; GLuint capeTex = charRenderer->loadTexture(capePath); if (capeTex != 0) { charRenderer->setTextureSlotOverride(instanceId, static_cast(cloakSlot), capeTex); LOG_INFO("Cloak texture applied: ", capePath); } } } } else { // No cloak equipped — clear override so model's default (white) shows charRenderer->clearTextureSlotOverride(instanceId, static_cast(cloakSlot)); } } } // ============================================================ // World Map // ============================================================ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { auto& app = core::Application::getInstance(); auto* renderer = app.getRenderer(); auto* assetMgr = app.getAssetManager(); if (!renderer || !assetMgr) return; worldMap.initialize(assetMgr); // Keep map name in sync with minimap's map name auto* minimap = renderer->getMinimap(); if (minimap) { worldMap.setMapName(minimap->getMapName()); } worldMap.setServerExplorationMask( gameHandler.getPlayerExploredZoneMasks(), gameHandler.hasPlayerExploredZoneMasks()); glm::vec3 playerPos = renderer->getCharacterPosition(); auto* window = app.getWindow(); int screenW = window ? window->getWidth() : 1280; int screenH = window ? window->getHeight() : 720; worldMap.render(playerPos, screenW, screenH); } // ============================================================ // Action Bar (Phase 3) // ============================================================ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) { if (spellId == 0 || !am) return 0; // Check cache first auto cit = spellIconCache_.find(spellId); if (cit != spellIconCache_.end()) return cit->second; // Lazy-load SpellIcon.dbc and Spell.dbc icon IDs if (!spellIconDbLoaded_) { spellIconDbLoaded_ = true; // Load SpellIcon.dbc: field 0 = ID, field 1 = icon path auto iconDbc = am->loadDBC("SpellIcon.dbc"); const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr; if (iconDbc && iconDbc->isLoaded()) { for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) { uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0); std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1); if (!path.empty() && id > 0) { spellIconPaths_[id] = path; } } } // Load Spell.dbc: SpellIconID field auto spellDbc = am->loadDBC("Spell.dbc"); const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; if (spellDbc && spellDbc->isLoaded()) { uint32_t fieldCount = spellDbc->getFieldCount(); // Try expansion layout first auto tryLoadIcons = [&](uint32_t idField, uint32_t iconField) { spellIconIds_.clear(); if (iconField >= fieldCount) return; for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) { uint32_t id = spellDbc->getUInt32(i, idField); uint32_t iconId = spellDbc->getUInt32(i, iconField); if (id > 0 && iconId > 0) { spellIconIds_[id] = iconId; } } }; // If the DBC has WotLK-range field count (≥200 fields), it's the binary // WotLK Spell.dbc (CSV fallback). Use WotLK layout regardless of expansion, // since Turtle/Classic CSV files are garbled and fall back to WotLK binary. if (fieldCount >= 200) { tryLoadIcons(0, 133); // WotLK IconID field } else if (spellL) { tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]); } // Fallback to WotLK field 133 if expansion layout yielded nothing if (spellIconIds_.empty() && fieldCount > 133) { tryLoadIcons(0, 133); } } } // Look up spellId -> SpellIconID -> icon path auto iit = spellIconIds_.find(spellId); if (iit == spellIconIds_.end()) { spellIconCache_[spellId] = 0; return 0; } auto pit = spellIconPaths_.find(iit->second); if (pit == spellIconPaths_.end()) { spellIconCache_[spellId] = 0; return 0; } // Path from DBC has no extension — append .blp std::string iconPath = pit->second + ".blp"; auto blpData = am->readFile(iconPath); if (blpData.empty()) { spellIconCache_[spellId] = 0; return 0; } auto image = pipeline::BLPLoader::load(blpData); if (!image.isValid()) { spellIconCache_[spellId] = 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); spellIconCache_[spellId] = texId; return texId; } void GameScreen::renderActionBar(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; auto* assetMgr = core::Application::getInstance().getAssetManager(); float slotSize = 48.0f; float spacing = 4.0f; float padding = 8.0f; float barW = 12 * slotSize + 11 * spacing + padding * 2; float barH = slotSize + 24.0f; float barX = (screenW - barW) / 2.0f; float barY = screenH - barH; ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); if (ImGui::Begin("##ActionBar", nullptr, flags)) { const auto& bar = gameHandler.getActionBar(); static const char* keyLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="}; for (int i = 0; i < 12; ++i) { if (i > 0) ImGui::SameLine(0, spacing); ImGui::BeginGroup(); ImGui::PushID(i); const auto& slot = bar[i]; bool onCooldown = !slot.isReady(); auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); if (!name.empty()) return name; return "Spell #" + std::to_string(spellId); }; // Try to get icon texture for this slot GLuint iconTex = 0; const game::ItemDef* barItemDef = nullptr; uint32_t itemDisplayInfoId = 0; std::string itemNameFromQuery; if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { iconTex = getSpellIcon(slot.id, assetMgr); } else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { // Search backpack auto& inv = gameHandler.getInventory(); for (int bi = 0; bi < inv.getBackpackSize(); bi++) { const auto& bs = inv.getBackpackSlot(bi); if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; } } // Search equipped slots if (!barItemDef) { for (int ei = 0; ei < game::Inventory::NUM_EQUIP_SLOTS; ei++) { const auto& es = inv.getEquipSlot(static_cast(ei)); if (!es.empty() && es.item.itemId == slot.id) { barItemDef = &es.item; break; } } } // Search extra bags if (!barItemDef) { for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS && !barItemDef; bag++) { for (int si = 0; si < inv.getBagSize(bag); si++) { const auto& bs = inv.getBagSlot(bag, si); if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; } } } } if (barItemDef && barItemDef->displayInfoId != 0) { itemDisplayInfoId = barItemDef->displayInfoId; } // Fallback: use item info cache (from server query responses) if (itemDisplayInfoId == 0) { if (auto* info = gameHandler.getItemInfo(slot.id)) { itemDisplayInfoId = info->displayInfoId; if (itemNameFromQuery.empty() && !info->name.empty()) itemNameFromQuery = info->name; } } if (itemDisplayInfoId != 0) { iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId); } } bool clicked = false; if (iconTex) { // Render icon-based button ImVec4 tintColor(1, 1, 1, 1); ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f); if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); bgColor = ImVec4(0.1f, 0.1f, 0.1f, 0.8f); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), ImVec2(0, 0), ImVec2(1, 1), bgColor, tintColor); } else { // Fallback to text button if (onCooldown) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); } else if (slot.isEmpty()) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); } else { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); } char label[32]; if (slot.type == game::ActionBarSlot::SPELL) { std::string spellName = getSpellName(slot.id); if (spellName.size() > 6) spellName = spellName.substr(0, 6); snprintf(label, sizeof(label), "%s", spellName.c_str()); } else if (slot.type == game::ActionBarSlot::ITEM && barItemDef) { std::string itemName = barItemDef->name; if (itemName.size() > 6) itemName = itemName.substr(0, 6); snprintf(label, sizeof(label), "%s", itemName.c_str()); } else if (slot.type == game::ActionBarSlot::ITEM) { snprintf(label, sizeof(label), "Item"); } else if (slot.type == game::ActionBarSlot::MACRO) { snprintf(label, sizeof(label), "Macro"); } else { snprintf(label, sizeof(label), "--"); } clicked = ImGui::Button(label, ImVec2(slotSize, slotSize)); ImGui::PopStyleColor(); } bool rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseReleased(ImGuiMouseButton_Left); // Drop dragged spell from spellbook onto this slot // (mouse release over slot — button click won't fire since press was in spellbook) if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) { gameHandler.setActionBarSlot(i, game::ActionBarSlot::SPELL, spellbookScreen.getDragSpellId()); spellbookScreen.consumeDragSpell(); } else if (clicked && inventoryScreen.isHoldingItem()) { // Drop held item from inventory onto action bar const auto& held = inventoryScreen.getHeldItem(); gameHandler.setActionBarSlot(i, game::ActionBarSlot::ITEM, held.itemId); inventoryScreen.returnHeldItem(gameHandler.getInventory()); } else if (clicked && actionBarDragSlot_ >= 0) { // Dropping a dragged action bar slot onto another slot - swap or place if (i != actionBarDragSlot_) { const auto& dragSrc = bar[actionBarDragSlot_]; auto srcType = dragSrc.type; auto srcId = dragSrc.id; gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id); gameHandler.setActionBarSlot(i, srcType, srcId); } actionBarDragSlot_ = -1; actionBarDragIcon_ = 0; } else if (clicked && !slot.isEmpty()) { // Left-click on non-empty slot: cast spell or use item if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) { uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; gameHandler.castSpell(slot.id, target); } else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { gameHandler.useItemById(slot.id); } } else if (rightClicked && !slot.isEmpty()) { // Right-click on non-empty slot: pick up for dragging actionBarDragSlot_ = i; actionBarDragIcon_ = iconTex; } // Tooltip if (ImGui::IsItemHovered() && !slot.isEmpty() && slot.id != 0) { ImGui::BeginTooltip(); if (slot.type == game::ActionBarSlot::SPELL) { std::string fullName = getSpellName(slot.id); ImGui::Text("%s", fullName.c_str()); // Hearthstone: show bind point info if (slot.id == 8690) { uint32_t mapId = 0; glm::vec3 pos; if (gameHandler.getHomeBind(mapId, pos)) { const char* mapName = "Unknown"; switch (mapId) { case 0: mapName = "Eastern Kingdoms"; break; case 1: mapName = "Kalimdor"; break; case 530: mapName = "Outland"; break; case 571: mapName = "Northrend"; break; } ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName); } ImGui::TextDisabled("Use: Teleport home"); } } else if (slot.type == game::ActionBarSlot::ITEM) { if (barItemDef && !barItemDef->name.empty()) { ImGui::Text("%s", barItemDef->name.c_str()); } else if (!itemNameFromQuery.empty()) { ImGui::Text("%s", itemNameFromQuery.c_str()); } else { ImGui::Text("Item #%u", slot.id); } } // Show cooldown time remaining if (onCooldown) { float cd = slot.cooldownRemaining; if (cd >= 60.0f) { int mins = static_cast(cd) / 60; int secs = static_cast(cd) % 60; ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Cooldown: %d min %d sec", mins, secs); } else { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Cooldown: %.1f sec", cd); } } ImGui::EndTooltip(); } // Cooldown overlay if (onCooldown && iconTex) { // Draw cooldown text centered over the icon ImVec2 btnMin = ImGui::GetItemRectMin(); ImVec2 btnMax = ImGui::GetItemRectMax(); char cdText[16]; snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining); ImVec2 textSize = ImGui::CalcTextSize(cdText); float cx = btnMin.x + (btnMax.x - btnMin.x - textSize.x) * 0.5f; float cy = btnMin.y + (btnMax.y - btnMin.y - textSize.y) * 0.5f; ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 0, 255), cdText); } else if (onCooldown) { char cdText[16]; snprintf(cdText, sizeof(cdText), "%.0f", slot.cooldownRemaining); ImGui::SetCursorPosY(ImGui::GetCursorPosY() - slotSize / 2 - 8); ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", cdText); } // Key label below ImGui::TextDisabled("%s", keyLabels[i]); ImGui::PopID(); ImGui::EndGroup(); } } ImGui::End(); ImGui::PopStyleColor(); ImGui::PopStyleVar(4); // Handle action bar drag: render icon at cursor and detect drop outside if (actionBarDragSlot_ >= 0) { ImVec2 mousePos = ImGui::GetMousePos(); // Draw dragged icon at cursor if (actionBarDragIcon_) { ImGui::GetForegroundDrawList()->AddImage( (ImTextureID)(uintptr_t)actionBarDragIcon_, ImVec2(mousePos.x - 20, mousePos.y - 20), ImVec2(mousePos.x + 20, mousePos.y + 20)); } else { ImGui::GetForegroundDrawList()->AddRectFilled( ImVec2(mousePos.x - 20, mousePos.y - 20), ImVec2(mousePos.x + 20, mousePos.y + 20), IM_COL32(80, 80, 120, 180)); } // On right mouse release, check if outside the action bar area if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { bool insideBar = (mousePos.x >= barX && mousePos.x <= barX + barW && mousePos.y >= barY && mousePos.y <= barY + barH); if (!insideBar) { // Dropped outside - clear the slot gameHandler.setActionBarSlot(actionBarDragSlot_, game::ActionBarSlot::EMPTY, 0); } actionBarDragSlot_ = -1; actionBarDragIcon_ = 0; } } } // ============================================================ // Bag Bar // ============================================================ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; auto* assetMgr = core::Application::getInstance().getAssetManager(); float slotSize = 42.0f; float spacing = 4.0f; float padding = 6.0f; // 5 slots: backpack + 4 bags float barW = 5 * slotSize + 4 * spacing + padding * 2; float barH = slotSize + padding * 2; // Position in bottom right corner float barX = screenW - barW - 10.0f; float barY = screenH - barH - 10.0f; ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); if (ImGui::Begin("##BagBar", nullptr, flags)) { auto& inv = gameHandler.getInventory(); // Load backpack icon if needed if (!backpackIconTexture_ && assetMgr && assetMgr->isInitialized()) { auto blpData = assetMgr->readFile("Interface\\Buttons\\Button-Backpack-Up.blp"); if (!blpData.empty()) { auto image = pipeline::BLPLoader::load(blpData); if (image.isValid()) { glGenTextures(1, &backpackIconTexture_); glBindTexture(GL_TEXTURE_2D, backpackIconTexture_); 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); } } } // Track bag slot screen rects for drop detection ImVec2 bagSlotMins[4], bagSlotMaxs[4]; GLuint bagIcons[4] = {}; // Slots 1-4: Bag slots (leftmost) for (int i = 0; i < 4; ++i) { if (i > 0) ImGui::SameLine(0, spacing); ImGui::PushID(i + 1); game::EquipSlot bagSlot = static_cast(static_cast(game::EquipSlot::BAG1) + i); const auto& bagItem = inv.getEquipSlot(bagSlot); GLuint bagIcon = 0; if (!bagItem.empty() && bagItem.item.displayInfoId != 0) { bagIcon = inventoryScreen.getItemIcon(bagItem.item.displayInfoId); } bagIcons[i] = bagIcon; // Render the slot as an invisible button so we control all interaction ImVec2 cpos = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("##bagSlot", ImVec2(slotSize, slotSize)); bagSlotMins[i] = cpos; bagSlotMaxs[i] = ImVec2(cpos.x + slotSize, cpos.y + slotSize); ImDrawList* dl = ImGui::GetWindowDrawList(); // Draw background + icon if (bagIcon) { dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(25, 25, 25, 230)); dl->AddImage((ImTextureID)(uintptr_t)bagIcon, bagSlotMins[i], bagSlotMaxs[i]); } else { dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(38, 38, 38, 204)); } // Hover highlight bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); if (hovered && bagBarPickedSlot_ < 0) { dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 100)); } // Track which slot was pressed for drag detection if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && bagBarPickedSlot_ < 0 && bagIcon) { bagBarDragSource_ = i; } // Click toggles bag open/close (handled in mouse release section below) // Dim the slot being dragged if (bagBarPickedSlot_ == i) { dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(0, 0, 0, 150)); } // Tooltip if (hovered && bagBarPickedSlot_ < 0) { if (bagIcon) ImGui::SetTooltip("%s", bagItem.item.name.c_str()); else ImGui::SetTooltip("Empty Bag Slot"); } // Open bag indicator if (inventoryScreen.isSeparateBags() && inventoryScreen.isBagOpen(i)) { dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f); } // Accept dragged item from inventory if (hovered && inventoryScreen.isHoldingItem()) { const auto& heldItem = inventoryScreen.getHeldItem(); if (heldItem.bagSlots > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { auto& inventory = gameHandler.getInventory(); inventory.setEquipSlot(bagSlot, heldItem); inventoryScreen.returnHeldItem(inventory); } } ImGui::PopID(); } // Drag lifecycle: press on a slot sets bagBarDragSource_, // dragging 3+ pixels promotes to bagBarPickedSlot_ (visual drag), // releasing completes swap or click if (bagBarDragSource_ >= 0) { if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 3.0f) && bagBarPickedSlot_ < 0) { // Mouse moved enough — start visual drag bagBarPickedSlot_ = bagBarDragSource_; } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { if (bagBarPickedSlot_ >= 0) { // Was dragging — check for drop target ImVec2 mousePos = ImGui::GetIO().MousePos; int dropTarget = -1; for (int j = 0; j < 4; ++j) { if (j == bagBarPickedSlot_) continue; if (mousePos.x >= bagSlotMins[j].x && mousePos.x <= bagSlotMaxs[j].x && mousePos.y >= bagSlotMins[j].y && mousePos.y <= bagSlotMaxs[j].y) { dropTarget = j; break; } } if (dropTarget >= 0) { gameHandler.swapBagSlots(bagBarPickedSlot_, dropTarget); } bagBarPickedSlot_ = -1; } else { // Was just a click (no drag) — toggle bag int slot = bagBarDragSource_; auto equip = static_cast(static_cast(game::EquipSlot::BAG1) + slot); if (!inv.getEquipSlot(equip).empty()) { if (inventoryScreen.isSeparateBags()) inventoryScreen.toggleBag(slot); else inventoryScreen.toggle(); } } bagBarDragSource_ = -1; } } // Backpack (rightmost slot) ImGui::SameLine(0, spacing); ImGui::PushID(0); if (backpackIconTexture_) { if (ImGui::ImageButton("##backpack", (ImTextureID)(uintptr_t)backpackIconTexture_, ImVec2(slotSize, slotSize), ImVec2(0, 0), ImVec2(1, 1), ImVec4(0.1f, 0.1f, 0.1f, 0.9f), ImVec4(1, 1, 1, 1))) { if (inventoryScreen.isSeparateBags()) inventoryScreen.toggleBackpack(); else inventoryScreen.toggle(); } } else { if (ImGui::Button("B", ImVec2(slotSize, slotSize))) { if (inventoryScreen.isSeparateBags()) inventoryScreen.toggleBackpack(); else inventoryScreen.toggle(); } } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Backpack"); } if (inventoryScreen.isSeparateBags() && inventoryScreen.isBackpackOpen()) { ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 r0 = ImGui::GetItemRectMin(); ImVec2 r1 = ImGui::GetItemRectMax(); dl->AddRect(r0, r1, IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f); } ImGui::PopID(); } ImGui::End(); ImGui::PopStyleColor(); ImGui::PopStyleVar(4); // Draw dragged bag icon following cursor if (bagBarPickedSlot_ >= 0) { auto& inv2 = gameHandler.getInventory(); auto pickedEquip = static_cast( static_cast(game::EquipSlot::BAG1) + bagBarPickedSlot_); const auto& pickedItem = inv2.getEquipSlot(pickedEquip); GLuint pickedIcon = 0; if (!pickedItem.empty() && pickedItem.item.displayInfoId != 0) { pickedIcon = inventoryScreen.getItemIcon(pickedItem.item.displayInfoId); } if (pickedIcon) { ImVec2 mousePos = ImGui::GetIO().MousePos; float sz = 40.0f; ImVec2 p0(mousePos.x - sz * 0.5f, mousePos.y - sz * 0.5f); ImVec2 p1(mousePos.x + sz * 0.5f, mousePos.y + sz * 0.5f); ImDrawList* fg = ImGui::GetForegroundDrawList(); fg->AddImage((ImTextureID)(uintptr_t)pickedIcon, p0, p1); fg->AddRect(p0, p1, IM_COL32(200, 200, 200, 255), 0.0f, 0, 2.0f); } } } // ============================================================ // XP Bar // ============================================================ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp(); if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized) uint32_t currentXp = gameHandler.getPlayerXp(); auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; // Position just above the action bar float slotSize = 48.0f; float spacing = 4.0f; float padding = 8.0f; float barW = 12 * slotSize + 11 * spacing + padding * 2; float barH = slotSize + 24.0f; float actionBarY = screenH - barH; float xpBarH = 20.0f; float xpBarW = barW; float xpBarX = (screenW - xpBarW) / 2.0f; float xpBarY = actionBarY - xpBarH - 2.0f; ImGui::SetNextWindowPos(ImVec2(xpBarX, xpBarY), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(xpBarW, xpBarH + 4.0f), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f)); if (ImGui::Begin("##XpBar", nullptr, flags)) { float pct = static_cast(currentXp) / static_cast(nextLevelXp); if (pct > 1.0f) pct = 1.0f; // Custom segmented XP bar (20 bubbles) ImVec2 barMin = ImGui::GetCursorScreenPos(); ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, xpBarH - 4.0f); ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); auto* drawList = ImGui::GetWindowDrawList(); ImU32 bg = IM_COL32(15, 15, 20, 220); ImU32 fg = IM_COL32(148, 51, 238, 255); ImU32 seg = IM_COL32(35, 35, 45, 255); drawList->AddRectFilled(barMin, barMax, bg, 2.0f); drawList->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f); float fillW = barSize.x * pct; if (fillW > 0.0f) { drawList->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), fg, 2.0f); } const int segments = 20; float segW = barSize.x / static_cast(segments); for (int i = 1; i < segments; ++i) { float x = barMin.x + segW * i; drawList->AddLine(ImVec2(x, barMin.y + 1.0f), ImVec2(x, barMax.y - 1.0f), seg, 1.0f); } char overlay[96]; snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp); ImVec2 textSize = ImGui::CalcTextSize(overlay); float tx = barMin.x + (barSize.x - textSize.x) * 0.5f; float ty = barMin.y + (barSize.y - textSize.y) * 0.5f; drawList->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), overlay); ImGui::Dummy(barSize); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(2); } // ============================================================ // Cast Bar (Phase 3) // ============================================================ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { if (!gameHandler.isCasting()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float barW = 300.0f; float barX = (screenW - barW) / 2.0f; float barY = screenH - 120.0f; ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(barW, 40), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.9f)); if (ImGui::Begin("##CastBar", nullptr, flags)) { float progress = gameHandler.getCastProgress(); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.6f, 0.2f, 1.0f)); char overlay[64]; uint32_t currentSpellId = gameHandler.getCurrentCastSpellId(); if (gameHandler.getCurrentCastSpellId() == 0) { snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining()); } else { const std::string& spellName = gameHandler.getSpellName(currentSpellId); if (!spellName.empty()) snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); else snprintf(overlay, sizeof(overlay), "Casting... (%.1fs)", gameHandler.getCastTimeRemaining()); } ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); ImGui::PopStyleColor(); } ImGui::End(); ImGui::PopStyleColor(); ImGui::PopStyleVar(); } // ============================================================ // Floating Combat Text (Phase 2) // ============================================================ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { const auto& entries = gameHandler.getCombatText(); if (entries.empty()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; // Render combat text entries overlaid on screen ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(screenW, 400)); ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; if (ImGui::Begin("##CombatText", nullptr, flags)) { // Incoming events (enemy attacks player) float near screen center (over the player). // Outgoing events (player attacks enemy) float on the right side (near the target). const float incomingX = screenW * 0.40f; const float outgoingX = screenW * 0.68f; int inIdx = 0, outIdx = 0; for (const auto& entry : entries) { float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME); float yOffset = 200.0f - entry.age * 60.0f; const bool outgoing = entry.isPlayerSource; ImVec4 color; char text[64]; switch (entry.type) { case game::CombatTextEntry::MELEE_DAMAGE: case game::CombatTextEntry::SPELL_DAMAGE: snprintf(text, sizeof(text), "-%d", entry.amount); color = outgoing ? ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red break; case game::CombatTextEntry::CRIT_DAMAGE: snprintf(text, sizeof(text), "-%d!", entry.amount); color = outgoing ? ImVec4(1.0f, 0.8f, 0.0f, alpha) : // Outgoing crit = bright yellow ImVec4(1.0f, 0.5f, 0.0f, alpha); // Incoming crit = orange break; case game::CombatTextEntry::HEAL: snprintf(text, sizeof(text), "+%d", entry.amount); color = ImVec4(0.3f, 1.0f, 0.3f, alpha); break; case game::CombatTextEntry::CRIT_HEAL: snprintf(text, sizeof(text), "+%d!", entry.amount); color = ImVec4(0.3f, 1.0f, 0.3f, alpha); break; case game::CombatTextEntry::MISS: snprintf(text, sizeof(text), "Miss"); color = ImVec4(0.7f, 0.7f, 0.7f, alpha); break; case game::CombatTextEntry::DODGE: // outgoing=true: enemy dodged player's attack // outgoing=false: player dodged incoming attack snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge"); color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) : ImVec4(0.4f, 0.9f, 1.0f, alpha); break; case game::CombatTextEntry::PARRY: snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry"); color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) : ImVec4(0.4f, 0.9f, 1.0f, alpha); break; default: snprintf(text, sizeof(text), "%d", entry.amount); color = ImVec4(1.0f, 1.0f, 1.0f, alpha); break; } // Outgoing → right side (near target), incoming → center-left (near player) int& idx = outgoing ? outIdx : inIdx; float baseX = outgoing ? outgoingX : incomingX; float xOffset = baseX + (idx % 3 - 1) * 60.0f; ++idx; ImGui::SetCursorPos(ImVec2(xOffset, yOffset)); ImGui::TextColored(color, "%s", text); } } ImGui::End(); } // ============================================================ // Party Frames (Phase 4) // ============================================================ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (!gameHandler.isInGroup()) return; const auto& partyData = gameHandler.getPartyData(); float frameY = 120.0f; ImGui::SetNextWindowPos(ImVec2(10.0f, frameY), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f)); if (ImGui::Begin("##PartyFrames", nullptr, flags)) { for (const auto& member : partyData.members) { ImGui::PushID(static_cast(member.guid)); // Clickable name to target if (ImGui::Selectable(member.name.c_str(), gameHandler.getTargetGuid() == member.guid)) { gameHandler.setTarget(member.guid); } // Try to show health from entity auto entity = gameHandler.getEntityManager().getEntity(member.guid); if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) { auto unit = std::static_pointer_cast(entity); uint32_t hp = unit->getHealth(); uint32_t maxHp = unit->getMaxHealth(); if (maxHp > 0) { float pct = static_cast(hp) / static_cast(maxHp); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::ProgressBar(pct, ImVec2(-1, 12), ""); ImGui::PopStyleColor(); } } ImGui::Separator(); ImGui::PopID(); } } ImGui::End(); ImGui::PopStyleColor(); ImGui::PopStyleVar(); } // ============================================================ // Group Invite Popup (Phase 4) // ============================================================ void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGroupInvite()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); if (ImGui::Begin("Group Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(130, 30))) { gameHandler.acceptGroupInvite(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(130, 30))) { gameHandler.declineGroupInvite(); } } ImGui::End(); } void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) { if (!gameHandler.hasPendingGuildInvite()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); if (ImGui::Begin("Guild Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { ImGui::TextWrapped("%s has invited you to join %s.", gameHandler.getPendingGuildInviterName().c_str(), gameHandler.getPendingGuildInviteGuildName().c_str()); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(155, 30))) { gameHandler.acceptGuildInvite(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(155, 30))) { gameHandler.declineGuildInvite(); } } ImGui::End(); } void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { // O key toggle (WoW default Social/Guild keybind) if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) { showGuildRoster_ = !showGuildRoster_; if (showGuildRoster_) { if (!gameHandler.isInGuild()) { gameHandler.addLocalChatMessage(game::MessageChatData{ game::ChatType::SYSTEM, game::ChatLanguage::UNIVERSAL, 0, "", 0, "", "You are not in a guild.", "", 0}); showGuildRoster_ = false; return; } // Re-query guild name if we have guildId but no name yet if (gameHandler.getGuildName().empty()) { const auto* ch = gameHandler.getActiveCharacter(); if (ch && ch->hasGuild()) { gameHandler.queryGuildInfo(ch->guildId); } } gameHandler.requestGuildRoster(); } } if (!showGuildRoster_) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once); ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once); std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Roster") : "Guild Roster"; bool open = showGuildRoster_; if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) { if (!gameHandler.hasGuildRoster()) { ImGui::Text("Loading roster..."); } else { const auto& roster = gameHandler.getGuildRoster(); // MOTD if (!roster.motd.empty()) { ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str()); ImGui::Separator(); } // Count online int onlineCount = 0; for (const auto& m : roster.members) { if (m.online) ++onlineCount; } ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount); ImGui::Separator(); const auto& rankNames = gameHandler.getGuildRankNames(); // Table if (ImGui::BeginTable("GuildRoster", 7, ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Sortable)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort); ImGui::TableSetupColumn("Rank"); ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f); ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f); ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("Note"); ImGui::TableSetupColumn("Officer Note"); ImGui::TableHeadersRow(); // Online members first, then offline auto sortedMembers = roster.members; std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) { if (a.online != b.online) return a.online > b.online; return a.name < b.name; }); static const char* classNames[] = { "Unknown", "Warrior", "Paladin", "Hunter", "Rogue", "Priest", "Death Knight", "Shaman", "Mage", "Warlock", "", "Druid" }; for (const auto& m : sortedMembers) { ImGui::TableNextRow(); ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%s", m.name.c_str()); // Right-click context menu if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { selectedGuildMember_ = m.name; ImGui::OpenPopup("GuildMemberContext"); } ImGui::TableNextColumn(); // Show rank name instead of index if (m.rankIndex < rankNames.size()) { ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str()); } else { ImGui::TextColored(textColor, "Rank %u", m.rankIndex); } ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%u", m.level); ImGui::TableNextColumn(); const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown"; ImGui::TextColored(textColor, "%s", className); ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%u", m.zoneId); ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%s", m.publicNote.c_str()); ImGui::TableNextColumn(); ImGui::TextColored(textColor, "%s", m.officerNote.c_str()); } ImGui::EndTable(); } // Context menu popup if (ImGui::BeginPopup("GuildMemberContext")) { ImGui::Text("%s", selectedGuildMember_.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Promote")) { gameHandler.promoteGuildMember(selectedGuildMember_); } if (ImGui::MenuItem("Demote")) { gameHandler.demoteGuildMember(selectedGuildMember_); } if (ImGui::MenuItem("Kick")) { gameHandler.kickGuildMember(selectedGuildMember_); } ImGui::Separator(); if (ImGui::MenuItem("Set Public Note...")) { showGuildNoteEdit_ = true; editingOfficerNote_ = false; guildNoteEditBuffer_[0] = '\0'; // Pre-fill with existing note for (const auto& mem : roster.members) { if (mem.name == selectedGuildMember_) { snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str()); break; } } } if (ImGui::MenuItem("Set Officer Note...")) { showGuildNoteEdit_ = true; editingOfficerNote_ = true; guildNoteEditBuffer_[0] = '\0'; for (const auto& mem : roster.members) { if (mem.name == selectedGuildMember_) { snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str()); break; } } } ImGui::Separator(); if (ImGui::MenuItem("Set as Leader")) { gameHandler.setGuildLeader(selectedGuildMember_); } ImGui::EndPopup(); } // Note edit modal if (showGuildNoteEdit_) { ImGui::OpenPopup("EditGuildNote"); showGuildNoteEdit_ = false; } if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("%s %s for %s:", editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str()); ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_)); if (ImGui::Button("Save")) { if (editingOfficerNote_) { gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_); } else { gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_); } ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } } } ImGui::End(); showGuildRoster_ = open; } // ============================================================ // Buff/Debuff Bar (Phase 3) // ============================================================ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { const auto& auras = gameHandler.getPlayerAuras(); if (auras.empty()) return; // Count non-empty auras int activeCount = 0; for (const auto& a : auras) { if (!a.isEmpty()) activeCount++; } if (activeCount == 0) return; auto* assetMgr = core::Application::getInstance().getAssetManager(); // Position below the player frame in top-left constexpr float ICON_SIZE = 32.0f; constexpr int ICONS_PER_ROW = 8; float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f; // Dock under player frame in top-left (player frame is at 10, 30 with ~110px height) ImGui::SetNextWindowPos(ImVec2(10.0f, 145.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); if (ImGui::Begin("##BuffBar", nullptr, flags)) { int shown = 0; for (size_t i = 0; i < auras.size() && shown < 16; ++i) { const auto& aura = auras[i]; if (aura.isEmpty()) continue; if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine(); ImGui::PushID(static_cast(i)); bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); // Try to get spell icon GLuint iconTex = 0; if (assetMgr) { iconTex = getSpellIcon(aura.spellId, assetMgr); } if (iconTex) { ImGui::PushStyleColor(ImGuiCol_Button, borderColor); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); ImGui::ImageButton("##aura", (ImTextureID)(uintptr_t)iconTex, ImVec2(ICON_SIZE - 4, ICON_SIZE - 4)); ImGui::PopStyleVar(); ImGui::PopStyleColor(); } else { ImGui::PushStyleColor(ImGuiCol_Button, borderColor); char label[8]; snprintf(label, sizeof(label), "%u", aura.spellId); ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE)); ImGui::PopStyleColor(); } // Right-click to cancel buffs / dismount if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { if (gameHandler.isMounted()) { gameHandler.dismount(); } else if (isBuff) { gameHandler.cancelAura(aura.spellId); } } // Tooltip with spell name and live countdown if (ImGui::IsItemHovered()) { std::string name = spellbookScreen.lookupSpellName(aura.spellId, assetMgr); if (name.empty()) name = "Spell #" + std::to_string(aura.spellId); uint64_t nowMs = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); int32_t remaining = aura.getRemainingMs(nowMs); if (remaining > 0) { int seconds = remaining / 1000; if (seconds < 60) { ImGui::SetTooltip("%s (%ds)", name.c_str(), seconds); } else { ImGui::SetTooltip("%s (%dm %ds)", name.c_str(), seconds / 60, seconds % 60); } } else { ImGui::SetTooltip("%s", name.c_str()); } } ImGui::PopID(); shown++; } } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); } // ============================================================ // Loot Window (Phase 5) // ============================================================ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { if (!gameHandler.isLootWindowOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); bool open = true; if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { const auto& loot = gameHandler.getCurrentLoot(); // Gold if (loot.gold > 0) { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc", loot.getGold(), loot.getSilver(), loot.getCopper()); ImGui::Separator(); } // 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; game::ItemQuality quality = game::ItemQuality::COMMON; if (info && !info->name.empty()) { itemName = info->name; quality = static_cast(info->quality); } else { itemName = "Item #" + std::to_string(item.itemId); } ImVec4 qColor = InventoryScreen::getQualityColor(quality); // Get item icon uint32_t displayId = item.displayInfoId; if (displayId == 0 && info) displayId = info->displayInfoId; GLuint iconTex = inventoryScreen.getItemIcon(displayId); ImVec2 cursor = ImGui::GetCursorScreenPos(); float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f); // Invisible selectable for click handling if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) { lootSlotClicked = item.slotIndex; } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { lootSlotClicked = item.slotIndex; } bool hovered = ImGui::IsItemHovered(); ImDrawList* drawList = ImGui::GetWindowDrawList(); // Draw hover highlight if (hovered) { drawList->AddRectFilled(cursor, ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f, cursor.y + rowH), IM_COL32(255, 255, 255, 30)); } // Draw icon if (iconTex) { drawList->AddImage((ImTextureID)(uintptr_t)iconTex, cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize)); drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), ImGui::ColorConvertFloat4ToU32(qColor)); } else { drawList->AddRectFilled(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), IM_COL32(40, 40, 50, 200)); drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), IM_COL32(80, 80, 80, 200)); } // Draw item name float textX = cursor.x + iconSize + 6.0f; float textY = cursor.y + 2.0f; drawList->AddText(ImVec2(textX, textY), ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str()); // Draw count if > 1 if (item.count > 1) { char countStr[32]; snprintf(countStr, sizeof(countStr), "x%u", item.count); float countY = textY + ImGui::GetTextLineHeight(); drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr); } 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) { gameHandler.closeLoot(); } ImGui::Spacing(); if (ImGui::Button("Close", ImVec2(-1, 0))) { gameHandler.closeLoot(); } } ImGui::End(); if (!open) { gameHandler.closeLoot(); } } // ============================================================ // Gossip Window (Phase 5) // ============================================================ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { if (!gameHandler.isGossipWindowOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always); bool open = true; if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { const auto& gossip = gameHandler.getCurrentGossip(); // NPC name (from creature cache) auto npcEntity = gameHandler.getEntityManager().getEntity(gossip.npcGuid); if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(npcEntity); if (!unit->getName().empty()) { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str()); ImGui::Separator(); } } ImGui::Spacing(); // Gossip option icons - matches WoW GossipOptionIcon enum static const char* gossipIcons[] = { "[Chat]", // 0 = GOSSIP_ICON_CHAT "[Vendor]", // 1 = GOSSIP_ICON_VENDOR "[Taxi]", // 2 = GOSSIP_ICON_TAXI "[Trainer]", // 3 = GOSSIP_ICON_TRAINER "[Interact]", // 4 = GOSSIP_ICON_INTERACT_1 "[Interact]", // 5 = GOSSIP_ICON_INTERACT_2 "[Banker]", // 6 = GOSSIP_ICON_MONEY_BAG (banker) "[Chat]", // 7 = GOSSIP_ICON_TALK "[Tabard]", // 8 = GOSSIP_ICON_TABARD "[Battlemaster]", // 9 = GOSSIP_ICON_BATTLE "[Option]", // 10 = GOSSIP_ICON_DOT }; // Default text for server-sent gossip option placeholders static const std::unordered_map gossipPlaceholders = { {"GOSSIP_OPTION_BANKER", "I would like to check my deposit box."}, {"GOSSIP_OPTION_AUCTIONEER", "I'd like to browse your auctions."}, {"GOSSIP_OPTION_VENDOR", "I want to browse your goods."}, {"GOSSIP_OPTION_TAXIVENDOR", "I'd like to fly."}, {"GOSSIP_OPTION_TRAINER", "I seek training."}, {"GOSSIP_OPTION_INNKEEPER", "Make this inn your home."}, {"GOSSIP_OPTION_SPIRITGUIDE", "Return me to life."}, {"GOSSIP_OPTION_SPIRITHEALER", "Bring me back to life."}, {"GOSSIP_OPTION_STABLEPET", "I'd like to stable my pet."}, {"GOSSIP_OPTION_ARMORER", "I need to repair my equipment."}, {"GOSSIP_OPTION_GOSSIP", "What can you tell me?"}, {"GOSSIP_OPTION_BATTLEFIELD", "I'd like to go to the battleground."}, {"GOSSIP_OPTION_TABARDDESIGNER", "I want to create a guild tabard."}, {"GOSSIP_OPTION_PETITIONER", "I want to create a guild."}, }; for (const auto& opt : gossip.options) { ImGui::PushID(static_cast(opt.id)); // Determine icon label - use text-based detection for shared icons const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]"; if (opt.text == "GOSSIP_OPTION_AUCTIONEER") icon = "[Auctioneer]"; else if (opt.text == "GOSSIP_OPTION_BANKER") icon = "[Banker]"; else if (opt.text == "GOSSIP_OPTION_VENDOR") icon = "[Vendor]"; else if (opt.text == "GOSSIP_OPTION_TRAINER") icon = "[Trainer]"; else if (opt.text == "GOSSIP_OPTION_INNKEEPER") icon = "[Innkeeper]"; else if (opt.text == "GOSSIP_OPTION_STABLEPET") icon = "[Stable Master]"; else if (opt.text == "GOSSIP_OPTION_ARMORER") icon = "[Repair]"; // Resolve placeholder text from server std::string displayText = opt.text; auto placeholderIt = gossipPlaceholders.find(displayText); if (placeholderIt != gossipPlaceholders.end()) { displayText = placeholderIt->second; } std::string processedText = replaceGenderPlaceholders(displayText, gameHandler); std::string label = std::string(icon) + " " + processedText; if (ImGui::Selectable(label.c_str())) { gameHandler.selectGossipOption(opt.id); } ImGui::PopID(); } // Fallback: some spirit healers don't send gossip options. if (gossip.options.empty() && gameHandler.isPlayerGhost()) { bool isSpirit = false; if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(npcEntity); std::string name = unit->getName(); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); if (name.find("spirit healer") != std::string::npos || name.find("spirit guide") != std::string::npos) { isSpirit = true; } } if (isSpirit) { if (ImGui::Selectable("[Spiritguide] Return to Graveyard")) { gameHandler.activateSpiritHealer(gossip.npcGuid); gameHandler.closeGossip(); } } } // Quest items if (!gossip.quests.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:"); for (size_t qi = 0; qi < gossip.quests.size(); qi++) { const auto& quest = gossip.quests[qi]; ImGui::PushID(static_cast(qi)); char qlabel[256]; snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str()); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.3f, 1.0f)); if (ImGui::Selectable(qlabel)) { gameHandler.selectGossipQuest(quest.questId); } ImGui::PopStyleColor(); ImGui::PopID(); } } ImGui::Spacing(); if (ImGui::Button("Close", ImVec2(-1, 0))) { gameHandler.closeGossip(); } } ImGui::End(); if (!open) { gameHandler.closeGossip(); } } // ============================================================ // Quest Details Window // ============================================================ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) { if (!gameHandler.isQuestDetailsOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing); bool open = true; const auto& quest = gameHandler.getQuestDetails(); std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler); if (ImGui::Begin(processedTitle.c_str(), &open)) { // Quest description if (!quest.details.empty()) { std::string processedDetails = replaceGenderPlaceholders(quest.details, gameHandler); ImGui::TextWrapped("%s", processedDetails.c_str()); } // Objectives if (!quest.objectives.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Objectives:"); std::string processedObjectives = replaceGenderPlaceholders(quest.objectives, gameHandler); ImGui::TextWrapped("%s", processedObjectives.c_str()); } // Rewards if (quest.rewardXp > 0 || quest.rewardMoney > 0) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:"); if (quest.rewardXp > 0) { ImGui::Text(" %u experience", quest.rewardXp); } if (quest.rewardMoney > 0) { uint32_t gold = quest.rewardMoney / 10000; uint32_t silver = (quest.rewardMoney % 10000) / 100; uint32_t copper = quest.rewardMoney % 100; if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper); else if (silver > 0) ImGui::Text(" %us %uc", silver, copper); else ImGui::Text(" %uc", copper); } } if (quest.suggestedPlayers > 1) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Suggested players: %u", quest.suggestedPlayers); } // Accept / Decline buttons ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button("Accept", ImVec2(buttonW, 0))) { gameHandler.acceptQuest(); } ImGui::SameLine(); if (ImGui::Button("Decline", ImVec2(buttonW, 0))) { gameHandler.declineQuest(); } } ImGui::End(); if (!open) { gameHandler.declineQuest(); } } // ============================================================ // Quest Request Items Window (turn-in progress check) // ============================================================ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { if (!gameHandler.isQuestRequestItemsOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(450, 350), ImGuiCond_Appearing); bool open = true; const auto& quest = gameHandler.getQuestRequestItems(); auto countItemInInventory = [&](uint32_t itemId) -> uint32_t { const auto& inv = gameHandler.getInventory(); uint32_t total = 0; for (int i = 0; i < inv.getBackpackSize(); ++i) { const auto& slot = inv.getBackpackSlot(i); if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount; } for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; ++bag) { int bagSize = inv.getBagSize(bag); for (int s = 0; s < bagSize; ++s) { const auto& slot = inv.getBagSlot(bag, s); if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount; } } return total; }; std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler); if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) { if (!quest.completionText.empty()) { std::string processedCompletionText = replaceGenderPlaceholders(quest.completionText, gameHandler); ImGui::TextWrapped("%s", processedCompletionText.c_str()); } // Required items if (!quest.requiredItems.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:"); for (const auto& item : quest.requiredItems) { uint32_t have = countItemInInventory(item.itemId); bool enough = have >= item.count; auto* info = gameHandler.getItemInfo(item.itemId); const char* name = (info && info->valid) ? info->name.c_str() : nullptr; if (name && *name) { ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f), " %s %u/%u", name, have, item.count); } else { ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f), " Item %u %u/%u", item.itemId, have, item.count); } } } if (quest.requiredMoney > 0) { ImGui::Spacing(); uint32_t g = quest.requiredMoney / 10000; uint32_t s = (quest.requiredMoney % 10000) / 100; uint32_t c = quest.requiredMoney % 100; ImGui::Text("Required money: %ug %us %uc", g, s, c); } // Complete / Cancel buttons ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) { gameHandler.completeQuest(); } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) { gameHandler.closeQuestRequestItems(); } if (!quest.isCompletable()) { ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated."); } } ImGui::End(); if (!open) { gameHandler.closeQuestRequestItems(); } } // ============================================================ // Quest Offer Reward Window (choose reward) // ============================================================ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) { if (!gameHandler.isQuestOfferRewardOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing); bool open = true; const auto& quest = gameHandler.getQuestOfferReward(); static int selectedChoice = -1; // Auto-select if only one choice reward if (quest.choiceRewards.size() == 1 && selectedChoice == -1) { selectedChoice = 0; } std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler); if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) { if (!quest.rewardText.empty()) { std::string processedRewardText = replaceGenderPlaceholders(quest.rewardText, gameHandler); ImGui::TextWrapped("%s", processedRewardText.c_str()); } // Choice rewards (pick one) if (!quest.choiceRewards.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose a reward:"); for (size_t i = 0; i < quest.choiceRewards.size(); ++i) { const auto& item = quest.choiceRewards[i]; auto* info = gameHandler.getItemInfo(item.itemId); bool selected = (selectedChoice == static_cast(i)); // Get item icon if we have displayInfoId uint32_t iconTex = 0; if (info && info->valid && info->displayInfoId != 0) { iconTex = inventoryScreen.getItemIcon(info->displayInfoId); } // Quality color ImVec4 qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (poor) if (info && info->valid) { switch (info->quality) { case 1: qualityColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common (white) case 2: qualityColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); break; // Uncommon (green) case 3: qualityColor = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); break; // Rare (blue) case 4: qualityColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic (purple) case 5: qualityColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // Legendary (orange) } } // Render item with icon + visible selectable label ImGui::PushID(static_cast(i)); std::string label; if (info && info->valid && !info->name.empty()) { label = info->name; } else { label = "Item " + std::to_string(item.itemId); } if (item.count > 1) { label += " x" + std::to_string(item.count); } if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 24))) { selectedChoice = static_cast(i); } if (ImGui::IsItemHovered() && iconTex) { ImGui::SetTooltip("Reward option"); } if (iconTex) { ImGui::SameLine(); ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18)); } ImGui::PopID(); } } // Fixed rewards (always given) if (!quest.fixedRewards.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "You will also receive:"); for (const auto& item : quest.fixedRewards) { auto* info = gameHandler.getItemInfo(item.itemId); if (info && info->valid) ImGui::Text(" %s x%u", info->name.c_str(), item.count); else ImGui::Text(" Item %u x%u", item.itemId, item.count); } } // Money / XP rewards if (quest.rewardXp > 0 || quest.rewardMoney > 0) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Rewards:"); if (quest.rewardXp > 0) ImGui::Text(" %u experience", quest.rewardXp); if (quest.rewardMoney > 0) { uint32_t g = quest.rewardMoney / 10000; uint32_t s = (quest.rewardMoney % 10000) / 100; uint32_t c = quest.rewardMoney % 100; if (g > 0) ImGui::Text(" %ug %us %uc", g, s, c); else if (s > 0) ImGui::Text(" %us %uc", s, c); else ImGui::Text(" %uc", c); } } // Complete button ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; bool canComplete = quest.choiceRewards.empty() || selectedChoice >= 0; if (!canComplete) ImGui::BeginDisabled(); if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) { uint32_t rewardIdx = 0; if (!quest.choiceRewards.empty() && selectedChoice >= 0 && selectedChoice < static_cast(quest.choiceRewards.size())) { // Server expects the original slot index from its fixed-size reward array. rewardIdx = quest.choiceRewards[static_cast(selectedChoice)].choiceSlot; } gameHandler.chooseQuestReward(rewardIdx); selectedChoice = -1; } if (!canComplete) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) { gameHandler.closeQuestOfferReward(); selectedChoice = -1; } } ImGui::End(); if (!open) { gameHandler.closeQuestOfferReward(); selectedChoice = -1; } } // ============================================================ // Vendor Window (Phase 5) // ============================================================ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (!gameHandler.isVendorWindowOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Vendor", &open)) { const auto& vendor = gameHandler.getVendorItems(); // Show player money uint64_t money = gameHandler.getMoneyCopper(); uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); ImGui::Separator(); ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell"); ImGui::Separator(); const auto& buyback = gameHandler.getBuybackItems(); if (!buyback.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Buy Back"); if (ImGui::BeginTable("BuybackTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f); ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f); ImGui::TableHeadersRow(); // Show only the most recently sold item (LIFO). const int i = 0; const auto& entry = buyback[0]; uint32_t sellPrice = entry.item.sellPrice; if (sellPrice == 0) { if (auto* info = gameHandler.getItemInfo(entry.item.itemId); info && info->valid) { sellPrice = info->sellPrice; } } uint64_t price = static_cast(sellPrice) * static_cast(entry.count > 0 ? entry.count : 1); uint32_t g = static_cast(price / 10000); uint32_t s = static_cast((price / 100) % 100); uint32_t c = static_cast(price % 100); bool canAfford = money >= price; ImGui::TableNextRow(); ImGui::PushID(8000 + i); ImGui::TableSetColumnIndex(0); const char* name = entry.item.name.empty() ? "Unknown Item" : entry.item.name.c_str(); if (entry.count > 1) { ImGui::Text("%s x%u", name, entry.count); } else { ImGui::Text("%s", name); } ImGui::TableSetColumnIndex(1); if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::Text("%ug %us %uc", g, s, c); if (!canAfford) ImGui::PopStyleColor(); ImGui::TableSetColumnIndex(2); if (!canAfford) ImGui::BeginDisabled(); if (ImGui::SmallButton("Buy Back##buyback_0")) { gameHandler.buyBackItem(0); } if (!canAfford) ImGui::EndDisabled(); ImGui::PopID(); ImGui::EndTable(); } ImGui::Separator(); } if (vendor.items.empty()) { ImGui::TextDisabled("This vendor has nothing for sale."); } else { if (ImGui::BeginTable("VendorTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableSetupColumn("Stock", ImGuiTableColumnFlags_WidthFixed, 60.0f); ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f); ImGui::TableHeadersRow(); // Quality colors (matching WoW) static const ImVec4 qualityColors[] = { ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0 Poor (gray) ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1 Common (white) ImVec4(0.12f, 1.0f, 0.0f, 1.0f), // 2 Uncommon (green) ImVec4(0.0f, 0.44f, 0.87f, 1.0f), // 3 Rare (blue) ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4 Epic (purple) ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5 Legendary (orange) }; for (int vi = 0; vi < static_cast(vendor.items.size()); ++vi) { const auto& item = vendor.items[vi]; ImGui::TableNextRow(); ImGui::PushID(vi); ImGui::TableSetColumnIndex(0); auto* info = gameHandler.getItemInfo(item.itemId); if (info && info->valid) { uint32_t q = info->quality < 6 ? info->quality : 1; ImGui::TextColored(qualityColors[q], "%s", info->name.c_str()); // Tooltip with stats on hover if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextColored(qualityColors[q], "%s", info->name.c_str()); if (info->damageMax > 0.0f) { ImGui::Text("%.0f - %.0f Damage", info->damageMin, info->damageMax); if (info->delayMs > 0) { float speed = static_cast(info->delayMs) / 1000.0f; float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed; ImGui::Text("Speed %.2f", speed); ImGui::Text("%.1f damage per second", dps); } } if (info->armor > 0) ImGui::Text("Armor: %d", info->armor); if (info->stamina > 0) ImGui::Text("+%d Stamina", info->stamina); if (info->strength > 0) ImGui::Text("+%d Strength", info->strength); if (info->agility > 0) ImGui::Text("+%d Agility", info->agility); if (info->intellect > 0) ImGui::Text("+%d Intellect", info->intellect); if (info->spirit > 0) ImGui::Text("+%d Spirit", info->spirit); ImGui::EndTooltip(); } } else { ImGui::Text("Item %u", item.itemId); } ImGui::TableSetColumnIndex(1); if (item.buyPrice == 0 && item.extendedCost != 0) { // Token-only item (no gold cost) ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[Tokens]"); } else { uint32_t g = item.buyPrice / 10000; uint32_t s = (item.buyPrice / 100) % 100; uint32_t c = item.buyPrice % 100; bool canAfford = money >= item.buyPrice; if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::Text("%ug %us %uc", g, s, c); if (!canAfford) ImGui::PopStyleColor(); } ImGui::TableSetColumnIndex(2); if (item.maxCount < 0) { ImGui::Text("Inf"); } else { ImGui::Text("%d", item.maxCount); } ImGui::TableSetColumnIndex(3); std::string buyBtnId = "Buy##vendor_" + std::to_string(vi); if (ImGui::SmallButton(buyBtnId.c_str())) { gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, 1); } ImGui::PopID(); } ImGui::EndTable(); } } } ImGui::End(); if (!open) { gameHandler.closeVendor(); } } // ============================================================ // Trainer // ============================================================ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { if (!gameHandler.isTrainerWindowOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Trainer", &open)) { const auto& trainer = gameHandler.getTrainerSpells(); // NPC name auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid); if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(npcEntity); if (!unit->getName().empty()) { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str()); } } // Greeting if (!trainer.greeting.empty()) { ImGui::TextWrapped("%s", trainer.greeting.c_str()); } ImGui::Separator(); // Player money uint64_t money = gameHandler.getMoneyCopper(); uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); // Filter checkbox static bool showUnavailable = false; ImGui::Checkbox("Show unavailable spells", &showUnavailable); ImGui::Separator(); if (trainer.spells.empty()) { ImGui::TextDisabled("This trainer has nothing to teach you."); } else { // Known spells for checking const auto& knownSpells = gameHandler.getKnownSpells(); auto isKnown = [&](uint32_t id) { if (id == 0) return true; // Check if spell is in knownSpells list bool found = knownSpells.count(id); if (found) return true; // Also check if spell is in trainer list with state=2 (explicitly known) // state=0 means unavailable (could be no prereqs, wrong level, etc.) - don't count as known for (const auto& ts : trainer.spells) { if (ts.spellId == id && ts.state == 2) { return true; } } return false; }; uint32_t playerLevel = gameHandler.getPlayerLevel(); // Renders spell rows into the current table auto renderSpellRows = [&](const std::vector& spells) { for (const auto* spell : spells) { // Check prerequisites client-side first bool prereq1Met = isKnown(spell->chainNode1); bool prereq2Met = isKnown(spell->chainNode2); bool prereq3Met = isKnown(spell->chainNode3); bool prereqsMet = prereq1Met && prereq2Met && prereq3Met; bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel); bool alreadyKnown = isKnown(spell->spellId); // Dynamically determine effective state based on current prerequisites // Server sends state, but we override if prerequisites are now met uint8_t effectiveState = spell->state; if (spell->state == 1 && prereqsMet && levelMet) { // Server said unavailable, but we now meet all requirements effectiveState = 0; // Treat as available } // Filter: skip unavailable spells if checkbox is unchecked // Use effectiveState so spells with newly met prereqs aren't filtered if (!showUnavailable && effectiveState == 1) { continue; } ImGui::TableNextRow(); ImGui::PushID(static_cast(spell->spellId)); ImVec4 color; const char* statusLabel; // WotLK trainer states: 0=available, 1=unavailable, 2=known if (effectiveState == 2 || alreadyKnown) { color = ImVec4(0.3f, 0.9f, 0.3f, 1.0f); statusLabel = "Known"; } else if (effectiveState == 0) { color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); statusLabel = "Available"; } else { color = ImVec4(0.6f, 0.3f, 0.3f, 1.0f); statusLabel = "Unavailable"; } // Spell name ImGui::TableSetColumnIndex(0); const std::string& name = gameHandler.getSpellName(spell->spellId); const std::string& rank = gameHandler.getSpellRank(spell->spellId); if (!name.empty()) { if (!rank.empty()) ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str()); else ImGui::TextColored(color, "%s", name.c_str()); } else { ImGui::TextColored(color, "Spell #%u", spell->spellId); } if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); if (!name.empty()) { ImGui::Text("%s", name.c_str()); if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str()); } ImGui::Text("Status: %s", statusLabel); if (spell->reqLevel > 0) { ImVec4 lvlColor = levelMet ? ImVec4(0.7f, 0.7f, 0.7f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel); } if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue); auto showPrereq = [&](uint32_t node) { if (node == 0) return; bool met = isKnown(node); const std::string& pname = gameHandler.getSpellName(node); ImVec4 pcolor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); if (!pname.empty()) ImGui::TextColored(pcolor, "Requires: %s%s", pname.c_str(), met ? " (known)" : ""); else ImGui::TextColored(pcolor, "Requires: Spell #%u%s", node, met ? " (known)" : ""); }; showPrereq(spell->chainNode1); showPrereq(spell->chainNode2); showPrereq(spell->chainNode3); ImGui::EndTooltip(); } // Level ImGui::TableSetColumnIndex(1); ImGui::TextColored(color, "%u", spell->reqLevel); // Cost ImGui::TableSetColumnIndex(2); if (spell->spellCost > 0) { uint32_t g = spell->spellCost / 10000; uint32_t s = (spell->spellCost / 100) % 100; uint32_t c = spell->spellCost % 100; bool canAfford = money >= spell->spellCost; ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); ImGui::TextColored(costColor, "%ug %us %uc", g, s, c); } else { ImGui::TextColored(color, "Free"); } // Train button - only enabled if available, affordable, prereqs met ImGui::TableSetColumnIndex(3); // Use effectiveState so newly available spells (after learning prereqs) can be trained bool canTrain = !alreadyKnown && effectiveState == 0 && prereqsMet && levelMet && (money >= spell->spellCost); // Debug logging for first 3 spells to see why buttons are disabled static int logCount = 0; static uint64_t lastTrainerGuid = 0; if (trainer.trainerGuid != lastTrainerGuid) { logCount = 0; lastTrainerGuid = trainer.trainerGuid; } if (logCount < 3) { LOG_INFO("Trainer button debug: spellId=", spell->spellId, " alreadyKnown=", alreadyKnown, " state=", (int)spell->state, " prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")", " levelMet=", levelMet, " reqLevel=", spell->reqLevel, " playerLevel=", playerLevel, " chain1=", spell->chainNode1, " chain2=", spell->chainNode2, " chain3=", spell->chainNode3, " canAfford=", (money >= spell->spellCost), " canTrain=", canTrain); logCount++; } if (!canTrain) ImGui::BeginDisabled(); if (ImGui::SmallButton("Train")) { gameHandler.trainSpell(spell->spellId); } if (!canTrain) ImGui::EndDisabled(); ImGui::PopID(); } }; auto renderSpellTable = [&](const char* tableId, const std::vector& spells) { if (ImGui::BeginTable(tableId, 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f); ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f); ImGui::TableHeadersRow(); renderSpellRows(spells); ImGui::EndTable(); } }; const auto& tabs = gameHandler.getTrainerTabs(); if (tabs.size() > 1) { // Multiple tabs - show tab bar if (ImGui::BeginTabBar("TrainerTabs")) { for (size_t i = 0; i < tabs.size(); i++) { char tabLabel[64]; snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)", tabs[i].name.c_str(), tabs[i].spells.size()); if (ImGui::BeginTabItem(tabLabel)) { char tableId[32]; snprintf(tableId, sizeof(tableId), "TT%zu", i); renderSpellTable(tableId, tabs[i].spells); ImGui::EndTabItem(); } } ImGui::EndTabBar(); } } else { // Single tab or no categorization - flat list std::vector allSpells; allSpells.reserve(trainer.spells.size()); for (const auto& spell : trainer.spells) { allSpells.push_back(&spell); } renderSpellTable("TrainerTable", allSpells); } } } ImGui::End(); if (!open) { gameHandler.closeTrainer(); } } // ============================================================ // Teleporter Panel // ============================================================ // ============================================================ // Escape Menu // ============================================================ void GameScreen::renderEscapeMenu() { if (!showEscapeMenu) return; ImGuiIO& io = ImGui::GetIO(); float screenW = io.DisplaySize.x; float screenH = io.DisplaySize.y; ImVec2 size(260.0f, 220.0f); ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); ImGui::SetNextWindowSize(size, ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar; if (ImGui::Begin("##EscapeMenu", nullptr, flags)) { ImGui::Text("Game Menu"); ImGui::Separator(); if (ImGui::Button("Logout", ImVec2(-1, 0))) { core::Application::getInstance().logoutToLogin(); showEscapeMenu = false; showEscapeSettingsNotice = false; } if (ImGui::Button("Quit", ImVec2(-1, 0))) { auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { if (auto* music = renderer->getMusicManager()) { music->stopMusic(0.0f); } } core::Application::getInstance().shutdown(); } if (ImGui::Button("Settings", ImVec2(-1, 0))) { showEscapeSettingsNotice = false; showSettingsWindow = true; settingsInit = false; showEscapeMenu = false; } ImGui::Spacing(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f)); if (ImGui::Button("Back to Game", ImVec2(-1, 0))) { showEscapeMenu = false; showEscapeSettingsNotice = false; } ImGui::PopStyleVar(); } ImGui::End(); } // ============================================================ // Taxi Window // ============================================================ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { if (!gameHandler.isTaxiWindowOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always); bool open = true; if (ImGui::Begin("Flight Master", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { const auto& taxiData = gameHandler.getTaxiData(); const auto& nodes = gameHandler.getTaxiNodes(); uint32_t currentNode = gameHandler.getTaxiCurrentNode(); // Get current node's map to filter destinations uint32_t currentMapId = 0; auto curIt = nodes.find(currentNode); if (curIt != nodes.end()) { currentMapId = curIt->second.mapId; ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Current: %s", curIt->second.name.c_str()); ImGui::Separator(); } ImGui::Text("Select a destination:"); ImGui::Spacing(); static uint32_t selectedNodeId = 0; int destCount = 0; if (ImGui::BeginTable("TaxiNodes", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Destination", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 60.0f); ImGui::TableHeadersRow(); for (const auto& [nodeId, node] : nodes) { if (nodeId == currentNode) continue; if (node.mapId != currentMapId) continue; if (!taxiData.isNodeKnown(nodeId)) continue; uint32_t costCopper = gameHandler.getTaxiCostTo(nodeId); uint32_t gold = costCopper / 10000; uint32_t silver = (costCopper / 100) % 100; uint32_t copper = costCopper % 100; ImGui::PushID(static_cast(nodeId)); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); bool isSelected = (selectedNodeId == nodeId); if (ImGui::Selectable(node.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { selectedNodeId = nodeId; LOG_INFO("Taxi UI: Selected dest=", nodeId); if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { LOG_INFO("Taxi UI: Double-click activate dest=", nodeId); gameHandler.activateTaxi(nodeId); } } ImGui::TableSetColumnIndex(1); if (gold > 0) { ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); } else if (silver > 0) { ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us %uc", silver, copper); } else { ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper); } ImGui::TableSetColumnIndex(2); if (ImGui::SmallButton("Fly")) { selectedNodeId = nodeId; LOG_INFO("Taxi UI: Fly clicked dest=", nodeId); gameHandler.activateTaxi(nodeId); } ImGui::PopID(); destCount++; } ImGui::EndTable(); } if (destCount == 0) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No destinations available."); } ImGui::Spacing(); ImGui::Separator(); if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) { LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId); gameHandler.activateTaxi(selectedNodeId); } if (ImGui::Button("Close", ImVec2(-1, 0))) { gameHandler.closeTaxi(); } } ImGui::End(); if (!open) { gameHandler.closeTaxi(); } } // ============================================================ // Death Screen // ============================================================ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { if (!gameHandler.showDeathDialog()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; // Dark red overlay covering the whole screen ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(screenW, screenH)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.0f, 0.0f, 0.45f)); ImGui::Begin("##DeathOverlay", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing); ImGui::End(); ImGui::PopStyleColor(); // "Release Spirit" dialog centered on screen float dlgW = 280.0f; float dlgH = 100.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.0f, 0.0f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); if (ImGui::Begin("##DeathDialog", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { ImGui::Spacing(); // Center "You are dead." text const char* deathText = "You are dead."; float textW = ImGui::CalcTextSize(deathText).x; ImGui::SetCursorPosX((dlgW - textW) / 2); ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText); ImGui::Spacing(); ImGui::Spacing(); // Center the Release Spirit button float btnW = 180.0f; ImGui::SetCursorPosX((dlgW - btnW) / 2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f)); if (ImGui::Button("Release Spirit", ImVec2(btnW, 30))) { gameHandler.releaseSpirit(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) { if (!gameHandler.showResurrectDialog()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float dlgW = 300.0f; float dlgH = 110.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.8f, 1.0f)); if (ImGui::Begin("##ResurrectDialog", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { ImGui::Spacing(); const char* text = "Return to life?"; float textW = ImGui::CalcTextSize(text).x; ImGui::SetCursorPosX((dlgW - textW) / 2); ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text); ImGui::Spacing(); ImGui::Spacing(); float btnW = 100.0f; float spacing = 20.0f; ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); if (ImGui::Button("Accept", ImVec2(btnW, 30))) { gameHandler.acceptResurrect(); } ImGui::PopStyleColor(2); ImGui::SameLine(0, spacing); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f)); if (ImGui::Button("Decline", ImVec2(btnW, 30))) { gameHandler.declineResurrect(); } ImGui::PopStyleColor(2); } ImGui::End(); ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } // ============================================================ // Settings Window // ============================================================ void GameScreen::renderSettingsWindow() { if (!showSettingsWindow) return; auto* window = core::Application::getInstance().getWindow(); auto* renderer = core::Application::getInstance().getRenderer(); if (!window) return; static const int kResolutions[][2] = { {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}, }; static const int kResCount = sizeof(kResolutions) / sizeof(kResolutions[0]); constexpr int kDefaultResW = 1920; constexpr int kDefaultResH = 1080; constexpr bool kDefaultFullscreen = false; constexpr bool kDefaultVsync = true; constexpr bool kDefaultShadows = false; constexpr int kDefaultMusicVolume = 30; constexpr float kDefaultMouseSensitivity = 0.2f; constexpr bool kDefaultInvertMouse = false; int defaultResIndex = 0; for (int i = 0; i < kResCount; i++) { if (kResolutions[i][0] == kDefaultResW && kResolutions[i][1] == kDefaultResH) { defaultResIndex = i; break; } } if (!settingsInit) { pendingFullscreen = window->isFullscreen(); pendingVsync = window->isVsyncEnabled(); pendingShadows = renderer ? renderer->areShadowsEnabled() : true; if (renderer) { // Read non-volume settings from actual state (volumes come from saved settings) if (auto* cameraController = renderer->getCameraController()) { pendingMouseSensitivity = cameraController->getMouseSensitivity(); pendingInvertMouse = cameraController->isInvertMouse(); } } pendingResIndex = 0; int curW = window->getWidth(); int curH = window->getHeight(); for (int i = 0; i < kResCount; i++) { if (kResolutions[i][0] == curW && kResolutions[i][1] == curH) { pendingResIndex = i; break; } } pendingUiOpacity = static_cast(std::lround(uiOpacity_ * 100.0f)); pendingMinimapRotate = minimapRotate_; pendingMinimapSquare = minimapSquare_; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setRotateWithCamera(minimapRotate_); minimap->setSquareShape(minimapSquare_); } if (auto* zm = renderer->getZoneManager()) { pendingUseOriginalSoundtrack = zm->getUseOriginalSoundtrack(); } } settingsInit = true; } ImGuiIO& io = ImGui::GetIO(); float screenW = io.DisplaySize.x; float screenH = io.DisplaySize.y; ImVec2 size(520.0f, std::min(screenH * 0.9f, 720.0f)); ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); ImGui::SetNextWindowSize(size, ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar; if (ImGui::Begin("##SettingsWindow", nullptr, flags)) { ImGui::Text("Settings"); ImGui::Separator(); if (ImGui::BeginTabBar("SettingsTabs", ImGuiTabBarFlags_None)) { // ============================================================ // VIDEO TAB // ============================================================ if (ImGui::BeginTabItem("Video")) { ImGui::Spacing(); if (ImGui::Checkbox("Fullscreen", &pendingFullscreen)) { window->setFullscreen(pendingFullscreen); saveSettings(); } if (ImGui::Checkbox("VSync", &pendingVsync)) { window->setVsync(pendingVsync); saveSettings(); } if (ImGui::Checkbox("Shadows", &pendingShadows)) { if (renderer) renderer->setShadowsEnabled(pendingShadows); saveSettings(); } const char* resLabel = "Resolution"; const char* resItems[kResCount]; char resBuf[kResCount][16]; for (int i = 0; i < kResCount; i++) { snprintf(resBuf[i], sizeof(resBuf[i]), "%dx%d", kResolutions[i][0], kResolutions[i][1]); resItems[i] = resBuf[i]; } if (ImGui::Combo(resLabel, &pendingResIndex, resItems, kResCount)) { window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); saveSettings(); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) { pendingFullscreen = kDefaultFullscreen; pendingVsync = kDefaultVsync; pendingShadows = kDefaultShadows; pendingResIndex = defaultResIndex; window->setFullscreen(pendingFullscreen); window->setVsync(pendingVsync); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); if (renderer) renderer->setShadowsEnabled(pendingShadows); saveSettings(); } ImGui::EndTabItem(); } // ============================================================ // AUDIO TAB // ============================================================ if (ImGui::BeginTabItem("Audio")) { ImGui::Spacing(); ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); // Helper lambda to apply audio settings auto applyAudioSettings = [&]() { if (!renderer) return; float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(soundMuted_ ? 0.0f : masterScale); if (auto* music = renderer->getMusicManager()) { music->setVolume(static_cast(pendingMusicVolume * masterScale)); } if (auto* ambient = renderer->getAmbientSoundManager()) { ambient->setVolumeScale(pendingAmbientVolume / 100.0f * masterScale); } if (auto* ui = renderer->getUiSoundManager()) { ui->setVolumeScale(pendingUiVolume / 100.0f * masterScale); } if (auto* combat = renderer->getCombatSoundManager()) { combat->setVolumeScale(pendingCombatVolume / 100.0f * masterScale); } if (auto* spell = renderer->getSpellSoundManager()) { spell->setVolumeScale(pendingSpellVolume / 100.0f * masterScale); } if (auto* movement = renderer->getMovementSoundManager()) { movement->setVolumeScale(pendingMovementVolume / 100.0f * masterScale); } if (auto* footstep = renderer->getFootstepManager()) { footstep->setVolumeScale(pendingFootstepVolume / 100.0f * masterScale); } if (auto* npcVoice = renderer->getNpcVoiceManager()) { npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f * masterScale); } if (auto* mount = renderer->getMountSoundManager()) { mount->setVolumeScale(pendingMountVolume / 100.0f * masterScale); } if (auto* activity = renderer->getActivitySoundManager()) { activity->setVolumeScale(pendingActivityVolume / 100.0f * masterScale); } saveSettings(); }; ImGui::Text("Master Volume"); if (ImGui::SliderInt("##MasterVolume", &pendingMasterVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::Separator(); if (ImGui::Checkbox("Original Soundtrack", &pendingUseOriginalSoundtrack)) { if (renderer) { if (auto* zm = renderer->getZoneManager()) { zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack); } } saveSettings(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Include original music tracks in zone music rotation"); ImGui::Separator(); ImGui::Text("Music"); if (ImGui::SliderInt("##MusicVolume", &pendingMusicVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::Spacing(); ImGui::Text("Ambient Sounds"); if (ImGui::SliderInt("##AmbientVolume", &pendingAmbientVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Weather, zones, cities, emitters"); ImGui::Spacing(); ImGui::Text("UI Sounds"); if (ImGui::SliderInt("##UiVolume", &pendingUiVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Buttons, loot, quest complete"); ImGui::Spacing(); ImGui::Text("Combat Sounds"); if (ImGui::SliderInt("##CombatVolume", &pendingCombatVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Weapon swings, impacts, grunts"); ImGui::Spacing(); ImGui::Text("Spell Sounds"); if (ImGui::SliderInt("##SpellVolume", &pendingSpellVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Magic casting and impacts"); ImGui::Spacing(); ImGui::Text("Movement Sounds"); if (ImGui::SliderInt("##MovementVolume", &pendingMovementVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Water splashes, jump/land"); ImGui::Spacing(); ImGui::Text("Footsteps"); if (ImGui::SliderInt("##FootstepVolume", &pendingFootstepVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::Spacing(); ImGui::Text("NPC Voices"); if (ImGui::SliderInt("##NpcVoiceVolume", &pendingNpcVoiceVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::Spacing(); ImGui::Text("Mount Sounds"); if (ImGui::SliderInt("##MountVolume", &pendingMountVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::Spacing(); ImGui::Text("Activity Sounds"); if (ImGui::SliderInt("##ActivityVolume", &pendingActivityVolume, 0, 100, "%d%%")) { applyAudioSettings(); } ImGui::TextWrapped("Swimming, eating, drinking"); ImGui::EndChild(); if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) { pendingMasterVolume = 100; pendingMusicVolume = kDefaultMusicVolume; pendingAmbientVolume = 100; pendingUiVolume = 100; pendingCombatVolume = 100; pendingSpellVolume = 100; pendingMovementVolume = 100; pendingFootstepVolume = 100; pendingNpcVoiceVolume = 100; pendingMountVolume = 100; pendingActivityVolume = 100; applyAudioSettings(); } ImGui::EndTabItem(); } // ============================================================ // GAMEPLAY TAB // ============================================================ if (ImGui::BeginTabItem("Gameplay")) { ImGui::Spacing(); ImGui::Text("Controls"); ImGui::Separator(); if (ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f")) { if (renderer) { if (auto* cameraController = renderer->getCameraController()) { cameraController->setMouseSensitivity(pendingMouseSensitivity); } } saveSettings(); } if (ImGui::Checkbox("Invert Mouse", &pendingInvertMouse)) { if (renderer) { if (auto* cameraController = renderer->getCameraController()) { cameraController->setInvertMouse(pendingInvertMouse); } } saveSettings(); } ImGui::Spacing(); ImGui::Spacing(); ImGui::Text("Interface"); ImGui::Separator(); if (ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%")) { uiOpacity_ = static_cast(pendingUiOpacity) / 100.0f; saveSettings(); } if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) { // Force north-up minimap. minimapRotate_ = false; pendingMinimapRotate = false; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setRotateWithCamera(false); } } saveSettings(); } if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) { minimapSquare_ = pendingMinimapSquare; if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->setSquareShape(minimapSquare_); } } saveSettings(); } // Zoom controls ImGui::Text("Minimap Zoom:"); ImGui::SameLine(); if (ImGui::Button(" - ")) { if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->zoomOut(); saveSettings(); } } } ImGui::SameLine(); if (ImGui::Button(" + ")) { if (renderer) { if (auto* minimap = renderer->getMinimap()) { minimap->zoomIn(); saveSettings(); } } } ImGui::Spacing(); ImGui::Text("Loot"); ImGui::Separator(); if (ImGui::Checkbox("Auto Loot", &pendingAutoLoot)) { saveSettings(); // per-frame sync applies pendingAutoLoot to gameHandler } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Automatically pick up all items when looting"); ImGui::Spacing(); ImGui::Text("Bags"); ImGui::Separator(); if (ImGui::Checkbox("Separate Bag Windows", &pendingSeparateBags)) { inventoryScreen.setSeparateBags(pendingSeparateBags); saveSettings(); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Restore Gameplay Defaults", ImVec2(-1, 0))) { pendingMouseSensitivity = kDefaultMouseSensitivity; pendingInvertMouse = kDefaultInvertMouse; pendingUiOpacity = 65; pendingMinimapRotate = false; pendingMinimapSquare = false; pendingSeparateBags = true; inventoryScreen.setSeparateBags(true); uiOpacity_ = 0.65f; minimapRotate_ = false; minimapSquare_ = false; if (renderer) { if (auto* cameraController = renderer->getCameraController()) { cameraController->setMouseSensitivity(pendingMouseSensitivity); cameraController->setInvertMouse(pendingInvertMouse); } if (auto* minimap = renderer->getMinimap()) { minimap->setRotateWithCamera(minimapRotate_); minimap->setSquareShape(minimapSquare_); } } saveSettings(); } ImGui::EndTabItem(); } // ============================================================ // CHAT TAB // ============================================================ if (ImGui::BeginTabItem("Chat")) { ImGui::Spacing(); ImGui::Text("Appearance"); ImGui::Separator(); if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps_)) { saveSettings(); } ImGui::SetItemTooltip("Show [HH:MM] before each chat message"); const char* fontSizes[] = { "Small", "Medium", "Large" }; if (ImGui::Combo("Chat Font Size", &chatFontSize_, fontSizes, 3)) { saveSettings(); } ImGui::Spacing(); ImGui::Spacing(); ImGui::Text("Auto-Join Channels"); ImGui::Separator(); if (ImGui::Checkbox("General", &chatAutoJoinGeneral_)) saveSettings(); if (ImGui::Checkbox("Trade", &chatAutoJoinTrade_)) saveSettings(); if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense_)) saveSettings(); if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG_)) saveSettings(); if (ImGui::Checkbox("Local", &chatAutoJoinLocal_)) saveSettings(); ImGui::Spacing(); ImGui::Spacing(); ImGui::Text("Joined Channels"); ImGui::Separator(); ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) { chatShowTimestamps_ = false; chatFontSize_ = 1; chatAutoJoinGeneral_ = true; chatAutoJoinTrade_ = true; chatAutoJoinLocalDefense_ = true; chatAutoJoinLFG_ = true; chatAutoJoinLocal_ = true; saveSettings(); } ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImGui::Spacing(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f)); if (ImGui::Button("Back to Game", ImVec2(-1, 0))) { showSettingsWindow = false; } ImGui::PopStyleVar(); } ImGui::End(); } void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) { const auto& statuses = gameHandler.getNpcQuestStatuses(); if (statuses.empty()) return; auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; auto* window = core::Application::getInstance().getWindow(); if (!camera || !window) return; float screenW = static_cast(window->getWidth()); float screenH = static_cast(window->getHeight()); glm::mat4 viewProj = camera->getViewProjectionMatrix(); auto* drawList = ImGui::GetForegroundDrawList(); for (const auto& [guid, status] : statuses) { // Only show markers for available (!) and reward/completable (?) const char* marker = nullptr; ImU32 color = IM_COL32(255, 210, 0, 255); // yellow if (status == game::QuestGiverStatus::AVAILABLE) { marker = "!"; } else if (status == game::QuestGiverStatus::AVAILABLE_LOW) { marker = "!"; color = IM_COL32(160, 160, 160, 255); // gray } else if (status == game::QuestGiverStatus::REWARD || status == game::QuestGiverStatus::REWARD_REP) { marker = "?"; } else if (status == game::QuestGiverStatus::INCOMPLETE) { marker = "?"; color = IM_COL32(160, 160, 160, 255); // gray } else { continue; } // Get entity position (canonical coords) auto entity = gameHandler.getEntityManager().getEntity(guid); if (!entity) continue; glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); // Get model height for offset float heightOffset = 3.0f; glm::vec3 boundsCenter; float boundsRadius = 0.0f; if (core::Application::getInstance().getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) { heightOffset = boundsRadius * 2.0f + 1.0f; } renderPos.z += heightOffset; // Project to screen glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); if (clipPos.w <= 0.0f) continue; glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w); float sx = (ndc.x + 1.0f) * 0.5f * screenW; float sy = (1.0f - ndc.y) * 0.5f * screenH; // Skip if off-screen if (sx < -50 || sx > screenW + 50 || sy < -50 || sy > screenH + 50) continue; // Scale text size based on distance float dist = clipPos.w; float fontSize = std::clamp(800.0f / dist, 14.0f, 48.0f); // Draw outlined text: 4 shadow copies then main text ImFont* font = ImGui::GetFont(); ImU32 outlineColor = IM_COL32(0, 0, 0, 220); float off = std::max(1.0f, fontSize * 0.06f); ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, marker); float tx = sx - textSize.x * 0.5f; float ty = sy - textSize.y * 0.5f; drawList->AddText(font, fontSize, ImVec2(tx - off, ty), outlineColor, marker); drawList->AddText(font, fontSize, ImVec2(tx + off, ty), outlineColor, marker); drawList->AddText(font, fontSize, ImVec2(tx, ty - off), outlineColor, marker); drawList->AddText(font, fontSize, ImVec2(tx, ty + off), outlineColor, marker); drawList->AddText(font, fontSize, ImVec2(tx, ty), color, marker); } } void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { const auto& statuses = gameHandler.getNpcQuestStatuses(); auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; auto* minimap = renderer ? renderer->getMinimap() : nullptr; auto* window = core::Application::getInstance().getWindow(); if (!camera || !minimap || !window) return; float screenW = static_cast(window->getWidth()); // Minimap parameters (matching minimap.cpp) float mapSize = 200.0f; float margin = 10.0f; float mapRadius = mapSize * 0.5f; float centerX = screenW - margin - mapRadius; float centerY = margin + mapRadius; float viewRadius = 400.0f; // Player position in render coords auto& mi = gameHandler.getMovementInfo(); glm::vec3 playerRender = core::coords::canonicalToRender(glm::vec3(mi.x, mi.y, mi.z)); // Camera bearing for minimap rotation float bearing = 0.0f; float cosB = 1.0f; float sinB = 0.0f; if (minimap->isRotateWithCamera()) { glm::vec3 fwd = camera->getForward(); bearing = std::atan2(-fwd.x, fwd.y); cosB = std::cos(bearing); sinB = std::sin(bearing); } if (statuses.empty()) return; auto* drawList = ImGui::GetForegroundDrawList(); for (const auto& [guid, status] : statuses) { ImU32 dotColor; const char* marker = nullptr; if (status == game::QuestGiverStatus::AVAILABLE) { dotColor = IM_COL32(255, 210, 0, 255); marker = "!"; } else if (status == game::QuestGiverStatus::AVAILABLE_LOW) { dotColor = IM_COL32(160, 160, 160, 255); marker = "!"; } else if (status == game::QuestGiverStatus::REWARD || status == game::QuestGiverStatus::REWARD_REP) { dotColor = IM_COL32(255, 210, 0, 255); marker = "?"; } else if (status == game::QuestGiverStatus::INCOMPLETE) { dotColor = IM_COL32(160, 160, 160, 255); marker = "?"; } else { continue; } auto entity = gameHandler.getEntityManager().getEntity(guid); if (!entity) continue; glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); glm::vec3 npcRender = core::coords::canonicalToRender(canonical); // Offset from player in render coords float dx = npcRender.x - playerRender.x; float dy = npcRender.y - playerRender.y; // Rotate by camera bearing (minimap north-up rotation) float rx = dx * cosB - dy * sinB; float ry = dx * sinB + dy * cosB; // Scale to minimap pixels float px = rx / viewRadius * mapRadius; float py = -ry / viewRadius * mapRadius; // screen Y is inverted // Clamp to circle float distFromCenter = std::sqrt(px * px + py * py); if (distFromCenter > mapRadius - 4.0f) { float scale = (mapRadius - 4.0f) / distFromCenter; px *= scale; py *= scale; } float sx = centerX + px; float sy = centerY + py; // Draw dot with marker text drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, dotColor); ImFont* font = ImGui::GetFont(); ImVec2 textSize = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, marker); drawList->AddText(font, 11.0f, ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f), IM_COL32(0, 0, 0, 255), marker); } auto applyMuteState = [&]() { auto* activeRenderer = core::Application::getInstance().getRenderer(); float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); if (!activeRenderer) return; if (auto* music = activeRenderer->getMusicManager()) { music->setVolume(static_cast(pendingMusicVolume * masterScale)); } if (auto* ambient = activeRenderer->getAmbientSoundManager()) { ambient->setVolumeScale(pendingAmbientVolume / 100.0f * masterScale); } if (auto* ui = activeRenderer->getUiSoundManager()) { ui->setVolumeScale(pendingUiVolume / 100.0f * masterScale); } if (auto* combat = activeRenderer->getCombatSoundManager()) { combat->setVolumeScale(pendingCombatVolume / 100.0f * masterScale); } if (auto* spell = activeRenderer->getSpellSoundManager()) { spell->setVolumeScale(pendingSpellVolume / 100.0f * masterScale); } if (auto* movement = activeRenderer->getMovementSoundManager()) { movement->setVolumeScale(pendingMovementVolume / 100.0f * masterScale); } if (auto* footstep = activeRenderer->getFootstepManager()) { footstep->setVolumeScale(pendingFootstepVolume / 100.0f * masterScale); } if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) { npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f * masterScale); } if (auto* mount = activeRenderer->getMountSoundManager()) { mount->setVolumeScale(pendingMountVolume / 100.0f * masterScale); } if (auto* activity = activeRenderer->getActivitySoundManager()) { activity->setVolumeScale(pendingActivityVolume / 100.0f * masterScale); } }; // Speaker mute button at the minimap top-right corner ImGui::SetNextWindowPos(ImVec2(centerX + mapRadius - 26.0f, centerY - mapRadius + 4.0f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(22.0f, 22.0f), ImGuiCond_Always); ImGuiWindowFlags muteFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground; if (ImGui::Begin("##MinimapMute", nullptr, muteFlags)) { ImDrawList* draw = ImGui::GetWindowDrawList(); ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 size(20.0f, 20.0f); if (ImGui::InvisibleButton("##MinimapMuteButton", size)) { soundMuted_ = !soundMuted_; if (soundMuted_) { preMuteVolume_ = audio::AudioEngine::instance().getMasterVolume(); } applyMuteState(); saveSettings(); } bool hovered = ImGui::IsItemHovered(); ImU32 bg = soundMuted_ ? IM_COL32(135, 42, 42, 230) : IM_COL32(38, 38, 38, 210); if (hovered) bg = soundMuted_ ? IM_COL32(160, 58, 58, 230) : IM_COL32(65, 65, 65, 220); ImU32 fg = IM_COL32(255, 255, 255, 245); draw->AddRectFilled(p, ImVec2(p.x + size.x, p.y + size.y), bg, 4.0f); draw->AddRect(ImVec2(p.x + 0.5f, p.y + 0.5f), ImVec2(p.x + size.x - 0.5f, p.y + size.y - 0.5f), IM_COL32(255, 255, 255, 42), 4.0f); draw->AddRectFilled(ImVec2(p.x + 4.0f, p.y + 8.0f), ImVec2(p.x + 7.0f, p.y + 12.0f), fg, 1.0f); draw->AddTriangleFilled(ImVec2(p.x + 7.0f, p.y + 7.0f), ImVec2(p.x + 7.0f, p.y + 13.0f), ImVec2(p.x + 11.8f, p.y + 10.0f), fg); if (soundMuted_) { draw->AddLine(ImVec2(p.x + 13.5f, p.y + 6.2f), ImVec2(p.x + 17.2f, p.y + 13.8f), fg, 1.8f); draw->AddLine(ImVec2(p.x + 17.2f, p.y + 6.2f), ImVec2(p.x + 13.5f, p.y + 13.8f), fg, 1.8f); } else { draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 3.6f, -0.7f, 0.7f, 12); draw->PathStroke(fg, 0, 1.4f); draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 5.5f, -0.7f, 0.7f, 12); draw->PathStroke(fg, 0, 1.2f); } if (hovered) ImGui::SetTooltip(soundMuted_ ? "Unmute" : "Mute"); } ImGui::End(); // Zoom buttons at the bottom edge of the minimap ImGui::SetNextWindowPos(ImVec2(centerX - 22, centerY + mapRadius - 30), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(44, 24), ImGuiCond_Always); ImGuiWindowFlags zoomFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground; if (ImGui::Begin("##MinimapZoom", nullptr, zoomFlags)) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0)); if (ImGui::SmallButton("-")) { if (minimap) minimap->zoomOut(); } ImGui::SameLine(); if (ImGui::SmallButton("+")) { if (minimap) minimap->zoomIn(); } ImGui::PopStyleVar(2); } ImGui::End(); // "New Mail" indicator below the minimap if (gameHandler.hasNewMail()) { float indicatorX = centerX - mapRadius; float indicatorY = centerY + mapRadius + 4.0f; ImGui::SetNextWindowPos(ImVec2(indicatorX, indicatorY), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(mapRadius * 2.0f, 22), ImGuiCond_Always); ImGuiWindowFlags mailFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs; if (ImGui::Begin("##NewMailIndicator", nullptr, mailFlags)) { // Pulsing effect float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 3.0f); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, pulse), "New Mail!"); } ImGui::End(); } } std::string GameScreen::getSettingsPath() { std::string dir; #ifdef _WIN32 const char* appdata = std::getenv("APPDATA"); dir = appdata ? std::string(appdata) + "\\wowee" : "."; #else const char* home = std::getenv("HOME"); dir = home ? std::string(home) + "/.wowee" : "."; #endif return dir + "/settings.cfg"; } std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) { // Get player gender, pronouns, and name game::Gender gender = game::Gender::NONBINARY; std::string playerName = "Adventurer"; const auto* character = gameHandler.getActiveCharacter(); if (character) { gender = character->gender; if (!character->name.empty()) { playerName = character->name; } } game::Pronouns pronouns = game::Pronouns::forGender(gender); std::string result = text; // Helper to trim whitespace auto trim = [](std::string& s) { const char* ws = " \t\n\r"; size_t start = s.find_first_not_of(ws); if (start == std::string::npos) { s.clear(); return; } size_t end = s.find_last_not_of(ws); s = s.substr(start, end - start + 1); }; // Replace $g/$G placeholders first. size_t pos = 0; while ((pos = result.find('$', pos)) != std::string::npos) { if (pos + 1 >= result.length()) break; char marker = result[pos + 1]; if (marker != 'g' && marker != 'G') { pos++; continue; } size_t endPos = result.find(';', pos); if (endPos == std::string::npos) { pos += 2; continue; } std::string placeholder = result.substr(pos + 2, endPos - pos - 2); // Split by colons std::vector parts; size_t start = 0; size_t colonPos; while ((colonPos = placeholder.find(':', start)) != std::string::npos) { std::string part = placeholder.substr(start, colonPos - start); trim(part); parts.push_back(part); start = colonPos + 1; } // Add the last part std::string lastPart = placeholder.substr(start); trim(lastPart); parts.push_back(lastPart); // Select appropriate text based on gender std::string replacement; if (parts.size() >= 3) { // Three options: male, female, nonbinary switch (gender) { case game::Gender::MALE: replacement = parts[0]; break; case game::Gender::FEMALE: replacement = parts[1]; break; case game::Gender::NONBINARY: replacement = parts[2]; break; } } else if (parts.size() >= 2) { // Two options: male, female (use first for nonbinary) switch (gender) { case game::Gender::MALE: replacement = parts[0]; break; case game::Gender::FEMALE: replacement = parts[1]; break; case game::Gender::NONBINARY: // Default to gender-neutral: use the shorter/simpler option replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1]; break; } } else { // Malformed placeholder pos = endPos + 1; continue; } result.replace(pos, endPos - pos + 1, replacement); pos += replacement.length(); } // Replace simple placeholders. // $n = player name // $p = subject pronoun (he/she/they) // $o = object pronoun (him/her/them) // $s = possessive adjective (his/her/their) // $S = possessive pronoun (his/hers/theirs) // $b/$B = line break pos = 0; while ((pos = result.find('$', pos)) != std::string::npos) { if (pos + 1 >= result.length()) break; char code = result[pos + 1]; std::string replacement; switch (code) { case 'n': case 'N': replacement = playerName; break; case 'p': replacement = pronouns.subject; break; case 'o': replacement = pronouns.object; break; case 's': replacement = pronouns.possessive; break; case 'S': replacement = pronouns.possessiveP; break; case 'b': case 'B': replacement = "\n"; break; case 'g': case 'G': pos++; continue; default: pos++; continue; } result.replace(pos, 2, replacement); pos += replacement.length(); } // WoW markup linebreak token. pos = 0; while ((pos = result.find("|n", pos)) != std::string::npos) { result.replace(pos, 2, "\n"); pos += 1; } pos = 0; while ((pos = result.find("|N", pos)) != std::string::npos) { result.replace(pos, 2, "\n"); pos += 1; } return result; } void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) { if (chatBubbles_.empty()) return; auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; if (!camera) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; // Get delta time from ImGui float dt = ImGui::GetIO().DeltaTime; glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix(); // Update and render bubbles for (int i = static_cast(chatBubbles_.size()) - 1; i >= 0; --i) { auto& bubble = chatBubbles_[i]; bubble.timeRemaining -= dt; if (bubble.timeRemaining <= 0.0f) { chatBubbles_.erase(chatBubbles_.begin() + i); continue; } // Get entity position auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid); if (!entity) continue; // Convert canonical → render coordinates, offset up by 2.5 units for bubble above head glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f); glm::vec3 renderPos = core::coords::canonicalToRender(canonical); // Project to screen glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); if (clipPos.w <= 0.0f) continue; // Behind camera glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w); float screenX = (ndc.x * 0.5f + 0.5f) * screenW; float screenY = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; // Flip Y // Skip if off-screen if (screenX < -200.0f || screenX > screenW + 200.0f || screenY < -100.0f || screenY > screenH + 100.0f) continue; // Fade alpha over last 2 seconds float alpha = 1.0f; if (bubble.timeRemaining < 2.0f) { alpha = bubble.timeRemaining / 2.0f; } // Draw bubble window std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid); ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f)); ImGui::SetNextWindowBgAlpha(0.7f * alpha); ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4)); ImGui::Begin(winId.c_str(), nullptr, flags); ImVec4 textColor = bubble.isYell ? ImVec4(1.0f, 0.2f, 0.2f, alpha) : ImVec4(1.0f, 1.0f, 1.0f, alpha); ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushTextWrapPos(200.0f); ImGui::TextWrapped("%s", bubble.message.c_str()); ImGui::PopTextWrapPos(); ImGui::PopStyleColor(); ImGui::End(); ImGui::PopStyleVar(2); } } void GameScreen::saveSettings() { std::string path = getSettingsPath(); std::filesystem::path dir = std::filesystem::path(path).parent_path(); std::error_code ec; std::filesystem::create_directories(dir, ec); std::ofstream out(path); if (!out.is_open()) { LOG_WARNING("Could not save settings to ", path); return; } // Interface out << "ui_opacity=" << pendingUiOpacity << "\n"; out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n"; out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; // Audio out << "sound_muted=" << (soundMuted_ ? 1 : 0) << "\n"; out << "use_original_soundtrack=" << (pendingUseOriginalSoundtrack ? 1 : 0) << "\n"; out << "master_volume=" << pendingMasterVolume << "\n"; out << "music_volume=" << pendingMusicVolume << "\n"; out << "ambient_volume=" << pendingAmbientVolume << "\n"; out << "ui_volume=" << pendingUiVolume << "\n"; out << "combat_volume=" << pendingCombatVolume << "\n"; out << "spell_volume=" << pendingSpellVolume << "\n"; out << "movement_volume=" << pendingMovementVolume << "\n"; out << "footstep_volume=" << pendingFootstepVolume << "\n"; out << "npc_voice_volume=" << pendingNpcVoiceVolume << "\n"; out << "mount_volume=" << pendingMountVolume << "\n"; out << "activity_volume=" << pendingActivityVolume << "\n"; // Gameplay out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n"; // Controls out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n"; out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n"; // Chat out << "chat_active_tab=" << activeChatTab_ << "\n"; out << "chat_timestamps=" << (chatShowTimestamps_ ? 1 : 0) << "\n"; out << "chat_font_size=" << chatFontSize_ << "\n"; out << "chat_autojoin_general=" << (chatAutoJoinGeneral_ ? 1 : 0) << "\n"; out << "chat_autojoin_trade=" << (chatAutoJoinTrade_ ? 1 : 0) << "\n"; out << "chat_autojoin_localdefense=" << (chatAutoJoinLocalDefense_ ? 1 : 0) << "\n"; out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 1 : 0) << "\n"; out << "chat_autojoin_local=" << (chatAutoJoinLocal_ ? 1 : 0) << "\n"; LOG_INFO("Settings saved to ", path); } void GameScreen::loadSettings() { std::string path = getSettingsPath(); std::ifstream in(path); if (!in.is_open()) return; std::string line; while (std::getline(in, line)) { size_t eq = line.find('='); if (eq == std::string::npos) continue; std::string key = line.substr(0, eq); std::string val = line.substr(eq + 1); try { // Interface if (key == "ui_opacity") { int v = std::stoi(val); if (v >= 20 && v <= 100) { pendingUiOpacity = v; uiOpacity_ = static_cast(v) / 100.0f; } } else if (key == "minimap_rotate") { // Ignore persisted rotate state; keep north-up. minimapRotate_ = false; pendingMinimapRotate = false; } else if (key == "minimap_square") { int v = std::stoi(val); minimapSquare_ = (v != 0); pendingMinimapSquare = minimapSquare_; } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags); } // Audio else if (key == "sound_muted") { soundMuted_ = (std::stoi(val) != 0); if (soundMuted_) { // Apply mute on load; preMuteVolume_ will be set when AudioEngine is available audio::AudioEngine::instance().setMasterVolume(0.0f); } } else if (key == "use_original_soundtrack") pendingUseOriginalSoundtrack = (std::stoi(val) != 0); else if (key == "master_volume") pendingMasterVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "music_volume") pendingMusicVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "ambient_volume") pendingAmbientVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "ui_volume") pendingUiVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "combat_volume") pendingCombatVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "spell_volume") pendingSpellVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "movement_volume") pendingMovementVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "footstep_volume") pendingFootstepVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "npc_voice_volume") pendingNpcVoiceVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "mount_volume") pendingMountVolume = std::clamp(std::stoi(val), 0, 100); else if (key == "activity_volume") pendingActivityVolume = std::clamp(std::stoi(val), 0, 100); // Gameplay else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0); // Controls else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f); else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0); // Chat else if (key == "chat_active_tab") activeChatTab_ = std::clamp(std::stoi(val), 0, 3); else if (key == "chat_timestamps") chatShowTimestamps_ = (std::stoi(val) != 0); else if (key == "chat_font_size") chatFontSize_ = std::clamp(std::stoi(val), 0, 2); else if (key == "chat_autojoin_general") chatAutoJoinGeneral_ = (std::stoi(val) != 0); else if (key == "chat_autojoin_trade") chatAutoJoinTrade_ = (std::stoi(val) != 0); else if (key == "chat_autojoin_localdefense") chatAutoJoinLocalDefense_ = (std::stoi(val) != 0); else if (key == "chat_autojoin_lfg") chatAutoJoinLFG_ = (std::stoi(val) != 0); else if (key == "chat_autojoin_local") chatAutoJoinLocal_ = (std::stoi(val) != 0); } catch (...) {} } LOG_INFO("Settings loaded from ", path); } // ============================================================ // Mail Window // ============================================================ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { if (!gameHandler.isMailboxOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Mailbox", &open)) { const auto& inbox = gameHandler.getMailInbox(); // Top bar: money + compose button uint64_t money = gameHandler.getMoneyCopper(); uint32_t mg = static_cast(money / 10000); uint32_t ms = static_cast((money / 100) % 100); uint32_t mc = static_cast(money % 100); ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); ImGui::SameLine(ImGui::GetWindowWidth() - 100); if (ImGui::Button("Compose")) { mailRecipientBuffer_[0] = '\0'; mailSubjectBuffer_[0] = '\0'; mailBodyBuffer_[0] = '\0'; mailComposeMoney_[0] = 0; mailComposeMoney_[1] = 0; mailComposeMoney_[2] = 0; gameHandler.openMailCompose(); } ImGui::Separator(); if (inbox.empty()) { ImGui::TextDisabled("No mail."); } else { // Two-panel layout: left = mail list, right = selected mail detail float listWidth = 220.0f; // Left panel - mail list ImGui::BeginChild("MailList", ImVec2(listWidth, 0), true); for (size_t i = 0; i < inbox.size(); ++i) { const auto& mail = inbox[i]; ImGui::PushID(static_cast(i)); bool selected = (gameHandler.getSelectedMailIndex() == static_cast(i)); std::string label = mail.subject.empty() ? "(No Subject)" : mail.subject; // Unread indicator if (!mail.read) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.5f, 1.0f)); } if (ImGui::Selectable(label.c_str(), selected)) { gameHandler.setSelectedMailIndex(static_cast(i)); // Mark as read if (!mail.read) { gameHandler.mailMarkAsRead(mail.messageId); } } if (!mail.read) { ImGui::PopStyleColor(); } // Sub-info line ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), " From: %s", mail.senderName.c_str()); if (mail.money > 0) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), " [G]"); } if (!mail.attachments.empty()) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]"); } ImGui::PopID(); } ImGui::EndChild(); ImGui::SameLine(); // Right panel - selected mail detail ImGui::BeginChild("MailDetail", ImVec2(0, 0), true); int sel = gameHandler.getSelectedMailIndex(); if (sel >= 0 && sel < static_cast(inbox.size())) { const auto& mail = inbox[sel]; ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s", mail.subject.empty() ? "(No Subject)" : mail.subject.c_str()); ImGui::Text("From: %s", mail.senderName.c_str()); if (mail.messageType == 2) { ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]"); } ImGui::Separator(); // Body text if (!mail.body.empty()) { ImGui::TextWrapped("%s", mail.body.c_str()); ImGui::Separator(); } // Money if (mail.money > 0) { uint32_t g = mail.money / 10000; uint32_t s = (mail.money / 100) % 100; uint32_t c = mail.money % 100; ImGui::Text("Money: %ug %us %uc", g, s, c); ImGui::SameLine(); if (ImGui::SmallButton("Take Money")) { gameHandler.mailTakeMoney(mail.messageId); } } // COD warning if (mail.cod > 0) { uint32_t g = mail.cod / 10000; uint32_t s = (mail.cod / 100) % 100; uint32_t c = mail.cod % 100; ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "COD: %ug %us %uc (you pay this to take items)", g, s, c); } // Attachments if (!mail.attachments.empty()) { ImGui::Text("Attachments: %zu", mail.attachments.size()); for (size_t j = 0; j < mail.attachments.size(); ++j) { const auto& att = mail.attachments[j]; ImGui::PushID(static_cast(j)); auto* info = gameHandler.getItemInfo(att.itemId); if (info && info->valid) { ImGui::BulletText("%s x%u", info->name.c_str(), att.stackCount); } else { ImGui::BulletText("Item %u x%u", att.itemId, att.stackCount); gameHandler.ensureItemInfo(att.itemId); } ImGui::SameLine(); if (ImGui::SmallButton("Take")) { gameHandler.mailTakeItem(mail.messageId, att.slot); } ImGui::PopID(); } } ImGui::Spacing(); ImGui::Separator(); // Action buttons if (ImGui::Button("Delete")) { gameHandler.mailDelete(mail.messageId); } ImGui::SameLine(); if (mail.messageType == 0 && ImGui::Button("Reply")) { // Pre-fill compose with sender as recipient strncpy(mailRecipientBuffer_, mail.senderName.c_str(), sizeof(mailRecipientBuffer_) - 1); mailRecipientBuffer_[sizeof(mailRecipientBuffer_) - 1] = '\0'; std::string reSubject = "Re: " + mail.subject; strncpy(mailSubjectBuffer_, reSubject.c_str(), sizeof(mailSubjectBuffer_) - 1); mailSubjectBuffer_[sizeof(mailSubjectBuffer_) - 1] = '\0'; mailBodyBuffer_[0] = '\0'; mailComposeMoney_[0] = 0; mailComposeMoney_[1] = 0; mailComposeMoney_[2] = 0; gameHandler.openMailCompose(); } } else { ImGui::TextDisabled("Select a mail to read."); } ImGui::EndChild(); } } ImGui::End(); if (!open) { gameHandler.closeMailbox(); } } void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { if (!gameHandler.isMailComposeOpen()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, screenH / 2 - 200), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(380, 400), ImGuiCond_Appearing); bool open = true; if (ImGui::Begin("Send Mail", &open)) { ImGui::Text("To:"); ImGui::SameLine(60); ImGui::SetNextItemWidth(-1); ImGui::InputText("##MailTo", mailRecipientBuffer_, sizeof(mailRecipientBuffer_)); ImGui::Text("Subject:"); ImGui::SameLine(60); ImGui::SetNextItemWidth(-1); ImGui::InputText("##MailSubject", mailSubjectBuffer_, sizeof(mailSubjectBuffer_)); ImGui::Text("Body:"); ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_), ImVec2(-1, 150)); ImGui::Text("Money:"); ImGui::SameLine(60); ImGui::SetNextItemWidth(60); ImGui::InputInt("##MailGold", &mailComposeMoney_[0], 0, 0); if (mailComposeMoney_[0] < 0) mailComposeMoney_[0] = 0; ImGui::SameLine(); ImGui::Text("g"); ImGui::SameLine(); ImGui::SetNextItemWidth(40); ImGui::InputInt("##MailSilver", &mailComposeMoney_[1], 0, 0); if (mailComposeMoney_[1] < 0) mailComposeMoney_[1] = 0; if (mailComposeMoney_[1] > 99) mailComposeMoney_[1] = 99; ImGui::SameLine(); ImGui::Text("s"); ImGui::SameLine(); ImGui::SetNextItemWidth(40); ImGui::InputInt("##MailCopper", &mailComposeMoney_[2], 0, 0); if (mailComposeMoney_[2] < 0) mailComposeMoney_[2] = 0; if (mailComposeMoney_[2] > 99) mailComposeMoney_[2] = 99; ImGui::SameLine(); ImGui::Text("c"); uint32_t totalMoney = static_cast(mailComposeMoney_[0]) * 10000 + static_cast(mailComposeMoney_[1]) * 100 + static_cast(mailComposeMoney_[2]); ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: 30c"); ImGui::Spacing(); bool canSend = (strlen(mailRecipientBuffer_) > 0); if (!canSend) ImGui::BeginDisabled(); if (ImGui::Button("Send", ImVec2(80, 0))) { gameHandler.sendMail(mailRecipientBuffer_, mailSubjectBuffer_, mailBodyBuffer_, totalMoney); } if (!canSend) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(80, 0))) { gameHandler.closeMailCompose(); } } ImGui::End(); if (!open) { gameHandler.closeMailCompose(); } } // ============================================================ // Bank Window // ============================================================ void GameScreen::renderBankWindow(game::GameHandler& gameHandler) { if (!gameHandler.isBankOpen()) return; bool open = true; ImGui::SetNextWindowSize(ImVec2(480, 420), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Bank", &open)) { ImGui::End(); if (!open) gameHandler.closeBank(); return; } auto& inv = gameHandler.getInventory(); // Main bank slots (28 = 7 columns × 4 rows) ImGui::Text("Bank Slots"); ImGui::Separator(); for (int i = 0; i < game::Inventory::BANK_SLOTS; i++) { if (i % 7 != 0) ImGui::SameLine(); const auto& slot = inv.getBankSlot(i); ImGui::PushID(i + 1000); if (slot.empty()) { ImGui::Button("##bank", ImVec2(42, 42)); } else { auto* info = gameHandler.getItemInfo(slot.item.itemId); ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); std::string label = std::to_string(slot.item.stackCount > 1 ? slot.item.stackCount : 0); if (slot.item.stackCount <= 1) label = "##b" + std::to_string(i); if (ImGui::Button(label.c_str(), ImVec2(42, 42))) { // Right-click to withdraw: bag=0xFF means bank, slot=i // Use CMSG_AUTOSTORE_BANK_ITEM with bank container // WoW bank slots are inventory slots 39-66 (BANK_SLOT_1 = 39) gameHandler.withdrawItem(0xFF, static_cast(39 + i)); } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextColored(qc, "%s", slot.item.name.c_str()); if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); ImGui::EndTooltip(); } } ImGui::PopID(); } // Bank bag slots ImGui::Spacing(); ImGui::Separator(); ImGui::Text("Bank Bags"); uint8_t purchased = inv.getPurchasedBankBagSlots(); for (int i = 0; i < game::Inventory::BANK_BAG_SLOTS; i++) { if (i > 0) ImGui::SameLine(); ImGui::PushID(i + 2000); int bagSize = inv.getBankBagSize(i); if (i < static_cast(purchased) || bagSize > 0) { if (ImGui::Button(bagSize > 0 ? std::to_string(bagSize).c_str() : "Empty", ImVec2(50, 30))) { // Could open bag contents } } else { if (ImGui::Button("Buy", ImVec2(50, 30))) { gameHandler.buyBankSlot(); } } ImGui::PopID(); } // Show expanded bank bag contents for (int bagIdx = 0; bagIdx < game::Inventory::BANK_BAG_SLOTS; bagIdx++) { int bagSize = inv.getBankBagSize(bagIdx); if (bagSize <= 0) continue; ImGui::Spacing(); ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize); for (int s = 0; s < bagSize; s++) { if (s % 7 != 0) ImGui::SameLine(); const auto& slot = inv.getBankBagSlot(bagIdx, s); ImGui::PushID(3000 + bagIdx * 100 + s); if (slot.empty()) { ImGui::Button("##bb", ImVec2(42, 42)); } else { ImVec4 qc = InventoryScreen::getQualityColor(slot.item.quality); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); std::string lbl = slot.item.stackCount > 1 ? std::to_string(slot.item.stackCount) : ("##bb" + std::to_string(bagIdx * 100 + s)); if (ImGui::Button(lbl.c_str(), ImVec2(42, 42))) { // Withdraw from bank bag: bank bag container indices start at 67 gameHandler.withdrawItem(static_cast(67 + bagIdx), static_cast(s)); } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextColored(qc, "%s", slot.item.name.c_str()); if (slot.item.stackCount > 1) ImGui::Text("Count: %u", slot.item.stackCount); ImGui::EndTooltip(); } } ImGui::PopID(); } } ImGui::End(); if (!open) gameHandler.closeBank(); } // ============================================================ // Guild Bank Window // ============================================================ void GameScreen::renderGuildBankWindow(game::GameHandler& gameHandler) { if (!gameHandler.isGuildBankOpen()) return; bool open = true; ImGui::SetNextWindowSize(ImVec2(520, 500), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Guild Bank", &open)) { ImGui::End(); if (!open) gameHandler.closeGuildBank(); return; } const auto& data = gameHandler.getGuildBankData(); uint8_t activeTab = gameHandler.getGuildBankActiveTab(); // Money display uint32_t gold = static_cast(data.money / 10000); uint32_t silver = static_cast((data.money / 100) % 100); uint32_t copper = static_cast(data.money % 100); ImGui::Text("Guild Bank Money: "); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); // Tab bar if (!data.tabs.empty()) { for (size_t i = 0; i < data.tabs.size(); i++) { if (i > 0) ImGui::SameLine(); bool selected = (i == activeTab); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); std::string tabLabel = data.tabs[i].tabName.empty() ? ("Tab " + std::to_string(i + 1)) : data.tabs[i].tabName; if (ImGui::Button(tabLabel.c_str())) { gameHandler.queryGuildBankTab(static_cast(i)); } if (selected) ImGui::PopStyleColor(); } } // Buy tab button if (data.tabs.size() < 6) { ImGui::SameLine(); if (ImGui::Button("Buy Tab")) { gameHandler.buyGuildBankTab(); } } ImGui::Separator(); // Tab items (98 slots = 14 columns × 7 rows) for (size_t i = 0; i < data.tabItems.size(); i++) { if (i % 14 != 0) ImGui::SameLine(); const auto& item = data.tabItems[i]; ImGui::PushID(static_cast(i) + 5000); if (item.itemEntry == 0) { ImGui::Button("##gb", ImVec2(34, 34)); } else { auto* info = gameHandler.getItemInfo(item.itemEntry); game::ItemQuality quality = game::ItemQuality::COMMON; std::string name = "Item " + std::to_string(item.itemEntry); if (info) { quality = static_cast(info->quality); name = info->name; } ImVec4 qc = InventoryScreen::getQualityColor(quality); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qc.x * 0.3f, qc.y * 0.3f, qc.z * 0.3f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qc.x * 0.5f, qc.y * 0.5f, qc.z * 0.5f, 0.9f)); std::string lbl = item.stackCount > 1 ? std::to_string(item.stackCount) : ("##gi" + std::to_string(i)); if (ImGui::Button(lbl.c_str(), ImVec2(34, 34))) { // Withdraw: auto-store to first free bag slot gameHandler.guildBankWithdrawItem(activeTab, item.slotId, 0xFF, 0); } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextColored(qc, "%s", name.c_str()); if (item.stackCount > 1) ImGui::Text("Count: %u", item.stackCount); ImGui::EndTooltip(); } } ImGui::PopID(); } // Money deposit/withdraw ImGui::Separator(); ImGui::Text("Money:"); ImGui::SameLine(); ImGui::SetNextItemWidth(60); ImGui::InputInt("##gbg", &guildBankMoneyInput_[0], 0); ImGui::SameLine(); ImGui::Text("g"); ImGui::SameLine(); ImGui::SetNextItemWidth(40); ImGui::InputInt("##gbs", &guildBankMoneyInput_[1], 0); ImGui::SameLine(); ImGui::Text("s"); ImGui::SameLine(); ImGui::SetNextItemWidth(40); ImGui::InputInt("##gbc", &guildBankMoneyInput_[2], 0); ImGui::SameLine(); ImGui::Text("c"); ImGui::SameLine(); if (ImGui::Button("Deposit")) { uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2]; if (amount > 0) gameHandler.depositGuildBankMoney(amount); } ImGui::SameLine(); if (ImGui::Button("Withdraw")) { uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2]; if (amount > 0) gameHandler.withdrawGuildBankMoney(amount); } if (data.withdrawAmount >= 0) { ImGui::Text("Remaining withdrawals: %d", data.withdrawAmount); } ImGui::End(); if (!open) gameHandler.closeGuildBank(); } // ============================================================ // Auction House Window // ============================================================ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { if (!gameHandler.isAuctionHouseOpen()) return; bool open = true; ImGui::SetNextWindowSize(ImVec2(650, 500), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Auction House", &open)) { ImGui::End(); if (!open) gameHandler.closeAuctionHouse(); return; } int tab = gameHandler.getAuctionActiveTab(); // Tab buttons const char* tabNames[] = {"Browse", "Bids", "Auctions"}; for (int i = 0; i < 3; i++) { if (i > 0) ImGui::SameLine(); bool selected = (tab == i); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f)); if (ImGui::Button(tabNames[i], ImVec2(100, 0))) { gameHandler.setAuctionActiveTab(i); if (i == 1) gameHandler.auctionListBidderItems(); else if (i == 2) gameHandler.auctionListOwnerItems(); } if (selected) ImGui::PopStyleColor(); } ImGui::Separator(); if (tab == 0) { // Browse tab - Search filters ImGui::SetNextItemWidth(200); ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_)); ImGui::SameLine(); ImGui::SetNextItemWidth(50); ImGui::InputInt("Min Lv", &auctionLevelMin_, 0); ImGui::SameLine(); ImGui::SetNextItemWidth(50); ImGui::InputInt("Max Lv", &auctionLevelMax_, 0); const char* qualities[] = {"All", "Poor", "Common", "Uncommon", "Rare", "Epic", "Legendary"}; ImGui::SetNextItemWidth(100); ImGui::Combo("Quality", &auctionQuality_, qualities, 7); ImGui::SameLine(); float delay = gameHandler.getAuctionSearchDelay(); if (delay > 0.0f) { ImGui::BeginDisabled(); ImGui::Button("Search..."); ImGui::EndDisabled(); } else { if (ImGui::Button("Search")) { uint32_t q = auctionQuality_ > 0 ? static_cast(auctionQuality_ - 1) : 0xFFFFFFFF; gameHandler.auctionSearch(auctionSearchName_, static_cast(auctionLevelMin_), static_cast(auctionLevelMax_), q, 0xFFFFFFFF, 0xFFFFFFFF, 0, 0); } } ImGui::Separator(); // Results table const auto& results = gameHandler.getAuctionBrowseResults(); ImGui::Text("%zu results (of %u total)", results.auctions.size(), results.totalCount); if (ImGui::BeginChild("AuctionResults", ImVec2(0, -80), true)) { if (ImGui::BeginTable("AuctionTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableHeadersRow(); for (size_t i = 0; i < results.auctions.size(); i++) { const auto& auction = results.auctions[i]; auto* info = gameHandler.getItemInfo(auction.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry)); game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImVec4 qc = InventoryScreen::getQualityColor(quality); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextColored(qc, "%s", name.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%u", auction.stackCount); ImGui::TableSetColumnIndex(2); // Time left display uint32_t mins = auction.timeLeftMs / 60000; if (mins > 720) ImGui::Text("Long"); else if (mins > 120) ImGui::Text("Medium"); else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short"); ImGui::TableSetColumnIndex(3); { uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid; ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); } ImGui::TableSetColumnIndex(4); if (auction.buyoutPrice > 0) { ImGui::Text("%ug%us%uc", auction.buyoutPrice / 10000, (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); } else { ImGui::TextDisabled("--"); } ImGui::TableSetColumnIndex(5); ImGui::PushID(static_cast(i) + 7000); if (auction.buyoutPrice > 0 && ImGui::SmallButton("Buy")) { gameHandler.auctionBuyout(auction.auctionId, auction.buyoutPrice); } if (auction.buyoutPrice > 0) ImGui::SameLine(); if (ImGui::SmallButton("Bid")) { uint32_t bidAmt = auction.currentBid > 0 ? auction.currentBid + auction.minBidIncrement : auction.startBid; gameHandler.auctionPlaceBid(auction.auctionId, bidAmt); } ImGui::PopID(); } ImGui::EndTable(); } } ImGui::EndChild(); // Sell section ImGui::Separator(); ImGui::Text("Sell:"); ImGui::SameLine(); ImGui::Text("Bid:"); ImGui::SameLine(); ImGui::SetNextItemWidth(50); ImGui::InputInt("##sbg", &auctionSellBid_[0], 0); ImGui::SameLine(); ImGui::Text("g"); ImGui::SameLine(); ImGui::SetNextItemWidth(35); ImGui::InputInt("##sbs", &auctionSellBid_[1], 0); ImGui::SameLine(); ImGui::Text("s"); ImGui::SameLine(); ImGui::SetNextItemWidth(35); ImGui::InputInt("##sbc", &auctionSellBid_[2], 0); ImGui::SameLine(); ImGui::Text("c"); ImGui::Text(" "); ImGui::SameLine(); ImGui::Text("Buyout:"); ImGui::SameLine(); ImGui::SetNextItemWidth(50); ImGui::InputInt("##sbog", &auctionSellBuyout_[0], 0); ImGui::SameLine(); ImGui::Text("g"); ImGui::SameLine(); ImGui::SetNextItemWidth(35); ImGui::InputInt("##sbos", &auctionSellBuyout_[1], 0); ImGui::SameLine(); ImGui::Text("s"); ImGui::SameLine(); ImGui::SetNextItemWidth(35); ImGui::InputInt("##sboc", &auctionSellBuyout_[2], 0); ImGui::SameLine(); ImGui::Text("c"); const char* durations[] = {"12 hours", "24 hours", "48 hours"}; ImGui::SameLine(); ImGui::SetNextItemWidth(90); ImGui::Combo("##dur", &auctionSellDuration_, durations, 3); } else if (tab == 1) { // Bids tab const auto& results = gameHandler.getAuctionBidderResults(); ImGui::Text("Your Bids: %zu items", results.auctions.size()); if (ImGui::BeginTable("BidTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Your Bid", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableHeadersRow(); for (const auto& a : results.auctions) { auto* info = gameHandler.getItemInfo(a.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%u", a.stackCount); ImGui::TableSetColumnIndex(2); ImGui::Text("%ug%us%uc", a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); uint32_t mins = a.timeLeftMs / 60000; if (mins > 720) ImGui::Text("Long"); else if (mins > 120) ImGui::Text("Medium"); else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short"); } ImGui::EndTable(); } } else if (tab == 2) { // Auctions tab (your listings) const auto& results = gameHandler.getAuctionOwnerResults(); ImGui::Text("Your Auctions: %zu items", results.auctions.size()); if (ImGui::BeginTable("OwnerTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("##cancel", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableHeadersRow(); for (size_t i = 0; i < results.auctions.size(); i++) { const auto& a = results.auctions[i]; auto* info = gameHandler.getItemInfo(a.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextColored(InventoryScreen::getQualityColor(quality), "%s", name.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%u", a.stackCount); ImGui::TableSetColumnIndex(2); { uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid; ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); } ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); ImGui::PushID(static_cast(i) + 8000); if (ImGui::SmallButton("Cancel")) { gameHandler.auctionCancelItem(a.auctionId); } ImGui::PopID(); } ImGui::EndTable(); } } ImGui::End(); if (!open) gameHandler.closeAuctionHouse(); } // ============================================================ // Level-Up Ding Animation // ============================================================ void GameScreen::triggerDing(uint32_t newLevel) { dingTimer_ = DING_DURATION; dingLevel_ = newLevel; auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { if (auto* sfx = renderer->getUiSoundManager()) { sfx->playLevelUp(); } renderer->playEmote("cheer"); } } void GameScreen::renderDingEffect() { if (dingTimer_ <= 0.0f) return; float dt = ImGui::GetIO().DeltaTime; dingTimer_ -= dt; if (dingTimer_ < 0.0f) dingTimer_ = 0.0f; float alpha = dingTimer_ < 0.8f ? (dingTimer_ / 0.8f) : 1.0f; // fade out last 0.8s ImGuiIO& io = ImGui::GetIO(); float cx = io.DisplaySize.x * 0.5f; float cy = io.DisplaySize.y * 0.5f; ImDrawList* draw = ImGui::GetForegroundDrawList(); // "LEVEL X!" text — visible for first 2.2s if (dingTimer_ > 0.8f) { ImFont* font = ImGui::GetFont(); float baseSize = ImGui::GetFontSize(); float fontSize = baseSize * 2.8f; char buf[32]; snprintf(buf, sizeof(buf), "LEVEL %u!", dingLevel_); ImVec2 sz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, buf); float tx = cx - sz.x * 0.5f; float ty = cy - sz.y * 0.5f - 20.0f; // Drop shadow draw->AddText(font, fontSize, ImVec2(tx + 3, ty + 3), IM_COL32(0, 0, 0, (int)(alpha * 200)), buf); // Gold text draw->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(255, 215, 0, (int)(alpha * 255)), buf); // "DING!" subtitle const char* ding = "DING!"; float dingSize = baseSize * 1.8f; ImVec2 dingSz = font->CalcTextSizeA(dingSize, FLT_MAX, 0.0f, ding); float dx = cx - dingSz.x * 0.5f; float dy = ty + sz.y + 6.0f; draw->AddText(font, dingSize, ImVec2(dx + 2, dy + 2), IM_COL32(0, 0, 0, (int)(alpha * 180)), ding); draw->AddText(font, dingSize, ImVec2(dx, dy), IM_COL32(255, 255, 150, (int)(alpha * 255)), ding); } } }} // namespace wowee::ui