From 4bc5064515fa8614d8350b460699c6df3c29933d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 4 Feb 2026 11:31:08 -0800 Subject: [PATCH] Add spellbook, fix WMO floor clipping, and polish UI/visuals - Add spellbook screen (P key) with Spell.dbc name lookup and action bar assignment - Default Attack and Hearthstone spells available in single player - Fix WMO floor clipping (gryphon roost) by tightening ceiling rejection threshold - Darken ocean water, increase wave motion and opacity - Add M2 model distance fade-in to prevent pop-in - Reposition chat window, add slash/enter key focus - Remove debug key commands (keep only F1 perf HUD, N minimap) - Performance: return chat history by const ref, use deque for O(1) pop_front --- CMakeLists.txt | 2 + include/game/game_handler.hpp | 5 +- include/rendering/m2_renderer.hpp | 1 + include/rendering/wmo_renderer.hpp | 1 + include/ui/game_screen.hpp | 2 + include/ui/spellbook_screen.hpp | 38 ++++ src/core/application.cpp | 272 ----------------------------- src/game/game_handler.cpp | 105 ++++++----- src/game/world_packets.cpp | 10 +- src/rendering/m2_renderer.cpp | 84 ++++++--- src/rendering/renderer.cpp | 10 +- src/rendering/terrain_manager.cpp | 7 +- src/rendering/terrain_renderer.cpp | 78 +++++---- src/rendering/water_renderer.cpp | 21 +-- src/rendering/wmo_renderer.cpp | 34 ++-- src/ui/game_screen.cpp | 52 +++--- src/ui/spellbook_screen.cpp | 195 +++++++++++++++++++++ 17 files changed, 486 insertions(+), 431 deletions(-) create mode 100644 include/ui/spellbook_screen.hpp create mode 100644 src/ui/spellbook_screen.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 57dfb17b..304d1e98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,7 @@ set(WOWEE_SOURCES src/ui/character_screen.cpp src/ui/game_screen.cpp src/ui/inventory_screen.cpp + src/ui/spellbook_screen.cpp # Main src/main.cpp @@ -226,6 +227,7 @@ set(WOWEE_HEADERS include/ui/character_screen.hpp include/ui/game_screen.hpp include/ui/inventory_screen.hpp + include/ui/spellbook_screen.hpp ) # Create executable diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4accefc9..da7a497a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +152,7 @@ public: * @param maxMessages Maximum number of messages to return (0 = all) * @return Vector of chat messages */ - std::vector getChatHistory(size_t maxMessages = 50) const; + const std::deque& getChatHistory() const { return chatHistory; } /** * Add a locally-generated chat message (e.g., emote feedback) @@ -401,7 +402,7 @@ private: EntityManager entityManager; // Manages all entities in view // Chat - std::vector chatHistory; // Recent chat messages + std::deque chatHistory; // Recent chat messages size_t maxChatHistory = 100; // Maximum chat messages to keep // Targeting diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index dec4fa77..a4cdf104 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -48,6 +48,7 @@ struct M2ModelGPU { bool collisionSmallSolidProp = false; bool collisionNarrowVerticalProp = false; bool collisionNoBlock = false; + bool collisionStatue = false; std::string name; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index e693ff74..800fde10 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -235,6 +235,7 @@ private: glm::mat4 invModelMatrix; // Cached inverse for collision glm::vec3 worldBoundsMin; glm::vec3 worldBoundsMax; + std::vector> worldGroupBounds; void updateModelMatrix(); }; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index bd5179db..738d7906 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -3,6 +3,7 @@ #include "game/game_handler.hpp" #include "game/inventory.hpp" #include "ui/inventory_screen.hpp" +#include "ui/spellbook_screen.hpp" #include #include @@ -110,6 +111,7 @@ private: * Inventory screen */ InventoryScreen inventoryScreen; + SpellbookScreen spellbookScreen; }; }} // namespace wowee::ui diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp new file mode 100644 index 00000000..56ab906d --- /dev/null +++ b/include/ui/spellbook_screen.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "game/game_handler.hpp" +#include +#include +#include + +namespace wowee { + +namespace pipeline { class AssetManager; } + +namespace ui { + +class SpellbookScreen { +public: + void render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager); + bool isOpen() const { return open; } + void toggle() { open = !open; } + void setOpen(bool o) { open = o; } + +private: + bool open = false; + bool pKeyWasDown = false; + + // Spell name cache (loaded from Spell.dbc) + bool dbcLoaded = false; + bool dbcLoadAttempted = false; + std::unordered_map spellNames; + + // Action bar assignment + int assigningSlot = -1; // Which action bar slot is being assigned (-1 = none) + + void loadSpellDBC(pipeline::AssetManager* assetManager); + std::string getSpellName(uint32_t spellId) const; +}; + +} // namespace ui +} // namespace wowee diff --git a/src/core/application.cpp b/src/core/application.cpp index 66c56d33..be5e64b9 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -214,282 +214,10 @@ void Application::run() { LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF"); } } - // F2: Toggle wireframe - else if (event.key.keysym.scancode == SDL_SCANCODE_F2) { - static bool wireframe = false; - wireframe = !wireframe; - if (renderer) { - renderer->setWireframeMode(wireframe); - LOG_INFO("Wireframe mode: ", wireframe ? "ON" : "OFF"); - } - } - // F3: Load test terrain (if in main menu/auth state) - else if (event.key.keysym.scancode == SDL_SCANCODE_F3) { - if (assetManager && assetManager->isInitialized()) { - LOG_INFO("Loading test terrain..."); - // Load a test ADT tile (Elwynn Forest) - if (renderer->loadTestTerrain(assetManager.get(), - "World\\Maps\\Azeroth\\Azeroth_32_49.adt")) { - LOG_INFO("Test terrain loaded! Use WASD/QE to move, hold right mouse to look"); - } - } else { - LOG_WARNING("Asset manager not initialized. Set WOW_DATA_PATH environment variable."); - } - } - // F4: Toggle frustum culling - else if (event.key.keysym.scancode == SDL_SCANCODE_F4) { - if (renderer && renderer->getTerrainRenderer()) { - static bool culling = true; - culling = !culling; - renderer->getTerrainRenderer()->setFrustumCulling(culling); - LOG_INFO("Frustum culling: ", culling ? "ON" : "OFF"); - } - } - // F5: Show rendering statistics - else if (event.key.keysym.scancode == SDL_SCANCODE_F5) { - if (renderer && renderer->getTerrainRenderer()) { - auto* terrain = renderer->getTerrainRenderer(); - LOG_INFO("=== Rendering Statistics ==="); - LOG_INFO(" Total chunks: ", terrain->getChunkCount()); - LOG_INFO(" Rendered: ", terrain->getRenderedChunkCount()); - LOG_INFO(" Culled: ", terrain->getCulledChunkCount()); - LOG_INFO(" Triangles: ", terrain->getTriangleCount()); - - if (terrain->getChunkCount() > 0) { - float visiblePercent = (terrain->getRenderedChunkCount() * 100.0f) / terrain->getChunkCount(); - LOG_INFO(" Visible: ", static_cast(visiblePercent), "%"); - } - - // Show terrain manager stats - if (renderer->getTerrainManager()) { - auto* manager = renderer->getTerrainManager(); - LOG_INFO(" Loaded tiles: ", manager->getLoadedTileCount()); - auto currentTile = manager->getCurrentTile(); - LOG_INFO(" Current tile: [", currentTile.x, ",", currentTile.y, "]"); - } - } - } - // F6: Load multi-tile terrain area - else if (event.key.keysym.scancode == SDL_SCANCODE_F6) { - if (assetManager && assetManager->isInitialized()) { - LOG_INFO("Loading 3x3 terrain area (Elwynn Forest)..."); - // Load 3x3 grid of tiles (Elwynn Forest area) - if (renderer->loadTerrainArea("Azeroth", 32, 49, 1)) { - LOG_INFO("Terrain area loaded! Streaming enabled."); - LOG_INFO("Move around to see dynamic tile loading/unloading"); - } - } else { - LOG_WARNING("Asset manager not initialized. Set WOW_DATA_PATH environment variable."); - } - } - // F7: Toggle terrain streaming - else if (event.key.keysym.scancode == SDL_SCANCODE_F7) { - if (renderer && renderer->getTerrainManager()) { - static bool streaming = true; - streaming = !streaming; - renderer->setTerrainStreaming(streaming); - } - } - // F8: Toggle water rendering - else if (event.key.keysym.scancode == SDL_SCANCODE_F8) { - if (renderer && renderer->getWaterRenderer()) { - static bool water = true; - water = !water; - renderer->getWaterRenderer()->setEnabled(water); - LOG_INFO("Water rendering: ", water ? "ON" : "OFF"); - } - } - // F9: Toggle time progression - else if (event.key.keysym.scancode == SDL_SCANCODE_F9) { - if (renderer && renderer->getSkybox()) { - bool progression = !renderer->getSkybox()->isTimeProgressionEnabled(); - renderer->getSkybox()->setTimeProgression(progression); - LOG_INFO("Time progression: ", progression ? "ON" : "OFF"); - } - } - // Plus/Equals: Advance time - else if (event.key.keysym.scancode == SDL_SCANCODE_EQUALS || - event.key.keysym.scancode == SDL_SCANCODE_KP_PLUS) { - if (renderer && renderer->getSkybox()) { - float time = renderer->getSkybox()->getTimeOfDay() + 1.0f; - renderer->getSkybox()->setTimeOfDay(time); - LOG_INFO("Time of day: ", static_cast(time), ":00"); - } - } - // Minus: Rewind time - else if (event.key.keysym.scancode == SDL_SCANCODE_MINUS || - event.key.keysym.scancode == SDL_SCANCODE_KP_MINUS) { - if (renderer && renderer->getSkybox()) { - float time = renderer->getSkybox()->getTimeOfDay() - 1.0f; - renderer->getSkybox()->setTimeOfDay(time); - LOG_INFO("Time of day: ", static_cast(time), ":00"); - } - } - // F10: Toggle celestial rendering (sun/moon) - else if (event.key.keysym.scancode == SDL_SCANCODE_F10) { - if (renderer && renderer->getCelestial()) { - bool enabled = !renderer->getCelestial()->isEnabled(); - renderer->getCelestial()->setEnabled(enabled); - LOG_INFO("Celestial rendering: ", enabled ? "ON" : "OFF"); - } - } - // F11: Toggle star field - else if (event.key.keysym.scancode == SDL_SCANCODE_F11) { - if (renderer && renderer->getStarField()) { - bool enabled = !renderer->getStarField()->isEnabled(); - renderer->getStarField()->setEnabled(enabled); - LOG_INFO("Star field: ", enabled ? "ON" : "OFF"); - } - } - // F12: Toggle distance fog - else if (event.key.keysym.scancode == SDL_SCANCODE_F12) { - if (renderer && renderer->getTerrainRenderer()) { - bool enabled = !renderer->getTerrainRenderer()->isFogEnabled(); - renderer->getTerrainRenderer()->setFogEnabled(enabled); - LOG_INFO("Distance fog: ", enabled ? "ON" : "OFF"); - } - } - // C: Toggle clouds - else if (event.key.keysym.scancode == SDL_SCANCODE_C) { - if (renderer && renderer->getClouds()) { - bool enabled = !renderer->getClouds()->isEnabled(); - renderer->getClouds()->setEnabled(enabled); - LOG_INFO("Clouds: ", enabled ? "ON" : "OFF"); - } - } - // [ (Left bracket): Decrease cloud density - else if (event.key.keysym.scancode == SDL_SCANCODE_LEFTBRACKET) { - if (renderer && renderer->getClouds()) { - float density = renderer->getClouds()->getDensity() - 0.1f; - renderer->getClouds()->setDensity(density); - LOG_INFO("Cloud density: ", static_cast(density * 100), "%"); - } - } - // ] (Right bracket): Increase cloud density - else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHTBRACKET) { - if (renderer && renderer->getClouds()) { - float density = renderer->getClouds()->getDensity() + 0.1f; - renderer->getClouds()->setDensity(density); - LOG_INFO("Cloud density: ", static_cast(density * 100), "%"); - } - } - // L: Toggle lens flare - else if (event.key.keysym.scancode == SDL_SCANCODE_L) { - if (renderer && renderer->getLensFlare()) { - bool enabled = !renderer->getLensFlare()->isEnabled(); - renderer->getLensFlare()->setEnabled(enabled); - LOG_INFO("Lens flare: ", enabled ? "ON" : "OFF"); - } - } - // , (Comma): Decrease lens flare intensity - else if (event.key.keysym.scancode == SDL_SCANCODE_COMMA) { - if (renderer && renderer->getLensFlare()) { - float intensity = renderer->getLensFlare()->getIntensity() - 0.1f; - renderer->getLensFlare()->setIntensity(intensity); - LOG_INFO("Lens flare intensity: ", static_cast(intensity * 100), "%"); - } - } - // . (Period): Increase lens flare intensity - else if (event.key.keysym.scancode == SDL_SCANCODE_PERIOD) { - if (renderer && renderer->getLensFlare()) { - float intensity = renderer->getLensFlare()->getIntensity() + 0.1f; - renderer->getLensFlare()->setIntensity(intensity); - LOG_INFO("Lens flare intensity: ", static_cast(intensity * 100), "%"); - } - } - // M: Toggle moon phase cycling - else if (event.key.keysym.scancode == SDL_SCANCODE_M) { - if (renderer && renderer->getCelestial()) { - bool cycling = !renderer->getCelestial()->isMoonPhaseCycling(); - renderer->getCelestial()->setMoonPhaseCycling(cycling); - LOG_INFO("Moon phase cycling: ", cycling ? "ON" : "OFF"); - } - } - // ; (Semicolon): Previous moon phase - else if (event.key.keysym.scancode == SDL_SCANCODE_SEMICOLON) { - if (renderer && renderer->getCelestial()) { - float phase = renderer->getCelestial()->getMoonPhase() - 0.05f; - if (phase < 0.0f) phase += 1.0f; - renderer->getCelestial()->setMoonPhase(phase); - - // Log phase name - const char* phaseName = "Unknown"; - if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New Moon"; - else if (phase < 0.1875f) phaseName = "Waxing Crescent"; - else if (phase < 0.3125f) phaseName = "First Quarter"; - else if (phase < 0.4375f) phaseName = "Waxing Gibbous"; - else if (phase < 0.5625f) phaseName = "Full Moon"; - else if (phase < 0.6875f) phaseName = "Waning Gibbous"; - else if (phase < 0.8125f) phaseName = "Last Quarter"; - else phaseName = "Waning Crescent"; - - LOG_INFO("Moon phase: ", phaseName, " (", static_cast(phase * 100), "%)"); - } - } - // ' (Apostrophe): Next moon phase - else if (event.key.keysym.scancode == SDL_SCANCODE_APOSTROPHE) { - if (renderer && renderer->getCelestial()) { - float phase = renderer->getCelestial()->getMoonPhase() + 0.05f; - if (phase >= 1.0f) phase -= 1.0f; - renderer->getCelestial()->setMoonPhase(phase); - - // Log phase name - const char* phaseName = "Unknown"; - if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New Moon"; - else if (phase < 0.1875f) phaseName = "Waxing Crescent"; - else if (phase < 0.3125f) phaseName = "First Quarter"; - else if (phase < 0.4375f) phaseName = "Waxing Gibbous"; - else if (phase < 0.5625f) phaseName = "Full Moon"; - else if (phase < 0.6875f) phaseName = "Waning Gibbous"; - else if (phase < 0.8125f) phaseName = "Last Quarter"; - else phaseName = "Waning Crescent"; - - LOG_INFO("Moon phase: ", phaseName, " (", static_cast(phase * 100), "%)"); - } - } - // X key reserved for sit (handled in camera_controller) - // < (Shift+,): Decrease weather intensity - else if (event.key.keysym.scancode == SDL_SCANCODE_COMMA && - (event.key.keysym.mod & KMOD_SHIFT)) { - if (renderer && renderer->getWeather()) { - float intensity = renderer->getWeather()->getIntensity() - 0.1f; - renderer->getWeather()->setIntensity(intensity); - LOG_INFO("Weather intensity: ", static_cast(intensity * 100), "%"); - } - } - // > (Shift+.): Increase weather intensity - else if (event.key.keysym.scancode == SDL_SCANCODE_PERIOD && - (event.key.keysym.mod & KMOD_SHIFT)) { - if (renderer && renderer->getWeather()) { - float intensity = renderer->getWeather()->getIntensity() + 0.1f; - renderer->getWeather()->setIntensity(intensity); - LOG_INFO("Weather intensity: ", static_cast(intensity * 100), "%"); - } - } - // K: Spawn player character at camera position - else if (event.key.keysym.scancode == SDL_SCANCODE_K) { - spawnPlayerCharacter(); - } - // J: Remove all characters - else if (event.key.keysym.scancode == SDL_SCANCODE_J) { - if (renderer && renderer->getCharacterRenderer()) { - // Note: CharacterRenderer doesn't have removeAll(), so we'd need to track IDs - // For now, just log - LOG_INFO("Character removal not yet implemented"); - } - } // N: Toggle minimap else if (event.key.keysym.scancode == SDL_SCANCODE_N) { if (renderer && renderer->getMinimap()) { renderer->getMinimap()->toggle(); - LOG_INFO("Minimap ", renderer->getMinimap()->isEnabled() ? "enabled" : "disabled"); - } - } - // P: Remove all WMO buildings (O key removed) - else if (event.key.keysym.scancode == SDL_SCANCODE_P) { - if (renderer && renderer->getWMORenderer()) { - renderer->getWMORenderer()->clearInstances(); - LOG_INFO("Cleared all WMO instances"); } } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d184028c..1d8e8ac6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -13,6 +13,16 @@ namespace game { GameHandler::GameHandler() { LOG_DEBUG("GameHandler created"); + + // Default spells always available + knownSpells.push_back(6603); // Attack + knownSpells.push_back(8690); // Hearthstone + + // Default action bar layout + actionBar[0].type = ActionBarSlot::SPELL; + actionBar[0].id = 6603; // Attack in slot 1 + actionBar[11].type = ActionBarSlot::SPELL; + actionBar[11].id = 8690; // Hearthstone in slot 12 } GameHandler::~GameHandler() { @@ -780,19 +790,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } - // Extract health/mana/power from fields (Phase 2) + // Extract health/mana/power from fields (Phase 2) — single pass if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); - auto hpIt = block.fields.find(24); // UNIT_FIELD_HEALTH - if (hpIt != block.fields.end()) unit->setHealth(hpIt->second); - auto maxHpIt = block.fields.find(32); // UNIT_FIELD_MAXHEALTH - if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second); - auto powerIt = block.fields.find(25); // UNIT_FIELD_POWER1 - if (powerIt != block.fields.end()) unit->setPower(powerIt->second); - auto maxPowerIt = block.fields.find(33); // UNIT_FIELD_MAXPOWER1 - if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second); - auto levelIt = block.fields.find(54); // UNIT_FIELD_LEVEL - if (levelIt != block.fields.end()) unit->setLevel(levelIt->second); + for (const auto& [key, val] : block.fields) { + switch (key) { + case 24: unit->setHealth(val); break; + case 25: unit->setPower(val); break; + case 32: unit->setMaxHealth(val); break; + case 33: unit->setMaxPower(val); break; + case 54: unit->setLevel(val); break; + default: break; + } + } } break; } @@ -805,19 +815,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { entity->setField(field.first, field.second); } - // Update cached health/mana/power values (Phase 2) + // Update cached health/mana/power values (Phase 2) — single pass if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); - auto hpIt = block.fields.find(24); - if (hpIt != block.fields.end()) unit->setHealth(hpIt->second); - auto maxHpIt = block.fields.find(32); - if (maxHpIt != block.fields.end()) unit->setMaxHealth(maxHpIt->second); - auto powerIt = block.fields.find(25); - if (powerIt != block.fields.end()) unit->setPower(powerIt->second); - auto maxPowerIt = block.fields.find(33); - if (maxPowerIt != block.fields.end()) unit->setMaxPower(maxPowerIt->second); - auto levelIt = block.fields.find(54); - if (levelIt != block.fields.end()) unit->setLevel(levelIt->second); + for (const auto& [key, val] : block.fields) { + switch (key) { + case 24: unit->setHealth(val); break; + case 25: unit->setPower(val); break; + case 32: unit->setMaxHealth(val); break; + case 33: unit->setMaxPower(val); break; + case 54: unit->setLevel(val); break; + default: break; + } + } } LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec); @@ -1013,22 +1023,10 @@ void GameHandler::tabTarget(float playerX, float playerY, float playerZ) { void GameHandler::addLocalChatMessage(const MessageChatData& msg) { chatHistory.push_back(msg); if (chatHistory.size() > maxChatHistory) { - chatHistory.erase(chatHistory.begin()); + chatHistory.pop_front(); } } -std::vector GameHandler::getChatHistory(size_t maxMessages) const { - if (maxMessages == 0 || maxMessages >= chatHistory.size()) { - return chatHistory; - } - - // Return last N messages - return std::vector( - chatHistory.end() - maxMessages, - chatHistory.end() - ); -} - // ============================================================ // Phase 1: Name Queries // ============================================================ @@ -1207,6 +1205,20 @@ void GameHandler::handleSpellHealLog(network::Packet& packet) { void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { if (state != WorldState::IN_WORLD || !socket) return; + + // Attack (6603) routes to auto-attack instead of cast + if (spellId == 6603) { + uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid; + if (target != 0) { + if (autoAttacking) { + stopAutoAttack(); + } else { + startAutoAttack(target); + } + } + return; + } + if (casting) return; // Already casting uint64_t target = targetGuid != 0 ? targetGuid : targetGuid; @@ -1249,6 +1261,14 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { knownSpells = data.spellIds; + // Ensure Attack (6603) and Hearthstone (8690) are always present + if (std::find(knownSpells.begin(), knownSpells.end(), 6603u) == knownSpells.end()) { + knownSpells.insert(knownSpells.begin(), 6603u); + } + if (std::find(knownSpells.begin(), knownSpells.end(), 8690u) == knownSpells.end()) { + knownSpells.push_back(8690u); + } + // Set initial cooldowns for (const auto& cd : data.cooldowns) { if (cd.cooldownMs > 0) { @@ -1256,10 +1276,17 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { } } - // Auto-populate action bar with first 12 spells - for (int i = 0; i < ACTION_BAR_SLOTS && i < static_cast(knownSpells.size()); ++i) { - actionBar[i].type = ActionBarSlot::SPELL; - actionBar[i].id = knownSpells[i]; + // Auto-populate action bar: Attack in slot 1, Hearthstone in slot 12, rest filled with known spells + actionBar[0].type = ActionBarSlot::SPELL; + actionBar[0].id = 6603; // Attack + actionBar[11].type = ActionBarSlot::SPELL; + actionBar[11].id = 8690; // Hearthstone + int slot = 1; + for (int i = 0; i < static_cast(knownSpells.size()) && slot < 11; ++i) { + if (knownSpells[i] == 6603 || knownSpells[i] == 8690) continue; + actionBar[slot].type = ActionBarSlot::SPELL; + actionBar[slot].id = knownSpells[i]; + slot++; } LOG_INFO("Learned ", knownSpells.size(), " spells"); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 638f84a5..87713b0b 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -757,11 +757,10 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { // Read sender name length + name uint32_t nameLen = packet.readUInt32(); if (nameLen > 0 && nameLen < 256) { - std::vector nameBuffer(nameLen); + data.senderName.resize(nameLen); for (uint32_t i = 0; i < nameLen; ++i) { - nameBuffer[i] = static_cast(packet.readUInt8()); + data.senderName[i] = static_cast(packet.readUInt8()); } - data.senderName = std::string(nameBuffer.begin(), nameBuffer.end()); } // Read receiver GUID (usually 0 for monsters) @@ -798,11 +797,10 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { // Read message if (messageLen > 0 && messageLen < 8192) { - std::vector msgBuffer(messageLen); + data.message.resize(messageLen); for (uint32_t i = 0; i < messageLen; ++i) { - msgBuffer[i] = static_cast(packet.readUInt8()); + data.message[i] = static_cast(packet.readUInt8()); } - data.message = std::string(msgBuffer.begin(), msgBuffer.end()); } // Read chat tag diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 52e5fb33..02445da5 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -72,10 +72,12 @@ float getEffectiveCollisionTopLocal(const M2ModelGPU& model, float h = localMax.z - localMin.z; if (model.collisionSteppedFountain) { - if (r > 0.88f) return localMin.z + h * 0.20f; // outer lip - if (r > 0.62f) return localMin.z + h * 0.42f; // mid step - if (r > 0.36f) return localMin.z + h * 0.66f; // inner step - return localMin.z + h * 0.90f; // center/top approach + if (r > 0.85f) return localMin.z + h * 0.18f; // outer lip + if (r > 0.65f) return localMin.z + h * 0.36f; // mid step + if (r > 0.45f) return localMin.z + h * 0.54f; // inner step + if (r > 0.28f) return localMin.z + h * 0.70f; // center platform / statue base + if (r > 0.14f) return localMin.z + h * 0.84f; // statue body / sword + return localMin.z + h * 0.96f; // statue head / top } // Low square curb/planter profile: @@ -239,6 +241,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { uniform sampler2D uTexture; uniform bool uHasTexture; uniform bool uAlphaTest; + uniform float uFadeAlpha; out vec4 FragColor; @@ -255,6 +258,12 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { discard; } + // Distance fade - discard nearly invisible fragments + float finalAlpha = texColor.a * uFadeAlpha; + if (finalAlpha < 0.02) { + discard; + } + vec3 normal = normalize(Normal); vec3 lightDir = normalize(uLightDir); @@ -265,7 +274,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) { vec3 diffuse = diff * texColor.rgb; vec3 result = ambient + diffuse; - FragColor = vec4(result, texColor.a); + FragColor = vec4(result, finalAlpha); } )"; @@ -364,7 +373,13 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bool isPlanter = (lowerName.find("planter") != std::string::npos); gpuModel.collisionPlanter = isPlanter; + bool statueName = + (lowerName.find("statue") != std::string::npos) || + (lowerName.find("monument") != std::string::npos) || + (lowerName.find("sculpture") != std::string::npos); + gpuModel.collisionStatue = statueName; bool smallSolidPropName = + statueName || (lowerName.find("crate") != std::string::npos) || (lowerName.find("box") != std::string::npos) || (lowerName.find("chest") != std::string::npos) || @@ -402,7 +417,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { !gpuModel.collisionSteppedLowPlatform && (narrowVerticalName || narrowVerticalShape); bool genericSolidPropShape = - (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f); + (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f) || + statueName; bool curbLikeName = (lowerName.find("curb") != std::string::npos) || (lowerName.find("planter") != std::string::npos) || @@ -619,12 +635,10 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: // Set up GL state for M2 rendering glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); - glDisable(GL_BLEND); // No blend leaking from prior renderers + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided - // Make models render with a bright color for debugging - // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Wireframe mode - // Build frustum for culling Frustum frustum; frustum.extractFromMatrix(projection * view); @@ -640,6 +654,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: // Distance-based culling threshold for M2 models const float maxRenderDistance = 180.0f; // Aggressive culling for city performance const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; + const float fadeStartFraction = 0.75f; // Start fading at 75% of max distance const glm::vec3 camPos = camera.getPosition(); for (const auto& instance : instances) { @@ -669,9 +684,25 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: continue; } + // Distance-based fade alpha for smooth pop-in + float fadeAlpha = 1.0f; + float fadeStartDistSq = effectiveMaxDistSq * fadeStartFraction * fadeStartFraction; + if (distSq > fadeStartDistSq) { + float dist = std::sqrt(distSq); + float effectiveMaxDist = std::sqrt(effectiveMaxDistSq); + float fadeStartDist = effectiveMaxDist * fadeStartFraction; + fadeAlpha = std::clamp((effectiveMaxDist - dist) / (effectiveMaxDist - fadeStartDist), 0.0f, 1.0f); + } + shader->setUniform("uModel", instance.modelMatrix); shader->setUniform("uTime", instance.animTime); shader->setUniform("uAnimScale", 0.0f); // Disabled - proper M2 animation needs bone/particle systems + shader->setUniform("uFadeAlpha", fadeAlpha); + + // Disable depth writes for fading objects to avoid z-fighting + if (fadeAlpha < 1.0f) { + glDepthMask(GL_FALSE); + } glBindVertexArray(model.vao); @@ -694,22 +725,15 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount++; } - // Check for GL errors (only first draw) - static bool checkedOnce = false; - if (!checkedOnce) { - checkedOnce = true; - GLenum err = glGetError(); - if (err != GL_NO_ERROR) { - LOG_ERROR("GL error after M2 draw: ", err); - } else { - LOG_INFO("M2 draw successful: ", model.indexCount, " indices"); - } - } - glBindVertexArray(0); + + if (fadeAlpha < 1.0f) { + glDepthMask(GL_TRUE); + } } - // Restore cull face state + // Restore state + glDisable(GL_BLEND); glEnable(GL_CULL_FACE); } @@ -942,8 +966,12 @@ std::optional M2Renderer::getFloorHeight(float glX, float glY, float glZ) // Reachability filter: allow a bit more climb for stepped low platforms. float maxStepUp = 1.0f; - if (model.collisionSmallSolidProp) { + if (model.collisionStatue) { + maxStepUp = 2.5f; + } else if (model.collisionSmallSolidProp) { maxStepUp = 2.0f; + } else if (model.collisionSteppedFountain) { + maxStepUp = 2.5f; } else if (model.collisionSteppedLowPlatform) { maxStepUp = model.collisionPlanter ? 3.0f : 2.4f; } @@ -1020,9 +1048,13 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, // Swept hard clamp for taller blockers only. // Low/stepable objects should be climbable and not "shove" the player off. float maxStepUp = 1.20f; - if (model.collisionSmallSolidProp) { + if (model.collisionStatue) { + maxStepUp = 2.5f; + } else if (model.collisionSmallSolidProp) { // Keep box/crate-class props hard-solid to prevent phase-through. maxStepUp = 0.75f; + } else if (model.collisionSteppedFountain) { + maxStepUp = 2.5f; } else if (model.collisionSteppedLowPlatform) { maxStepUp = model.collisionPlanter ? 2.8f : 2.4f; } @@ -1070,7 +1102,7 @@ bool M2Renderer::checkCollision(const glm::vec3& from, const glm::vec3& to, if (allowEscapeRelax) { continue; } - if (model.collisionSteppedLowPlatform && stepableLowObject) { + if ((model.collisionSteppedLowPlatform || model.collisionSteppedFountain) && stepableLowObject) { // Already on/near top surface: don't apply lateral push that ejects // the player from the object when landing. continue; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 05f69942..81cbb417 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -974,17 +974,17 @@ void Renderer::renderWorld(game::World* world) { swimEffects->render(*camera); } + // Compute view/projection once for all sub-renderers + const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f); + const glm::mat4& projection = camera ? camera->getProjectionMatrix() : glm::mat4(1.0f); + // Render characters (after weather) if (characterRenderer && camera) { - glm::mat4 view = camera->getViewMatrix(); - glm::mat4 projection = camera->getProjectionMatrix(); characterRenderer->render(*camera, view, projection); } // Render WMO buildings (after characters, before UI) if (wmoRenderer && camera) { - glm::mat4 view = camera->getViewMatrix(); - glm::mat4 projection = camera->getProjectionMatrix(); auto wmoStart = std::chrono::steady_clock::now(); wmoRenderer->render(*camera, view, projection); auto wmoEnd = std::chrono::steady_clock::now(); @@ -993,8 +993,6 @@ void Renderer::renderWorld(game::World* world) { // Render M2 doodads (trees, rocks, etc.) if (m2Renderer && camera) { - glm::mat4 view = camera->getViewMatrix(); - glm::mat4 projection = camera->getProjectionMatrix(); auto m2Start = std::chrono::steady_clock::now(); m2Renderer->render(*camera, view, projection); auto m2End = std::chrono::steady_clock::now(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index bbd4235e..fedd2678 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -936,9 +936,10 @@ std::optional TerrainManager::getDominantTextureAt(float glX, float int alphaY = glm::clamp(static_cast((fracY / 8.0f) * 63.0f), 0, 63); int alphaIndex = alphaY * 64 + alphaX; - std::vector weights(chunk.layers.size(), 0); + int weights[4] = {0, 0, 0, 0}; + size_t numLayers = std::min(chunk.layers.size(), static_cast(4)); int accum = 0; - for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) { + for (size_t layerIdx = 1; layerIdx < numLayers; layerIdx++) { int alpha = 0; if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast(alphaScratch.size())) { alpha = alphaScratch[alphaIndex]; @@ -950,7 +951,7 @@ std::optional TerrainManager::getDominantTextureAt(float glX, float size_t bestLayer = 0; int bestWeight = weights[0]; - for (size_t i = 1; i < weights.size(); i++) { + for (size_t i = 1; i < numLayers; i++) { if (weights[i] > bestWeight) { bestWeight = weights[i]; bestLayer = i; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 8cf16190..be7ffa2c 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -336,9 +336,12 @@ void TerrainRenderer::render(const Camera& camera) { frustum.extractFromMatrix(viewProj); } - // Render each chunk + // Render each chunk — track last-bound textures to skip redundant binds renderedChunks = 0; culledChunks = 0; + GLuint lastBound[7] = {0, 0, 0, 0, 0, 0, 0}; + int lastLayerConfig = -1; // track hasLayer1|hasLayer2|hasLayer3 bitmask + for (const auto& chunk : chunks) { if (!chunk.isValid()) { continue; @@ -350,48 +353,63 @@ void TerrainRenderer::render(const Camera& camera) { continue; } - // Bind textures - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, chunk.baseTexture); - shader->setUniform("uBaseTexture", 0); + // Bind base texture (slot 0) — skip if same as last chunk + if (chunk.baseTexture != lastBound[0]) { + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, chunk.baseTexture); + lastBound[0] = chunk.baseTexture; + } - // Bind layer textures and alphas + // Layer configuration bool hasLayer1 = chunk.layerTextures.size() > 0; bool hasLayer2 = chunk.layerTextures.size() > 1; bool hasLayer3 = chunk.layerTextures.size() > 2; + int layerConfig = (hasLayer1 ? 1 : 0) | (hasLayer2 ? 2 : 0) | (hasLayer3 ? 4 : 0); - shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0); - shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0); - shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0); + if (layerConfig != lastLayerConfig) { + shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0); + shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0); + shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0); + lastLayerConfig = layerConfig; + } if (hasLayer1) { - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]); - shader->setUniform("uLayer1Texture", 1); - - glActiveTexture(GL_TEXTURE4); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]); - shader->setUniform("uLayer1Alpha", 4); + if (chunk.layerTextures[0] != lastBound[1]) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]); + lastBound[1] = chunk.layerTextures[0]; + } + if (chunk.alphaTextures[0] != lastBound[4]) { + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]); + lastBound[4] = chunk.alphaTextures[0]; + } } if (hasLayer2) { - glActiveTexture(GL_TEXTURE2); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]); - shader->setUniform("uLayer2Texture", 2); - - glActiveTexture(GL_TEXTURE5); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]); - shader->setUniform("uLayer2Alpha", 5); + if (chunk.layerTextures[1] != lastBound[2]) { + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]); + lastBound[2] = chunk.layerTextures[1]; + } + if (chunk.alphaTextures[1] != lastBound[5]) { + glActiveTexture(GL_TEXTURE5); + glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]); + lastBound[5] = chunk.alphaTextures[1]; + } } if (hasLayer3) { - glActiveTexture(GL_TEXTURE3); - glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]); - shader->setUniform("uLayer3Texture", 3); - - glActiveTexture(GL_TEXTURE6); - glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]); - shader->setUniform("uLayer3Alpha", 6); + if (chunk.layerTextures[2] != lastBound[3]) { + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]); + lastBound[3] = chunk.layerTextures[2]; + } + if (chunk.alphaTextures[2] != lastBound[6]) { + glActiveTexture(GL_TEXTURE6); + glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]); + lastBound[6] = chunk.alphaTextures[2]; + } } // Draw chunk diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 0543ce39..730fd694 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -355,10 +355,7 @@ void WaterRenderer::render(const Camera& camera, float time) { return; } - GLboolean cullEnabled = glIsEnabled(GL_CULL_FACE); - if (cullEnabled) { - glDisable(GL_CULL_FACE); - } + glDisable(GL_CULL_FACE); // Enable alpha blending for transparent water glEnable(GL_BLEND); @@ -395,10 +392,10 @@ void WaterRenderer::render(const Camera& camera, float time) { // City/canal liquid profile: clearer water + stronger ripples/sun shimmer. // Stormwind canals typically use LiquidType 5 in this data set. bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); - float waveAmp = canalProfile ? 0.07f : 0.038f; - float waveFreq = canalProfile ? 0.30f : 0.22f; - float waveSpeed = canalProfile ? 1.20f : 0.90f; - float shimmerStrength = canalProfile ? 0.95f : 0.35f; + float waveAmp = canalProfile ? 0.07f : 0.12f; + float waveFreq = canalProfile ? 0.30f : 0.18f; + float waveSpeed = canalProfile ? 1.20f : 1.60f; + float shimmerStrength = canalProfile ? 0.95f : 0.50f; float alphaScale = canalProfile ? 0.72f : 1.00f; waterShader->setUniform("waterColor", color); @@ -418,9 +415,7 @@ void WaterRenderer::render(const Camera& camera, float time) { // Restore state glDepthMask(GL_TRUE); glDisable(GL_BLEND); - if (cullEnabled) { - glEnable(GL_CULL_FACE); - } + glEnable(GL_CULL_FACE); } void WaterRenderer::createWaterMesh(WaterSurface& surface) { @@ -747,7 +742,7 @@ glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { case 0: // Water return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); case 1: // Ocean - return glm::vec4(0.14f, 0.36f, 0.58f, 1.0f); + return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f); case 2: // Magma return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); case 3: // Slime @@ -760,7 +755,7 @@ glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 1: return 0.48f; // Ocean + case 1: return 0.68f; // Ocean case 2: return 0.72f; // Magma case 3: return 0.62f; // Slime default: return 0.38f; // Water diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 008d6aec..1dd02981 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -316,6 +316,16 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position transformAABB(instance.modelMatrix, model.boundingBoxMin, model.boundingBoxMax, instance.worldBoundsMin, instance.worldBoundsMax); + // Pre-compute world-space group bounds to avoid per-frame transformAABB + instance.worldGroupBounds.reserve(model.groups.size()); + for (const auto& group : model.groups) { + glm::vec3 gMin, gMax; + transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax); + gMin -= glm::vec3(0.5f); + gMax += glm::vec3(0.5f); + instance.worldGroupBounds.emplace_back(gMin, gMax); + } + instances.push_back(instance); size_t idx = instances.size() - 1; instanceIndexById[instance.id] = idx; @@ -480,22 +490,16 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: const ModelData& model = modelIt->second; shader->setUniform("uModel", instance.modelMatrix); - // Render all groups - for (const auto& group : model.groups) { - // Proper frustum culling using AABB test - if (frustumCulling) { - // Transform all AABB corners to avoid false culling on rotated groups. - glm::vec3 actualMin, actualMax; - transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, actualMin, actualMax); - // Small pad reduces edge flicker from precision/camera jitter. - actualMin -= glm::vec3(0.5f); - actualMax += glm::vec3(0.5f); - if (!frustum.intersectsAABB(actualMin, actualMax)) { + // Render all groups using cached world-space bounds + for (size_t gi = 0; gi < model.groups.size(); ++gi) { + if (frustumCulling && gi < instance.worldGroupBounds.size()) { + const auto& [gMin, gMax] = instance.worldGroupBounds[gi]; + if (!frustum.intersectsAABB(gMin, gMax)) { continue; } } - renderGroup(group, model, instance.modelMatrix, view, projection); + renderGroup(model.groups[gi], model, instance.modelMatrix, view, projection); } } @@ -991,8 +995,10 @@ std::optional WMORenderer::getFloorHeight(float glX, float glY, float glZ glm::vec3 hitLocal = localOrigin + localDir * t; glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f)); - // Only use floors below or near the query point - if (hitWorld.z <= glZ + 2.0f) { + // Only use floors below or near the query point. + // Callers already elevate glZ by +5..+6; keep buffer small + // to avoid selecting ceilings above the player. + if (hitWorld.z <= glZ + 0.5f) { if (!bestFloor || hitWorld.z > *bestFloor) { bestFloor = hitWorld.z; } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index c6a7d4da..eaf54344 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8,8 +8,6 @@ #include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" #include -#include -#include #include #include @@ -86,6 +84,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderGossipWindow(gameHandler); renderVendorWindow(gameHandler); + // Spellbook (P key toggle handled inside) + spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager()); + // Inventory (B key toggle handled inside) inventoryScreen.render(gameHandler.getInventory()); @@ -200,9 +201,9 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) { // GUID ImGui::TableSetColumnIndex(0); - std::stringstream guidStr; - guidStr << "0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << guid; - ImGui::Text("%s", guidStr.str().c_str()); + char guidStr[24]; + snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid); + ImGui::Text("%s", guidStr); // Type ImGui::TableSetColumnIndex(1); @@ -258,9 +259,16 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) { } void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { - ImGui::SetNextWindowSize(ImVec2(600, 300), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(520, 390), ImGuiCond_FirstUseEver); - ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse); + 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 + ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(chatX, chatY), ImGuiCond_Always); + ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); // Chat history const auto& chatHistory = gameHandler.getChatHistory(); @@ -271,21 +279,13 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImVec4 color = getChatTypeColor(msg.type); ImGui::PushStyleColor(ImGuiCol_Text, color); - std::stringstream ss; - if (msg.type == game::ChatType::TEXT_EMOTE) { - ss << "You " << msg.message; + ImGui::TextWrapped("You %s", msg.message.c_str()); + } else if (!msg.senderName.empty()) { + ImGui::TextWrapped("[%s] %s: %s", getChatTypeName(msg.type), msg.senderName.c_str(), msg.message.c_str()); } else { - ss << "[" << getChatTypeName(msg.type) << "] "; - - if (!msg.senderName.empty()) { - ss << msg.senderName << ": "; - } - - ss << msg.message; + ImGui::TextWrapped("[%s] %s", getChatTypeName(msg.type), msg.message.c_str()); } - - ImGui::TextWrapped("%s", ss.str().c_str()); ImGui::PopStyleColor(); } @@ -379,6 +379,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } } + // Slash key: focus chat input + if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { + refocusChatInput = true; + chatInputBuffer[0] = '/'; + chatInputBuffer[1] = '\0'; + } + + // Enter key: focus chat input (empty) + if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) { + refocusChatInput = true; + } + // Left-click targeting (when mouse not captured by UI) if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) { auto* renderer = core::Application::getInstance().getRenderer(); diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp new file mode 100644 index 00000000..b233b7bf --- /dev/null +++ b/src/ui/spellbook_screen.cpp @@ -0,0 +1,195 @@ +#include "ui/spellbook_screen.hpp" +#include "core/input.hpp" +#include "core/application.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { namespace ui { + +void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { + if (dbcLoadAttempted) return; + dbcLoadAttempted = true; + + if (!assetManager || !assetManager->isInitialized()) return; + + auto dbc = assetManager->loadDBC("Spell.dbc"); + if (!dbc || !dbc->isLoaded()) { + LOG_WARNING("Spellbook: Could not load Spell.dbc"); + return; + } + + // WoW 3.3.5a Spell.dbc: field 0 = SpellID, field 136 = SpellName_enUS + // Validate field count to determine name field index + uint32_t fieldCount = dbc->getFieldCount(); + uint32_t nameField = 136; + + if (fieldCount < 137) { + LOG_WARNING("Spellbook: Spell.dbc has ", fieldCount, " fields, expected 234+"); + // Try a heuristic: for smaller DBCs, name might be elsewhere + if (fieldCount > 10) { + nameField = fieldCount > 140 ? 136 : 1; + } else { + return; + } + } + + uint32_t count = dbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t spellId = dbc->getUInt32(i, 0); + std::string name = dbc->getString(i, nameField); + if (!name.empty() && spellId > 0) { + spellNames[spellId] = name; + } + } + + dbcLoaded = true; + LOG_INFO("Spellbook: Loaded ", spellNames.size(), " spell names from Spell.dbc"); +} + +std::string SpellbookScreen::getSpellName(uint32_t spellId) const { + auto it = spellNames.find(spellId); + if (it != spellNames.end()) { + return it->second; + } + char buf[32]; + snprintf(buf, sizeof(buf), "Spell #%u", spellId); + return buf; +} + +void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) { + // P key toggle (edge-triggered) + bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard; + bool pDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_P); + if (pDown && !pKeyWasDown) { + open = !open; + } + pKeyWasDown = pDown; + + if (!open) return; + + // Lazy-load Spell.dbc on first open + if (!dbcLoadAttempted) { + loadSpellDBC(assetManager); + } + + 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 bookW = 340.0f; + float bookH = std::min(500.0f, screenH - 120.0f); + float bookX = screenW - bookW - 10.0f; + float bookY = 80.0f; + + ImGui::SetNextWindowPos(ImVec2(bookX, bookY), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(bookW, bookH), ImGuiCond_FirstUseEver); + + bool windowOpen = open; + if (ImGui::Begin("Spellbook", &windowOpen)) { + const auto& spells = gameHandler.getKnownSpells(); + + if (spells.empty()) { + ImGui::TextDisabled("No spells known."); + } else { + ImGui::Text("%zu spells known", spells.size()); + ImGui::Separator(); + + // Action bar assignment mode indicator + if (assigningSlot >= 0) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), + "Click a spell to assign to slot %d", assigningSlot + 1); + if (ImGui::SmallButton("Cancel")) { + assigningSlot = -1; + } + ImGui::Separator(); + } + + // Spell list + ImGui::BeginChild("SpellList", ImVec2(0, -60), true); + + for (uint32_t spellId : spells) { + ImGui::PushID(static_cast(spellId)); + + std::string name = getSpellName(spellId); + float cd = gameHandler.getSpellCooldown(spellId); + bool onCooldown = cd > 0.0f; + + // Color based on state + if (onCooldown) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + } + + // Spell entry - clickable + char label[256]; + if (onCooldown) { + snprintf(label, sizeof(label), "%s (%.1fs)", name.c_str(), cd); + } else { + snprintf(label, sizeof(label), "%s", name.c_str()); + } + + if (ImGui::Selectable(label, false, ImGuiSelectableFlags_AllowDoubleClick)) { + if (assigningSlot >= 0) { + // Assign to action bar slot + gameHandler.setActionBarSlot(assigningSlot, + game::ActionBarSlot::SPELL, spellId); + assigningSlot = -1; + } else if (ImGui::IsMouseDoubleClicked(0)) { + // Double-click to cast + uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; + gameHandler.castSpell(spellId, target); + } + } + + // Tooltip with spell ID + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", name.c_str()); + ImGui::TextDisabled("Spell ID: %u", spellId); + if (!onCooldown) { + ImGui::TextDisabled("Double-click to cast"); + ImGui::TextDisabled("Use action bar buttons below to assign"); + } + ImGui::EndTooltip(); + } + + if (onCooldown) { + ImGui::PopStyleColor(); + } + + ImGui::PopID(); + } + + ImGui::EndChild(); + + // Action bar quick-assign buttons + ImGui::Separator(); + ImGui::Text("Assign to:"); + ImGui::SameLine(); + static const char* slotLabels[] = {"1","2","3","4","5","6","7","8","9","0","-","="}; + for (int i = 0; i < 12; ++i) { + if (i > 0) ImGui::SameLine(0, 2); + ImGui::PushID(100 + i); + bool isAssigning = (assigningSlot == i); + if (isAssigning) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.6f, 0.2f, 1.0f)); + } + if (ImGui::SmallButton(slotLabels[i])) { + assigningSlot = isAssigning ? -1 : i; + } + if (isAssigning) { + ImGui::PopStyleColor(); + } + ImGui::PopID(); + } + } + } + ImGui::End(); + + if (!windowOpen) { + open = false; + } +} + +}} // namespace wowee::ui