mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +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
129bbac9b3
commit
0605d1522d
16 changed files with 611 additions and 30 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
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<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) {
|
||||
if (amount == 0) return;
|
||||
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() {
|
||||
// CMSG_CHAR_ENUM has no body - just the opcode
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue