diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dffbcdc..e1113df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ set(WOWEE_SOURCES src/ui/ui_manager.cpp src/ui/auth_screen.cpp src/ui/realm_screen.cpp + src/ui/character_create_screen.cpp src/ui/character_screen.cpp src/ui/game_screen.cpp src/ui/inventory_screen.cpp @@ -226,6 +227,7 @@ set(WOWEE_HEADERS include/ui/ui_manager.hpp include/ui/auth_screen.hpp include/ui/realm_screen.hpp + include/ui/character_create_screen.hpp include/ui/character_screen.hpp include/ui/game_screen.hpp include/ui/inventory_screen.hpp diff --git a/include/core/application.hpp b/include/core/application.hpp index 2acd7c20..e32103b8 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -20,6 +20,7 @@ namespace core { enum class AppState { AUTHENTICATION, REALM_SELECTION, + CHARACTER_CREATION, CHARACTER_SELECTION, IN_GAME, DISCONNECTED diff --git a/include/game/character.hpp b/include/game/character.hpp index 995c0a27..705f81a2 100644 --- a/include/game/character.hpp +++ b/include/game/character.hpp @@ -110,6 +110,14 @@ struct Character { bool hasPet() const { return pet.exists(); } }; +// Race/class combo and appearance range validation (WoW 3.3.5a) +bool isValidRaceClassCombo(Race race, Class cls); +uint8_t getMaxSkin(Race race, Gender gender); +uint8_t getMaxFace(Race race, Gender gender); +uint8_t getMaxHairStyle(Race race, Gender gender); +uint8_t getMaxHairColor(Race race, Gender gender); +uint8_t getMaxFacialFeature(Race race, Gender gender); + /** * Get human-readable race name */ diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 8834d407..81218c8a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -102,6 +102,11 @@ public: */ const std::vector& getCharacters() const { return characters; } + void createCharacter(const CharCreateData& data); + + using CharCreateCallback = std::function; + void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); } + /** * Select and log in with a character * @param characterGuid GUID of character to log in with @@ -210,6 +215,7 @@ public: // Single-player mode void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; } bool isSinglePlayerMode() const { return singlePlayerMode_; } + void simulateMotd(const std::vector& lines); // NPC death callback (single-player) using NpcDeathCallback = std::function; @@ -375,6 +381,9 @@ private: void handleGroupUninvite(network::Packet& packet); void handlePartyCommandResult(network::Packet& packet); + // ---- Character creation handler ---- + void handleCharCreateResponse(network::Packet& packet); + // ---- XP handler ---- void handleXpGain(network::Packet& packet); @@ -515,6 +524,7 @@ private: // Callbacks WorldConnectSuccessCallback onSuccess; WorldConnectFailureCallback onFailure; + CharCreateCallback charCreateCallback_; // ---- XP tracking ---- uint32_t playerXp_ = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 3c78809c..5d31dbb6 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -12,6 +12,7 @@ enum class Opcode : uint16_t { // ---- Client to Server (Core) ---- CMSG_PING = 0x1DC, CMSG_AUTH_SESSION = 0x1ED, + CMSG_CHAR_CREATE = 0x036, CMSG_CHAR_ENUM = 0x037, CMSG_PLAYER_LOGIN = 0x03D, @@ -35,6 +36,7 @@ enum class Opcode : uint16_t { // ---- Server to Client (Core) ---- SMSG_AUTH_CHALLENGE = 0x1EC, SMSG_AUTH_RESPONSE = 0x1EE, + SMSG_CHAR_CREATE = 0x03A, SMSG_CHAR_ENUM = 0x03B, SMSG_PONG = 0x1DD, SMSG_LOGIN_VERIFY_WORLD = 0x236, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 6956b648..178e7237 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2,6 +2,7 @@ #include "network/packet.hpp" #include "game/opcodes.hpp" +#include "game/character.hpp" #include "game/entity.hpp" #include "game/spell_defines.hpp" #include "game/group_defines.hpp" @@ -167,6 +168,47 @@ public: static bool parse(network::Packet& packet, CharEnumResponse& response); }; +// ============================================================ +// Character Creation +// ============================================================ + +enum class CharCreateResult : uint8_t { + SUCCESS = 0x00, + ERROR = 0x01, + FAILED = 0x02, + NAME_IN_USE = 0x03, + DISABLED = 0x04, + PVP_TEAMS_VIOLATION = 0x05, + SERVER_LIMIT = 0x06, + ACCOUNT_LIMIT = 0x07, +}; + +struct CharCreateData { + std::string name; + Race race; + Class characterClass; + Gender gender; + uint8_t skin = 0; + uint8_t face = 0; + uint8_t hairStyle = 0; + uint8_t hairColor = 0; + uint8_t facialHair = 0; +}; + +class CharCreatePacket { +public: + static network::Packet build(const CharCreateData& data); +}; + +struct CharCreateResponseData { + CharCreateResult result; +}; + +class CharCreateResponseParser { +public: + static bool parse(network::Packet& packet, CharCreateResponseData& data); +}; + /** * CMSG_PLAYER_LOGIN packet builder * diff --git a/include/ui/character_create_screen.hpp b/include/ui/character_create_screen.hpp new file mode 100644 index 00000000..2a4668fd --- /dev/null +++ b/include/ui/character_create_screen.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "game/character.hpp" +#include "game/world_packets.hpp" +#include +#include +#include +#include + +namespace wowee { +namespace game { class GameHandler; } + +namespace ui { + +class CharacterCreateScreen { +public: + CharacterCreateScreen(); + + void render(game::GameHandler& gameHandler); + void setOnCreate(std::function cb) { onCreate = std::move(cb); } + void setOnCancel(std::function cb) { onCancel = std::move(cb); } + void setStatus(const std::string& msg, bool isError = false); + void reset(); + +private: + char nameBuffer[13] = {}; // WoW max name = 12 chars + null + int raceIndex = 0; + int classIndex = 0; + int genderIndex = 0; + int skin = 0, face = 0, hairStyle = 0, hairColor = 0, facialHair = 0; + std::string statusMessage; + bool statusIsError = false; + + std::vector availableClasses; + void updateAvailableClasses(); + + std::function onCreate; + std::function onCancel; +}; + +} // namespace ui +} // namespace wowee diff --git a/include/ui/character_screen.hpp b/include/ui/character_screen.hpp index c22fc51f..3cb038a9 100644 --- a/include/ui/character_screen.hpp +++ b/include/ui/character_screen.hpp @@ -30,6 +30,8 @@ public: onCharacterSelected = callback; } + void setOnCreateCharacter(std::function cb) { onCreateCharacter = std::move(cb); } + /** * Check if a character has been selected */ @@ -51,6 +53,7 @@ private: // Callbacks std::function onCharacterSelected; + std::function onCreateCharacter; /** * Update status message diff --git a/include/ui/ui_manager.hpp b/include/ui/ui_manager.hpp index ddb2be98..ba1e4c09 100644 --- a/include/ui/ui_manager.hpp +++ b/include/ui/ui_manager.hpp @@ -2,6 +2,7 @@ #include "ui/auth_screen.hpp" #include "ui/realm_screen.hpp" +#include "ui/character_create_screen.hpp" #include "ui/character_screen.hpp" #include "ui/game_screen.hpp" #include @@ -64,6 +65,7 @@ public: */ AuthScreen& getAuthScreen() { return *authScreen; } RealmScreen& getRealmScreen() { return *realmScreen; } + CharacterCreateScreen& getCharacterCreateScreen() { return *characterCreateScreen; } CharacterScreen& getCharacterScreen() { return *characterScreen; } GameScreen& getGameScreen() { return *gameScreen; } @@ -73,6 +75,7 @@ private: // UI Screens std::unique_ptr authScreen; std::unique_ptr realmScreen; + std::unique_ptr characterCreateScreen; std::unique_ptr characterScreen; std::unique_ptr gameScreen; diff --git a/src/core/application.cpp b/src/core/application.cpp index 31b053cd..7a12d557 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -296,6 +296,9 @@ void Application::setState(AppState newState) { case AppState::REALM_SELECTION: // Show realm screen break; + case AppState::CHARACTER_CREATION: + // Show character create screen + break; case AppState::CHARACTER_SELECTION: // Show character screen break; @@ -340,6 +343,10 @@ void Application::update(float deltaTime) { } break; + case AppState::CHARACTER_CREATION: + // Character creation update + break; + case AppState::CHARACTER_SELECTION: // Character selection update break; @@ -437,10 +444,13 @@ void Application::setupUICallbacks() { setState(AppState::REALM_SELECTION); }); - // Single-player mode callback + // Single-player mode callback — go to character creation first uiManager->getAuthScreen().setOnSinglePlayer([this]() { - LOG_INFO("Single-player mode selected"); - startSinglePlayer(); + LOG_INFO("Single-player mode selected, opening character creation"); + singlePlayerMode = true; + gameHandler->setSinglePlayerMode(true); + uiManager->getCharacterCreateScreen().reset(); + setState(AppState::CHARACTER_CREATION); }); // Realm selection callback @@ -472,7 +482,54 @@ void Application::setupUICallbacks() { // Character selection callback uiManager->getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) { LOG_INFO("Character selected: GUID=0x", std::hex, characterGuid, std::dec); - setState(AppState::IN_GAME); + if (singlePlayerMode) { + // Use created character's data for level/HP + for (const auto& ch : gameHandler->getCharacters()) { + if (ch.guid == characterGuid) { + uint32_t maxHp = 20 + static_cast(ch.level) * 10; + gameHandler->initLocalPlayerStats(ch.level, maxHp, maxHp); + break; + } + } + startSinglePlayer(); + } else { + setState(AppState::IN_GAME); + } + }); + + // Character create screen callbacks + uiManager->getCharacterCreateScreen().setOnCreate([this](const game::CharCreateData& data) { + gameHandler->createCharacter(data); + }); + + uiManager->getCharacterCreateScreen().setOnCancel([this]() { + if (singlePlayerMode) { + setState(AppState::AUTHENTICATION); + singlePlayerMode = false; + gameHandler->setSinglePlayerMode(false); + } else { + setState(AppState::CHARACTER_SELECTION); + } + }); + + // Character create result callback + gameHandler->setCharCreateCallback([this](bool success, const std::string& msg) { + if (success) { + if (singlePlayerMode) { + // In single-player, go straight to character selection showing the new character + setState(AppState::CHARACTER_SELECTION); + } else { + setState(AppState::CHARACTER_SELECTION); + } + } else { + uiManager->getCharacterCreateScreen().setStatus(msg, true); + } + }); + + // "Create Character" button on character screen + uiManager->getCharacterScreen().setOnCreateCharacter([this]() { + uiManager->getCharacterCreateScreen().reset(); + setState(AppState::CHARACTER_CREATION); }); } @@ -919,9 +976,12 @@ void Application::startSinglePlayer() { // Enable single-player combat mode on game handler if (gameHandler) { gameHandler->setSinglePlayerMode(true); - uint32_t level = 10; - uint32_t maxHealth = 20 + level * 10; - gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth); + // Only init stats with defaults if not already set (e.g. via character creation) + if (gameHandler->getLocalPlayerMaxHealth() == 0) { + uint32_t level = 10; + uint32_t maxHealth = 20 + level * 10; + gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth); + } } // Create world object for single-player @@ -938,27 +998,6 @@ void Application::startSinglePlayer() { // Load weapon models for equipped items (after inventory is populated) loadEquippedWeapons(); - // Emulate server MOTD in single-player - if (gameHandler) { - std::vector motdLines; - if (const char* motdEnv = std::getenv("WOW_SP_MOTD")) { - std::string raw = motdEnv; - size_t start = 0; - while (start <= raw.size()) { - size_t pos = raw.find('|', start); - if (pos == std::string::npos) pos = raw.size(); - std::string line = raw.substr(start, pos - start); - if (!line.empty()) motdLines.push_back(line); - start = pos + 1; - if (pos == raw.size()) break; - } - } - if (motdLines.empty()) { - motdLines.push_back("Wowee Single Player"); - } - gameHandler->simulateMotd(motdLines); - } - // --- Loading screen: load terrain and wait for streaming before spawning --- const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN")); // Canonical WoW coords: +X=North, +Y=West, +Z=Up @@ -1099,6 +1138,26 @@ void Application::startSinglePlayer() { // Go directly to game setState(AppState::IN_GAME); + // Emulate server MOTD in single-player (after entering game) + if (gameHandler) { + std::vector motdLines; + if (const char* motdEnv = std::getenv("WOW_SP_MOTD")) { + std::string raw = motdEnv; + size_t start = 0; + while (start <= raw.size()) { + size_t pos = raw.find('|', start); + if (pos == std::string::npos) pos = raw.size(); + std::string line = raw.substr(start, pos - start); + if (!line.empty()) motdLines.push_back(line); + start = pos + 1; + if (pos == raw.size()) break; + } + } + if (motdLines.empty()) { + motdLines.push_back("Wowee Single Player"); + } + gameHandler->simulateMotd(motdLines); + } LOG_INFO("Single-player mode started - press F1 for performance HUD"); } diff --git a/src/game/character.cpp b/src/game/character.cpp index 88bf78f6..dc9908b4 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -3,6 +3,54 @@ namespace wowee { namespace game { +bool isValidRaceClassCombo(Race race, Class cls) { + // WoW 3.3.5a valid race/class combinations + switch (race) { + case Race::HUMAN: + return cls == Class::WARRIOR || cls == Class::PALADIN || cls == Class::ROGUE || + cls == Class::PRIEST || cls == Class::MAGE || cls == Class::WARLOCK || + cls == Class::DEATH_KNIGHT; + case Race::ORC: + return cls == Class::WARRIOR || cls == Class::HUNTER || cls == Class::ROGUE || + cls == Class::SHAMAN || cls == Class::WARLOCK || cls == Class::DEATH_KNIGHT; + case Race::DWARF: + return cls == Class::WARRIOR || cls == Class::PALADIN || cls == Class::HUNTER || + cls == Class::ROGUE || cls == Class::PRIEST || cls == Class::DEATH_KNIGHT; + case Race::NIGHT_ELF: + return cls == Class::WARRIOR || cls == Class::HUNTER || cls == Class::ROGUE || + cls == Class::PRIEST || cls == Class::DRUID || cls == Class::DEATH_KNIGHT; + case Race::UNDEAD: + return cls == Class::WARRIOR || cls == Class::ROGUE || cls == Class::PRIEST || + cls == Class::MAGE || cls == Class::WARLOCK || cls == Class::DEATH_KNIGHT; + case Race::TAUREN: + return cls == Class::WARRIOR || cls == Class::HUNTER || cls == Class::DRUID || + cls == Class::SHAMAN || cls == Class::DEATH_KNIGHT; + case Race::GNOME: + return cls == Class::WARRIOR || cls == Class::ROGUE || cls == Class::MAGE || + cls == Class::WARLOCK || cls == Class::DEATH_KNIGHT; + case Race::TROLL: + return cls == Class::WARRIOR || cls == Class::HUNTER || cls == Class::ROGUE || + cls == Class::PRIEST || cls == Class::SHAMAN || cls == Class::MAGE || + cls == Class::DEATH_KNIGHT; + case Race::BLOOD_ELF: + return cls == Class::PALADIN || cls == Class::HUNTER || cls == Class::ROGUE || + cls == Class::PRIEST || cls == Class::MAGE || cls == Class::WARLOCK || + cls == Class::DEATH_KNIGHT; + case Race::DRAENEI: + return cls == Class::WARRIOR || cls == Class::PALADIN || cls == Class::HUNTER || + cls == Class::PRIEST || cls == Class::SHAMAN || cls == Class::MAGE || + cls == Class::DEATH_KNIGHT; + default: + return false; + } +} + +uint8_t getMaxSkin(Race /*race*/, Gender /*gender*/) { return 9; } +uint8_t getMaxFace(Race /*race*/, Gender /*gender*/) { return 9; } +uint8_t getMaxHairStyle(Race /*race*/, Gender /*gender*/) { return 11; } +uint8_t getMaxHairColor(Race /*race*/, Gender /*gender*/) { return 9; } +uint8_t getMaxFacialFeature(Race /*race*/, Gender /*gender*/) { return 8; } + const char* getRaceName(Race race) { switch (race) { case Race::HUMAN: return "Human"; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4c615881..c25fab61 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -501,6 +501,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_CHAR_CREATE: + handleCharCreateResponse(packet); + break; + case Opcode::SMSG_CHAR_ENUM: if (state == WorldState::CHAR_LIST_REQUESTED) { handleCharEnum(packet); @@ -822,6 +826,81 @@ void GameHandler::handleCharEnum(network::Packet& packet) { LOG_INFO("Ready to select character"); } +void GameHandler::createCharacter(const CharCreateData& data) { + if (singlePlayerMode_) { + // Create character locally + Character ch; + ch.guid = 0x0000000100000001ULL + characters.size(); + ch.name = data.name; + ch.race = data.race; + ch.characterClass = data.characterClass; + ch.gender = data.gender; + ch.level = 1; + ch.appearanceBytes = (static_cast(data.skin)) | + (static_cast(data.face) << 8) | + (static_cast(data.hairStyle) << 16) | + (static_cast(data.hairColor) << 24); + ch.facialFeatures = data.facialHair; + ch.zoneId = 12; // Elwynn Forest default + ch.mapId = 0; + ch.x = -8949.95f; + ch.y = -132.493f; + ch.z = 83.5312f; + ch.guildId = 0; + ch.flags = 0; + ch.pet = {}; + characters.push_back(ch); + + LOG_INFO("Single-player character created: ", ch.name); + if (charCreateCallback_) { + charCreateCallback_(true, "Character created!"); + } + return; + } + + // Online mode: send packet to server + if (!socket) { + LOG_WARNING("Cannot create character: not connected"); + if (charCreateCallback_) { + charCreateCallback_(false, "Not connected to server"); + } + return; + } + + auto packet = CharCreatePacket::build(data); + socket->send(packet); + LOG_INFO("CMSG_CHAR_CREATE sent for: ", data.name); +} + +void GameHandler::handleCharCreateResponse(network::Packet& packet) { + CharCreateResponseData data; + if (!CharCreateResponseParser::parse(packet, data)) { + LOG_ERROR("Failed to parse SMSG_CHAR_CREATE"); + return; + } + + if (data.result == CharCreateResult::SUCCESS) { + LOG_INFO("Character created successfully"); + requestCharacterList(); + if (charCreateCallback_) { + charCreateCallback_(true, "Character created!"); + } + } else { + std::string msg; + switch (data.result) { + case CharCreateResult::NAME_IN_USE: msg = "Name already in use"; break; + case CharCreateResult::DISABLED: msg = "Character creation disabled"; break; + case CharCreateResult::SERVER_LIMIT: msg = "Server character limit reached"; break; + case CharCreateResult::ACCOUNT_LIMIT: msg = "Account character limit reached"; break; + default: msg = "Character creation failed"; break; + } + LOG_WARNING("Character creation failed: ", msg); + if (charCreateCallback_) { + charCreateCallback_(false, msg); + } + } +} + void GameHandler::selectCharacter(uint64_t characterGuid) { if (state != WorldState::CHAR_LIST_RECEIVED) { LOG_WARNING("Cannot select character in state: ", (int)state); @@ -2489,6 +2568,15 @@ void GameHandler::simulateXpGain(uint64_t victimGuid, uint32_t totalXp) { handleXpGain(packet); } +void GameHandler::simulateMotd(const std::vector& lines) { + network::Packet packet(static_cast(Opcode::SMSG_MOTD)); + packet.writeUInt32(static_cast(lines.size())); + for (const auto& line : lines) { + packet.writeString(line); + } + handleMotd(packet); +} + void GameHandler::addMoneyCopper(uint32_t amount) { if (amount == 0) return; playerMoneyCopper_ += amount; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 1ab3011d..c7229515 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -202,6 +202,35 @@ const char* getAuthResultString(AuthResult result) { } } +// ============================================================ +// Character Creation +// ============================================================ + +network::Packet CharCreatePacket::build(const CharCreateData& data) { + network::Packet packet(static_cast(Opcode::CMSG_CHAR_CREATE)); + + 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(data.skin); + packet.writeUInt8(data.face); + packet.writeUInt8(data.hairStyle); + packet.writeUInt8(data.hairColor); + packet.writeUInt8(data.facialHair); + packet.writeUInt8(0); // outfitId, always 0 + + LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name); + + return packet; +} + +bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponseData& data) { + data.result = static_cast(packet.readUInt8()); + LOG_INFO("SMSG_CHAR_CREATE result: ", static_cast(data.result)); + return true; +} + network::Packet CharEnumPacket::build() { // CMSG_CHAR_ENUM has no body - just the opcode network::Packet packet(static_cast(Opcode::CMSG_CHAR_ENUM)); diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp new file mode 100644 index 00000000..2cb349ba --- /dev/null +++ b/src/ui/character_create_screen.cpp @@ -0,0 +1,226 @@ +#include "ui/character_create_screen.hpp" +#include "game/game_handler.hpp" +#include +#include + +namespace wowee { +namespace ui { + +static const game::Race allRaces[] = { + // Alliance + game::Race::HUMAN, game::Race::DWARF, game::Race::NIGHT_ELF, + game::Race::GNOME, game::Race::DRAENEI, + // Horde + game::Race::ORC, game::Race::UNDEAD, game::Race::TAUREN, + game::Race::TROLL, game::Race::BLOOD_ELF, +}; +static constexpr int RACE_COUNT = 10; +static constexpr int ALLIANCE_COUNT = 5; + +static const game::Class allClasses[] = { + game::Class::WARRIOR, game::Class::PALADIN, game::Class::HUNTER, + game::Class::ROGUE, game::Class::PRIEST, game::Class::DEATH_KNIGHT, + game::Class::SHAMAN, game::Class::MAGE, game::Class::WARLOCK, + game::Class::DRUID, +}; + +CharacterCreateScreen::CharacterCreateScreen() { + reset(); +} + +void CharacterCreateScreen::reset() { + std::memset(nameBuffer, 0, sizeof(nameBuffer)); + raceIndex = 0; + classIndex = 0; + genderIndex = 0; + skin = 0; + face = 0; + hairStyle = 0; + hairColor = 0; + facialHair = 0; + statusMessage.clear(); + statusIsError = false; + updateAvailableClasses(); +} + +void CharacterCreateScreen::setStatus(const std::string& msg, bool isError) { + statusMessage = msg; + statusIsError = isError; +} + +void CharacterCreateScreen::updateAvailableClasses() { + availableClasses.clear(); + game::Race race = allRaces[raceIndex]; + for (auto cls : allClasses) { + if (game::isValidRaceClassCombo(race, cls)) { + availableClasses.push_back(cls); + } + } + // Clamp class index + if (classIndex >= static_cast(availableClasses.size())) { + classIndex = 0; + } +} + +void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + ImVec2 winSize(600, 520); + ImGui::SetNextWindowSize(winSize, ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2((displaySize.x - winSize.x) * 0.5f, + (displaySize.y - winSize.y) * 0.5f), + ImGuiCond_FirstUseEver); + + ImGui::Begin("Create Character", nullptr, ImGuiWindowFlags_NoCollapse); + + ImGui::Text("Create Character"); + ImGui::Separator(); + ImGui::Spacing(); + + // Name input + ImGui::Text("Name:"); + ImGui::SameLine(100); + ImGui::SetNextItemWidth(200); + ImGui::InputText("##name", nameBuffer, sizeof(nameBuffer)); + + ImGui::Spacing(); + + // Race selection + ImGui::Text("Race:"); + ImGui::SameLine(100); + ImGui::BeginGroup(); + ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "Alliance:"); + ImGui::SameLine(); + for (int i = 0; i < ALLIANCE_COUNT; ++i) { + if (i > 0) ImGui::SameLine(); + bool selected = (raceIndex == i); + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 1.0f, 0.8f)); + if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) { + if (raceIndex != i) { + raceIndex = i; + classIndex = 0; + skin = face = hairStyle = hairColor = facialHair = 0; + updateAvailableClasses(); + } + } + if (selected) ImGui::PopStyleColor(); + } + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:"); + ImGui::SameLine(); + for (int i = ALLIANCE_COUNT; i < RACE_COUNT; ++i) { + if (i > ALLIANCE_COUNT) ImGui::SameLine(); + bool selected = (raceIndex == i); + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.3f, 0.3f, 0.8f)); + if (ImGui::SmallButton(game::getRaceName(allRaces[i]))) { + if (raceIndex != i) { + raceIndex = i; + classIndex = 0; + skin = face = hairStyle = hairColor = facialHair = 0; + updateAvailableClasses(); + } + } + if (selected) ImGui::PopStyleColor(); + } + ImGui::EndGroup(); + + ImGui::Spacing(); + + // Class selection + ImGui::Text("Class:"); + ImGui::SameLine(100); + if (!availableClasses.empty()) { + ImGui::BeginGroup(); + for (int i = 0; i < static_cast(availableClasses.size()); ++i) { + if (i > 0) ImGui::SameLine(); + bool selected = (classIndex == i); + if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 0.8f)); + if (ImGui::SmallButton(game::getClassName(availableClasses[i]))) { + classIndex = i; + } + if (selected) ImGui::PopStyleColor(); + } + ImGui::EndGroup(); + } + + ImGui::Spacing(); + + // Gender + ImGui::Text("Gender:"); + ImGui::SameLine(100); + ImGui::RadioButton("Male", &genderIndex, 0); + ImGui::SameLine(); + ImGui::RadioButton("Female", &genderIndex, 1); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Appearance sliders + game::Race currentRace = allRaces[raceIndex]; + game::Gender currentGender = static_cast(genderIndex); + + ImGui::Text("Appearance"); + ImGui::Spacing(); + + auto slider = [](const char* label, int* val, int maxVal) { + ImGui::Text("%s", label); + ImGui::SameLine(120); + ImGui::SetNextItemWidth(200); + char id[32]; + snprintf(id, sizeof(id), "##%s", label); + ImGui::SliderInt(id, val, 0, maxVal); + }; + + slider("Skin", &skin, game::getMaxSkin(currentRace, currentGender)); + slider("Face", &face, game::getMaxFace(currentRace, currentGender)); + slider("Hair Style", &hairStyle, game::getMaxHairStyle(currentRace, currentGender)); + slider("Hair Color", &hairColor, game::getMaxHairColor(currentRace, currentGender)); + slider("Facial Feature", &facialHair, game::getMaxFacialFeature(currentRace, currentGender)); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Status message + if (!statusMessage.empty()) { + ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + ImGui::TextColored(color, "%s", statusMessage.c_str()); + ImGui::Spacing(); + } + + // Buttons + if (ImGui::Button("Create", ImVec2(150, 35))) { + std::string name(nameBuffer); + if (name.empty()) { + setStatus("Please enter a character name.", true); + } else if (availableClasses.empty()) { + setStatus("No valid class for this race.", true); + } else { + game::CharCreateData data; + data.name = name; + data.race = allRaces[raceIndex]; + data.characterClass = availableClasses[classIndex]; + data.gender = currentGender; + data.skin = static_cast(skin); + data.face = static_cast(face); + data.hairStyle = static_cast(hairStyle); + data.hairColor = static_cast(hairColor); + data.facialHair = static_cast(facialHair); + if (onCreate) { + onCreate(data); + } + } + } + + ImGui::SameLine(); + + if (ImGui::Button("Back", ImVec2(150, 35))) { + if (onCancel) { + onCancel(); + } + } + + ImGui::End(); +} + +} // namespace ui +} // namespace wowee diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index da6c4a9a..d4a3fb58 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -148,7 +148,10 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ss << "Entering world with " << character.name << "..."; setStatus(ss.str()); - gameHandler.selectCharacter(character.guid); + // Only send CMSG_PLAYER_LOGIN in online mode + if (!gameHandler.isSinglePlayerMode()) { + gameHandler.selectCharacter(character.guid); + } // Call callback if (onCharacterSelected) { @@ -169,7 +172,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::Separator(); ImGui::Spacing(); - // Back/Refresh buttons + // Back/Refresh/Create buttons if (ImGui::Button("Refresh", ImVec2(120, 0))) { if (gameHandler.getState() == game::WorldState::READY || gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) { @@ -178,6 +181,14 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { } } + ImGui::SameLine(); + + if (ImGui::Button("Create Character", ImVec2(150, 0))) { + if (onCreateCharacter) { + onCreateCharacter(); + } + } + ImGui::End(); } diff --git a/src/ui/ui_manager.cpp b/src/ui/ui_manager.cpp index 78c0d7a2..bedfcd27 100644 --- a/src/ui/ui_manager.cpp +++ b/src/ui/ui_manager.cpp @@ -15,6 +15,7 @@ UIManager::UIManager() { // Create screen instances authScreen = std::make_unique(); realmScreen = std::make_unique(); + characterCreateScreen = std::make_unique(); characterScreen = std::make_unique(); gameScreen = std::make_unique(); } @@ -101,6 +102,12 @@ void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler, } break; + case core::AppState::CHARACTER_CREATION: + if (gameHandler) { + characterCreateScreen->render(*gameHandler); + } + break; + case core::AppState::CHARACTER_SELECTION: if (gameHandler) { characterScreen->render(*gameHandler);