mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Add character creation screen with race/class/appearance customization
Implements a full character creation UI integrated into the existing flow. In single-player mode, auth screen now goes to character creation before entering the world. In online mode, a "Create Character" button on the character selection screen sends CMSG_CHAR_CREATE to the server. Includes WoW 3.3.5a race/class combo validation and appearance range limits.
This commit is contained in:
parent
f19c060078
commit
cf54db4554
16 changed files with 611 additions and 30 deletions
|
|
@ -142,6 +142,7 @@ set(WOWEE_SOURCES
|
||||||
src/ui/ui_manager.cpp
|
src/ui/ui_manager.cpp
|
||||||
src/ui/auth_screen.cpp
|
src/ui/auth_screen.cpp
|
||||||
src/ui/realm_screen.cpp
|
src/ui/realm_screen.cpp
|
||||||
|
src/ui/character_create_screen.cpp
|
||||||
src/ui/character_screen.cpp
|
src/ui/character_screen.cpp
|
||||||
src/ui/game_screen.cpp
|
src/ui/game_screen.cpp
|
||||||
src/ui/inventory_screen.cpp
|
src/ui/inventory_screen.cpp
|
||||||
|
|
@ -226,6 +227,7 @@ set(WOWEE_HEADERS
|
||||||
include/ui/ui_manager.hpp
|
include/ui/ui_manager.hpp
|
||||||
include/ui/auth_screen.hpp
|
include/ui/auth_screen.hpp
|
||||||
include/ui/realm_screen.hpp
|
include/ui/realm_screen.hpp
|
||||||
|
include/ui/character_create_screen.hpp
|
||||||
include/ui/character_screen.hpp
|
include/ui/character_screen.hpp
|
||||||
include/ui/game_screen.hpp
|
include/ui/game_screen.hpp
|
||||||
include/ui/inventory_screen.hpp
|
include/ui/inventory_screen.hpp
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ namespace core {
|
||||||
enum class AppState {
|
enum class AppState {
|
||||||
AUTHENTICATION,
|
AUTHENTICATION,
|
||||||
REALM_SELECTION,
|
REALM_SELECTION,
|
||||||
|
CHARACTER_CREATION,
|
||||||
CHARACTER_SELECTION,
|
CHARACTER_SELECTION,
|
||||||
IN_GAME,
|
IN_GAME,
|
||||||
DISCONNECTED
|
DISCONNECTED
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,14 @@ struct Character {
|
||||||
bool hasPet() const { return pet.exists(); }
|
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
|
* Get human-readable race name
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,11 @@ public:
|
||||||
*/
|
*/
|
||||||
const std::vector<Character>& getCharacters() const { return characters; }
|
const std::vector<Character>& getCharacters() const { return characters; }
|
||||||
|
|
||||||
|
void createCharacter(const CharCreateData& data);
|
||||||
|
|
||||||
|
using CharCreateCallback = std::function<void(bool success, const std::string& message)>;
|
||||||
|
void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select and log in with a character
|
* Select and log in with a character
|
||||||
* @param characterGuid GUID of character to log in with
|
* @param characterGuid GUID of character to log in with
|
||||||
|
|
@ -210,6 +215,7 @@ public:
|
||||||
// Single-player mode
|
// Single-player mode
|
||||||
void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; }
|
void setSinglePlayerMode(bool sp) { singlePlayerMode_ = sp; }
|
||||||
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
bool isSinglePlayerMode() const { return singlePlayerMode_; }
|
||||||
|
void simulateMotd(const std::vector<std::string>& lines);
|
||||||
|
|
||||||
// NPC death callback (single-player)
|
// NPC death callback (single-player)
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||||
|
|
@ -375,6 +381,9 @@ private:
|
||||||
void handleGroupUninvite(network::Packet& packet);
|
void handleGroupUninvite(network::Packet& packet);
|
||||||
void handlePartyCommandResult(network::Packet& packet);
|
void handlePartyCommandResult(network::Packet& packet);
|
||||||
|
|
||||||
|
// ---- Character creation handler ----
|
||||||
|
void handleCharCreateResponse(network::Packet& packet);
|
||||||
|
|
||||||
// ---- XP handler ----
|
// ---- XP handler ----
|
||||||
void handleXpGain(network::Packet& packet);
|
void handleXpGain(network::Packet& packet);
|
||||||
|
|
||||||
|
|
@ -515,6 +524,7 @@ private:
|
||||||
// Callbacks
|
// Callbacks
|
||||||
WorldConnectSuccessCallback onSuccess;
|
WorldConnectSuccessCallback onSuccess;
|
||||||
WorldConnectFailureCallback onFailure;
|
WorldConnectFailureCallback onFailure;
|
||||||
|
CharCreateCallback charCreateCallback_;
|
||||||
|
|
||||||
// ---- XP tracking ----
|
// ---- XP tracking ----
|
||||||
uint32_t playerXp_ = 0;
|
uint32_t playerXp_ = 0;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ enum class Opcode : uint16_t {
|
||||||
// ---- Client to Server (Core) ----
|
// ---- Client to Server (Core) ----
|
||||||
CMSG_PING = 0x1DC,
|
CMSG_PING = 0x1DC,
|
||||||
CMSG_AUTH_SESSION = 0x1ED,
|
CMSG_AUTH_SESSION = 0x1ED,
|
||||||
|
CMSG_CHAR_CREATE = 0x036,
|
||||||
CMSG_CHAR_ENUM = 0x037,
|
CMSG_CHAR_ENUM = 0x037,
|
||||||
CMSG_PLAYER_LOGIN = 0x03D,
|
CMSG_PLAYER_LOGIN = 0x03D,
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ enum class Opcode : uint16_t {
|
||||||
// ---- Server to Client (Core) ----
|
// ---- Server to Client (Core) ----
|
||||||
SMSG_AUTH_CHALLENGE = 0x1EC,
|
SMSG_AUTH_CHALLENGE = 0x1EC,
|
||||||
SMSG_AUTH_RESPONSE = 0x1EE,
|
SMSG_AUTH_RESPONSE = 0x1EE,
|
||||||
|
SMSG_CHAR_CREATE = 0x03A,
|
||||||
SMSG_CHAR_ENUM = 0x03B,
|
SMSG_CHAR_ENUM = 0x03B,
|
||||||
SMSG_PONG = 0x1DD,
|
SMSG_PONG = 0x1DD,
|
||||||
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
SMSG_LOGIN_VERIFY_WORLD = 0x236,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "network/packet.hpp"
|
#include "network/packet.hpp"
|
||||||
#include "game/opcodes.hpp"
|
#include "game/opcodes.hpp"
|
||||||
|
#include "game/character.hpp"
|
||||||
#include "game/entity.hpp"
|
#include "game/entity.hpp"
|
||||||
#include "game/spell_defines.hpp"
|
#include "game/spell_defines.hpp"
|
||||||
#include "game/group_defines.hpp"
|
#include "game/group_defines.hpp"
|
||||||
|
|
@ -167,6 +168,47 @@ public:
|
||||||
static bool parse(network::Packet& packet, CharEnumResponse& response);
|
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
|
* CMSG_PLAYER_LOGIN packet builder
|
||||||
*
|
*
|
||||||
|
|
|
||||||
42
include/ui/character_create_screen.hpp
Normal file
42
include/ui/character_create_screen.hpp
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/character.hpp"
|
||||||
|
#include "game/world_packets.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace game { class GameHandler; }
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
class CharacterCreateScreen {
|
||||||
|
public:
|
||||||
|
CharacterCreateScreen();
|
||||||
|
|
||||||
|
void render(game::GameHandler& gameHandler);
|
||||||
|
void setOnCreate(std::function<void(const game::CharCreateData&)> cb) { onCreate = std::move(cb); }
|
||||||
|
void setOnCancel(std::function<void()> 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<game::Class> availableClasses;
|
||||||
|
void updateAvailableClasses();
|
||||||
|
|
||||||
|
std::function<void(const game::CharCreateData&)> onCreate;
|
||||||
|
std::function<void()> onCancel;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -30,6 +30,8 @@ public:
|
||||||
onCharacterSelected = callback;
|
onCharacterSelected = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setOnCreateCharacter(std::function<void()> cb) { onCreateCharacter = std::move(cb); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a character has been selected
|
* Check if a character has been selected
|
||||||
*/
|
*/
|
||||||
|
|
@ -51,6 +53,7 @@ private:
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
std::function<void(uint64_t)> onCharacterSelected;
|
std::function<void(uint64_t)> onCharacterSelected;
|
||||||
|
std::function<void()> onCreateCharacter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status message
|
* Update status message
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "ui/auth_screen.hpp"
|
#include "ui/auth_screen.hpp"
|
||||||
#include "ui/realm_screen.hpp"
|
#include "ui/realm_screen.hpp"
|
||||||
|
#include "ui/character_create_screen.hpp"
|
||||||
#include "ui/character_screen.hpp"
|
#include "ui/character_screen.hpp"
|
||||||
#include "ui/game_screen.hpp"
|
#include "ui/game_screen.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -64,6 +65,7 @@ public:
|
||||||
*/
|
*/
|
||||||
AuthScreen& getAuthScreen() { return *authScreen; }
|
AuthScreen& getAuthScreen() { return *authScreen; }
|
||||||
RealmScreen& getRealmScreen() { return *realmScreen; }
|
RealmScreen& getRealmScreen() { return *realmScreen; }
|
||||||
|
CharacterCreateScreen& getCharacterCreateScreen() { return *characterCreateScreen; }
|
||||||
CharacterScreen& getCharacterScreen() { return *characterScreen; }
|
CharacterScreen& getCharacterScreen() { return *characterScreen; }
|
||||||
GameScreen& getGameScreen() { return *gameScreen; }
|
GameScreen& getGameScreen() { return *gameScreen; }
|
||||||
|
|
||||||
|
|
@ -73,6 +75,7 @@ private:
|
||||||
// UI Screens
|
// UI Screens
|
||||||
std::unique_ptr<AuthScreen> authScreen;
|
std::unique_ptr<AuthScreen> authScreen;
|
||||||
std::unique_ptr<RealmScreen> realmScreen;
|
std::unique_ptr<RealmScreen> realmScreen;
|
||||||
|
std::unique_ptr<CharacterCreateScreen> characterCreateScreen;
|
||||||
std::unique_ptr<CharacterScreen> characterScreen;
|
std::unique_ptr<CharacterScreen> characterScreen;
|
||||||
std::unique_ptr<GameScreen> gameScreen;
|
std::unique_ptr<GameScreen> gameScreen;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,9 @@ void Application::setState(AppState newState) {
|
||||||
case AppState::REALM_SELECTION:
|
case AppState::REALM_SELECTION:
|
||||||
// Show realm screen
|
// Show realm screen
|
||||||
break;
|
break;
|
||||||
|
case AppState::CHARACTER_CREATION:
|
||||||
|
// Show character create screen
|
||||||
|
break;
|
||||||
case AppState::CHARACTER_SELECTION:
|
case AppState::CHARACTER_SELECTION:
|
||||||
// Show character screen
|
// Show character screen
|
||||||
break;
|
break;
|
||||||
|
|
@ -340,6 +343,10 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case AppState::CHARACTER_CREATION:
|
||||||
|
// Character creation update
|
||||||
|
break;
|
||||||
|
|
||||||
case AppState::CHARACTER_SELECTION:
|
case AppState::CHARACTER_SELECTION:
|
||||||
// Character selection update
|
// Character selection update
|
||||||
break;
|
break;
|
||||||
|
|
@ -437,10 +444,13 @@ void Application::setupUICallbacks() {
|
||||||
setState(AppState::REALM_SELECTION);
|
setState(AppState::REALM_SELECTION);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Single-player mode callback
|
// Single-player mode callback — go to character creation first
|
||||||
uiManager->getAuthScreen().setOnSinglePlayer([this]() {
|
uiManager->getAuthScreen().setOnSinglePlayer([this]() {
|
||||||
LOG_INFO("Single-player mode selected");
|
LOG_INFO("Single-player mode selected, opening character creation");
|
||||||
startSinglePlayer();
|
singlePlayerMode = true;
|
||||||
|
gameHandler->setSinglePlayerMode(true);
|
||||||
|
uiManager->getCharacterCreateScreen().reset();
|
||||||
|
setState(AppState::CHARACTER_CREATION);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Realm selection callback
|
// Realm selection callback
|
||||||
|
|
@ -472,7 +482,54 @@ void Application::setupUICallbacks() {
|
||||||
// Character selection callback
|
// Character selection callback
|
||||||
uiManager->getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) {
|
uiManager->getCharacterScreen().setOnCharacterSelected([this](uint64_t characterGuid) {
|
||||||
LOG_INFO("Character selected: GUID=0x", std::hex, characterGuid, std::dec);
|
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<uint32_t>(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
|
// Enable single-player combat mode on game handler
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
gameHandler->setSinglePlayerMode(true);
|
gameHandler->setSinglePlayerMode(true);
|
||||||
uint32_t level = 10;
|
// Only init stats with defaults if not already set (e.g. via character creation)
|
||||||
uint32_t maxHealth = 20 + level * 10;
|
if (gameHandler->getLocalPlayerMaxHealth() == 0) {
|
||||||
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
uint32_t level = 10;
|
||||||
|
uint32_t maxHealth = 20 + level * 10;
|
||||||
|
gameHandler->initLocalPlayerStats(level, maxHealth, maxHealth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create world object for single-player
|
// Create world object for single-player
|
||||||
|
|
@ -938,27 +998,6 @@ void Application::startSinglePlayer() {
|
||||||
// Load weapon models for equipped items (after inventory is populated)
|
// Load weapon models for equipped items (after inventory is populated)
|
||||||
loadEquippedWeapons();
|
loadEquippedWeapons();
|
||||||
|
|
||||||
// Emulate server MOTD in single-player
|
|
||||||
if (gameHandler) {
|
|
||||||
std::vector<std::string> 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 ---
|
// --- Loading screen: load terrain and wait for streaming before spawning ---
|
||||||
const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN"));
|
const SpawnPreset* spawnPreset = selectSpawnPreset(std::getenv("WOW_SPAWN"));
|
||||||
// Canonical WoW coords: +X=North, +Y=West, +Z=Up
|
// Canonical WoW coords: +X=North, +Y=West, +Z=Up
|
||||||
|
|
@ -1099,6 +1138,26 @@ void Application::startSinglePlayer() {
|
||||||
|
|
||||||
// Go directly to game
|
// Go directly to game
|
||||||
setState(AppState::IN_GAME);
|
setState(AppState::IN_GAME);
|
||||||
|
// Emulate server MOTD in single-player (after entering game)
|
||||||
|
if (gameHandler) {
|
||||||
|
std::vector<std::string> 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");
|
LOG_INFO("Single-player mode started - press F1 for performance HUD");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,54 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace game {
|
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) {
|
const char* getRaceName(Race race) {
|
||||||
switch (race) {
|
switch (race) {
|
||||||
case Race::HUMAN: return "Human";
|
case Race::HUMAN: return "Human";
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Opcode::SMSG_CHAR_CREATE:
|
||||||
|
handleCharCreateResponse(packet);
|
||||||
|
break;
|
||||||
|
|
||||||
case Opcode::SMSG_CHAR_ENUM:
|
case Opcode::SMSG_CHAR_ENUM:
|
||||||
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
if (state == WorldState::CHAR_LIST_REQUESTED) {
|
||||||
handleCharEnum(packet);
|
handleCharEnum(packet);
|
||||||
|
|
@ -822,6 +826,81 @@ void GameHandler::handleCharEnum(network::Packet& packet) {
|
||||||
LOG_INFO("Ready to select character");
|
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<uint32_t>(data.skin)) |
|
||||||
|
(static_cast<uint32_t>(data.face) << 8) |
|
||||||
|
(static_cast<uint32_t>(data.hairStyle) << 16) |
|
||||||
|
(static_cast<uint32_t>(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) {
|
void GameHandler::selectCharacter(uint64_t characterGuid) {
|
||||||
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
if (state != WorldState::CHAR_LIST_RECEIVED) {
|
||||||
LOG_WARNING("Cannot select character in state: ", (int)state);
|
LOG_WARNING("Cannot select character in state: ", (int)state);
|
||||||
|
|
@ -2489,6 +2568,15 @@ void GameHandler::simulateXpGain(uint64_t victimGuid, uint32_t totalXp) {
|
||||||
handleXpGain(packet);
|
handleXpGain(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::simulateMotd(const std::vector<std::string>& lines) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::SMSG_MOTD));
|
||||||
|
packet.writeUInt32(static_cast<uint32_t>(lines.size()));
|
||||||
|
for (const auto& line : lines) {
|
||||||
|
packet.writeString(line);
|
||||||
|
}
|
||||||
|
handleMotd(packet);
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::addMoneyCopper(uint32_t amount) {
|
void GameHandler::addMoneyCopper(uint32_t amount) {
|
||||||
if (amount == 0) return;
|
if (amount == 0) return;
|
||||||
playerMoneyCopper_ += amount;
|
playerMoneyCopper_ += amount;
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,35 @@ const char* getAuthResultString(AuthResult result) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Character Creation
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
network::Packet CharCreatePacket::build(const CharCreateData& data) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_CREATE));
|
||||||
|
|
||||||
|
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(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<CharCreateResult>(packet.readUInt8());
|
||||||
|
LOG_INFO("SMSG_CHAR_CREATE result: ", static_cast<int>(data.result));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
network::Packet CharEnumPacket::build() {
|
network::Packet CharEnumPacket::build() {
|
||||||
// CMSG_CHAR_ENUM has no body - just the opcode
|
// CMSG_CHAR_ENUM has no body - just the opcode
|
||||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
|
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
|
||||||
|
|
|
||||||
226
src/ui/character_create_screen.cpp
Normal file
226
src/ui/character_create_screen.cpp
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
#include "ui/character_create_screen.hpp"
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<int>(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<int>(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<game::Gender>(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<uint8_t>(skin);
|
||||||
|
data.face = static_cast<uint8_t>(face);
|
||||||
|
data.hairStyle = static_cast<uint8_t>(hairStyle);
|
||||||
|
data.hairColor = static_cast<uint8_t>(hairColor);
|
||||||
|
data.facialHair = static_cast<uint8_t>(facialHair);
|
||||||
|
if (onCreate) {
|
||||||
|
onCreate(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::Button("Back", ImVec2(150, 35))) {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -148,7 +148,10 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||||
ss << "Entering world with " << character.name << "...";
|
ss << "Entering world with " << character.name << "...";
|
||||||
setStatus(ss.str());
|
setStatus(ss.str());
|
||||||
|
|
||||||
gameHandler.selectCharacter(character.guid);
|
// Only send CMSG_PLAYER_LOGIN in online mode
|
||||||
|
if (!gameHandler.isSinglePlayerMode()) {
|
||||||
|
gameHandler.selectCharacter(character.guid);
|
||||||
|
}
|
||||||
|
|
||||||
// Call callback
|
// Call callback
|
||||||
if (onCharacterSelected) {
|
if (onCharacterSelected) {
|
||||||
|
|
@ -169,7 +172,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Back/Refresh buttons
|
// Back/Refresh/Create buttons
|
||||||
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
|
||||||
if (gameHandler.getState() == game::WorldState::READY ||
|
if (gameHandler.getState() == game::WorldState::READY ||
|
||||||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
|
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();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ UIManager::UIManager() {
|
||||||
// Create screen instances
|
// Create screen instances
|
||||||
authScreen = std::make_unique<AuthScreen>();
|
authScreen = std::make_unique<AuthScreen>();
|
||||||
realmScreen = std::make_unique<RealmScreen>();
|
realmScreen = std::make_unique<RealmScreen>();
|
||||||
|
characterCreateScreen = std::make_unique<CharacterCreateScreen>();
|
||||||
characterScreen = std::make_unique<CharacterScreen>();
|
characterScreen = std::make_unique<CharacterScreen>();
|
||||||
gameScreen = std::make_unique<GameScreen>();
|
gameScreen = std::make_unique<GameScreen>();
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +102,12 @@ void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler,
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case core::AppState::CHARACTER_CREATION:
|
||||||
|
if (gameHandler) {
|
||||||
|
characterCreateScreen->render(*gameHandler);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case core::AppState::CHARACTER_SELECTION:
|
case core::AppState::CHARACTER_SELECTION:
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
characterScreen->render(*gameHandler);
|
characterScreen->render(*gameHandler);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue