Add nonbinary gender support with pronoun system and server compatibility

Extends gender system beyond WoW's binary male/female to support nonbinary characters with proper they/them pronouns. Implements client-side gender mapping (nonbinary→male) for 3.3.5a server compatibility while preserving player identity through local config persistence. Adds pronoun placeholders ($p/$o/$s/$S) and three-option gender text parsing ($g<male>:<female>:<nonbinary>;) for inclusive quest and dialog text.
This commit is contained in:
Kelsi 2026-02-09 17:39:21 -08:00
parent 28aa88608f
commit 0071c24713
10 changed files with 421 additions and 32 deletions

View file

@ -45,9 +45,41 @@ enum class Class : uint8_t {
*/
enum class Gender : uint8_t {
MALE = 0,
FEMALE = 1
FEMALE = 1,
NONBINARY = 2
};
/**
* Pronoun set for text substitution
*/
struct Pronouns {
std::string subject; // he/she/they
std::string object; // him/her/them
std::string possessive; // his/her/their
std::string possessiveP; // his/hers/theirs
static Pronouns forGender(Gender gender) {
switch (gender) {
case Gender::MALE:
return {"he", "him", "his", "his"};
case Gender::FEMALE:
return {"she", "her", "her", "hers"};
case Gender::NONBINARY:
return {"they", "them", "their", "theirs"};
default:
return {"they", "them", "their", "theirs"};
}
}
};
/**
* Convert gender to server-compatible value (WoW 3.3.5a only supports binary genders)
* Nonbinary is mapped to MALE for server communication while preserving client-side identity
*/
inline Gender toServerGender(Gender gender) {
return (gender == Gender::FEMALE) ? Gender::FEMALE : Gender::MALE;
}
/**
* Equipment item data
*/

View file

@ -6,6 +6,7 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <algorithm>
namespace wowee {
namespace pipeline { class AssetManager; }
@ -36,6 +37,12 @@ public:
void setRotateWithCamera(bool rotate) { rotateWithCamera = rotate; }
bool isRotateWithCamera() const { return rotateWithCamera; }
void setSquareShape(bool square) { squareShape = square; }
bool isSquareShape() const { return squareShape; }
void zoomIn() { viewRadius = std::max(100.0f, viewRadius - 50.0f); }
void zoomOut() { viewRadius = std::min(800.0f, viewRadius + 50.0f); }
// Public accessors for WorldMap
GLuint getOrLoadTileTexture(int tileX, int tileY);
void ensureTRSParsed() { if (!trsParsed) parseTRS(); }
@ -79,6 +86,7 @@ private:
float viewRadius = 400.0f; // world units visible in minimap radius
bool enabled = true;
bool rotateWithCamera = false;
bool squareShape = false;
// Throttling
float updateIntervalSec = 0.25f;

View file

@ -80,10 +80,13 @@ private:
bool pendingInvertMouse = false;
int pendingUiOpacity = 65;
bool pendingMinimapRotate = false;
bool pendingMinimapSquare = false;
// UI element transparency (0.0 = fully transparent, 1.0 = fully opaque)
float uiOpacity_ = 0.65f;
bool minimapRotate_ = false;
bool minimapSquare_ = false;
bool minimapSettingsApplied_ = false;
/**
* Render player info window
@ -194,6 +197,9 @@ private:
static std::string getSettingsPath();
// Gender placeholder replacement
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler);
// Left-click targeting: distinguish click from camera drag
glm::vec2 leftClickPressPos_ = glm::vec2(0.0f);
bool leftClickWasPress_ = false;

View file

@ -88,50 +88,54 @@ const char* getGenderName(Gender gender) {
switch (gender) {
case Gender::MALE: return "Male";
case Gender::FEMALE: return "Female";
case Gender::NONBINARY: return "Nonbinary";
default: return "Unknown";
}
}
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);
switch (race) {
case Race::HUMAN:
return gender == Gender::FEMALE
return useFemale
? "Character\\Human\\Female\\HumanFemale.m2"
: "Character\\Human\\Male\\HumanMale.m2";
case Race::ORC:
return gender == Gender::FEMALE
return useFemale
? "Character\\Orc\\Female\\OrcFemale.m2"
: "Character\\Orc\\Male\\OrcMale.m2";
case Race::DWARF:
return gender == Gender::FEMALE
return useFemale
? "Character\\Dwarf\\Female\\DwarfFemale.m2"
: "Character\\Dwarf\\Male\\DwarfMale.m2";
case Race::NIGHT_ELF:
return gender == Gender::FEMALE
return useFemale
? "Character\\NightElf\\Female\\NightElfFemale.m2"
: "Character\\NightElf\\Male\\NightElfMale.m2";
case Race::UNDEAD:
return gender == Gender::FEMALE
return useFemale
? "Character\\Scourge\\Female\\ScourgeFemale.m2"
: "Character\\Scourge\\Male\\ScourgeMale.m2";
case Race::TAUREN:
return gender == Gender::FEMALE
return useFemale
? "Character\\Tauren\\Female\\TaurenFemale.m2"
: "Character\\Tauren\\Male\\TaurenMale.m2";
case Race::GNOME:
return gender == Gender::FEMALE
return useFemale
? "Character\\Gnome\\Female\\GnomeFemale.m2"
: "Character\\Gnome\\Male\\GnomeMale.m2";
case Race::TROLL:
return gender == Gender::FEMALE
return useFemale
? "Character\\Troll\\Female\\TrollFemale.m2"
: "Character\\Troll\\Male\\TrollMale.m2";
case Race::BLOOD_ELF:
return gender == Gender::FEMALE
return useFemale
? "Character\\BloodElf\\Female\\BloodElfFemale.m2"
: "Character\\BloodElf\\Male\\BloodElfMale.m2";
case Race::DRAENEI:
return gender == Gender::FEMALE
return useFemale
? "Character\\Draenei\\Female\\DraeneiFemale.m2"
: "Character\\Draenei\\Male\\DraeneiMale.m2";
default:

View file

@ -5924,6 +5924,7 @@ void GameHandler::saveCharacterConfig() {
}
out << "character_guid=" << playerGuid << "\n";
out << "gender=" << static_cast<int>(ch->gender) << "\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";
@ -5943,6 +5944,7 @@ void GameHandler::loadCharacterConfig() {
std::array<int, ACTION_BAR_SLOTS> types{};
std::array<uint32_t, ACTION_BAR_SLOTS> ids{};
bool hasSlots = false;
int savedGender = -1;
std::string line;
while (std::getline(in, line)) {
@ -5953,6 +5955,8 @@ void GameHandler::loadCharacterConfig() {
if (key == "character_guid") {
try { savedGuid = std::stoull(val); } catch (...) {}
} else if (key == "gender") {
try { savedGender = 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_"
@ -5980,6 +5984,17 @@ void GameHandler::loadCharacterConfig() {
return;
}
// Apply saved gender (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));
break;
}
}
}
if (hasSlots) {
for (int i = 0; i < ACTION_BAR_SLOTS; i++) {
actionBar[i].type = static_cast<ActionBarSlot::Type>(types[i]);

View file

@ -257,10 +257,13 @@ const char* getAuthResultString(AuthResult result) {
network::Packet CharCreatePacket::build(const CharCreateData& data) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_CREATE));
// Convert nonbinary gender to server-compatible value (servers only support male/female)
Gender serverGender = toServerGender(data.gender);
packet.writeString(data.name); // null-terminated name
packet.writeUInt8(static_cast<uint8_t>(data.race));
packet.writeUInt8(static_cast<uint8_t>(data.characterClass));
packet.writeUInt8(static_cast<uint8_t>(data.gender));
packet.writeUInt8(static_cast<uint8_t>(serverGender));
packet.writeUInt8(data.skin);
packet.writeUInt8(data.face);
packet.writeUInt8(data.hairStyle);
@ -272,6 +275,7 @@ network::Packet CharCreatePacket::build(const CharCreateData& data) {
" race=", static_cast<int>(data.race),
" class=", static_cast<int>(data.characterClass),
" gender=", static_cast<int>(data.gender),
" (server gender=", static_cast<int>(serverGender), ")",
" skin=", static_cast<int>(data.skin),
" face=", static_cast<int>(data.face),
" hair=", static_cast<int>(data.hairStyle),

View file

@ -143,6 +143,7 @@ bool Minimap::initialize(int size) {
uniform float uRotation;
uniform float uArrowRotation;
uniform float uZoomRadius;
uniform bool uSquareShape;
out vec4 FragColor;
@ -168,7 +169,8 @@ bool Minimap::initialize(int size) {
void main() {
vec2 centered = TexCoord - 0.5;
float dist = length(centered);
if (dist > 0.5) discard;
float maxDist = uSquareShape ? max(abs(centered.x), abs(centered.y)) : dist;
if (maxDist > 0.5) discard;
// Rotate screen coords → composite UV offset
// Composite: U increases east, V increases south
@ -186,9 +188,9 @@ bool Minimap::initialize(int size) {
vec2 uv = uPlayerUV + offset;
vec3 color = texture(uComposite, uv).rgb;
// Thin dark border at circle edge
if (dist > 0.49) {
color = mix(color, vec3(0.08), smoothstep(0.49, 0.5, dist));
// Thin dark border at edge
if (maxDist > 0.49) {
color = mix(color, vec3(0.08), smoothstep(0.49, 0.5, maxDist));
}
// Player arrow at center (always points up = forward)
@ -509,6 +511,7 @@ void Minimap::renderQuad(const Camera& playerCamera, const glm::vec3& centerWorl
arrowRotation = std::atan2(-fwd.x, fwd.y);
}
quadShader->setUniform("uArrowRotation", arrowRotation);
quadShader->setUniform("uSquareShape", squareShape);
quadShader->setUniform("uComposite", 0);
glActiveTexture(GL_TEXTURE0);

View file

@ -362,6 +362,8 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
ImGui::RadioButton("Male", &genderIndex, 0);
ImGui::SameLine();
ImGui::RadioButton("Female", &genderIndex, 1);
ImGui::SameLine();
ImGui::RadioButton("Nonbinary", &genderIndex, 2);
ImGui::Spacing();
ImGui::Separator();

View file

@ -72,6 +72,18 @@ void GameScreen::render(game::GameHandler& gameHandler) {
float prevAlpha = ImGui::GetStyle().Alpha;
ImGui::GetStyle().Alpha = uiOpacity_;
// Apply initial minimap settings when renderer becomes available
if (!minimapSettingsApplied_) {
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->setRotateWithCamera(minimapRotate_);
minimap->setSquareShape(minimapSquare_);
minimapSettingsApplied_ = true;
}
}
}
// Process targeting input before UI windows
processTargetInput(gameHandler);
@ -3115,8 +3127,9 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
for (const auto& opt : gossip.options) {
ImGui::PushID(static_cast<int>(opt.id));
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
std::string processedText = replaceGenderPlaceholders(opt.text, gameHandler);
char label[256];
snprintf(label, sizeof(label), "%s %s", icon, opt.text.c_str());
snprintf(label, sizeof(label), "%s %s", icon, processedText.c_str());
if (ImGui::Selectable(label)) {
gameHandler.selectGossipOption(opt.id);
}
@ -3191,10 +3204,12 @@ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
bool open = true;
const auto& quest = gameHandler.getQuestDetails();
if (ImGui::Begin(quest.title.c_str(), &open)) {
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open)) {
// Quest description
if (!quest.details.empty()) {
ImGui::TextWrapped("%s", quest.details.c_str());
std::string processedDetails = replaceGenderPlaceholders(quest.details, gameHandler);
ImGui::TextWrapped("%s", processedDetails.c_str());
}
// Objectives
@ -3202,7 +3217,8 @@ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Objectives:");
ImGui::TextWrapped("%s", quest.objectives.c_str());
std::string processedObjectives = replaceGenderPlaceholders(quest.objectives, gameHandler);
ImGui::TextWrapped("%s", processedObjectives.c_str());
}
// Rewards
@ -3264,9 +3280,11 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
bool open = true;
const auto& quest = gameHandler.getQuestRequestItems();
if (ImGui::Begin(quest.title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.completionText.empty()) {
ImGui::TextWrapped("%s", quest.completionText.c_str());
std::string processedCompletionText = replaceGenderPlaceholders(quest.completionText, gameHandler);
ImGui::TextWrapped("%s", processedCompletionText.c_str());
}
// Required items
@ -3335,9 +3353,11 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
const auto& quest = gameHandler.getQuestOfferReward();
static int selectedChoice = -1;
if (ImGui::Begin(quest.title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!quest.rewardText.empty()) {
ImGui::TextWrapped("%s", quest.rewardText.c_str());
std::string processedRewardText = replaceGenderPlaceholders(quest.rewardText, gameHandler);
ImGui::TextWrapped("%s", processedRewardText.c_str());
}
// Choice rewards (pick one)
@ -3771,6 +3791,7 @@ void GameScreen::renderEscapeMenu() {
showEscapeSettingsNotice = false;
showSettingsWindow = true;
settingsInit = false;
showEscapeMenu = false;
}
ImGui::Spacing();
@ -4095,9 +4116,11 @@ void GameScreen::renderSettingsWindow() {
}
pendingUiOpacity = static_cast<int>(uiOpacity_ * 100.0f + 0.5f);
pendingMinimapRotate = minimapRotate_;
pendingMinimapSquare = minimapSquare_;
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->setRotateWithCamera(minimapRotate_);
minimap->setSquareShape(minimapSquare_);
}
}
settingsInit = true;
@ -4347,6 +4370,35 @@ void GameScreen::renderSettingsWindow() {
}
saveSettings();
}
if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) {
minimapSquare_ = pendingMinimapSquare;
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->setSquareShape(minimapSquare_);
}
}
saveSettings();
}
// Zoom controls
ImGui::Text("Minimap Zoom:");
ImGui::SameLine();
if (ImGui::Button(" - ")) {
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->zoomOut();
saveSettings();
}
}
}
ImGui::SameLine();
if (ImGui::Button(" + ")) {
if (renderer) {
if (auto* minimap = renderer->getMinimap()) {
minimap->zoomIn();
saveSettings();
}
}
}
ImGui::Spacing();
ImGui::Separator();
@ -4357,8 +4409,10 @@ void GameScreen::renderSettingsWindow() {
pendingInvertMouse = kDefaultInvertMouse;
pendingUiOpacity = 65;
pendingMinimapRotate = false;
pendingMinimapSquare = false;
uiOpacity_ = 0.65f;
minimapRotate_ = false;
minimapSquare_ = false;
if (renderer) {
if (auto* cameraController = renderer->getCameraController()) {
cameraController->setMouseSensitivity(pendingMouseSensitivity);
@ -4366,6 +4420,7 @@ void GameScreen::renderSettingsWindow() {
}
if (auto* minimap = renderer->getMinimap()) {
minimap->setRotateWithCamera(minimapRotate_);
minimap->setSquareShape(minimapSquare_);
}
}
saveSettings();
@ -4559,6 +4614,26 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f),
IM_COL32(0, 0, 0, 255), marker);
}
// Add zoom buttons at the bottom edge of the minimap
ImGui::SetNextWindowPos(ImVec2(centerX - 30, centerY + mapRadius - 30), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(60, 24), ImGuiCond_Always);
ImGuiWindowFlags zoomFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoBackground;
if (ImGui::Begin("##MinimapZoom", nullptr, zoomFlags)) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0));
if (ImGui::SmallButton("-")) {
if (minimap) minimap->zoomOut();
}
ImGui::SameLine();
if (ImGui::SmallButton("+")) {
if (minimap) minimap->zoomIn();
}
ImGui::PopStyleVar(2);
}
ImGui::End();
}
std::string GameScreen::getSettingsPath() {
@ -4573,6 +4648,120 @@ std::string GameScreen::getSettingsPath() {
return dir + "/settings.cfg";
}
std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
// Get player gender and pronouns
game::Gender gender = game::Gender::NONBINARY;
const auto* character = gameHandler.getActiveCharacter();
if (character) {
gender = character->gender;
}
game::Pronouns pronouns = game::Pronouns::forGender(gender);
std::string result = text;
// Helper to trim whitespace
auto trim = [](std::string& s) {
s.erase(0, s.find_first_not_of(" \t\n\r"));
s.erase(s.find_last_not_of(" \t\n\r") + 1);
};
// Replace pronoun placeholders first (simpler, no complex parsing)
// $p = subject pronoun (he/she/they)
// $o = object pronoun (him/her/them)
// $s = possessive adjective (his/her/their)
// $S = possessive pronoun (his/hers/theirs)
size_t pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char code = result[pos + 1];
std::string replacement;
switch (code) {
case 'p': replacement = pronouns.subject; break;
case 'o': replacement = pronouns.object; break;
case 's': replacement = pronouns.possessive; break;
case 'S': replacement = pronouns.possessiveP; break;
case 'g':
// Handle $g separately below
pos++;
continue;
default:
pos++;
continue;
}
// Replace the pronoun placeholder
result.replace(pos, 2, replacement);
pos += replacement.length();
}
// Find and replace all $g placeholders (gender-specific text)
// Format: $g<male>:<female>; or $g<male>:<female>:<nonbinary>;
pos = 0;
while ((pos = result.find("$g", pos)) != std::string::npos) {
size_t endPos = result.find(';', pos);
if (endPos == std::string::npos) break;
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
// Split by colons
std::vector<std::string> parts;
size_t start = 0;
size_t colonPos;
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
std::string part = placeholder.substr(start, colonPos - start);
trim(part);
parts.push_back(part);
start = colonPos + 1;
}
// Add the last part
std::string lastPart = placeholder.substr(start);
trim(lastPart);
parts.push_back(lastPart);
// Select appropriate text based on gender
std::string replacement;
if (parts.size() >= 3) {
// Three options: male, female, nonbinary
switch (gender) {
case game::Gender::MALE:
replacement = parts[0];
break;
case game::Gender::FEMALE:
replacement = parts[1];
break;
case game::Gender::NONBINARY:
replacement = parts[2];
break;
}
} else if (parts.size() >= 2) {
// Two options: male, female (use first for nonbinary)
switch (gender) {
case game::Gender::MALE:
replacement = parts[0];
break;
case game::Gender::FEMALE:
replacement = parts[1];
break;
case game::Gender::NONBINARY:
// Default to gender-neutral: use the shorter/simpler option
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
break;
}
} else {
// Malformed placeholder
pos = endPos + 1;
continue;
}
result.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
return result;
}
void GameScreen::saveSettings() {
std::string path = getSettingsPath();
std::filesystem::path dir = std::filesystem::path(path).parent_path();
@ -4585,8 +4774,28 @@ void GameScreen::saveSettings() {
return;
}
// Interface
out << "ui_opacity=" << pendingUiOpacity << "\n";
out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n";
out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n";
// Audio
out << "master_volume=" << pendingMasterVolume << "\n";
out << "music_volume=" << pendingMusicVolume << "\n";
out << "ambient_volume=" << pendingAmbientVolume << "\n";
out << "ui_volume=" << pendingUiVolume << "\n";
out << "combat_volume=" << pendingCombatVolume << "\n";
out << "spell_volume=" << pendingSpellVolume << "\n";
out << "movement_volume=" << pendingMovementVolume << "\n";
out << "footstep_volume=" << pendingFootstepVolume << "\n";
out << "npc_voice_volume=" << pendingNpcVoiceVolume << "\n";
out << "mount_volume=" << pendingMountVolume << "\n";
out << "activity_volume=" << pendingActivityVolume << "\n";
// Controls
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n";
LOG_INFO("Settings saved to ", path);
}
@ -4602,21 +4811,39 @@ void GameScreen::loadSettings() {
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
if (key == "ui_opacity") {
try {
try {
// Interface
if (key == "ui_opacity") {
int v = std::stoi(val);
if (v >= 20 && v <= 100) {
pendingUiOpacity = v;
uiOpacity_ = static_cast<float>(v) / 100.0f;
}
} catch (...) {}
} else if (key == "minimap_rotate") {
try {
} else if (key == "minimap_rotate") {
int v = std::stoi(val);
minimapRotate_ = (v != 0);
pendingMinimapRotate = minimapRotate_;
} catch (...) {}
}
} else if (key == "minimap_square") {
int v = std::stoi(val);
minimapSquare_ = (v != 0);
pendingMinimapSquare = minimapSquare_;
}
// Audio
else if (key == "master_volume") pendingMasterVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "music_volume") pendingMusicVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "ambient_volume") pendingAmbientVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "ui_volume") pendingUiVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "combat_volume") pendingCombatVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "spell_volume") pendingSpellVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "movement_volume") pendingMovementVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "footstep_volume") pendingFootstepVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "npc_voice_volume") pendingNpcVoiceVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "mount_volume") pendingMountVolume = std::clamp(std::stoi(val), 0, 100);
else if (key == "activity_volume") pendingActivityVolume = std::clamp(std::stoi(val), 0, 100);
// Controls
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
} catch (...) {}
}
LOG_INFO("Settings loaded from ", path);
}

View file

@ -5,6 +5,93 @@
namespace wowee { namespace ui {
namespace {
// Helper function to replace gender placeholders and pronouns
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler) {
game::Gender gender = game::Gender::NONBINARY;
const auto* character = gameHandler.getActiveCharacter();
if (character) {
gender = character->gender;
}
game::Pronouns pronouns = game::Pronouns::forGender(gender);
std::string result = text;
auto trim = [](std::string& s) {
s.erase(0, s.find_first_not_of(" \t\n\r"));
s.erase(s.find_last_not_of(" \t\n\r") + 1);
};
// Replace pronoun placeholders
size_t pos = 0;
while ((pos = result.find('$', pos)) != std::string::npos) {
if (pos + 1 >= result.length()) break;
char code = result[pos + 1];
std::string replacement;
switch (code) {
case 'p': replacement = pronouns.subject; break;
case 'o': replacement = pronouns.object; break;
case 's': replacement = pronouns.possessive; break;
case 'S': replacement = pronouns.possessiveP; break;
case 'g': pos++; continue;
default: pos++; continue;
}
result.replace(pos, 2, replacement);
pos += replacement.length();
}
// Replace $g placeholders
pos = 0;
while ((pos = result.find("$g", pos)) != std::string::npos) {
size_t endPos = result.find(';', pos);
if (endPos == std::string::npos) break;
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
std::vector<std::string> parts;
size_t start = 0;
size_t colonPos;
while ((colonPos = placeholder.find(':', start)) != std::string::npos) {
std::string part = placeholder.substr(start, colonPos - start);
trim(part);
parts.push_back(part);
start = colonPos + 1;
}
std::string lastPart = placeholder.substr(start);
trim(lastPart);
parts.push_back(lastPart);
std::string replacement;
if (parts.size() >= 3) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY: replacement = parts[2]; break;
}
} else if (parts.size() >= 2) {
switch (gender) {
case game::Gender::MALE: replacement = parts[0]; break;
case game::Gender::FEMALE: replacement = parts[1]; break;
case game::Gender::NONBINARY:
replacement = parts[0].length() <= parts[1].length() ? parts[0] : parts[1];
break;
}
} else {
pos = endPos + 1;
continue;
}
result.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
return result;
}
} // anonymous namespace
void QuestLogScreen::render(game::GameHandler& gameHandler) {
// L key toggle (edge-triggered)
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
@ -64,7 +151,8 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
if (!sel.objectives.empty()) {
ImGui::Separator();
ImGui::TextWrapped("%s", sel.objectives.c_str());
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
ImGui::TextWrapped("%s", processedObjectives.c_str());
}
// Abandon button