#include "ui/character_create_screen.hpp" #include "rendering/character_preview.hpp" #include "rendering/renderer.hpp" #include "core/application.hpp" #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" #include #include #include namespace wowee { namespace ui { // Full WotLK race/class lists (used as defaults when no expansion constraints set) static const game::Race kAllRaces[] = { // Alliance game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF, game::Race::GNOME, game::Race::DRAENEI, // Horde game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN, game::Race::TROLL, game::Race::BLOOD_ELF, }; static constexpr int kAllRaceCount = 10; static constexpr int kAllianceCount = 5; static const game::Class kAllClasses[] = { game::Class::WARRIOR, game::Class::PALADIN, game::Class::HUNTER, game::Class::ROGUE, game::Class::PRIEST, game::Class::DEATH_KNIGHT, game::Class::SHAMAN, game::Class::MAGE, game::Class::WARLOCK, game::Class::DRUID, }; CharacterCreateScreen::CharacterCreateScreen() { reset(); } CharacterCreateScreen::~CharacterCreateScreen() = default; void CharacterCreateScreen::setExpansionConstraints( const std::vector& races, const std::vector& classes) { // Build filtered race list: alliance first, then horde availableRaces_.clear(); expansionClasses_.clear(); if (!races.empty()) { // Alliance races in display order for (auto r : std::initializer_list{ game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF, game::Race::GNOME, game::Race::DRAENEI}) { if (std::find(races.begin(), races.end(), static_cast(r)) != races.end()) { availableRaces_.push_back(r); } } allianceRaceCount_ = static_cast(availableRaces_.size()); // Horde races in display order for (auto r : std::initializer_list{ game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN, game::Race::TROLL, game::Race::BLOOD_ELF}) { if (std::find(races.begin(), races.end(), static_cast(r)) != races.end()) { availableRaces_.push_back(r); } } } if (!classes.empty()) { for (auto cls : kAllClasses) { if (std::find(classes.begin(), classes.end(), static_cast(cls)) != classes.end()) { expansionClasses_.push_back(cls); } } } // If no constraints provided, fall back to WotLK defaults if (availableRaces_.empty()) { availableRaces_.assign(kAllRaces, kAllRaces + kAllRaceCount); allianceRaceCount_ = kAllianceCount; } raceIndex = 0; classIndex = 0; updateAvailableClasses(); } void CharacterCreateScreen::reset() { std::memset(nameBuffer, 0, sizeof(nameBuffer)); raceIndex = 0; classIndex = 0; genderIndex = 0; skin = 0; face = 0; hairStyle = 0; hairColor = 0; facialHair = 0; maxSkin = 9; maxFace = 9; maxHairStyle = 11; maxHairColor = 9; maxFacialHair = 8; statusMessage.clear(); statusIsError = false; createTimer_ = -1.0f; hairColorIds_.clear(); // Populate default races if not yet set by setExpansionConstraints if (availableRaces_.empty()) { availableRaces_.assign(kAllRaces, kAllRaces + kAllRaceCount); allianceRaceCount_ = kAllianceCount; } updateAvailableClasses(); // Reset preview tracking to force model reload on next render prevRaceIndex_ = -1; prevGenderIndex_ = -1; prevSkin_ = -1; prevFace_ = -1; prevHairStyle_ = -1; prevHairColor_ = -1; prevFacialHair_ = -1; prevRangeRace_ = -1; prevRangeGender_ = -1; prevRangeSkin_ = -1; prevRangeHairStyle_ = -1; } void CharacterCreateScreen::initializePreview(pipeline::AssetManager* am) { assetManager_ = am; if (!preview_) { preview_ = std::make_unique(); if (preview_->initialize(am)) { auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) renderer->registerPreview(preview_.get()); } } // Force model reload prevRaceIndex_ = -1; } void CharacterCreateScreen::update(float deltaTime) { if (preview_) { preview_->update(deltaTime); } // Timeout waiting for server response if (createTimer_ >= 0.0f) { createTimer_ += deltaTime; if (createTimer_ > 10.0f) { createTimer_ = -1.0f; setStatus("Server did not respond. Try again.", true); } } } void CharacterCreateScreen::setStatus(const std::string& msg, bool isError) { statusMessage = msg; statusIsError = isError; if (isError || msg.empty()) { createTimer_ = -1.0f; // Stop waiting on error/clear } } void CharacterCreateScreen::updateAvailableClasses() { availableClasses.clear(); if (availableRaces_.empty() || raceIndex >= static_cast(availableRaces_.size())) return; game::Race race = availableRaces_[raceIndex]; for (auto cls : kAllClasses) { if (!game::isValidRaceClassCombo(race, cls)) continue; // If expansion constraints set, only allow listed classes if (!expansionClasses_.empty()) { if (std::find(expansionClasses_.begin(), expansionClasses_.end(), cls) == expansionClasses_.end()) continue; } availableClasses.push_back(cls); } // Clamp class index if (classIndex >= static_cast(availableClasses.size())) { classIndex = 0; } } void CharacterCreateScreen::updatePreviewIfNeeded() { if (!preview_) return; bool changed = (raceIndex != prevRaceIndex_ || genderIndex != prevGenderIndex_ || bodyTypeIndex != prevBodyTypeIndex_ || skin != prevSkin_ || face != prevFace_ || hairStyle != prevHairStyle_ || hairColor != prevHairColor_ || facialHair != prevFacialHair_); if (changed) { bool useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine uint8_t hairColorId = 0; if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast(hairColorIds_.size())) { hairColorId = hairColorIds_[hairColor]; } else { hairColorId = static_cast(hairColor); } preview_->loadCharacter( availableRaces_[raceIndex], static_cast(genderIndex), static_cast(skin), static_cast(face), static_cast(hairStyle), hairColorId, static_cast(facialHair), useFemaleModel); prevRaceIndex_ = raceIndex; prevGenderIndex_ = genderIndex; prevBodyTypeIndex_ = bodyTypeIndex; prevSkin_ = skin; prevFace_ = face; prevHairStyle_ = hairStyle; prevHairColor_ = hairColor; prevFacialHair_ = facialHair; } } void CharacterCreateScreen::updateAppearanceRanges() { if (raceIndex == prevRangeRace_ && genderIndex == prevRangeGender_ && skin == prevRangeSkin_ && hairStyle == prevRangeHairStyle_) { return; } prevRangeRace_ = raceIndex; prevRangeGender_ = genderIndex; prevRangeSkin_ = skin; prevRangeHairStyle_ = hairStyle; maxSkin = 9; maxFace = 9; maxHairStyle = 11; maxHairColor = 9; maxFacialHair = 8; hairColorIds_.clear(); if (!assetManager_) return; auto dbc = assetManager_->loadDBC("CharSections.dbc"); if (!dbc) return; uint32_t targetRaceId = static_cast(availableRaces_[raceIndex]); uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; int skinMax = -1; int hairStyleMax = -1; for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 0 && variationIndex == 0) { skinMax = std::max(skinMax, static_cast(colorIndex)); } else if (baseSection == 3) { hairStyleMax = std::max(hairStyleMax, static_cast(variationIndex)); } } if (skinMax >= 0) { maxSkin = skinMax; if (skin > maxSkin) skin = maxSkin; } if (hairStyleMax >= 0) { maxHairStyle = hairStyleMax; if (hairStyle > maxHairStyle) hairStyle = maxHairStyle; } int faceMax = -1; std::vector hairColorIds; for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 1 && colorIndex == static_cast(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex)); } else if (baseSection == 3 && variationIndex == static_cast(hairStyle)) { if (colorIndex <= 255) { hairColorIds.push_back(static_cast(colorIndex)); } } } if (faceMax >= 0) { maxFace = faceMax; if (face > maxFace) face = maxFace; } // Hair colors: use actual available DBC IDs (not "0..maxId"), since IDs may be sparse. if (!hairColorIds.empty()) { std::sort(hairColorIds.begin(), hairColorIds.end()); hairColorIds.erase(std::unique(hairColorIds.begin(), hairColorIds.end()), hairColorIds.end()); hairColorIds_ = std::move(hairColorIds); maxHairColor = std::max(0, static_cast(hairColorIds_.size()) - 1); if (hairColor > maxHairColor) hairColor = maxHairColor; if (hairColor < 0) hairColor = 0; } int facialMax = -1; auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc"); const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr; if (facialDbc) { for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) { uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0); uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1); if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2); facialMax = std::max(facialMax, static_cast(variation)); } } if (facialMax >= 0) { maxFacialHair = facialMax; } else if (targetSexId == 1) { maxFacialHair = 0; } if (facialHair > maxFacialHair) { facialHair = maxFacialHair; } } void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { // Render the preview to FBO before the ImGui frame if (preview_) { updatePreviewIfNeeded(); preview_->render(); preview_->requestComposite(); } ImVec2 displaySize = ImGui::GetIO().DisplaySize; bool hasPreview = (preview_ && preview_->getTextureId() != 0); float previewWidth = hasPreview ? 320.0f : 0.0f; float controlsWidth = 540.0f; float totalWidth = hasPreview ? (previewWidth + controlsWidth + 16.0f) : 600.0f; float totalHeight = 580.0f; ImGui::SetNextWindowSize(ImVec2(totalWidth, totalHeight), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2((displaySize.x - totalWidth) * 0.5f, (displaySize.y - totalHeight) * 0.5f), ImGuiCond_Always); ImGui::Begin("Create Character", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); if (hasPreview) { // Left panel: 3D preview ImGui::BeginChild("##preview_panel", ImVec2(previewWidth, -40.0f), false); { // Display the FBO texture (flip Y for OpenGL) float imgW = previewWidth - 8.0f; float imgH = imgW * (static_cast(preview_->getHeight()) / static_cast(preview_->getWidth())); if (imgH > totalHeight - 80.0f) { imgH = totalHeight - 80.0f; imgW = imgH * (static_cast(preview_->getWidth()) / static_cast(preview_->getHeight())); } if (preview_->getTextureId()) { ImGui::Image( reinterpret_cast(preview_->getTextureId()), ImVec2(imgW, imgH)); } // Mouse drag rotation on the preview image if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { float deltaX = ImGui::GetIO().MouseDelta.x; preview_->rotate(deltaX * 0.2f); } ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Drag to rotate"); } ImGui::EndChild(); ImGui::SameLine(); // Right panel: controls ImGui::BeginChild("##controls_panel", ImVec2(0, -40.0f), false); } // Name input ImGui::Text("Name:"); ImGui::SameLine(80); ImGui::SetNextItemWidth(200); ImGui::InputText("##name", nameBuffer, sizeof(nameBuffer)); ImGui::Spacing(); // Race selection (filtered by expansion) int raceCount = static_cast(availableRaces_.size()); ImGui::Text("Race:"); ImGui::Spacing(); ImGui::Indent(10.0f); if (allianceRaceCount_ > 0) { ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "Alliance:"); ImGui::SameLine(); for (int i = 0; i < allianceRaceCount_; ++i) { if (i > 0) ImGui::SameLine(); bool selected = (raceIndex == i); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 1.0f, 0.8f)); if (ImGui::SmallButton(game::getRaceName(availableRaces_[i]))) { if (raceIndex != i) { raceIndex = i; classIndex = 0; skin = face = hairStyle = hairColor = facialHair = 0; updateAvailableClasses(); } } if (selected) ImGui::PopStyleColor(); } } if (allianceRaceCount_ < raceCount) { ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:"); ImGui::SameLine(); for (int i = allianceRaceCount_; i < raceCount; ++i) { if (i > allianceRaceCount_) ImGui::SameLine(); bool selected = (raceIndex == i); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.3f, 0.3f, 0.8f)); if (ImGui::SmallButton(game::getRaceName(availableRaces_[i]))) { if (raceIndex != i) { raceIndex = i; classIndex = 0; skin = face = hairStyle = hairColor = facialHair = 0; updateAvailableClasses(); } } if (selected) ImGui::PopStyleColor(); } } ImGui::Unindent(10.0f); ImGui::Spacing(); // Class selection ImGui::Text("Class:"); ImGui::SameLine(80); if (!availableClasses.empty()) { ImGui::BeginGroup(); for (int i = 0; i < static_cast(availableClasses.size()); ++i) { if (i > 0) ImGui::SameLine(); bool selected = (classIndex == i); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 0.8f)); if (ImGui::SmallButton(game::getClassName(availableClasses[i]))) { classIndex = i; } if (selected) ImGui::PopStyleColor(); } ImGui::EndGroup(); } ImGui::Spacing(); // Gender ImGui::Text("Gender:"); ImGui::SameLine(80); ImGui::RadioButton("Male", &genderIndex, 0); ImGui::SameLine(); ImGui::RadioButton("Female", &genderIndex, 1); ImGui::SameLine(); ImGui::RadioButton("Nonbinary", &genderIndex, 2); // Body type selection for nonbinary if (genderIndex == 2) { // Nonbinary ImGui::Text("Body Type:"); ImGui::SameLine(80); ImGui::RadioButton("Masculine", &bodyTypeIndex, 0); ImGui::SameLine(); ImGui::RadioButton("Feminine", &bodyTypeIndex, 1); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Appearance sliders updateAppearanceRanges(); game::Gender currentGender = static_cast(genderIndex); ImGui::Text("Appearance"); ImGui::Spacing(); float sliderWidth = hasPreview ? 180.0f : 200.0f; float labelCol = hasPreview ? 100.0f : 120.0f; auto slider = [&](const char* label, int* val, int maxVal) { ImGui::Text("%s", label); ImGui::SameLine(labelCol); ImGui::SetNextItemWidth(sliderWidth); char id[32]; snprintf(id, sizeof(id), "##%s", label); ImGui::SliderInt(id, val, 0, maxVal); }; slider("Skin", &skin, maxSkin); slider("Face", &face, maxFace); slider("Hair Style", &hairStyle, maxHairStyle); slider("Hair Color", &hairColor, maxHairColor); slider("Facial Feature", &facialHair, maxFacialHair); ImGui::Spacing(); // Status message if (!statusMessage.empty()) { ImGui::Separator(); ImGui::Spacing(); ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); ImGui::TextColored(color, "%s", statusMessage.c_str()); } if (hasPreview) { ImGui::EndChild(); // controls_panel } // Bottom buttons (outside children) ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Create", ImVec2(150, 35))) { std::string name(nameBuffer); // Trim whitespace size_t start = name.find_first_not_of(" \t\r\n"); size_t end = name.find_last_not_of(" \t\r\n"); if (start == std::string::npos) { name.clear(); } else { name = name.substr(start, end - start + 1); } if (name.empty()) { setStatus("Please enter a character name.", true); } else if (availableClasses.empty()) { setStatus("No valid class for this race.", true); } else { setStatus("Creating character...", false); createTimer_ = 0.0f; game::CharCreateData data; data.name = name; data.race = availableRaces_[raceIndex]; data.characterClass = availableClasses[classIndex]; data.gender = currentGender; data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine data.skin = static_cast(skin); data.face = static_cast(face); data.hairStyle = static_cast(hairStyle); if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast(hairColorIds_.size())) { data.hairColor = hairColorIds_[hairColor]; } else { data.hairColor = static_cast(hairColor); } data.facialHair = static_cast(facialHair); if (onCreate) { onCreate(data); } } } ImGui::SameLine(); if (ImGui::Button("Back", ImVec2(150, 35))) { if (onCancel) { onCancel(); } } ImGui::End(); } } // namespace ui } // namespace wowee