mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 15:50:20 +00:00
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets) - Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing - Fix stale character data between logins by replacing static init flag with per-character GUID tracking - Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer) - Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes - Reduce camera ground samples from 5 to 3 movement-aligned probes - Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
This commit is contained in:
parent
ca88860929
commit
751e6fdbde
11 changed files with 741 additions and 307 deletions
|
|
@ -1,5 +1,8 @@
|
|||
#include "ui/character_screen.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
|
|
@ -9,12 +12,67 @@ CharacterScreen::CharacterScreen() {
|
|||
}
|
||||
|
||||
void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
|
||||
// Size the window to fill most of the viewport
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
ImVec2 winSize(vpSize.x * 0.6f, vpSize.y * 0.7f);
|
||||
if (winSize.x < 700.0f) winSize.x = 700.0f;
|
||||
if (winSize.y < 500.0f) winSize.y = 500.0f;
|
||||
ImGui::SetNextWindowSize(winSize, ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(vpSize.x * 0.5f, vpSize.y * 0.5f),
|
||||
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||
|
||||
ImGui::Begin("Character Selection", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
|
||||
ImGui::Text("Select a Character");
|
||||
ImGui::Separator();
|
||||
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();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (characters.empty()) {
|
||||
ImGui::Text("No characters available.");
|
||||
// Bottom buttons even when empty
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Back", ImVec2(120, 36))) { if (onBack) onBack(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh", ImVec2(120, 36))) {
|
||||
if (gameHandler.getState() == game::WorldState::READY ||
|
||||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
||||
gameHandler.requestCharacterList();
|
||||
setStatus("Refreshing character list...");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Create Character", ImVec2(160, 36))) { if (onCreateCharacter) onCreateCharacter(); }
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore last-selected character (once per screen visit)
|
||||
if (!restoredLastCharacter) {
|
||||
uint64_t lastGuid = loadLastCharacter();
|
||||
if (lastGuid != 0) {
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
if (characters[i].guid == lastGuid) {
|
||||
selectedCharacterIndex = static_cast<int>(i);
|
||||
selectedCharacterGuid = lastGuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to first character if nothing matched
|
||||
if (selectedCharacterIndex < 0) {
|
||||
selectedCharacterIndex = 0;
|
||||
selectedCharacterGuid = characters[0].guid;
|
||||
}
|
||||
restoredLastCharacter = true;
|
||||
}
|
||||
|
||||
// Status message
|
||||
if (!statusMessage.empty()) {
|
||||
|
|
@ -24,200 +82,164 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// Get character list
|
||||
const auto& characters = gameHandler.getCharacters();
|
||||
// ── Two-column layout: character list (left) | details (right) ──
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float detailPanelW = 260.0f;
|
||||
float listW = availW - detailPanelW - ImGui::GetStyle().ItemSpacing.x;
|
||||
if (listW < 300.0f) { listW = availW; detailPanelW = 0.0f; }
|
||||
|
||||
// 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 {
|
||||
// Auto-highlight the first character if none selected yet
|
||||
if (selectedCharacterIndex < 0 && !characters.empty()) {
|
||||
selectedCharacterIndex = 0;
|
||||
selectedCharacterGuid = characters[0].guid;
|
||||
}
|
||||
float listH = ImGui::GetContentRegionAvail().y - 50.0f; // reserve bottom row for buttons
|
||||
|
||||
// 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();
|
||||
// ── Left: Character list ──
|
||||
ImGui::BeginChild("CharList", ImVec2(listW, listH), true);
|
||||
ImGui::Text("Characters");
|
||||
ImGui::Separator();
|
||||
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
const auto& character = characters[i];
|
||||
if (ImGui::BeginTable("CharactersTable", 5,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 2.0f);
|
||||
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 45.0f);
|
||||
ImGui::TableSetupColumn("Race", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 1.2f);
|
||||
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
ImGui::TableNextRow();
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
const auto& character = characters[i];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
|
||||
// Name column (selectable)
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
bool isSelected = (selectedCharacterIndex == static_cast<int>(i));
|
||||
bool isSelected = (selectedCharacterIndex == static_cast<int>(i));
|
||||
ImVec4 factionColor = getFactionColor(character.race);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, factionColor);
|
||||
|
||||
// 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::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable(character.name.c_str(), isSelected,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
selectedCharacterIndex = static_cast<int>(i);
|
||||
selectedCharacterGuid = character.guid;
|
||||
saveLastCharacter(character.guid);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
// Double-click to enter world
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
selectedCharacterIndex = static_cast<int>(i);
|
||||
selectedCharacterGuid = character.guid;
|
||||
saveLastCharacter(character.guid);
|
||||
characterSelected = true;
|
||||
gameHandler.selectCharacter(character.guid);
|
||||
if (onCharacterSelected) onCharacterSelected(character.guid);
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%d", character.level);
|
||||
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%s", game::getRaceName(character.race));
|
||||
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%s", game::getClassName(character.characterClass));
|
||||
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::Text("%d", character.zoneId);
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// ── Right: Details panel ──
|
||||
if (detailPanelW > 0.0f &&
|
||||
selectedCharacterIndex >= 0 &&
|
||||
selectedCharacterIndex < static_cast<int>(characters.size())) {
|
||||
|
||||
const auto& character = characters[selectedCharacterIndex];
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("CharDetails", ImVec2(detailPanelW, listH), true);
|
||||
|
||||
ImGui::TextColored(getFactionColor(character.race), "%s", character.name.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Text("Level %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::Spacing();
|
||||
ImGui::Text("Map %d, Zone %d", character.mapId, character.zoneId);
|
||||
|
||||
if (character.hasGuild()) {
|
||||
ImGui::Text("Guild ID: %d", character.guildId);
|
||||
} else {
|
||||
ImGui::TextDisabled("No Guild");
|
||||
}
|
||||
|
||||
if (character.hasPet()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Pet Lv%d (Family %d)", character.pet.level, character.pet.family);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Selected character details
|
||||
if (selectedCharacterIndex >= 0 && selectedCharacterIndex < static_cast<int>(characters.size())) {
|
||||
const auto& character = characters[selectedCharacterIndex];
|
||||
// Enter World button — full width
|
||||
float btnW = ImGui::GetContentRegionAvail().x;
|
||||
if (ImGui::Button("Enter World", ImVec2(btnW, 44))) {
|
||||
characterSelected = true;
|
||||
saveLastCharacter(character.guid);
|
||||
std::stringstream ss;
|
||||
ss << "Entering world with " << character.name << "...";
|
||||
setStatus(ss.str());
|
||||
gameHandler.selectCharacter(character.guid);
|
||||
if (onCharacterSelected) onCharacterSelected(character.guid);
|
||||
}
|
||||
|
||||
ImGui::Text("Character Details:");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
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:");
|
||||
// Delete
|
||||
if (!confirmDelete) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
|
||||
if (ImGui::Button("Delete Character", ImVec2(btnW, 36))) {
|
||||
confirmDelete = true;
|
||||
}
|
||||
|
||||
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");
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.0f, 0.0f, 1.0f));
|
||||
if (ImGui::Button("Confirm Delete?", ImVec2(btnW, 36))) {
|
||||
if (onDeleteCharacter) onDeleteCharacter(character.guid);
|
||||
confirmDelete = false;
|
||||
selectedCharacterIndex = -1;
|
||||
selectedCharacterGuid = 0;
|
||||
}
|
||||
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());
|
||||
|
||||
// Send CMSG_PLAYER_LOGIN to server
|
||||
gameHandler.selectCharacter(character.guid);
|
||||
|
||||
// Call callback
|
||||
if (onCharacterSelected) {
|
||||
onCharacterSelected(character.guid);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Delete Character button
|
||||
if (!confirmDelete) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
|
||||
if (ImGui::Button("Delete Character", ImVec2(150, 40))) {
|
||||
confirmDelete = true;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.0f, 0.0f, 1.0f));
|
||||
if (ImGui::Button("Confirm Delete?", ImVec2(150, 40))) {
|
||||
if (onDeleteCharacter) {
|
||||
onDeleteCharacter(character.guid);
|
||||
}
|
||||
confirmDelete = false;
|
||||
selectedCharacterIndex = -1;
|
||||
selectedCharacterGuid = 0;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(80, 40))) {
|
||||
confirmDelete = false;
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::Button("Cancel", ImVec2(btnW, 30))) {
|
||||
confirmDelete = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ── Bottom button row ──
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Back/Refresh/Create buttons
|
||||
if (ImGui::Button("Back", ImVec2(120, 0))) {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Button("Back", ImVec2(120, 36))) { if (onBack) onBack(); }
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
||||
if (ImGui::Button("Refresh", ImVec2(120, 36))) {
|
||||
if (gameHandler.getState() == game::WorldState::READY ||
|
||||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
||||
gameHandler.requestCharacterList();
|
||||
setStatus("Refreshing character list...");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Create Character", ImVec2(150, 0))) {
|
||||
if (onCreateCharacter) {
|
||||
onCreateCharacter();
|
||||
}
|
||||
if (ImGui::Button("Create Character", ImVec2(160, 36))) {
|
||||
if (onCreateCharacter) onCreateCharacter();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
|
@ -234,7 +256,7 @@ ImVec4 CharacterScreen::getFactionColor(game::Race race) const {
|
|||
race == game::Race::NIGHT_ELF ||
|
||||
race == game::Race::GNOME ||
|
||||
race == game::Race::DRAENEI) {
|
||||
return ImVec4(0.3f, 0.5f, 1.0f, 1.0f); // Blue
|
||||
return ImVec4(0.3f, 0.5f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
// Horde races: red
|
||||
|
|
@ -243,11 +265,35 @@ ImVec4 CharacterScreen::getFactionColor(game::Race race) const {
|
|||
race == game::Race::TAUREN ||
|
||||
race == game::Race::TROLL ||
|
||||
race == game::Race::BLOOD_ELF) {
|
||||
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
|
||||
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
}
|
||||
|
||||
// Default: white
|
||||
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
std::string CharacterScreen::getConfigDir() {
|
||||
#ifdef _WIN32
|
||||
const char* appdata = std::getenv("APPDATA");
|
||||
return appdata ? std::string(appdata) + "\\wowee" : ".";
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
return home ? std::string(home) + "/.wowee" : ".";
|
||||
#endif
|
||||
}
|
||||
|
||||
void CharacterScreen::saveLastCharacter(uint64_t guid) {
|
||||
std::string dir = getConfigDir();
|
||||
std::filesystem::create_directories(dir);
|
||||
std::ofstream f(dir + "/last_character.cfg");
|
||||
if (f) f << guid;
|
||||
}
|
||||
|
||||
uint64_t CharacterScreen::loadLastCharacter() {
|
||||
std::string path = getConfigDir() + "/last_character.cfg";
|
||||
std::ifstream f(path);
|
||||
uint64_t guid = 0;
|
||||
if (f) f >> guid;
|
||||
return guid;
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue