mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-05 00:33:51 +00:00
Initial commit: wowee native WoW 3.3.5a client
This commit is contained in:
commit
ce6cb8f38e
147 changed files with 32347 additions and 0 deletions
150
src/ui/auth_screen.cpp
Normal file
150
src/ui/auth_screen.cpp
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#include "ui/auth_screen.hpp"
|
||||
#include <imgui.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
AuthScreen::AuthScreen() {
|
||||
}
|
||||
|
||||
void AuthScreen::render(auth::AuthHandler& authHandler) {
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("WoW 3.3.5a Authentication", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
ImGui::Text("Connect to Authentication Server");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Server settings
|
||||
ImGui::Text("Server Settings");
|
||||
ImGui::InputText("Hostname", hostname, sizeof(hostname));
|
||||
ImGui::InputInt("Port", &port);
|
||||
if (port < 1) port = 1;
|
||||
if (port > 65535) port = 65535;
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Credentials
|
||||
ImGui::Text("Credentials");
|
||||
ImGui::InputText("Username", username, sizeof(username));
|
||||
|
||||
// Password with visibility toggle
|
||||
ImGuiInputTextFlags passwordFlags = showPassword ? 0 : ImGuiInputTextFlags_Password;
|
||||
ImGui::InputText("Password", password, sizeof(password), passwordFlags);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Show", &showPassword)) {
|
||||
// Checkbox state changed
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Connection status
|
||||
if (!statusMessage.empty()) {
|
||||
if (statusIsError) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
||||
}
|
||||
ImGui::TextWrapped("%s", statusMessage.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Connect button
|
||||
if (authenticating) {
|
||||
ImGui::Text("Authenticating...");
|
||||
|
||||
// Check authentication status
|
||||
auto state = authHandler.getState();
|
||||
if (state == auth::AuthState::AUTHENTICATED) {
|
||||
setStatus("Authentication successful!", false);
|
||||
authenticating = false;
|
||||
|
||||
// Call success callback
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} else if (state == auth::AuthState::FAILED) {
|
||||
setStatus("Authentication failed", true);
|
||||
authenticating = false;
|
||||
}
|
||||
} else {
|
||||
if (ImGui::Button("Connect", ImVec2(120, 0))) {
|
||||
attemptAuth(authHandler);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear", ImVec2(120, 0))) {
|
||||
statusMessage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Single-player mode button
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Single-Player Mode");
|
||||
ImGui::TextWrapped("Skip server connection and play offline with local rendering.");
|
||||
|
||||
if (ImGui::Button("Start Single Player", ImVec2(240, 30))) {
|
||||
// Call single-player callback
|
||||
if (onSinglePlayer) {
|
||||
onSinglePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Info text
|
||||
ImGui::TextWrapped("Enter your account credentials to connect to the authentication server.");
|
||||
ImGui::TextWrapped("Default port is 3724.");
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
|
||||
// Validate inputs
|
||||
if (strlen(username) == 0) {
|
||||
setStatus("Username cannot be empty", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strlen(password) == 0) {
|
||||
setStatus("Password cannot be empty", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strlen(hostname) == 0) {
|
||||
setStatus("Hostname cannot be empty", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt connection
|
||||
std::stringstream ss;
|
||||
ss << "Connecting to " << hostname << ":" << port << "...";
|
||||
setStatus(ss.str(), false);
|
||||
|
||||
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
|
||||
authenticating = true;
|
||||
setStatus("Connected, authenticating...", false);
|
||||
|
||||
// Send authentication credentials
|
||||
authHandler.authenticate(username, password);
|
||||
} else {
|
||||
setStatus("Failed to connect to server", true);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthScreen::setStatus(const std::string& message, bool isError) {
|
||||
statusMessage = message;
|
||||
statusIsError = isError;
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
211
src/ui/character_screen.cpp
Normal file
211
src/ui/character_screen.cpp
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
#include "ui/character_screen.hpp"
|
||||
#include <imgui.h>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
CharacterScreen::CharacterScreen() {
|
||||
}
|
||||
|
||||
void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Character Selection", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
ImGui::Text("Select a Character");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Status message
|
||||
if (!statusMessage.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
||||
ImGui::TextWrapped("%s", statusMessage.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Get character list
|
||||
const auto& characters = gameHandler.getCharacters();
|
||||
|
||||
// Request character list if not available
|
||||
if (characters.empty() && gameHandler.getState() == game::WorldState::READY) {
|
||||
ImGui::Text("Loading characters...");
|
||||
gameHandler.requestCharacterList();
|
||||
} else if (characters.empty()) {
|
||||
ImGui::Text("No characters available.");
|
||||
} else {
|
||||
// Character table
|
||||
if (ImGui::BeginTable("CharactersTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
||||
ImGui::TableSetupColumn("Race", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
||||
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
const auto& character = characters[i];
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Name column (selectable)
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool isSelected = (selectedCharacterIndex == static_cast<int>(i));
|
||||
|
||||
// Apply faction color to character name
|
||||
ImVec4 factionColor = getFactionColor(character.race);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, factionColor);
|
||||
|
||||
if (ImGui::Selectable(character.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
selectedCharacterIndex = static_cast<int>(i);
|
||||
selectedCharacterGuid = character.guid;
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Level column
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%d", character.level);
|
||||
|
||||
// Race column
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%s", game::getRaceName(character.race));
|
||||
|
||||
// Class column
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%s", game::getClassName(character.characterClass));
|
||||
|
||||
// Zone column
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::Text("%d", character.zoneId);
|
||||
|
||||
// Guild column
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
if (character.hasGuild()) {
|
||||
ImGui::Text("Yes");
|
||||
} else {
|
||||
ImGui::TextDisabled("No");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Selected character details
|
||||
if (selectedCharacterIndex >= 0 && selectedCharacterIndex < static_cast<int>(characters.size())) {
|
||||
const auto& character = characters[selectedCharacterIndex];
|
||||
|
||||
ImGui::Text("Character Details:");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Columns(2, nullptr, false);
|
||||
|
||||
// Left column
|
||||
ImGui::Text("Name:");
|
||||
ImGui::Text("Level:");
|
||||
ImGui::Text("Race:");
|
||||
ImGui::Text("Class:");
|
||||
ImGui::Text("Gender:");
|
||||
ImGui::Text("Location:");
|
||||
ImGui::Text("Guild:");
|
||||
if (character.hasPet()) {
|
||||
ImGui::Text("Pet:");
|
||||
}
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Right column
|
||||
ImGui::TextColored(getFactionColor(character.race), "%s", character.name.c_str());
|
||||
ImGui::Text("%d", character.level);
|
||||
ImGui::Text("%s", game::getRaceName(character.race));
|
||||
ImGui::Text("%s", game::getClassName(character.characterClass));
|
||||
ImGui::Text("%s", game::getGenderName(character.gender));
|
||||
ImGui::Text("Map %d, Zone %d", character.mapId, character.zoneId);
|
||||
if (character.hasGuild()) {
|
||||
ImGui::Text("Guild ID: %d", character.guildId);
|
||||
} else {
|
||||
ImGui::TextDisabled("None");
|
||||
}
|
||||
if (character.hasPet()) {
|
||||
ImGui::Text("Level %d (Family %d)", character.pet.level, character.pet.family);
|
||||
}
|
||||
|
||||
ImGui::Columns(1);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Enter World button
|
||||
if (ImGui::Button("Enter World", ImVec2(150, 40))) {
|
||||
characterSelected = true;
|
||||
std::stringstream ss;
|
||||
ss << "Entering world with " << character.name << "...";
|
||||
setStatus(ss.str());
|
||||
|
||||
gameHandler.selectCharacter(character.guid);
|
||||
|
||||
// Call callback
|
||||
if (onCharacterSelected) {
|
||||
onCharacterSelected(character.guid);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Display character GUID
|
||||
std::stringstream guidStr;
|
||||
guidStr << "GUID: 0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << character.guid;
|
||||
ImGui::TextDisabled("%s", guidStr.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Back/Refresh buttons
|
||||
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
||||
if (gameHandler.getState() == game::WorldState::READY ||
|
||||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
||||
gameHandler.requestCharacterList();
|
||||
setStatus("Refreshing character list...");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void CharacterScreen::setStatus(const std::string& message) {
|
||||
statusMessage = message;
|
||||
}
|
||||
|
||||
ImVec4 CharacterScreen::getFactionColor(game::Race race) const {
|
||||
// Alliance races: blue
|
||||
if (race == game::Race::HUMAN ||
|
||||
race == game::Race::DWARF ||
|
||||
race == game::Race::NIGHT_ELF ||
|
||||
race == game::Race::GNOME ||
|
||||
race == game::Race::DRAENEI) {
|
||||
return ImVec4(0.3f, 0.5f, 1.0f, 1.0f); // Blue
|
||||
}
|
||||
|
||||
// Horde races: red
|
||||
if (race == game::Race::ORC ||
|
||||
race == game::Race::UNDEAD ||
|
||||
race == game::Race::TAUREN ||
|
||||
race == game::Race::TROLL ||
|
||||
race == game::Race::BLOOD_ELF) {
|
||||
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
||||
}
|
||||
|
||||
// Default: white
|
||||
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
861
src/ui/game_screen.cpp
Normal file
861
src/ui/game_screen.cpp
Normal file
|
|
@ -0,0 +1,861 @@
|
|||
#include "ui/game_screen.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace {
|
||||
constexpr float ZEROPOINT = 32.0f * 533.33333f;
|
||||
|
||||
glm::vec3 wowToGL(float wowX, float wowY, float wowZ) {
|
||||
return { -(wowZ - ZEROPOINT), -(wowX - ZEROPOINT), wowY };
|
||||
}
|
||||
|
||||
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<wowee::game::Entity>& entity) {
|
||||
if (entity->getType() == wowee::game::ObjectType::PLAYER) {
|
||||
auto player = std::static_pointer_cast<wowee::game::Player>(entity);
|
||||
if (!player->getName().empty()) return player->getName();
|
||||
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
|
||||
if (!unit->getName().empty()) return unit->getName();
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
GameScreen::GameScreen() {
|
||||
}
|
||||
|
||||
void GameScreen::render(game::GameHandler& gameHandler) {
|
||||
// Process targeting input before UI windows
|
||||
processTargetInput(gameHandler);
|
||||
|
||||
// Main menu bar
|
||||
if (ImGui::BeginMainMenuBar()) {
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
ImGui::MenuItem("Player Info", nullptr, &showPlayerInfo);
|
||||
ImGui::MenuItem("Entity List", nullptr, &showEntityWindow);
|
||||
ImGui::MenuItem("Chat", nullptr, &showChatWindow);
|
||||
bool invOpen = inventoryScreen.isOpen();
|
||||
if (ImGui::MenuItem("Inventory", "B", &invOpen)) {
|
||||
inventoryScreen.setOpen(invOpen);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Inventory (B key toggle handled inside)
|
||||
inventoryScreen.render(gameHandler.getInventory());
|
||||
|
||||
if (inventoryScreen.consumeEquipmentDirty()) {
|
||||
updateCharacterGeosets(gameHandler.getInventory());
|
||||
updateCharacterTextures(gameHandler.getInventory());
|
||||
core::Application::getInstance().loadEquippedWeapons();
|
||||
}
|
||||
|
||||
// Update renderer face-target position
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
static glm::vec3 targetGLPos;
|
||||
if (gameHandler.hasTarget()) {
|
||||
auto target = gameHandler.getTarget();
|
||||
if (target) {
|
||||
targetGLPos = wowToGL(target->getX(), target->getY(), target->getZ());
|
||||
renderer->setTargetPosition(&targetGLPos);
|
||||
} else {
|
||||
renderer->setTargetPosition(nullptr);
|
||||
}
|
||||
} else {
|
||||
renderer->setTargetPosition(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<int>(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);
|
||||
std::stringstream guidStr;
|
||||
guidStr << "0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << guid;
|
||||
ImGui::Text("%s", guidStr.str().c_str());
|
||||
|
||||
// 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<game::Player>(entity);
|
||||
ImGui::Text("%s", player->getName().c_str());
|
||||
} else if (entity->getType() == game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(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) {
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 300), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(520, 390), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
// Chat history
|
||||
const auto& chatHistory = gameHandler.getChatHistory();
|
||||
|
||||
ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
|
||||
for (const auto& msg : chatHistory) {
|
||||
ImVec4 color = getChatTypeColor(msg.type);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, color);
|
||||
|
||||
std::stringstream ss;
|
||||
|
||||
if (msg.type == game::ChatType::TEXT_EMOTE) {
|
||||
ss << "You " << msg.message;
|
||||
} else {
|
||||
ss << "[" << getChatTypeName(msg.type) << "] ";
|
||||
|
||||
if (!msg.senderName.empty()) {
|
||||
ss << msg.senderName << ": ";
|
||||
}
|
||||
|
||||
ss << msg.message;
|
||||
}
|
||||
|
||||
ImGui::TextWrapped("%s", ss.str().c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Chat input
|
||||
ImGui::Text("Type:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(100);
|
||||
const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD" };
|
||||
ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 4);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Message:");
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
if (refocusChatInput) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
refocusChatInput = false;
|
||||
}
|
||||
if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
sendChatMessage(gameHandler);
|
||||
refocusChatInput = true;
|
||||
}
|
||||
|
||||
if (ImGui::IsItemActive()) {
|
||||
chatInputActive = true;
|
||||
} else {
|
||||
chatInputActive = false;
|
||||
}
|
||||
|
||||
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)) {
|
||||
gameHandler.clearTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// Left-click targeting (when mouse not captured by UI)
|
||||
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
|
||||
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<float>(window->getWidth());
|
||||
float screenH = static_cast<float>(window->getHeight());
|
||||
|
||||
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
||||
|
||||
float closestT = 1e30f;
|
||||
uint64_t closestGuid = 0;
|
||||
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
|
||||
|
||||
glm::vec3 entityGL = wowToGL(entity->getX(), entity->getY(), entity->getZ());
|
||||
// Add half-height offset so we target the body center, not feet
|
||||
entityGL.z += 3.0f;
|
||||
|
||||
float hitT;
|
||||
if (raySphereIntersect(ray, entityGL, 3.0f, hitT)) {
|
||||
if (hitT < closestT) {
|
||||
closestT = hitT;
|
||||
closestGuid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closestGuid != 0) {
|
||||
gameHandler.setTarget(closestGuid);
|
||||
}
|
||||
// Don't clear on miss — left-click is also used for camera orbit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
||||
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));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
|
||||
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();
|
||||
if (!characters.empty()) {
|
||||
// Use the first (or most recently selected) character
|
||||
const auto& ch = characters[0];
|
||||
playerName = ch.name;
|
||||
playerLevel = ch.level;
|
||||
// Characters don't store HP; use level-scaled estimate
|
||||
playerMaxHp = 20 + playerLevel * 10;
|
||||
playerHp = playerMaxHp;
|
||||
}
|
||||
|
||||
// Name in green (friendly player color)
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%s", playerName.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Lv %u", playerLevel);
|
||||
|
||||
// Health bar
|
||||
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
||||
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
|
||||
char overlay[64];
|
||||
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
|
||||
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
||||
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<float>(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;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
|
||||
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
||||
// Entity name and type
|
||||
std::string name = getEntityName(target);
|
||||
|
||||
ImVec4 nameColor;
|
||||
switch (target->getType()) {
|
||||
case game::ObjectType::PLAYER:
|
||||
nameColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
|
||||
break;
|
||||
case game::ObjectType::UNIT:
|
||||
nameColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
|
||||
break;
|
||||
default:
|
||||
nameColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::TextColored(nameColor, "%s", name.c_str());
|
||||
|
||||
// Level (for units/players)
|
||||
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
|
||||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Lv %u", unit->getLevel());
|
||||
|
||||
// Health bar
|
||||
uint32_t hp = unit->getHealth();
|
||||
uint32_t maxHp = unit->getMaxHealth();
|
||||
if (maxHp > 0) {
|
||||
float pct = static_cast<float>(hp) / static_cast<float>(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();
|
||||
} 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);
|
||||
|
||||
// Check for slash command emotes
|
||||
if (input.size() > 1 && input[0] == '/') {
|
||||
std::string command = input.substr(1);
|
||||
// Convert to lowercase
|
||||
for (char& c : command) c = std::tolower(c);
|
||||
|
||||
std::string emoteText = rendering::Renderer::getEmoteText(command);
|
||||
if (!emoteText.empty()) {
|
||||
// Play the emote animation
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
renderer->playEmote(command);
|
||||
}
|
||||
|
||||
// Build emote message — targeted or untargeted
|
||||
std::string chatText;
|
||||
if (gameHandler.hasTarget()) {
|
||||
auto target = gameHandler.getTarget();
|
||||
if (target) {
|
||||
std::string targetName = getEntityName(target);
|
||||
chatText = command + " at " + targetName + ".";
|
||||
} else {
|
||||
chatText = emoteText;
|
||||
}
|
||||
} else {
|
||||
chatText = emoteText;
|
||||
}
|
||||
|
||||
// Add local chat message
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::TEXT_EMOTE;
|
||||
msg.language = game::ChatLanguage::COMMON;
|
||||
msg.message = chatText;
|
||||
gameHandler.addLocalChatMessage(msg);
|
||||
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
// Not a recognized emote — fall through and send as normal chat
|
||||
}
|
||||
|
||||
game::ChatType type;
|
||||
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;
|
||||
default: type = game::ChatType::SAY; break;
|
||||
}
|
||||
|
||||
gameHandler.sendChatMessage(type, chatInputBuffer);
|
||||
|
||||
// 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::WHISPER: return "WHISPER";
|
||||
case game::ChatType::WHISPER_INFORM: return "TO";
|
||||
case game::ChatType::SYSTEM: return "SYSTEM";
|
||||
case game::ChatType::CHANNEL: return "CHANNEL";
|
||||
case game::ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
|
||||
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::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::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<pipeline::DBCFile> 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<uint32_t>(recIdx), 7 + groupField);
|
||||
};
|
||||
|
||||
// Helper: find first equipped item matching inventoryType, return its displayInfoId
|
||||
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(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<uint8_t> types) -> bool {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!slot.empty()) {
|
||||
for (uint8_t t : types) {
|
||||
if (slot.item.inventoryType == t) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Base geosets always present
|
||||
std::unordered_set<uint16_t> geosets;
|
||||
for (uint16_t i = 0; i <= 18; i++) {
|
||||
geosets.insert(i);
|
||||
}
|
||||
geosets.insert(101); // Hair
|
||||
geosets.insert(201); // Facial
|
||||
geosets.insert(701); // Ears
|
||||
|
||||
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
||||
// geosetGroup_1 > 0 → use mesh variant (502+), otherwise bare (501) + texture only
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : (did > 0 ? 501 : 501)));
|
||||
// geosetGroup_3 > 0 on robes also shows kilt legs (1302)
|
||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||
if (gg3 > 0) {
|
||||
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
||||
}
|
||||
}
|
||||
|
||||
// Legs: inventoryType 7
|
||||
// geosetGroup_1 > 0 → kilt/skirt mesh (1302+), otherwise bare legs (1301) + texture
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({7});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
// Only add leg geoset if robe hasn't already set a kilt geoset
|
||||
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
||||
}
|
||||
}
|
||||
|
||||
// Feet/Boots: inventoryType 8
|
||||
// geosetGroup_1 > 0 → boot mesh (402+), otherwise bare feet (401) + texture
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({8});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
||||
}
|
||||
|
||||
// Gloves/Hands: inventoryType 10
|
||||
// geosetGroup_1 > 0 → glove mesh (302+), otherwise bare hands (301)
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({10});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
||||
}
|
||||
|
||||
// Back/Cloak: inventoryType 16 — geoset only, no skin texture (cloaks are separate models)
|
||||
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
||||
|
||||
// Tabard: inventoryType 19
|
||||
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;
|
||||
|
||||
// Collect equipment texture regions from all equipped items
|
||||
std::vector<std::pair<int, std::string>> regionLayers;
|
||||
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
||||
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
||||
if (recIdx < 0) continue;
|
||||
|
||||
// DBC fields 15-22 = texture_1 through texture_8 (regions 0-7)
|
||||
// (binary DBC has inventoryIcon_2 at field 6, shifting fields +1 vs CSV)
|
||||
for (int region = 0; region < 8; region++) {
|
||||
uint32_t fieldIdx = 15 + region;
|
||||
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
||||
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;
|
||||
std::string malePath = base + "_M.blp";
|
||||
std::string unisexPath = base + "_U.blp";
|
||||
std::string fullPath;
|
||||
if (assetManager->fileExists(malePath)) {
|
||||
fullPath = malePath;
|
||||
} 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
|
||||
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
||||
if (newTex != 0) {
|
||||
charRenderer->setModelTexture(1, skinSlot, newTex);
|
||||
}
|
||||
|
||||
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
|
||||
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
|
||||
if (cloakSlot > 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<game::EquipSlot>(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)
|
||||
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
|
||||
if (!capeName.empty()) {
|
||||
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
||||
GLuint capeTex = charRenderer->loadTexture(capePath);
|
||||
if (capeTex != 0) {
|
||||
charRenderer->setModelTexture(1, cloakSlot, capeTex);
|
||||
LOG_INFO("Cloak texture applied: ", capePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No cloak equipped — reset to white fallback
|
||||
charRenderer->resetModelTexture(1, cloakSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
633
src/ui/inventory_screen.cpp
Normal file
633
src/ui/inventory_screen.cpp
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
#include "ui/inventory_screen.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <cstdio>
|
||||
|
||||
namespace wowee {
|
||||
namespace ui {
|
||||
|
||||
ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
||||
switch (quality) {
|
||||
case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey
|
||||
case game::ItemQuality::COMMON: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
|
||||
case game::ItemQuality::UNCOMMON: return ImVec4(0.12f, 1.0f, 0.0f, 1.0f); // Green
|
||||
case game::ItemQuality::RARE: return ImVec4(0.0f, 0.44f, 0.87f, 1.0f); // Blue
|
||||
case game::ItemQuality::EPIC: return ImVec4(0.64f, 0.21f, 0.93f, 1.0f); // Purple
|
||||
case game::ItemQuality::LEGENDARY: return ImVec4(1.0f, 0.50f, 0.0f, 1.0f); // Orange
|
||||
default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv) {
|
||||
switch (inventoryType) {
|
||||
case 1: return game::EquipSlot::HEAD;
|
||||
case 2: return game::EquipSlot::NECK;
|
||||
case 3: return game::EquipSlot::SHOULDERS;
|
||||
case 4: return game::EquipSlot::SHIRT;
|
||||
case 5: return game::EquipSlot::CHEST;
|
||||
case 6: return game::EquipSlot::WAIST;
|
||||
case 7: return game::EquipSlot::LEGS;
|
||||
case 8: return game::EquipSlot::FEET;
|
||||
case 9: return game::EquipSlot::WRISTS;
|
||||
case 10: return game::EquipSlot::HANDS;
|
||||
case 11: {
|
||||
// Ring: prefer empty slot, else RING1
|
||||
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
|
||||
return game::EquipSlot::RING1;
|
||||
return game::EquipSlot::RING2;
|
||||
}
|
||||
case 12: {
|
||||
// Trinket: prefer empty slot, else TRINKET1
|
||||
if (inv.getEquipSlot(game::EquipSlot::TRINKET1).empty())
|
||||
return game::EquipSlot::TRINKET1;
|
||||
return game::EquipSlot::TRINKET2;
|
||||
}
|
||||
case 13: // One-Hand
|
||||
case 21: // Main Hand
|
||||
return game::EquipSlot::MAIN_HAND;
|
||||
case 17: // Two-Hand
|
||||
return game::EquipSlot::MAIN_HAND;
|
||||
case 14: // Shield
|
||||
case 22: // Off Hand
|
||||
case 23: // Held In Off-hand
|
||||
return game::EquipSlot::OFF_HAND;
|
||||
case 15: // Ranged (bow/gun)
|
||||
case 25: // Thrown
|
||||
case 26: // Ranged
|
||||
return game::EquipSlot::RANGED;
|
||||
case 16: return game::EquipSlot::BACK;
|
||||
case 19: return game::EquipSlot::TABARD;
|
||||
case 20: return game::EquipSlot::CHEST; // Robe
|
||||
default: return game::EquipSlot::NUM_SLOTS;
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::pickupFromBackpack(game::Inventory& inv, int index) {
|
||||
const auto& slot = inv.getBackpackSlot(index);
|
||||
if (slot.empty()) return;
|
||||
holdingItem = true;
|
||||
heldItem = slot.item;
|
||||
heldSource = HeldSource::BACKPACK;
|
||||
heldBackpackIndex = index;
|
||||
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
||||
inv.clearBackpackSlot(index);
|
||||
}
|
||||
|
||||
void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
const auto& es = inv.getEquipSlot(slot);
|
||||
if (es.empty()) return;
|
||||
holdingItem = true;
|
||||
heldItem = es.item;
|
||||
heldSource = HeldSource::EQUIPMENT;
|
||||
heldBackpackIndex = -1;
|
||||
heldEquipSlot = slot;
|
||||
inv.clearEquipSlot(slot);
|
||||
equipmentDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||
if (!holdingItem) return;
|
||||
const auto& target = inv.getBackpackSlot(index);
|
||||
if (target.empty()) {
|
||||
inv.setBackpackSlot(index, heldItem);
|
||||
holdingItem = false;
|
||||
} else {
|
||||
// Swap
|
||||
game::ItemDef targetItem = target.item;
|
||||
inv.setBackpackSlot(index, heldItem);
|
||||
heldItem = targetItem;
|
||||
// Keep holding the swapped item - update source to this backpack slot
|
||||
heldSource = HeldSource::BACKPACK;
|
||||
heldBackpackIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
if (!holdingItem) return;
|
||||
|
||||
// Validate: check if the held item can go in this slot
|
||||
if (heldItem.inventoryType > 0) {
|
||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
|
||||
if (validSlot == game::EquipSlot::NUM_SLOTS) return; // Not equippable
|
||||
|
||||
// For rings/trinkets, allow either slot
|
||||
bool valid = (slot == validSlot);
|
||||
if (!valid) {
|
||||
if (heldItem.inventoryType == 11) // Ring
|
||||
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
|
||||
else if (heldItem.inventoryType == 12) // Trinket
|
||||
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
|
||||
}
|
||||
if (!valid) return;
|
||||
} else {
|
||||
return; // No inventoryType means not equippable
|
||||
}
|
||||
|
||||
const auto& target = inv.getEquipSlot(slot);
|
||||
if (target.empty()) {
|
||||
inv.setEquipSlot(slot, heldItem);
|
||||
holdingItem = false;
|
||||
} else {
|
||||
// Swap
|
||||
game::ItemDef targetItem = target.item;
|
||||
inv.setEquipSlot(slot, heldItem);
|
||||
heldItem = targetItem;
|
||||
heldSource = HeldSource::EQUIPMENT;
|
||||
heldEquipSlot = slot;
|
||||
}
|
||||
|
||||
// Two-handed weapon in main hand clears the off-hand slot
|
||||
if (slot == game::EquipSlot::MAIN_HAND &&
|
||||
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
||||
const auto& offHand = inv.getEquipSlot(game::EquipSlot::OFF_HAND);
|
||||
if (!offHand.empty()) {
|
||||
inv.addItem(offHand.item);
|
||||
inv.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
||||
}
|
||||
}
|
||||
|
||||
// Equipping off-hand unequips a 2H weapon from main hand
|
||||
if (slot == game::EquipSlot::OFF_HAND &&
|
||||
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
||||
inv.addItem(inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
||||
inv.clearEquipSlot(game::EquipSlot::MAIN_HAND);
|
||||
}
|
||||
|
||||
equipmentDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
||||
if (!holdingItem) return;
|
||||
// Return item to source
|
||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||
// If source slot is still empty, put it back
|
||||
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
|
||||
inv.setBackpackSlot(heldBackpackIndex, heldItem);
|
||||
} else {
|
||||
// Source was swapped into; find free slot
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
||||
if (inv.getEquipSlot(heldEquipSlot).empty()) {
|
||||
inv.setEquipSlot(heldEquipSlot, heldItem);
|
||||
equipmentDirty = true;
|
||||
} else {
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
} else {
|
||||
// Fallback: just add to inventory
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
holdingItem = false;
|
||||
}
|
||||
|
||||
void InventoryScreen::renderHeldItem() {
|
||||
if (!holdingItem) return;
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 mousePos = io.MousePos;
|
||||
float size = 36.0f;
|
||||
ImVec2 pos(mousePos.x - size * 0.5f, mousePos.y - size * 0.5f);
|
||||
|
||||
ImDrawList* drawList = ImGui::GetForegroundDrawList();
|
||||
ImVec4 qColor = getQualityColor(heldItem.quality);
|
||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||
|
||||
// Background
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
IM_COL32(40, 35, 30, 200));
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
|
||||
// Item abbreviation
|
||||
char abbr[4] = {};
|
||||
if (!heldItem.name.empty()) {
|
||||
abbr[0] = heldItem.name[0];
|
||||
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
|
||||
// Stack count
|
||||
if (heldItem.stackCount > 1) {
|
||||
char countStr[16];
|
||||
snprintf(countStr, sizeof(countStr), "%u", heldItem.stackCount);
|
||||
float cw = ImGui::CalcTextSize(countStr).x;
|
||||
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
|
||||
IM_COL32(255, 255, 255, 220), countStr);
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::render(game::Inventory& inventory) {
|
||||
// B key toggle (edge-triggered)
|
||||
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||
bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
||||
if (bDown && !bKeyWasDown) {
|
||||
open = !open;
|
||||
}
|
||||
bKeyWasDown = bDown;
|
||||
|
||||
if (!open) {
|
||||
// Cancel held item if inventory closes
|
||||
if (holdingItem) cancelPickup(inventory);
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape cancels held item
|
||||
if (holdingItem && !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_ESCAPE)) {
|
||||
cancelPickup(inventory);
|
||||
}
|
||||
|
||||
// Right-click anywhere while holding = cancel
|
||||
if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
cancelPickup(inventory);
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float screenW = io.DisplaySize.x;
|
||||
|
||||
// Position inventory window on the right side of the screen
|
||||
ImGui::SetNextWindowPos(ImVec2(screenW - 520.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(500.0f, 560.0f), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Inventory", &open, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Two-column layout: Equipment (left) | Backpack (right)
|
||||
ImGui::BeginChild("EquipPanel", ImVec2(200.0f, 0.0f), true);
|
||||
renderEquipmentPanel(inventory);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, 0.0f), true);
|
||||
renderBackpackPanel(inventory);
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Draw held item at cursor (on top of everything)
|
||||
renderHeldItem();
|
||||
}
|
||||
|
||||
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
|
||||
ImGui::Separator();
|
||||
|
||||
static const game::EquipSlot leftSlots[] = {
|
||||
game::EquipSlot::HEAD, game::EquipSlot::NECK,
|
||||
game::EquipSlot::SHOULDERS, game::EquipSlot::BACK,
|
||||
game::EquipSlot::CHEST, game::EquipSlot::SHIRT,
|
||||
game::EquipSlot::TABARD, game::EquipSlot::WRISTS,
|
||||
};
|
||||
static const game::EquipSlot rightSlots[] = {
|
||||
game::EquipSlot::HANDS, game::EquipSlot::WAIST,
|
||||
game::EquipSlot::LEGS, game::EquipSlot::FEET,
|
||||
game::EquipSlot::RING1, game::EquipSlot::RING2,
|
||||
game::EquipSlot::TRINKET1, game::EquipSlot::TRINKET2,
|
||||
};
|
||||
|
||||
constexpr float slotSize = 36.0f;
|
||||
constexpr float spacing = 4.0f;
|
||||
|
||||
// Two columns of equipment
|
||||
int rows = 8;
|
||||
for (int r = 0; r < rows; r++) {
|
||||
// Left slot
|
||||
{
|
||||
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
||||
const char* label = game::getEquipSlotName(leftSlots[r]);
|
||||
char id[64];
|
||||
snprintf(id, sizeof(id), "##eq_l_%d", r);
|
||||
ImGui::PushID(id);
|
||||
renderItemSlot(inventory, slot, slotSize, label,
|
||||
SlotKind::EQUIPMENT, -1, leftSlots[r]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::SameLine(slotSize + spacing + 60.0f);
|
||||
|
||||
// Right slot
|
||||
{
|
||||
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
||||
const char* label = game::getEquipSlotName(rightSlots[r]);
|
||||
char id[64];
|
||||
snprintf(id, sizeof(id), "##eq_r_%d", r);
|
||||
ImGui::PushID(id);
|
||||
renderItemSlot(inventory, slot, slotSize, label,
|
||||
SlotKind::EQUIPMENT, -1, rightSlots[r]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon row
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
static const game::EquipSlot weaponSlots[] = {
|
||||
game::EquipSlot::MAIN_HAND,
|
||||
game::EquipSlot::OFF_HAND,
|
||||
game::EquipSlot::RANGED,
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
const auto& slot = inventory.getEquipSlot(weaponSlots[i]);
|
||||
const char* label = game::getEquipSlotName(weaponSlots[i]);
|
||||
char id[64];
|
||||
snprintf(id, sizeof(id), "##eq_w_%d", i);
|
||||
ImGui::PushID(id);
|
||||
renderItemSlot(inventory, slot, slotSize, label,
|
||||
SlotKind::EQUIPMENT, -1, weaponSlots[i]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
||||
ImGui::Separator();
|
||||
|
||||
constexpr float slotSize = 40.0f;
|
||||
constexpr int columns = 4;
|
||||
|
||||
for (int i = 0; i < inventory.getBackpackSize(); i++) {
|
||||
if (i % columns != 0) ImGui::SameLine();
|
||||
|
||||
const auto& slot = inventory.getBackpackSlot(i);
|
||||
char id[32];
|
||||
snprintf(id, sizeof(id), "##bp_%d", i);
|
||||
ImGui::PushID(id);
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, i, game::EquipSlot::NUM_SLOTS);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Show extra bags if equipped
|
||||
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
||||
int bagSize = inventory.getBagSize(bag);
|
||||
if (bagSize <= 0) continue;
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
char bagLabel[32];
|
||||
snprintf(bagLabel, sizeof(bagLabel), "Bag %d", bag + 1);
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "%s", bagLabel);
|
||||
|
||||
for (int s = 0; s < bagSize; s++) {
|
||||
if (s % columns != 0) ImGui::SameLine();
|
||||
const auto& slot = inventory.getBagSlot(bag, s);
|
||||
char sid[32];
|
||||
snprintf(sid, sizeof(sid), "##bag%d_%d", bag, s);
|
||||
ImGui::PushID(sid);
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
float size, const char* label,
|
||||
SlotKind kind, int backpackIndex,
|
||||
game::EquipSlot equipSlot) {
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
bool isEmpty = slot.empty();
|
||||
|
||||
// Determine if this is a valid drop target for held item
|
||||
bool validDrop = false;
|
||||
if (holdingItem) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
validDrop = true; // Can always drop in backpack
|
||||
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
||||
validDrop = (equipSlot == validSlot);
|
||||
if (!validDrop && heldItem.inventoryType == 11)
|
||||
validDrop = (equipSlot == game::EquipSlot::RING1 || equipSlot == game::EquipSlot::RING2);
|
||||
if (!validDrop && heldItem.inventoryType == 12)
|
||||
validDrop = (equipSlot == game::EquipSlot::TRINKET1 || equipSlot == game::EquipSlot::TRINKET2);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
// Empty slot: dark grey background
|
||||
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
||||
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
||||
|
||||
// Highlight valid drop targets
|
||||
if (validDrop) {
|
||||
bgCol = IM_COL32(20, 50, 20, 200);
|
||||
borderCol = IM_COL32(0, 180, 0, 200);
|
||||
}
|
||||
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size), borderCol);
|
||||
|
||||
// Slot label for equipment slots
|
||||
if (label) {
|
||||
char abbr[4] = {};
|
||||
abbr[0] = label[0];
|
||||
if (label[1]) abbr[1] = label[1];
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + size * 0.3f),
|
||||
IM_COL32(80, 80, 80, 180), abbr);
|
||||
}
|
||||
|
||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||
|
||||
// Click interactions
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
placeInEquipment(inventory, equipSlot);
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip for empty equip slots
|
||||
if (label && ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.4f, 0.4f, 1.0f), "Empty");
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
} else {
|
||||
const auto& item = slot.item;
|
||||
ImVec4 qColor = getQualityColor(item.quality);
|
||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||
|
||||
// Highlight valid drop targets with green tint
|
||||
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
|
||||
if (holdingItem && validDrop) {
|
||||
bgCol = IM_COL32(30, 55, 30, 220);
|
||||
borderCol = IM_COL32(0, 200, 0, 220);
|
||||
}
|
||||
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
|
||||
// Item abbreviation (first 2 letters)
|
||||
char abbr[4] = {};
|
||||
if (!item.name.empty()) {
|
||||
abbr[0] = item.name[0];
|
||||
if (item.name.size() > 1) abbr[1] = item.name[1];
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
|
||||
// Stack count (bottom-right)
|
||||
if (item.stackCount > 1) {
|
||||
char countStr[16];
|
||||
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
|
||||
float cw = ImGui::CalcTextSize(countStr).x;
|
||||
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
|
||||
IM_COL32(255, 255, 255, 220), countStr);
|
||||
}
|
||||
|
||||
ImGui::InvisibleButton("slot", ImVec2(size, size));
|
||||
|
||||
// Left-click: pickup or place/swap
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
||||
if (!holdingItem) {
|
||||
// Pick up this item
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
pickupFromBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
}
|
||||
} else {
|
||||
// Holding an item - place or swap
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
||||
placeInEquipment(inventory, equipSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click: auto-equip from backpack, or unequip from equipment
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
||||
if (kind == SlotKind::EQUIPMENT) {
|
||||
// Unequip: move to free backpack slot
|
||||
int freeSlot = inventory.findFreeBackpackSlot();
|
||||
if (freeSlot >= 0) {
|
||||
inventory.setBackpackSlot(freeSlot, item);
|
||||
inventory.clearEquipSlot(equipSlot);
|
||||
equipmentDirty = true;
|
||||
}
|
||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
|
||||
// Auto-equip: find the right slot
|
||||
// Capture type before swap (item ref may become stale)
|
||||
uint8_t equippingType = item.inventoryType;
|
||||
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
|
||||
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
|
||||
const auto& eqSlot = inventory.getEquipSlot(targetSlot);
|
||||
if (eqSlot.empty()) {
|
||||
inventory.setEquipSlot(targetSlot, item);
|
||||
inventory.clearBackpackSlot(backpackIndex);
|
||||
} else {
|
||||
// Swap with equipped item
|
||||
game::ItemDef equippedItem = eqSlot.item;
|
||||
inventory.setEquipSlot(targetSlot, item);
|
||||
inventory.setBackpackSlot(backpackIndex, equippedItem);
|
||||
}
|
||||
// Two-handed weapon in main hand clears the off-hand
|
||||
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
|
||||
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
|
||||
if (!offHand.empty()) {
|
||||
inventory.addItem(offHand.item);
|
||||
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
|
||||
}
|
||||
}
|
||||
// Equipping off-hand unequips a 2H weapon from main hand
|
||||
if (targetSlot == game::EquipSlot::OFF_HAND &&
|
||||
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
|
||||
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
|
||||
inventory.clearEquipSlot(game::EquipSlot::MAIN_HAND);
|
||||
}
|
||||
equipmentDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered() && !holdingItem) {
|
||||
renderItemTooltip(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImVec4 qColor = getQualityColor(item.quality);
|
||||
ImGui::TextColored(qColor, "%s", item.name.c_str());
|
||||
|
||||
// Slot type
|
||||
if (item.inventoryType > 0) {
|
||||
const char* slotName = "";
|
||||
switch (item.inventoryType) {
|
||||
case 1: slotName = "Head"; break;
|
||||
case 2: slotName = "Neck"; break;
|
||||
case 3: slotName = "Shoulder"; break;
|
||||
case 4: slotName = "Shirt"; break;
|
||||
case 5: slotName = "Chest"; break;
|
||||
case 6: slotName = "Waist"; break;
|
||||
case 7: slotName = "Legs"; break;
|
||||
case 8: slotName = "Feet"; break;
|
||||
case 9: slotName = "Wrist"; break;
|
||||
case 10: slotName = "Hands"; break;
|
||||
case 11: slotName = "Finger"; break;
|
||||
case 12: slotName = "Trinket"; break;
|
||||
case 13: slotName = "One-Hand"; break;
|
||||
case 14: slotName = "Shield"; break;
|
||||
case 15: slotName = "Ranged"; break;
|
||||
case 16: slotName = "Back"; break;
|
||||
case 17: slotName = "Two-Hand"; break;
|
||||
case 18: slotName = "Bag"; break;
|
||||
case 19: slotName = "Tabard"; break;
|
||||
case 20: slotName = "Robe"; break;
|
||||
case 21: slotName = "Main Hand"; break;
|
||||
case 22: slotName = "Off Hand"; break;
|
||||
case 23: slotName = "Held In Off-hand"; break;
|
||||
case 25: slotName = "Thrown"; break;
|
||||
case 26: slotName = "Ranged"; break;
|
||||
default: slotName = ""; break;
|
||||
}
|
||||
if (slotName[0]) {
|
||||
if (!item.subclassName.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, item.subclassName.c_str());
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Armor
|
||||
if (item.armor > 0) {
|
||||
ImGui::Text("%d Armor", item.armor);
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (item.stamina != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Stamina", item.stamina);
|
||||
if (item.strength != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Strength", item.strength);
|
||||
if (item.agility != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Agility", item.agility);
|
||||
if (item.intellect != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Intellect", item.intellect);
|
||||
if (item.spirit != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Spirit", item.spirit);
|
||||
|
||||
// Stack info
|
||||
if (item.maxStack > 1) {
|
||||
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
|
||||
}
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
180
src/ui/realm_screen.cpp
Normal file
180
src/ui/realm_screen.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
#include "ui/realm_screen.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
RealmScreen::RealmScreen() {
|
||||
}
|
||||
|
||||
void RealmScreen::render(auth::AuthHandler& authHandler) {
|
||||
ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Realm Selection", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
ImGui::Text("Select a Realm");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Status message
|
||||
if (!statusMessage.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
||||
ImGui::TextWrapped("%s", statusMessage.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Get realm list
|
||||
const auto& realms = authHandler.getRealms();
|
||||
|
||||
if (realms.empty()) {
|
||||
ImGui::Text("No realms available. Requesting realm list...");
|
||||
authHandler.requestRealmList();
|
||||
} else {
|
||||
// Realm table
|
||||
if (ImGui::BeginTable("RealmsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||
ImGui::TableSetupColumn("Population", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("Characters", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (size_t i = 0; i < realms.size(); ++i) {
|
||||
const auto& realm = realms[i];
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Name column (selectable)
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool isSelected = (selectedRealmIndex == static_cast<int>(i));
|
||||
if (ImGui::Selectable(realm.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
selectedRealmIndex = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// Type column
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
if (realm.icon == 0) {
|
||||
ImGui::Text("Normal");
|
||||
} else if (realm.icon == 1) {
|
||||
ImGui::Text("PvP");
|
||||
} else if (realm.icon == 4) {
|
||||
ImGui::Text("RP");
|
||||
} else if (realm.icon == 6) {
|
||||
ImGui::Text("RP-PvP");
|
||||
} else {
|
||||
ImGui::Text("Type %d", realm.icon);
|
||||
}
|
||||
|
||||
// Population column
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImVec4 popColor = getPopulationColor(realm.population);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, popColor);
|
||||
if (realm.population < 0.5f) {
|
||||
ImGui::Text("Low");
|
||||
} else if (realm.population < 1.0f) {
|
||||
ImGui::Text("Medium");
|
||||
} else if (realm.population < 2.0f) {
|
||||
ImGui::Text("High");
|
||||
} else {
|
||||
ImGui::Text("Full");
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Characters column
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%d", realm.characters);
|
||||
|
||||
// Status column
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
const char* status = getRealmStatus(realm.flags);
|
||||
if (realm.lock) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::Text("Locked");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
||||
ImGui::Text("%s", status);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Selected realm info
|
||||
if (selectedRealmIndex >= 0 && selectedRealmIndex < static_cast<int>(realms.size())) {
|
||||
const auto& realm = realms[selectedRealmIndex];
|
||||
|
||||
ImGui::Text("Selected Realm:");
|
||||
ImGui::Indent();
|
||||
ImGui::Text("Name: %s", realm.name.c_str());
|
||||
ImGui::Text("Address: %s", realm.address.c_str());
|
||||
ImGui::Text("Characters: %d", realm.characters);
|
||||
if (realm.hasVersionInfo()) {
|
||||
ImGui::Text("Version: %d.%d.%d (build %d)",
|
||||
realm.majorVersion, realm.minorVersion, realm.patchVersion, realm.build);
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Connect button
|
||||
if (!realm.lock) {
|
||||
if (ImGui::Button("Enter Realm", ImVec2(120, 0))) {
|
||||
realmSelected = true;
|
||||
selectedRealmName = realm.name;
|
||||
selectedRealmAddress = realm.address;
|
||||
setStatus("Connecting to realm: " + realm.name);
|
||||
|
||||
// Call callback
|
||||
if (onRealmSelected) {
|
||||
onRealmSelected(selectedRealmName, selectedRealmAddress);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::Button("Realm Locked", ImVec2(120, 0));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Refresh button
|
||||
if (ImGui::Button("Refresh Realm List", ImVec2(150, 0))) {
|
||||
authHandler.requestRealmList();
|
||||
setStatus("Refreshing realm list...");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void RealmScreen::setStatus(const std::string& message) {
|
||||
statusMessage = message;
|
||||
}
|
||||
|
||||
const char* RealmScreen::getRealmStatus(uint8_t flags) const {
|
||||
if (flags & 0x01) return "Invalid";
|
||||
if (flags & 0x02) return "Offline";
|
||||
return "Online";
|
||||
}
|
||||
|
||||
ImVec4 RealmScreen::getPopulationColor(float population) const {
|
||||
if (population < 0.5f) {
|
||||
return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - Low
|
||||
} else if (population < 1.0f) {
|
||||
return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow - Medium
|
||||
} else if (population < 2.0f) {
|
||||
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange - High
|
||||
} else {
|
||||
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red - Full
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
144
src/ui/ui_manager.cpp
Normal file
144
src/ui/ui_manager.cpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#include "ui/ui_manager.hpp"
|
||||
#include "core/window.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "auth/auth_handler.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_sdl2.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
|
||||
namespace wowee {
|
||||
namespace ui {
|
||||
|
||||
UIManager::UIManager() {
|
||||
// Create screen instances
|
||||
authScreen = std::make_unique<AuthScreen>();
|
||||
realmScreen = std::make_unique<RealmScreen>();
|
||||
characterScreen = std::make_unique<CharacterScreen>();
|
||||
gameScreen = std::make_unique<GameScreen>();
|
||||
}
|
||||
|
||||
UIManager::~UIManager() = default;
|
||||
|
||||
bool UIManager::initialize(core::Window* win) {
|
||||
window = win;
|
||||
LOG_INFO("Initializing UI manager");
|
||||
|
||||
// Initialize ImGui
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||
|
||||
// Setup ImGui style
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Customize style for better WoW feel
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.WindowRounding = 6.0f;
|
||||
style.FrameRounding = 4.0f;
|
||||
style.GrabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
// WoW-inspired colors
|
||||
ImVec4* colors = style.Colors;
|
||||
colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.08f, 0.12f, 0.94f);
|
||||
colors[ImGuiCol_TitleBg] = ImVec4(0.10f, 0.10f, 0.15f, 1.00f);
|
||||
colors[ImGuiCol_TitleBgActive] = ImVec4(0.15f, 0.15f, 0.25f, 1.00f);
|
||||
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.25f, 0.40f, 1.00f);
|
||||
colors[ImGuiCol_ButtonHovered] = ImVec4(0.25f, 0.30f, 0.50f, 1.00f);
|
||||
colors[ImGuiCol_ButtonActive] = ImVec4(0.15f, 0.20f, 0.35f, 1.00f);
|
||||
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.40f, 0.55f);
|
||||
colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.30f, 0.50f, 0.80f);
|
||||
colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.25f, 0.45f, 1.00f);
|
||||
|
||||
// Initialize ImGui for SDL2 and OpenGL3
|
||||
ImGui_ImplSDL2_InitForOpenGL(window->getSDLWindow(), window->getGLContext());
|
||||
ImGui_ImplOpenGL3_Init("#version 330 core");
|
||||
|
||||
imguiInitialized = true;
|
||||
|
||||
LOG_INFO("UI manager initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void UIManager::shutdown() {
|
||||
if (imguiInitialized) {
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
imguiInitialized = false;
|
||||
}
|
||||
LOG_INFO("UI manager shutdown");
|
||||
}
|
||||
|
||||
void UIManager::update(float deltaTime) {
|
||||
if (!imguiInitialized) return;
|
||||
|
||||
// Start ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
}
|
||||
|
||||
void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler, game::GameHandler* gameHandler) {
|
||||
if (!imguiInitialized) return;
|
||||
|
||||
// Render appropriate screen based on application state
|
||||
switch (appState) {
|
||||
case core::AppState::AUTHENTICATION:
|
||||
if (authHandler) {
|
||||
authScreen->render(*authHandler);
|
||||
}
|
||||
break;
|
||||
|
||||
case core::AppState::REALM_SELECTION:
|
||||
if (authHandler) {
|
||||
realmScreen->render(*authHandler);
|
||||
}
|
||||
break;
|
||||
|
||||
case core::AppState::CHARACTER_SELECTION:
|
||||
if (gameHandler) {
|
||||
characterScreen->render(*gameHandler);
|
||||
}
|
||||
break;
|
||||
|
||||
case core::AppState::IN_GAME:
|
||||
if (gameHandler) {
|
||||
gameScreen->render(*gameHandler);
|
||||
}
|
||||
break;
|
||||
|
||||
case core::AppState::DISCONNECTED:
|
||||
// Show disconnected message
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f - 200,
|
||||
ImGui::GetIO().DisplaySize.y * 0.5f - 75),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("Disconnected", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
|
||||
ImGui::TextWrapped("You have been disconnected from the server.");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Return to Login", ImVec2(-1, 0))) {
|
||||
// Will be handled by application
|
||||
}
|
||||
ImGui::End();
|
||||
break;
|
||||
}
|
||||
|
||||
// Render ImGui
|
||||
ImGui::Render();
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
}
|
||||
|
||||
void UIManager::processEvent(const SDL_Event& event) {
|
||||
if (imguiInitialized) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue