diff --git a/include/game/character.hpp b/include/game/character.hpp index 0b0b4632..0c301ec3 100644 --- a/include/game/character.hpp +++ b/include/game/character.hpp @@ -114,6 +114,7 @@ struct Character { Race race; // Character race Class characterClass; // Character class (renamed from 'class' keyword) Gender gender; // Character gender + bool useFemaleModel = false; // For nonbinary: body type preference (client-side only) uint8_t level; // Character level (1-80) // Appearance @@ -167,8 +168,14 @@ const char* getGenderName(Gender gender); /** * Get M2 model path for a given race and gender + * useFemaleModel allows nonbinary characters to choose body type */ -std::string getPlayerModelPath(Race race, Gender gender); +std::string getPlayerModelPath(Race race, Gender gender, bool useFemaleModel = false); + +/** + * Get M2 model path for a character (uses character's body type preference) + */ +std::string getPlayerModelPath(const Character& character); } // namespace game } // namespace wowee diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index cf8d775c..4f9b85a5 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -234,6 +234,7 @@ struct CharCreateData { uint8_t hairStyle = 0; uint8_t hairColor = 0; uint8_t facialHair = 0; + bool useFemaleModel = false; // For nonbinary: choose body type }; class CharCreatePacket { diff --git a/include/rendering/character_preview.hpp b/include/rendering/character_preview.hpp index 4c0b5a68..eaf088cc 100644 --- a/include/rendering/character_preview.hpp +++ b/include/rendering/character_preview.hpp @@ -23,7 +23,7 @@ public: bool loadCharacter(game::Race race, game::Gender gender, uint8_t skin, uint8_t face, uint8_t hairStyle, uint8_t hairColor, - uint8_t facialHair); + uint8_t facialHair, bool useFemaleModel = false); void update(float deltaTime); void render(); diff --git a/include/ui/character_create_screen.hpp b/include/ui/character_create_screen.hpp index eb000991..e5757c34 100644 --- a/include/ui/character_create_screen.hpp +++ b/include/ui/character_create_screen.hpp @@ -33,6 +33,7 @@ private: int raceIndex = 0; int classIndex = 0; int genderIndex = 0; + int bodyTypeIndex = 0; // For nonbinary: 0=masculine, 1=feminine int skin = 0, face = 0, hairStyle = 0, hairColor = 0, facialHair = 0; int maxSkin = 9, maxFace = 9, maxHairStyle = 11, maxHairColor = 9, maxFacialHair = 8; std::string statusMessage; @@ -49,6 +50,7 @@ private: pipeline::AssetManager* assetManager_ = nullptr; int prevRaceIndex_ = -1; int prevGenderIndex_ = -1; + int prevBodyTypeIndex_ = -1; int prevSkin_ = -1; int prevFace_ = -1; int prevHairStyle_ = -1; diff --git a/src/game/character.cpp b/src/game/character.cpp index 611ec5c0..1e210d3c 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -93,9 +93,12 @@ const char* getGenderName(Gender gender) { } } -std::string getPlayerModelPath(Race race, Gender gender) { - // For nonbinary, default to male model (can be extended later for model selection) - bool useFemale = (gender == Gender::FEMALE); +std::string getPlayerModelPath(Race race, Gender gender, bool useFemaleModel) { + // Female always uses female model + // Nonbinary uses chosen model (useFemaleModel parameter) + // Male always uses male model + bool useFemale = (gender == Gender::FEMALE) || + (gender == Gender::NONBINARY && useFemaleModel); switch (race) { case Race::HUMAN: @@ -143,5 +146,9 @@ std::string getPlayerModelPath(Race race, Gender gender) { } } +std::string getPlayerModelPath(const Character& character) { + return getPlayerModelPath(character.race, character.gender, character.useFemaleModel); +} + } // namespace game } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ec66ebfc..8d1d448b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5925,6 +5925,7 @@ void GameHandler::saveCharacterConfig() { out << "character_guid=" << playerGuid << "\n"; out << "gender=" << static_cast(ch->gender) << "\n"; + out << "use_female_model=" << (ch->useFemaleModel ? 1 : 0) << "\n"; for (int i = 0; i < ACTION_BAR_SLOTS; i++) { out << "action_bar_" << i << "_type=" << static_cast(actionBar[i].type) << "\n"; out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n"; @@ -5945,6 +5946,7 @@ void GameHandler::loadCharacterConfig() { std::array ids{}; bool hasSlots = false; int savedGender = -1; + int savedUseFemaleModel = -1; std::string line; while (std::getline(in, line)) { @@ -5957,6 +5959,8 @@ void GameHandler::loadCharacterConfig() { try { savedGuid = std::stoull(val); } catch (...) {} } else if (key == "gender") { try { savedGender = std::stoi(val); } catch (...) {} + } else if (key == "use_female_model") { + try { savedUseFemaleModel = std::stoi(val); } catch (...) {} } else if (key.rfind("action_bar_", 0) == 0) { // Parse action_bar_N_type or action_bar_N_id size_t firstUnderscore = 11; // length of "action_bar_" @@ -5984,12 +5988,16 @@ void GameHandler::loadCharacterConfig() { return; } - // Apply saved gender (allows nonbinary to persist even though server only stores male/female) + // Apply saved gender and body type (allows nonbinary to persist even though server only stores male/female) if (savedGender >= 0 && savedGender <= 2) { for (auto& character : characters) { if (character.guid == playerGuid) { character.gender = static_cast(savedGender); - LOG_INFO("Applied saved gender: ", getGenderName(character.gender)); + if (savedUseFemaleModel >= 0) { + character.useFemaleModel = (savedUseFemaleModel != 0); + } + LOG_INFO("Applied saved gender: ", getGenderName(character.gender), + ", body type: ", (character.useFemaleModel ? "feminine" : "masculine")); break; } } diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index c4f60afa..bccdb6a7 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -97,7 +97,7 @@ void CharacterPreview::destroyFBO() { bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, uint8_t skin, uint8_t face, uint8_t hairStyle, uint8_t hairColor, - uint8_t facialHair) { + uint8_t facialHair, bool useFemaleModel) { if (!charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) { return false; } @@ -109,7 +109,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, modelLoaded_ = false; } - std::string m2Path = game::getPlayerModelPath(race, gender); + std::string m2Path = game::getPlayerModelPath(race, gender, useFemaleModel); std::string modelDir; std::string baseName; { diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index 5cb1ffd6..769826bf 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -105,6 +105,7 @@ void CharacterCreateScreen::updatePreviewIfNeeded() { bool changed = (raceIndex != prevRaceIndex_ || genderIndex != prevGenderIndex_ || + bodyTypeIndex != prevBodyTypeIndex_ || skin != prevSkin_ || face != prevFace_ || hairStyle != prevHairStyle_ || @@ -112,6 +113,7 @@ void CharacterCreateScreen::updatePreviewIfNeeded() { facialHair != prevFacialHair_); if (changed) { + bool useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine preview_->loadCharacter( allRaces[raceIndex], static_cast(genderIndex), @@ -119,10 +121,12 @@ void CharacterCreateScreen::updatePreviewIfNeeded() { static_cast(face), static_cast(hairStyle), static_cast(hairColor), - static_cast(facialHair)); + static_cast(facialHair), + useFemaleModel); prevRaceIndex_ = raceIndex; prevGenderIndex_ = genderIndex; + prevBodyTypeIndex_ = bodyTypeIndex; prevSkin_ = skin; prevFace_ = face; prevHairStyle_ = hairStyle; @@ -276,7 +280,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { // Mouse drag rotation on the preview image if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { float deltaX = ImGui::GetIO().MouseDelta.x; - preview_->rotate(deltaX * 0.5f); + preview_->rotate(deltaX * 0.2f); } ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Drag to rotate"); @@ -365,6 +369,15 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { 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(); @@ -433,6 +446,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { data.race = allRaces[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);