diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f21c0bdf..624b78ae 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1219,6 +1219,12 @@ public: uint32_t getPetUnlearnCost() const { return petUnlearnCost_; } void confirmPetUnlearn(); void cancelPetUnlearn() { petUnlearnPending_ = false; } + + // Barber shop + bool isBarberShopOpen() const { return barberShopOpen_; } + void closeBarberShop() { barberShopOpen_ = false; } + void sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); + /** True when ghost is within 40 yards of corpse position (same map). */ bool canReclaimCorpse() const; /** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */ @@ -2983,6 +2989,9 @@ private: uint64_t activeCharacterGuid_ = 0; Race playerRace_ = Race::HUMAN; + // Barber shop + bool barberShopOpen_ = false; + // ---- Phase 5: Loot ---- bool lootWindowOpen = false; bool autoLoot_ = false; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index e5492eb4..c0408743 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2796,5 +2796,12 @@ public: static network::Packet build(int32_t titleBit); }; +/** CMSG_ALTER_APPEARANCE – barber shop: change hair style, color, facial hair. + * Payload: uint32 hairStyle, uint32 hairColor, uint32 facialHair. */ +class AlterAppearancePacket { +public: + static network::Packet build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); +}; + } // namespace game } // namespace wowee diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index e9f4419a..72523cc6 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -366,6 +366,7 @@ private: void renderQuestOfferRewardWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); void renderTrainerWindow(game::GameHandler& gameHandler); + void renderBarberShopWindow(game::GameHandler& gameHandler); void renderStableWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); void renderLogoutCountdown(game::GameHandler& gameHandler); @@ -543,6 +544,15 @@ private: uint32_t vendorConfirmPrice_ = 0; std::string vendorConfirmItemName_; + // Barber shop UI state + int barberHairStyle_ = 0; + int barberHairColor_ = 0; + int barberFacialHair_ = 0; + int barberOrigHairStyle_ = 0; + int barberOrigHairColor_ = 0; + int barberOrigFacialHair_ = 0; + bool barberInitialized_ = false; + // Trainer search filter char trainerSearchFilter_[128] = ""; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5ffb2028..c831f1f3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2681,8 +2681,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_ENABLE_BARBER_SHOP: // Sent by server when player sits in barber chair — triggers barber shop UI - // No payload; we don't have barber shop UI yet, so just log LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); + barberShopOpen_ = true; break; case Opcode::SMSG_FEIGN_DEATH_RESISTED: addUIError("Your Feign Death was resisted."); @@ -4902,6 +4902,7 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t result = packet.readUInt32(); if (result == 0) { addSystemChatMessage("Hairstyle changed."); + barberShopOpen_ = false; } else { const char* msg = (result == 1) ? "Not enough money for new hairstyle." : (result == 2) ? "You are not at a barber shop." @@ -19180,6 +19181,13 @@ void GameHandler::confirmTalentWipe() { talentWipeCost_ = 0; } +void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { + if (state != WorldState::IN_WORLD || !socket) return; + auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair); + socket->send(pkt); + LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair); +} + // ============================================================ // Phase 4: Group/Party // ============================================================ diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 77ccf49c..e20c2d09 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -5878,5 +5878,14 @@ network::Packet SetTitlePacket::build(int32_t titleBit) { return p; } +network::Packet AlterAppearancePacket::build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { + // CMSG_ALTER_APPEARANCE: uint32 hairStyle + uint32 hairColor + uint32 facialHair + network::Packet p(wireOpcode(Opcode::CMSG_ALTER_APPEARANCE)); + p.writeUInt32(hairStyle); + p.writeUInt32(hairColor); + p.writeUInt32(facialHair); + return p; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 3f201d59..ef4081a1 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -733,6 +733,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestOfferRewardWindow(gameHandler); renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); + renderBarberShopWindow(gameHandler); renderStableWindow(gameHandler); renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); @@ -2763,6 +2764,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.closeGossip(); } else if (gameHandler.isVendorWindowOpen()) { gameHandler.closeVendor(); + } else if (gameHandler.isBarberShopOpen()) { + gameHandler.closeBarberShop(); } else if (gameHandler.isBankOpen()) { gameHandler.closeBank(); } else if (gameHandler.isTrainerWindowOpen()) { @@ -16806,6 +16809,119 @@ void GameScreen::renderEscapeMenu() { ImGui::End(); } +// ============================================================ +// Barber Shop Window +// ============================================================ + +void GameScreen::renderBarberShopWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isBarberShopOpen()) { + barberInitialized_ = false; + return; + } + + const auto* ch = gameHandler.getActiveCharacter(); + if (!ch) return; + + uint8_t race = static_cast(ch->race); + game::Gender gender = ch->gender; + game::Race raceEnum = ch->race; + + // Initialize sliders from current appearance + if (!barberInitialized_) { + barberOrigHairStyle_ = static_cast((ch->appearanceBytes >> 16) & 0xFF); + barberOrigHairColor_ = static_cast((ch->appearanceBytes >> 24) & 0xFF); + barberOrigFacialHair_ = static_cast(ch->facialFeatures); + barberHairStyle_ = barberOrigHairStyle_; + barberHairColor_ = barberOrigHairColor_; + barberFacialHair_ = barberOrigFacialHair_; + barberInitialized_ = true; + } + + int maxHairStyle = static_cast(game::getMaxHairStyle(raceEnum, gender)); + int maxHairColor = static_cast(game::getMaxHairColor(raceEnum, gender)); + int maxFacialHair = static_cast(game::getMaxFacialFeature(raceEnum, gender)); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + float winW = 300.0f; + float winH = 220.0f; + ImGui::SetNextWindowPos(ImVec2((screenW - winW) / 2.0f, (screenH - winH) / 2.0f), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Appearing); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; + bool open = true; + if (ImGui::Begin("Barber Shop", &open, flags)) { + ImGui::Text("Choose your new look:"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::PushItemWidth(-1); + + // Hair Style + ImGui::Text("Hair Style"); + ImGui::SliderInt("##HairStyle", &barberHairStyle_, 0, maxHairStyle, + "%d"); + + // Hair Color + ImGui::Text("Hair Color"); + ImGui::SliderInt("##HairColor", &barberHairColor_, 0, maxHairColor, + "%d"); + + // Facial Hair / Piercings / Markings + const char* facialLabel = (gender == game::Gender::FEMALE) ? "Piercings" : "Facial Hair"; + // Some races use "Markings" or "Tusks" etc. + if (race == 8 || race == 6) facialLabel = "Features"; // Trolls, Tauren + ImGui::Text("%s", facialLabel); + ImGui::SliderInt("##FacialHair", &barberFacialHair_, 0, maxFacialHair, + "%d"); + + ImGui::PopItemWidth(); + + ImGui::Spacing(); + ImGui::Separator(); + + // Show whether anything changed + bool changed = (barberHairStyle_ != barberOrigHairStyle_ || + barberHairColor_ != barberOrigHairColor_ || + barberFacialHair_ != barberOrigFacialHair_); + + // OK / Reset / Cancel buttons + float btnW = 80.0f; + float totalW = btnW * 3 + ImGui::GetStyle().ItemSpacing.x * 2; + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) / 2.0f); + + if (!changed) ImGui::BeginDisabled(); + if (ImGui::Button("OK", ImVec2(btnW, 0))) { + gameHandler.sendAlterAppearance( + static_cast(barberHairStyle_), + static_cast(barberHairColor_), + static_cast(barberFacialHair_)); + // Keep window open — server will respond with SMSG_BARBER_SHOP_RESULT + } + if (!changed) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (!changed) ImGui::BeginDisabled(); + if (ImGui::Button("Reset", ImVec2(btnW, 0))) { + barberHairStyle_ = barberOrigHairStyle_; + barberHairColor_ = barberOrigHairColor_; + barberFacialHair_ = barberOrigFacialHair_; + } + if (!changed) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(btnW, 0))) { + gameHandler.closeBarberShop(); + } + } + ImGui::End(); + + if (!open) { + gameHandler.closeBarberShop(); + } +} + // ============================================================ // Pet Stable Window // ============================================================