Kelsidavis-WoWee/src/ui/character_create_screen.cpp
Kelsi 7d115aaa70 Add per-expansion asset overlay system and fix CharSections DBC layout
Expansion overlays allow each expansion to supplement the base asset data
via an assetManifest field in expansion.json, loaded at priority 50 (below
HD packs). The asset extractor gains --reference-manifest for delta-only
extraction. Also fixes CharSections field indices (VariationIndex=4,
ColorIndex=5, Texture1=6) across all DBC layout references.
2026-02-14 00:00:26 -08:00

496 lines
17 KiB
C++

#include "ui/character_create_screen.hpp"
#include "rendering/character_preview.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.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();
}
CharacterCreateScreen::~CharacterCreateScreen() = default;
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;
maxSkin = 9;
maxFace = 9;
maxHairStyle = 11;
maxHairColor = 9;
maxFacialHair = 8;
statusMessage.clear();
statusIsError = false;
hairColorIds_.clear();
updateAvailableClasses();
// Reset preview tracking to force model reload on next render
prevRaceIndex_ = -1;
prevGenderIndex_ = -1;
prevSkin_ = -1;
prevFace_ = -1;
prevHairStyle_ = -1;
prevHairColor_ = -1;
prevFacialHair_ = -1;
prevRangeRace_ = -1;
prevRangeGender_ = -1;
prevRangeSkin_ = -1;
prevRangeHairStyle_ = -1;
}
void CharacterCreateScreen::initializePreview(pipeline::AssetManager* am) {
assetManager_ = am;
if (!preview_) {
preview_ = std::make_unique<rendering::CharacterPreview>();
preview_->initialize(am);
}
// Force model reload
prevRaceIndex_ = -1;
}
void CharacterCreateScreen::update(float deltaTime) {
if (preview_) {
preview_->update(deltaTime);
}
}
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::updatePreviewIfNeeded() {
if (!preview_) return;
bool changed = (raceIndex != prevRaceIndex_ ||
genderIndex != prevGenderIndex_ ||
bodyTypeIndex != prevBodyTypeIndex_ ||
skin != prevSkin_ ||
face != prevFace_ ||
hairStyle != prevHairStyle_ ||
hairColor != prevHairColor_ ||
facialHair != prevFacialHair_);
if (changed) {
bool useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
uint8_t hairColorId = 0;
if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast<int>(hairColorIds_.size())) {
hairColorId = hairColorIds_[hairColor];
} else {
hairColorId = static_cast<uint8_t>(hairColor);
}
preview_->loadCharacter(
allRaces[raceIndex],
static_cast<game::Gender>(genderIndex),
static_cast<uint8_t>(skin),
static_cast<uint8_t>(face),
static_cast<uint8_t>(hairStyle),
hairColorId,
static_cast<uint8_t>(facialHair),
useFemaleModel);
prevRaceIndex_ = raceIndex;
prevGenderIndex_ = genderIndex;
prevBodyTypeIndex_ = bodyTypeIndex;
prevSkin_ = skin;
prevFace_ = face;
prevHairStyle_ = hairStyle;
prevHairColor_ = hairColor;
prevFacialHair_ = facialHair;
}
}
void CharacterCreateScreen::updateAppearanceRanges() {
if (raceIndex == prevRangeRace_ &&
genderIndex == prevRangeGender_ &&
skin == prevRangeSkin_ &&
hairStyle == prevRangeHairStyle_) {
return;
}
prevRangeRace_ = raceIndex;
prevRangeGender_ = genderIndex;
prevRangeSkin_ = skin;
prevRangeHairStyle_ = hairStyle;
maxSkin = 9;
maxFace = 9;
maxHairStyle = 11;
maxHairColor = 9;
maxFacialHair = 8;
hairColorIds_.clear();
if (!assetManager_) return;
auto dbc = assetManager_->loadDBC("CharSections.dbc");
if (!dbc) return;
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]);
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
int skinMax = -1;
int hairStyleMax = -1;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
} else if (baseSection == 3) {
hairStyleMax = std::max(hairStyleMax, static_cast<int>(variationIndex));
}
}
if (skinMax >= 0) {
maxSkin = skinMax;
if (skin > maxSkin) skin = maxSkin;
}
if (hairStyleMax >= 0) {
maxHairStyle = hairStyleMax;
if (hairStyle > maxHairStyle) hairStyle = maxHairStyle;
}
int faceMax = -1;
std::vector<uint8_t> hairColorIds;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex));
} else if (baseSection == 3 && variationIndex == static_cast<uint32_t>(hairStyle)) {
if (colorIndex <= 255) {
hairColorIds.push_back(static_cast<uint8_t>(colorIndex));
}
}
}
if (faceMax >= 0) {
maxFace = faceMax;
if (face > maxFace) face = maxFace;
}
// Hair colors: use actual available DBC IDs (not "0..maxId"), since IDs may be sparse.
if (!hairColorIds.empty()) {
std::sort(hairColorIds.begin(), hairColorIds.end());
hairColorIds.erase(std::unique(hairColorIds.begin(), hairColorIds.end()), hairColorIds.end());
hairColorIds_ = std::move(hairColorIds);
maxHairColor = std::max(0, static_cast<int>(hairColorIds_.size()) - 1);
if (hairColor > maxHairColor) hairColor = maxHairColor;
if (hairColor < 0) hairColor = 0;
}
int facialMax = -1;
auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc");
const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
if (facialDbc) {
for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) {
uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0);
uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2);
facialMax = std::max(facialMax, static_cast<int>(variation));
}
}
if (facialMax >= 0) {
maxFacialHair = facialMax;
} else if (targetSexId == 1) {
maxFacialHair = 0;
}
if (facialHair > maxFacialHair) {
facialHair = maxFacialHair;
}
}
void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
// Render the preview to FBO before the ImGui frame
if (preview_) {
updatePreviewIfNeeded();
preview_->render();
}
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
bool hasPreview = (preview_ && preview_->getTextureId() != 0);
float previewWidth = hasPreview ? 320.0f : 0.0f;
float controlsWidth = 540.0f;
float totalWidth = hasPreview ? (previewWidth + controlsWidth + 16.0f) : 600.0f;
float totalHeight = 580.0f;
ImGui::SetNextWindowSize(ImVec2(totalWidth, totalHeight), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2((displaySize.x - totalWidth) * 0.5f,
(displaySize.y - totalHeight) * 0.5f),
ImGuiCond_Always);
ImGui::Begin("Create Character", nullptr,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
if (hasPreview) {
// Left panel: 3D preview
ImGui::BeginChild("##preview_panel", ImVec2(previewWidth, -40.0f), false);
{
// Display the FBO texture (flip Y for OpenGL)
float imgW = previewWidth - 8.0f;
float imgH = imgW * (static_cast<float>(preview_->getHeight()) /
static_cast<float>(preview_->getWidth()));
if (imgH > totalHeight - 80.0f) {
imgH = totalHeight - 80.0f;
imgW = imgH * (static_cast<float>(preview_->getWidth()) /
static_cast<float>(preview_->getHeight()));
}
ImGui::Image(
static_cast<ImTextureID>(preview_->getTextureId()),
ImVec2(imgW, imgH),
ImVec2(0.0f, 1.0f), // UV top-left (flipped Y)
ImVec2(1.0f, 0.0f)); // UV bottom-right (flipped Y)
// Mouse drag rotation on the preview image
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
float deltaX = ImGui::GetIO().MouseDelta.x;
preview_->rotate(deltaX * 0.2f);
}
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Drag to rotate");
}
ImGui::EndChild();
ImGui::SameLine();
// Right panel: controls
ImGui::BeginChild("##controls_panel", ImVec2(0, -40.0f), false);
}
// Name input
ImGui::Text("Name:");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(200);
ImGui::InputText("##name", nameBuffer, sizeof(nameBuffer));
ImGui::Spacing();
// Race selection
ImGui::Text("Race:");
ImGui::Spacing();
ImGui::Indent(10.0f);
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::Unindent(10.0f);
ImGui::Spacing();
// Class selection
ImGui::Text("Class:");
ImGui::SameLine(80);
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(80);
ImGui::RadioButton("Male", &genderIndex, 0);
ImGui::SameLine();
ImGui::RadioButton("Female", &genderIndex, 1);
ImGui::SameLine();
ImGui::RadioButton("Nonbinary", &genderIndex, 2);
// Body type selection for nonbinary
if (genderIndex == 2) { // Nonbinary
ImGui::Text("Body Type:");
ImGui::SameLine(80);
ImGui::RadioButton("Masculine", &bodyTypeIndex, 0);
ImGui::SameLine();
ImGui::RadioButton("Feminine", &bodyTypeIndex, 1);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Appearance sliders
updateAppearanceRanges();
game::Gender currentGender = static_cast<game::Gender>(genderIndex);
ImGui::Text("Appearance");
ImGui::Spacing();
float sliderWidth = hasPreview ? 180.0f : 200.0f;
float labelCol = hasPreview ? 100.0f : 120.0f;
auto slider = [&](const char* label, int* val, int maxVal) {
ImGui::Text("%s", label);
ImGui::SameLine(labelCol);
ImGui::SetNextItemWidth(sliderWidth);
char id[32];
snprintf(id, sizeof(id), "##%s", label);
ImGui::SliderInt(id, val, 0, maxVal);
};
slider("Skin", &skin, maxSkin);
slider("Face", &face, maxFace);
slider("Hair Style", &hairStyle, maxHairStyle);
slider("Hair Color", &hairColor, maxHairColor);
slider("Facial Feature", &facialHair, maxFacialHair);
ImGui::Spacing();
// Status message
if (!statusMessage.empty()) {
ImGui::Separator();
ImGui::Spacing();
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());
}
if (hasPreview) {
ImGui::EndChild(); // controls_panel
}
// Bottom buttons (outside children)
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Create", ImVec2(150, 35))) {
std::string name(nameBuffer);
// Trim whitespace
size_t start = name.find_first_not_of(" \t\r\n");
size_t end = name.find_last_not_of(" \t\r\n");
if (start == std::string::npos) {
name.clear();
} else {
name = name.substr(start, end - start + 1);
}
if (name.empty()) {
setStatus("Please enter a character name.", true);
} else if (availableClasses.empty()) {
setStatus("No valid class for this race.", true);
} else {
setStatus("Creating character...", false);
game::CharCreateData data;
data.name = name;
data.race = allRaces[raceIndex];
data.characterClass = availableClasses[classIndex];
data.gender = currentGender;
data.useFemaleModel = (genderIndex == 2 && bodyTypeIndex == 1); // Nonbinary + Feminine
data.skin = static_cast<uint8_t>(skin);
data.face = static_cast<uint8_t>(face);
data.hairStyle = static_cast<uint8_t>(hairStyle);
if (!hairColorIds_.empty() && hairColor >= 0 && hairColor < static_cast<int>(hairColorIds_.size())) {
data.hairColor = hairColorIds_[hairColor];
} else {
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