Add body type selection for nonbinary characters and reduce preview rotation sensitivity

Nonbinary characters can now choose between masculine and feminine body types in character creation, with real-time preview updates and full appearance customization. Body type preference is saved to character config and persists across sessions. Also reduces character preview drag-to-rotate sensitivity from 0.5 to 0.2 for better control.
This commit is contained in:
Kelsi 2026-02-09 17:56:04 -08:00
parent ebac7eb135
commit 7c28b80135
8 changed files with 50 additions and 11 deletions

View file

@ -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

View file

@ -5925,6 +5925,7 @@ void GameHandler::saveCharacterConfig() {
out << "character_guid=" << playerGuid << "\n";
out << "gender=" << static_cast<int>(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<int>(actionBar[i].type) << "\n";
out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n";
@ -5945,6 +5946,7 @@ void GameHandler::loadCharacterConfig() {
std::array<uint32_t, ACTION_BAR_SLOTS> 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<Gender>(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;
}
}

View file

@ -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;
{

View file

@ -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<game::Gender>(genderIndex),
@ -119,10 +121,12 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
static_cast<uint8_t>(face),
static_cast<uint8_t>(hairStyle),
static_cast<uint8_t>(hairColor),
static_cast<uint8_t>(facialHair));
static_cast<uint8_t>(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<uint8_t>(skin);
data.face = static_cast<uint8_t>(face);
data.hairStyle = static_cast<uint8_t>(hairStyle);