mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
e7f8fb557c
commit
1603456120
8 changed files with 50 additions and 11 deletions
|
|
@ -114,6 +114,7 @@ struct Character {
|
||||||
Race race; // Character race
|
Race race; // Character race
|
||||||
Class characterClass; // Character class (renamed from 'class' keyword)
|
Class characterClass; // Character class (renamed from 'class' keyword)
|
||||||
Gender gender; // Character gender
|
Gender gender; // Character gender
|
||||||
|
bool useFemaleModel = false; // For nonbinary: body type preference (client-side only)
|
||||||
uint8_t level; // Character level (1-80)
|
uint8_t level; // Character level (1-80)
|
||||||
|
|
||||||
// Appearance
|
// Appearance
|
||||||
|
|
@ -167,8 +168,14 @@ const char* getGenderName(Gender gender);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get M2 model path for a given race and 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 game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,7 @@ struct CharCreateData {
|
||||||
uint8_t hairStyle = 0;
|
uint8_t hairStyle = 0;
|
||||||
uint8_t hairColor = 0;
|
uint8_t hairColor = 0;
|
||||||
uint8_t facialHair = 0;
|
uint8_t facialHair = 0;
|
||||||
|
bool useFemaleModel = false; // For nonbinary: choose body type
|
||||||
};
|
};
|
||||||
|
|
||||||
class CharCreatePacket {
|
class CharCreatePacket {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public:
|
||||||
bool loadCharacter(game::Race race, game::Gender gender,
|
bool loadCharacter(game::Race race, game::Gender gender,
|
||||||
uint8_t skin, uint8_t face,
|
uint8_t skin, uint8_t face,
|
||||||
uint8_t hairStyle, uint8_t hairColor,
|
uint8_t hairStyle, uint8_t hairColor,
|
||||||
uint8_t facialHair);
|
uint8_t facialHair, bool useFemaleModel = false);
|
||||||
|
|
||||||
void update(float deltaTime);
|
void update(float deltaTime);
|
||||||
void render();
|
void render();
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ private:
|
||||||
int raceIndex = 0;
|
int raceIndex = 0;
|
||||||
int classIndex = 0;
|
int classIndex = 0;
|
||||||
int genderIndex = 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 skin = 0, face = 0, hairStyle = 0, hairColor = 0, facialHair = 0;
|
||||||
int maxSkin = 9, maxFace = 9, maxHairStyle = 11, maxHairColor = 9, maxFacialHair = 8;
|
int maxSkin = 9, maxFace = 9, maxHairStyle = 11, maxHairColor = 9, maxFacialHair = 8;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
|
|
@ -49,6 +50,7 @@ private:
|
||||||
pipeline::AssetManager* assetManager_ = nullptr;
|
pipeline::AssetManager* assetManager_ = nullptr;
|
||||||
int prevRaceIndex_ = -1;
|
int prevRaceIndex_ = -1;
|
||||||
int prevGenderIndex_ = -1;
|
int prevGenderIndex_ = -1;
|
||||||
|
int prevBodyTypeIndex_ = -1;
|
||||||
int prevSkin_ = -1;
|
int prevSkin_ = -1;
|
||||||
int prevFace_ = -1;
|
int prevFace_ = -1;
|
||||||
int prevHairStyle_ = -1;
|
int prevHairStyle_ = -1;
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,12 @@ const char* getGenderName(Gender gender) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string getPlayerModelPath(Race race, Gender gender) {
|
std::string getPlayerModelPath(Race race, Gender gender, bool useFemaleModel) {
|
||||||
// For nonbinary, default to male model (can be extended later for model selection)
|
// Female always uses female model
|
||||||
bool useFemale = (gender == Gender::FEMALE);
|
// Nonbinary uses chosen model (useFemaleModel parameter)
|
||||||
|
// Male always uses male model
|
||||||
|
bool useFemale = (gender == Gender::FEMALE) ||
|
||||||
|
(gender == Gender::NONBINARY && useFemaleModel);
|
||||||
|
|
||||||
switch (race) {
|
switch (race) {
|
||||||
case Race::HUMAN:
|
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 game
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -5925,6 +5925,7 @@ void GameHandler::saveCharacterConfig() {
|
||||||
|
|
||||||
out << "character_guid=" << playerGuid << "\n";
|
out << "character_guid=" << playerGuid << "\n";
|
||||||
out << "gender=" << static_cast<int>(ch->gender) << "\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++) {
|
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 << "_type=" << static_cast<int>(actionBar[i].type) << "\n";
|
||||||
out << "action_bar_" << i << "_id=" << actionBar[i].id << "\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{};
|
std::array<uint32_t, ACTION_BAR_SLOTS> ids{};
|
||||||
bool hasSlots = false;
|
bool hasSlots = false;
|
||||||
int savedGender = -1;
|
int savedGender = -1;
|
||||||
|
int savedUseFemaleModel = -1;
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(in, line)) {
|
while (std::getline(in, line)) {
|
||||||
|
|
@ -5957,6 +5959,8 @@ void GameHandler::loadCharacterConfig() {
|
||||||
try { savedGuid = std::stoull(val); } catch (...) {}
|
try { savedGuid = std::stoull(val); } catch (...) {}
|
||||||
} else if (key == "gender") {
|
} else if (key == "gender") {
|
||||||
try { savedGender = std::stoi(val); } catch (...) {}
|
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) {
|
} else if (key.rfind("action_bar_", 0) == 0) {
|
||||||
// Parse action_bar_N_type or action_bar_N_id
|
// Parse action_bar_N_type or action_bar_N_id
|
||||||
size_t firstUnderscore = 11; // length of "action_bar_"
|
size_t firstUnderscore = 11; // length of "action_bar_"
|
||||||
|
|
@ -5984,12 +5988,16 @@ void GameHandler::loadCharacterConfig() {
|
||||||
return;
|
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) {
|
if (savedGender >= 0 && savedGender <= 2) {
|
||||||
for (auto& character : characters) {
|
for (auto& character : characters) {
|
||||||
if (character.guid == playerGuid) {
|
if (character.guid == playerGuid) {
|
||||||
character.gender = static_cast<Gender>(savedGender);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ void CharacterPreview::destroyFBO() {
|
||||||
bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
||||||
uint8_t skin, uint8_t face,
|
uint8_t skin, uint8_t face,
|
||||||
uint8_t hairStyle, uint8_t hairColor,
|
uint8_t hairStyle, uint8_t hairColor,
|
||||||
uint8_t facialHair) {
|
uint8_t facialHair, bool useFemaleModel) {
|
||||||
if (!charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) {
|
if (!charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +109,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
||||||
modelLoaded_ = false;
|
modelLoaded_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string m2Path = game::getPlayerModelPath(race, gender);
|
std::string m2Path = game::getPlayerModelPath(race, gender, useFemaleModel);
|
||||||
std::string modelDir;
|
std::string modelDir;
|
||||||
std::string baseName;
|
std::string baseName;
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
|
||||||
|
|
||||||
bool changed = (raceIndex != prevRaceIndex_ ||
|
bool changed = (raceIndex != prevRaceIndex_ ||
|
||||||
genderIndex != prevGenderIndex_ ||
|
genderIndex != prevGenderIndex_ ||
|
||||||
|
bodyTypeIndex != prevBodyTypeIndex_ ||
|
||||||
skin != prevSkin_ ||
|
skin != prevSkin_ ||
|
||||||
face != prevFace_ ||
|
face != prevFace_ ||
|
||||||
hairStyle != prevHairStyle_ ||
|
hairStyle != prevHairStyle_ ||
|
||||||
|
|
@ -112,6 +113,7 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
|
||||||
facialHair != prevFacialHair_);
|
facialHair != prevFacialHair_);
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
bool useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
|
||||||
preview_->loadCharacter(
|
preview_->loadCharacter(
|
||||||
allRaces[raceIndex],
|
allRaces[raceIndex],
|
||||||
static_cast<game::Gender>(genderIndex),
|
static_cast<game::Gender>(genderIndex),
|
||||||
|
|
@ -119,10 +121,12 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
|
||||||
static_cast<uint8_t>(face),
|
static_cast<uint8_t>(face),
|
||||||
static_cast<uint8_t>(hairStyle),
|
static_cast<uint8_t>(hairStyle),
|
||||||
static_cast<uint8_t>(hairColor),
|
static_cast<uint8_t>(hairColor),
|
||||||
static_cast<uint8_t>(facialHair));
|
static_cast<uint8_t>(facialHair),
|
||||||
|
useFemaleModel);
|
||||||
|
|
||||||
prevRaceIndex_ = raceIndex;
|
prevRaceIndex_ = raceIndex;
|
||||||
prevGenderIndex_ = genderIndex;
|
prevGenderIndex_ = genderIndex;
|
||||||
|
prevBodyTypeIndex_ = bodyTypeIndex;
|
||||||
prevSkin_ = skin;
|
prevSkin_ = skin;
|
||||||
prevFace_ = face;
|
prevFace_ = face;
|
||||||
prevHairStyle_ = hairStyle;
|
prevHairStyle_ = hairStyle;
|
||||||
|
|
@ -276,7 +280,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
||||||
// Mouse drag rotation on the preview image
|
// Mouse drag rotation on the preview image
|
||||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||||
float deltaX = ImGui::GetIO().MouseDelta.x;
|
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");
|
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::SameLine();
|
||||||
ImGui::RadioButton("Nonbinary", &genderIndex, 2);
|
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::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
@ -433,6 +446,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
||||||
data.race = allRaces[raceIndex];
|
data.race = allRaces[raceIndex];
|
||||||
data.characterClass = availableClasses[classIndex];
|
data.characterClass = availableClasses[classIndex];
|
||||||
data.gender = currentGender;
|
data.gender = currentGender;
|
||||||
|
data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
|
||||||
data.skin = static_cast<uint8_t>(skin);
|
data.skin = static_cast<uint8_t>(skin);
|
||||||
data.face = static_cast<uint8_t>(face);
|
data.face = static_cast<uint8_t>(face);
|
||||||
data.hairStyle = static_cast<uint8_t>(hairStyle);
|
data.hairStyle = static_cast<uint8_t>(hairStyle);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue