mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-04 16:23:52 +00:00
Fix character appearance, previews, mount seat, and online unequip
This commit is contained in:
parent
4a023e773b
commit
275914b4db
19 changed files with 743 additions and 113 deletions
|
|
@ -49,6 +49,7 @@ void CharacterCreateScreen::reset() {
|
|||
maxFacialHair = 8;
|
||||
statusMessage.clear();
|
||||
statusIsError = false;
|
||||
hairColorIds_.clear();
|
||||
updateAvailableClasses();
|
||||
|
||||
// Reset preview tracking to force model reload on next render
|
||||
|
|
@ -114,13 +115,19 @@ void CharacterCreateScreen::updatePreviewIfNeeded() {
|
|||
|
||||
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),
|
||||
static_cast<uint8_t>(hairColor),
|
||||
hairColorId,
|
||||
static_cast<uint8_t>(facialHair),
|
||||
useFemaleModel);
|
||||
|
||||
|
|
@ -153,6 +160,7 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
maxHairStyle = 11;
|
||||
maxHairColor = 9;
|
||||
maxFacialHair = 8;
|
||||
hairColorIds_.clear();
|
||||
|
||||
if (!assetManager_) return;
|
||||
auto dbc = assetManager_->loadDBC("CharSections.dbc");
|
||||
|
|
@ -189,7 +197,7 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
}
|
||||
|
||||
int faceMax = -1;
|
||||
int hairColorMax = -1;
|
||||
std::vector<uint8_t> hairColorIds;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = dbc->getUInt32(r, 1);
|
||||
uint32_t sexId = dbc->getUInt32(r, 2);
|
||||
|
|
@ -202,7 +210,9 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
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)) {
|
||||
hairColorMax = std::max(hairColorMax, static_cast<int>(colorIndex));
|
||||
if (colorIndex <= 255) {
|
||||
hairColorIds.push_back(static_cast<uint8_t>(colorIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,9 +220,15 @@ void CharacterCreateScreen::updateAppearanceRanges() {
|
|||
maxFace = faceMax;
|
||||
if (face > maxFace) face = maxFace;
|
||||
}
|
||||
if (hairColorMax >= 0) {
|
||||
maxHairColor = hairColorMax;
|
||||
|
||||
// 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");
|
||||
|
|
@ -450,7 +466,11 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
|||
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);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
#include "ui/character_screen.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
|
@ -11,26 +15,59 @@ namespace wowee { namespace ui {
|
|||
CharacterScreen::CharacterScreen() {
|
||||
}
|
||||
|
||||
void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||
// Size the window to fill most of the viewport
|
||||
ImVec2 vpSize = ImGui::GetMainViewport()->Size;
|
||||
ImVec2 winSize(vpSize.x * 0.6f, vpSize.y * 0.7f);
|
||||
if (winSize.x < 700.0f) winSize.x = 700.0f;
|
||||
if (winSize.y < 500.0f) winSize.y = 500.0f;
|
||||
ImGui::SetNextWindowSize(winSize, ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(vpSize.x * 0.5f, vpSize.y * 0.5f),
|
||||
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||
static uint64_t hashEquipment(const std::vector<game::EquipmentItem>& eq) {
|
||||
// FNV-1a 64-bit over (displayModel, inventoryType, enchantment)
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
auto mix8 = [&](uint8_t b) {
|
||||
h ^= b;
|
||||
h *= 1099511628211ull;
|
||||
};
|
||||
auto mix32 = [&](uint32_t v) {
|
||||
mix8(static_cast<uint8_t>(v & 0xFF));
|
||||
mix8(static_cast<uint8_t>((v >> 8) & 0xFF));
|
||||
mix8(static_cast<uint8_t>((v >> 16) & 0xFF));
|
||||
mix8(static_cast<uint8_t>((v >> 24) & 0xFF));
|
||||
};
|
||||
for (const auto& it : eq) {
|
||||
mix32(it.displayModel);
|
||||
mix8(it.inventoryType);
|
||||
mix32(it.enchantment);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
ImGui::Begin("Character Selection", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
void CharacterScreen::render(game::GameHandler& gameHandler) {
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const ImVec2 pad(24.0f, 24.0f);
|
||||
ImVec2 winSize(vp->Size.x - pad.x * 2.0f, vp->Size.y - pad.y * 2.0f);
|
||||
if (winSize.x < 860.0f) winSize.x = 860.0f;
|
||||
if (winSize.y < 620.0f) winSize.y = 620.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + (vp->Size.x - winSize.x) * 0.5f,
|
||||
vp->Pos.y + (vp->Size.y - winSize.y) * 0.5f),
|
||||
ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(winSize, ImGuiCond_Always);
|
||||
|
||||
ImGui::Begin("Character Selection", nullptr,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
|
||||
|
||||
// Ensure we can render a preview even if the state transition hook didn't inject the AssetManager.
|
||||
if (!assetManager_) {
|
||||
assetManager_ = core::Application::getInstance().getAssetManager();
|
||||
}
|
||||
|
||||
// Get character list
|
||||
const auto& characters = gameHandler.getCharacters();
|
||||
|
||||
// Request character list if not available
|
||||
if (characters.empty() && gameHandler.getState() == game::WorldState::READY) {
|
||||
// Request character list if not available.
|
||||
// Also show a loading state while CHAR_LIST_REQUESTED is in-flight (characters may be cleared to avoid stale UI).
|
||||
if (characters.empty() &&
|
||||
(gameHandler.getState() == game::WorldState::READY ||
|
||||
gameHandler.getState() == game::WorldState::CHAR_LIST_REQUESTED)) {
|
||||
ImGui::Text("Loading characters...");
|
||||
gameHandler.requestCharacterList();
|
||||
if (gameHandler.getState() == game::WorldState::READY) {
|
||||
gameHandler.requestCharacterList();
|
||||
}
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
|
@ -54,6 +91,22 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the list refreshed, keep selection stable by GUID.
|
||||
if (selectedCharacterGuid != 0) {
|
||||
const bool needReselect =
|
||||
(selectedCharacterIndex < 0) ||
|
||||
(selectedCharacterIndex >= static_cast<int>(characters.size())) ||
|
||||
(characters[static_cast<size_t>(selectedCharacterIndex)].guid != selectedCharacterGuid);
|
||||
if (needReselect) {
|
||||
for (size_t i = 0; i < characters.size(); ++i) {
|
||||
if (characters[i].guid == selectedCharacterGuid) {
|
||||
selectedCharacterIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore last-selected character (once per screen visit)
|
||||
if (!restoredLastCharacter) {
|
||||
// Priority 1: Select newly created character if set
|
||||
|
|
@ -99,7 +152,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
// ── Two-column layout: character list (left) | details (right) ──
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float detailPanelW = 260.0f;
|
||||
float detailPanelW = 360.0f;
|
||||
float listW = availW - detailPanelW - ImGui::GetStyle().ItemSpacing.x;
|
||||
if (listW < 300.0f) { listW = availW; detailPanelW = 0.0f; }
|
||||
|
||||
|
|
@ -174,9 +227,83 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
|
||||
const auto& character = characters[selectedCharacterIndex];
|
||||
|
||||
// Keep the 3D preview in sync with the selected character.
|
||||
if (assetManager_ && assetManager_->isInitialized()) {
|
||||
if (!preview_) {
|
||||
preview_ = std::make_unique<rendering::CharacterPreview>();
|
||||
}
|
||||
if (!previewInitialized_) {
|
||||
previewInitialized_ = preview_->initialize(assetManager_);
|
||||
if (!previewInitialized_) {
|
||||
LOG_WARNING("CharacterScreen: failed to init CharacterPreview");
|
||||
preview_.reset();
|
||||
}
|
||||
}
|
||||
if (preview_) {
|
||||
const uint64_t equipHash = hashEquipment(character.equipment);
|
||||
const bool changed =
|
||||
(previewGuid_ != character.guid) ||
|
||||
(previewAppearanceBytes_ != character.appearanceBytes) ||
|
||||
(previewFacialFeatures_ != character.facialFeatures) ||
|
||||
(previewUseFemaleModel_ != character.useFemaleModel) ||
|
||||
(previewEquipHash_ != equipHash) ||
|
||||
(!preview_->isModelLoaded());
|
||||
|
||||
if (changed) {
|
||||
uint8_t skin = character.appearanceBytes & 0xFF;
|
||||
uint8_t face = (character.appearanceBytes >> 8) & 0xFF;
|
||||
uint8_t hairStyle = (character.appearanceBytes >> 16) & 0xFF;
|
||||
uint8_t hairColor = (character.appearanceBytes >> 24) & 0xFF;
|
||||
|
||||
if (preview_->loadCharacter(character.race, character.gender,
|
||||
skin, face, hairStyle, hairColor,
|
||||
character.facialFeatures, character.useFemaleModel)) {
|
||||
preview_->applyEquipment(character.equipment);
|
||||
}
|
||||
|
||||
previewGuid_ = character.guid;
|
||||
previewAppearanceBytes_ = character.appearanceBytes;
|
||||
previewFacialFeatures_ = character.facialFeatures;
|
||||
previewUseFemaleModel_ = character.useFemaleModel;
|
||||
previewEquipHash_ = equipHash;
|
||||
}
|
||||
|
||||
// Drive preview animation and render to its FBO.
|
||||
preview_->update(ImGui::GetIO().DeltaTime);
|
||||
preview_->render();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("CharDetails", ImVec2(detailPanelW, listH), true);
|
||||
|
||||
// 3D preview portrait
|
||||
if (preview_ && preview_->getTextureId() != 0) {
|
||||
float imgW = ImGui::GetContentRegionAvail().x;
|
||||
float imgH = imgW * (static_cast<float>(preview_->getHeight()) /
|
||||
static_cast<float>(preview_->getWidth()));
|
||||
// Clamp to avoid taking the entire panel
|
||||
float maxH = 320.0f;
|
||||
if (imgH > maxH) {
|
||||
imgH = maxH;
|
||||
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), // flip Y for OpenGL
|
||||
ImVec2(1.0f, 0.0f));
|
||||
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||
preview_->rotate(ImGui::GetIO().MouseDelta.x * 0.2f);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
} else if (!assetManager_ || !assetManager_->isInitialized()) {
|
||||
ImGui::TextDisabled("Preview unavailable (assets not loaded)");
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::TextColored(getFactionColor(character.race), "%s", character.name.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
|
|
|||
|
|
@ -2201,8 +2201,20 @@ void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
|
|||
for (uint16_t i = 0; i <= 18; i++) {
|
||||
geosets.insert(i);
|
||||
}
|
||||
geosets.insert(101); // Hair
|
||||
geosets.insert(201); // Facial
|
||||
// Hair/facial geosets must match the active character's appearance, otherwise
|
||||
// we end up forcing a default hair mesh (often perceived as "wrong hair").
|
||||
{
|
||||
uint8_t hairStyleId = 0;
|
||||
uint8_t facialId = 0;
|
||||
if (auto* gh = app.getGameHandler()) {
|
||||
if (const auto* ch = gh->getActiveCharacter()) {
|
||||
hairStyleId = static_cast<uint8_t>((ch->appearanceBytes >> 16) & 0xFF);
|
||||
facialId = ch->facialFeatures;
|
||||
}
|
||||
}
|
||||
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1)); // Group 1 hair
|
||||
geosets.insert(static_cast<uint16_t>(200 + facialId + 1)); // Group 2 facial
|
||||
}
|
||||
geosets.insert(701); // Ears
|
||||
|
||||
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
||||
|
|
|
|||
|
|
@ -1105,7 +1105,16 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
pickupFromBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
if (gameHandler_) {
|
||||
// Online mode: don't mutate local equipment state.
|
||||
game::MessageChatData msg{};
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Moving equipped items not supported yet (online mode).";
|
||||
gameHandler_->addLocalChatMessage(msg);
|
||||
} else {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
|
|
@ -1126,13 +1135,19 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
// Sell to vendor
|
||||
gameHandler_->sellItemBySlot(backpackIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
// Unequip: move to free backpack slot
|
||||
int freeSlot = inventory.findFreeBackpackSlot();
|
||||
if (freeSlot >= 0) {
|
||||
inventory.setBackpackSlot(freeSlot, item);
|
||||
inventory.clearEquipSlot(equipSlot);
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
if (gameHandler_) {
|
||||
// Online mode: request server-side unequip (move to first free backpack slot).
|
||||
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
||||
gameHandler_->unequipToBackpack(equipSlot);
|
||||
} else {
|
||||
// Offline mode: Unequip: move to free backpack slot
|
||||
int freeSlot = inventory.findFreeBackpackSlot();
|
||||
if (freeSlot >= 0) {
|
||||
inventory.setBackpackSlot(freeSlot, item);
|
||||
inventory.clearEquipSlot(equipSlot);
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
}
|
||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
if (gameHandler_) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ RealmScreen::RealmScreen() {
|
|||
}
|
||||
|
||||
void RealmScreen::render(auth::AuthHandler& authHandler) {
|
||||
ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Realm Selection", nullptr, ImGuiWindowFlags_NoCollapse);
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const ImVec2 pad(24.0f, 24.0f);
|
||||
ImVec2 winSize(vp->Size.x - pad.x * 2.0f, vp->Size.y - pad.y * 2.0f);
|
||||
if (winSize.x < 720.0f) winSize.x = 720.0f;
|
||||
if (winSize.y < 540.0f) winSize.y = 540.0f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + (vp->Size.x - winSize.x) * 0.5f,
|
||||
vp->Pos.y + (vp->Size.y - winSize.y) * 0.5f),
|
||||
ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(winSize, ImGuiCond_Always);
|
||||
ImGui::Begin("Realm Selection", nullptr,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
|
||||
|
||||
ImGui::Text("Select a Realm");
|
||||
ImGui::Separator();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue