mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
28aa88608f
commit
0071c24713
10 changed files with 421 additions and 32 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue