feat: implement barber shop UI with hair/facial customization

Adds a functional barber shop window triggered by SMSG_ENABLE_BARBER_SHOP.
Players can adjust hair style, hair color, and facial features using
sliders bounded by race/gender max values. Sends CMSG_ALTER_APPEARANCE
on confirm; server result closes the window on success. Escape key
also closes the barber shop.
This commit is contained in:
Kelsi 2026-03-18 11:58:01 -07:00
parent 8dfd916fe4
commit 64fd7eddf8
6 changed files with 160 additions and 1 deletions

View file

@ -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;

View file

@ -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

View file

@ -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] = "";

View file

@ -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
// ============================================================

View file

@ -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

View file

@ -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<uint8_t>(ch->race);
game::Gender gender = ch->gender;
game::Race raceEnum = ch->race;
// Initialize sliders from current appearance
if (!barberInitialized_) {
barberOrigHairStyle_ = static_cast<int>((ch->appearanceBytes >> 16) & 0xFF);
barberOrigHairColor_ = static_cast<int>((ch->appearanceBytes >> 24) & 0xFF);
barberOrigFacialHair_ = static_cast<int>(ch->facialFeatures);
barberHairStyle_ = barberOrigHairStyle_;
barberHairColor_ = barberOrigHairColor_;
barberFacialHair_ = barberOrigFacialHair_;
barberInitialized_ = true;
}
int maxHairStyle = static_cast<int>(game::getMaxHairStyle(raceEnum, gender));
int maxHairColor = static_cast<int>(game::getMaxHairColor(raceEnum, gender));
int maxFacialHair = static_cast<int>(game::getMaxFacialFeature(raceEnum, gender));
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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<uint32_t>(barberHairStyle_),
static_cast<uint32_t>(barberHairColor_),
static_cast<uint32_t>(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
// ============================================================