From 0071c24713582dea4ed3e5b9e6350e75fcb36b62 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 17:39:21 -0800 Subject: [PATCH] Add nonbinary gender support with pronoun system and server compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::;) for inclusive quest and dialog text. --- include/game/character.hpp | 34 +++- include/rendering/minimap.hpp | 8 + include/ui/game_screen.hpp | 6 + src/game/character.cpp | 24 +-- src/game/game_handler.cpp | 15 ++ src/game/world_packets.cpp | 6 +- src/rendering/minimap.cpp | 11 +- src/ui/character_create_screen.cpp | 2 + src/ui/game_screen.cpp | 257 +++++++++++++++++++++++++++-- src/ui/quest_log_screen.cpp | 90 +++++++++- 10 files changed, 421 insertions(+), 32 deletions(-) diff --git a/include/game/character.hpp b/include/game/character.hpp index ca2fe671..0b0b4632 100644 --- a/include/game/character.hpp +++ b/include/game/character.hpp @@ -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 */ diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp index f0adfaa5..06f1d765 100644 --- a/include/rendering/minimap.hpp +++ b/include/rendering/minimap.hpp @@ -6,6 +6,7 @@ #include #include #include +#include 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; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index a8303a44..49946fe3 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -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; diff --git a/src/game/character.cpp b/src/game/character.cpp index fd786e55..611ec5c0 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -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: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 99bf2027..ec66ebfc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5924,6 +5924,7 @@ void GameHandler::saveCharacterConfig() { } out << "character_guid=" << playerGuid << "\n"; + out << "gender=" << static_cast(ch->gender) << "\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"; @@ -5943,6 +5944,7 @@ void GameHandler::loadCharacterConfig() { std::array types{}; std::array 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(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(types[i]); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 67cb593c..2f5659f9 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -257,10 +257,13 @@ const char* getAuthResultString(AuthResult result) { network::Packet CharCreatePacket::build(const CharCreateData& data) { network::Packet packet(static_cast(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(data.race)); packet.writeUInt8(static_cast(data.characterClass)); - packet.writeUInt8(static_cast(data.gender)); + packet.writeUInt8(static_cast(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(data.race), " class=", static_cast(data.characterClass), " gender=", static_cast(data.gender), + " (server gender=", static_cast(serverGender), ")", " skin=", static_cast(data.skin), " face=", static_cast(data.face), " hair=", static_cast(data.hairStyle), diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 77c134e7..2143f226 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -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); diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index dacfbd01..5cb1ffd6 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -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(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 789774dd..5d3a9e46 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -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(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(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:; or $g::; + 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 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(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); } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index 14894d12..26282895 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -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 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