mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Add character screen model preview, item icons, stats panel, and fix targeting bugs
Enhanced the C-key character screen with a 3-column layout featuring a 3D character model preview (with drag-to-rotate), item icons loaded from BLP textures via ItemDisplayInfo.dbc, and a stats panel showing base + equipment bonuses. Fixed selection circle clipping under terrain by adding a Z offset, and corrected faction hostility logic that was wrongly marking hostile mobs as friendly.
This commit is contained in:
parent
7128ea1417
commit
394e91cd9e
12 changed files with 738 additions and 53 deletions
|
|
@ -72,8 +72,39 @@ public:
|
|||
y = py;
|
||||
z = pz;
|
||||
orientation = o;
|
||||
isMoving_ = false; // Instant position set cancels interpolation
|
||||
}
|
||||
|
||||
// Movement interpolation (syncs entity position with renderer during movement)
|
||||
void startMoveTo(float destX, float destY, float destZ, float destO, float durationSec) {
|
||||
if (durationSec <= 0.0f) {
|
||||
setPosition(destX, destY, destZ, destO);
|
||||
return;
|
||||
}
|
||||
moveStartX_ = x; moveStartY_ = y; moveStartZ_ = z;
|
||||
moveEndX_ = destX; moveEndY_ = destY; moveEndZ_ = destZ;
|
||||
moveDuration_ = durationSec;
|
||||
moveElapsed_ = 0.0f;
|
||||
orientation = destO;
|
||||
isMoving_ = true;
|
||||
}
|
||||
|
||||
void updateMovement(float deltaTime) {
|
||||
if (!isMoving_) return;
|
||||
moveElapsed_ += deltaTime;
|
||||
float t = moveElapsed_ / moveDuration_;
|
||||
if (t >= 1.0f) {
|
||||
x = moveEndX_; y = moveEndY_; z = moveEndZ_;
|
||||
isMoving_ = false;
|
||||
} else {
|
||||
x = moveStartX_ + (moveEndX_ - moveStartX_) * t;
|
||||
y = moveStartY_ + (moveEndY_ - moveStartY_) * t;
|
||||
z = moveStartZ_ + (moveEndZ_ - moveStartZ_) * t;
|
||||
}
|
||||
}
|
||||
|
||||
bool isEntityMoving() const { return isMoving_; }
|
||||
|
||||
// Object type
|
||||
ObjectType getType() const { return type; }
|
||||
void setType(ObjectType t) { type = t; }
|
||||
|
|
@ -108,6 +139,13 @@ protected:
|
|||
|
||||
// Update fields (dynamic values)
|
||||
std::map<uint16_t, uint32_t> fields;
|
||||
|
||||
// Movement interpolation state
|
||||
bool isMoving_ = false;
|
||||
float moveStartX_ = 0, moveStartY_ = 0, moveStartZ_ = 0;
|
||||
float moveEndX_ = 0, moveEndY_ = 0, moveEndZ_ = 0;
|
||||
float moveDuration_ = 0;
|
||||
float moveElapsed_ = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -162,6 +200,12 @@ public:
|
|||
// Returns true if NPC has interaction flags (gossip/vendor/quest/trainer)
|
||||
bool isInteractable() const { return npcFlags != 0; }
|
||||
|
||||
// Faction-based hostility
|
||||
uint32_t getFactionTemplate() const { return factionTemplate; }
|
||||
void setFactionTemplate(uint32_t f) { factionTemplate = f; }
|
||||
bool isHostile() const { return hostile; }
|
||||
void setHostile(bool h) { hostile = h; }
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
uint32_t health = 0;
|
||||
|
|
@ -174,6 +218,8 @@ protected:
|
|||
uint32_t displayId = 0;
|
||||
uint32_t unitFlags = 0;
|
||||
uint32_t npcFlags = 0;
|
||||
uint32_t factionTemplate = 0;
|
||||
bool hostile = false;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -294,6 +294,9 @@ public:
|
|||
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
||||
|
||||
// Faction hostility map (populated from FactionTemplate.dbc by Application)
|
||||
void setFactionHostileMap(std::unordered_map<uint32_t, bool> map) { factionHostileMap_ = std::move(map); }
|
||||
|
||||
// Creature move callback (online mode - triggered by SMSG_MONSTER_MOVE)
|
||||
// Parameters: guid, x, y, z (canonical), duration_ms (0 = instant)
|
||||
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
|
||||
|
|
@ -644,6 +647,13 @@ private:
|
|||
// Quest log
|
||||
std::vector<QuestLogEntry> questLog_;
|
||||
|
||||
// Faction hostility lookup (populated from FactionTemplate.dbc)
|
||||
std::unordered_map<uint32_t, bool> factionHostileMap_;
|
||||
bool isHostileFaction(uint32_t factionTemplateId) const {
|
||||
auto it = factionHostileMap_.find(factionTemplateId);
|
||||
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
|
||||
}
|
||||
|
||||
// Vendor
|
||||
bool vendorWindowOpen = false;
|
||||
ListInventoryData currentVendorItems;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ struct NpcSpawnDef {
|
|||
float rotation; // radians around Z
|
||||
float scale;
|
||||
bool isCritter; // critters don't do humanoid emotes
|
||||
uint32_t faction = 0; // faction template ID from creature_template
|
||||
uint32_t npcFlags = 0; // NPC interaction flags from creature_template
|
||||
};
|
||||
|
||||
struct NpcInstance {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ public:
|
|||
int getWidth() const { return fboWidth_; }
|
||||
int getHeight() const { return fboHeight_; }
|
||||
|
||||
CharacterRenderer* getCharacterRenderer() { return charRenderer_.get(); }
|
||||
uint32_t getInstanceId() const { return instanceId_; }
|
||||
uint32_t getModelId() const { return PREVIEW_MODEL_ID; }
|
||||
bool isModelLoaded() const { return modelLoaded_; }
|
||||
|
||||
private:
|
||||
void createFBO();
|
||||
void destroyFBO();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
#include "rendering/world_map.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "ui/inventory_screen.hpp"
|
||||
#include "ui/quest_log_screen.hpp"
|
||||
#include "ui/spellbook_screen.hpp"
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "game/inventory.hpp"
|
||||
#include "game/character.hpp"
|
||||
#include "game/world_packets.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <imgui.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline { class AssetManager; }
|
||||
namespace rendering { class CharacterPreview; class CharacterRenderer; }
|
||||
namespace game { class GameHandler; }
|
||||
namespace ui {
|
||||
|
||||
class InventoryScreen {
|
||||
public:
|
||||
~InventoryScreen();
|
||||
|
||||
/// Render bags window (B key). Positioned at bottom of screen.
|
||||
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
||||
|
||||
/// Render character screen (C key). Standalone equipment window.
|
||||
void renderCharacterScreen(game::Inventory& inventory);
|
||||
void renderCharacterScreen(game::GameHandler& gameHandler);
|
||||
|
||||
bool isOpen() const { return open; }
|
||||
void toggle() { open = !open; }
|
||||
|
|
@ -31,6 +39,21 @@ public:
|
|||
gameHandler_ = handler;
|
||||
}
|
||||
|
||||
/// Set asset manager for icon/model loading
|
||||
void setAssetManager(pipeline::AssetManager* am) { assetManager_ = am; }
|
||||
|
||||
/// Store player appearance for character preview
|
||||
void setPlayerAppearance(game::Race race, game::Gender gender,
|
||||
uint8_t skin, uint8_t face,
|
||||
uint8_t hairStyle, uint8_t hairColor,
|
||||
uint8_t facialHair);
|
||||
|
||||
/// Mark the character preview as needing equipment update
|
||||
void markPreviewDirty() { previewDirty_ = true; }
|
||||
|
||||
/// Update the preview animation (call each frame)
|
||||
void updatePreview(float deltaTime);
|
||||
|
||||
/// Returns true if equipment changed since last call, and clears the flag.
|
||||
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
||||
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
||||
|
|
@ -48,6 +71,30 @@ private:
|
|||
bool vendorMode_ = false;
|
||||
game::GameHandler* gameHandler_ = nullptr;
|
||||
|
||||
// Asset manager for icons and preview
|
||||
pipeline::AssetManager* assetManager_ = nullptr;
|
||||
|
||||
// Item icon cache: displayInfoId -> GL texture
|
||||
std::unordered_map<uint32_t, GLuint> iconCache_;
|
||||
GLuint getItemIcon(uint32_t displayInfoId);
|
||||
|
||||
// Character model preview
|
||||
std::unique_ptr<rendering::CharacterPreview> charPreview_;
|
||||
bool previewInitialized_ = false;
|
||||
bool previewDirty_ = false;
|
||||
|
||||
// Stored player appearance for preview
|
||||
game::Race playerRace_ = game::Race::HUMAN;
|
||||
game::Gender playerGender_ = game::Gender::MALE;
|
||||
uint8_t playerSkin_ = 0;
|
||||
uint8_t playerFace_ = 0;
|
||||
uint8_t playerHairStyle_ = 0;
|
||||
uint8_t playerHairColor_ = 0;
|
||||
uint8_t playerFacialHair_ = 0;
|
||||
|
||||
void initPreview();
|
||||
void updatePreviewEquipment(game::Inventory& inventory);
|
||||
|
||||
// Drag-and-drop held item state
|
||||
bool holdingItem = false;
|
||||
game::ItemDef heldItem;
|
||||
|
|
@ -58,6 +105,7 @@ private:
|
|||
|
||||
void renderEquipmentPanel(game::Inventory& inventory);
|
||||
void renderBackpackPanel(game::Inventory& inventory);
|
||||
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel);
|
||||
|
||||
// Slot rendering with interaction support
|
||||
enum class SlotKind { BACKPACK, EQUIPMENT };
|
||||
|
|
|
|||
|
|
@ -626,6 +626,30 @@ void Application::setupUICallbacks() {
|
|||
loadOnlineWorldTerrain(mapId, x, y, z);
|
||||
});
|
||||
|
||||
// Load faction hostility map from FactionTemplate.dbc (used for both single-player and online)
|
||||
if (assetManager && assetManager->isInitialized()) {
|
||||
if (auto dbc = assetManager->loadDBC("FactionTemplate.dbc"); dbc && dbc->isLoaded()) {
|
||||
uint32_t playerFriendGroup = 0;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
if (dbc->getUInt32(i, 0) == 1) { // Human player faction template
|
||||
playerFriendGroup = dbc->getUInt32(i, 4) | dbc->getUInt32(i, 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::unordered_map<uint32_t, bool> factionMap;
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
||||
uint32_t friendGroup = dbc->getUInt32(i, 4);
|
||||
bool hostile = (enemyGroup & playerFriendGroup) != 0;
|
||||
bool friendly = (friendGroup & playerFriendGroup) != 0;
|
||||
factionMap[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0);
|
||||
}
|
||||
gameHandler->setFactionHostileMap(std::move(factionMap));
|
||||
LOG_INFO("Loaded faction hostility data (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")");
|
||||
}
|
||||
}
|
||||
|
||||
// Creature spawn callback (online mode) - spawn creature models
|
||||
gameHandler->setCreatureSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
// Queue spawns to avoid hanging when many creatures appear at once
|
||||
|
|
|
|||
|
|
@ -835,6 +835,11 @@ void GameHandler::update(float deltaTime) {
|
|||
// Update combat text (Phase 2)
|
||||
updateCombatText(deltaTime);
|
||||
|
||||
// Update entity movement interpolation (keeps targeting in sync with visuals)
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
entity->updateMovement(deltaTime);
|
||||
}
|
||||
|
||||
// Single-player local combat
|
||||
if (singlePlayerMode_) {
|
||||
updateLocalCombat(deltaTime);
|
||||
|
|
@ -2480,6 +2485,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (it != block.fields.end() && it->second != 0) {
|
||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||
unit->setEntry(it->second);
|
||||
// Set name from cache immediately if available
|
||||
std::string cached = getCachedCreatureName(it->second);
|
||||
if (!cached.empty()) {
|
||||
unit->setName(cached);
|
||||
}
|
||||
queryCreatureInfo(it->second, block.guid);
|
||||
}
|
||||
}
|
||||
|
|
@ -2493,6 +2503,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
case 25: unit->setPower(val); break;
|
||||
case 32: unit->setMaxHealth(val); break;
|
||||
case 33: unit->setMaxPower(val); break;
|
||||
case 55: unit->setFactionTemplate(val); break; // UNIT_FIELD_FACTIONTEMPLATE
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||
|
|
@ -2500,6 +2511,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
default: break;
|
||||
}
|
||||
}
|
||||
// Determine hostility from faction template for online creatures
|
||||
if (unit->getFactionTemplate() != 0) {
|
||||
unit->setHostile(isHostileFaction(unit->getFactionTemplate()));
|
||||
}
|
||||
// Trigger creature spawn callback for units with displayId
|
||||
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() != 0) {
|
||||
if (creatureSpawnCallback_) {
|
||||
|
|
@ -2591,6 +2606,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
case 33: unit->setMaxPower(val); break;
|
||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||
case 54: unit->setLevel(val); break;
|
||||
case 55: // UNIT_FIELD_FACTIONTEMPLATE
|
||||
unit->setFactionTemplate(val);
|
||||
unit->setHostile(isHostileFaction(val));
|
||||
break;
|
||||
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||
default: break;
|
||||
}
|
||||
|
|
@ -3191,8 +3210,9 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// Set entity to destination for targeting/logic; renderer interpolates visually
|
||||
entity->setPosition(destCanonical.x, destCanonical.y, destCanonical.z, orientation);
|
||||
// Interpolate entity position alongside renderer (so targeting matches visual)
|
||||
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
||||
orientation, data.duration / 1000.0f);
|
||||
|
||||
// Notify renderer to smoothly move the creature
|
||||
if (creatureMoveCallback_) {
|
||||
|
|
|
|||
|
|
@ -497,6 +497,8 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
|||
uint32_t level = 1;
|
||||
uint32_t health = 100;
|
||||
std::string m2Path;
|
||||
uint32_t faction = 0;
|
||||
uint32_t npcFlags = 0;
|
||||
};
|
||||
std::unordered_map<uint32_t, TemplateRow> templates;
|
||||
|
||||
|
|
@ -546,20 +548,24 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
|||
}
|
||||
};
|
||||
|
||||
// Parse creature_template.sql: entry, modelid1(displayId), name, minlevel.
|
||||
// Parse creature_template.sql: entry, modelid1(displayId), name, minlevel, faction, npcflag.
|
||||
{
|
||||
std::ifstream in(tmplPath);
|
||||
processInsertStatements(in, [&](const std::vector<std::string>& cols) {
|
||||
if (cols.size() < 16) return true;
|
||||
if (cols.size() < 19) return true;
|
||||
try {
|
||||
uint32_t entry = static_cast<uint32_t>(std::stoul(cols[0]));
|
||||
uint32_t displayId = static_cast<uint32_t>(std::stoul(cols[6]));
|
||||
std::string name = unquoteSqlString(cols[10]);
|
||||
uint32_t minLevel = static_cast<uint32_t>(std::stoul(cols[14]));
|
||||
uint32_t faction = static_cast<uint32_t>(std::stoul(cols[17]));
|
||||
uint32_t npcflag = static_cast<uint32_t>(std::stoul(cols[18]));
|
||||
TemplateRow tr;
|
||||
tr.name = name.empty() ? ("Creature " + std::to_string(entry)) : name;
|
||||
tr.level = std::max(1u, minLevel);
|
||||
tr.health = 150 + tr.level * 35;
|
||||
tr.faction = faction;
|
||||
tr.npcFlags = npcflag;
|
||||
auto itModel = displayToModel.find(displayId);
|
||||
if (itModel != displayToModel.end()) {
|
||||
auto itPath = modelToPath.find(itModel->second);
|
||||
|
|
@ -604,6 +610,8 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
|||
def.level = it->second.level;
|
||||
def.health = std::max(it->second.health, curhealth);
|
||||
def.m2Path = it->second.m2Path;
|
||||
def.faction = it->second.faction;
|
||||
def.npcFlags = it->second.npcFlags;
|
||||
} else {
|
||||
def.entry = entry;
|
||||
def.name = "Creature " + std::to_string(entry);
|
||||
|
|
@ -709,6 +717,44 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
|||
}
|
||||
}
|
||||
|
||||
// Build faction hostility lookup from FactionTemplate.dbc.
|
||||
// Player is Alliance (Human) — faction template 1, friendGroup includes Alliance mask.
|
||||
// A creature is hostile if its enemyGroup overlaps the player's friendGroup.
|
||||
std::unordered_map<uint32_t, bool> factionHostile; // factionTemplateId → hostile to player
|
||||
{
|
||||
// FactionTemplate.dbc columns (3.3.5a):
|
||||
// 0: ID, 1: Faction, 2: Flags, 3: FactionGroup, 4: FriendGroup, 5: EnemyGroup,
|
||||
// 6-9: Enemies[4], 10-13: Friends[4]
|
||||
uint32_t playerFriendGroup = 0;
|
||||
if (auto dbc = am->loadDBC("FactionTemplate.dbc"); dbc && dbc->isLoaded()) {
|
||||
// First pass: find player faction template (ID 1) friendGroup
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
if (dbc->getUInt32(i, 0) == 1) {
|
||||
playerFriendGroup = dbc->getUInt32(i, 4); // FriendGroup
|
||||
// Also include our own factionGroup as friendly
|
||||
playerFriendGroup |= dbc->getUInt32(i, 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Second pass: classify each faction template
|
||||
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
|
||||
uint32_t id = dbc->getUInt32(i, 0);
|
||||
uint32_t enemyGroup = dbc->getUInt32(i, 5);
|
||||
uint32_t friendGroup = dbc->getUInt32(i, 4);
|
||||
// Hostile if creature's enemy groups overlap player's faction/friend groups
|
||||
bool hostile = (enemyGroup & playerFriendGroup) != 0;
|
||||
// Friendly only if creature's friendGroup explicitly includes player's groups
|
||||
bool friendly = (friendGroup & playerFriendGroup) != 0;
|
||||
// Hostile if explicitly hostile, or if no explicit relationship at all
|
||||
factionHostile[id] = hostile ? true : (!friendly && enemyGroup == 0 && friendGroup == 0);
|
||||
}
|
||||
LOG_INFO("NpcManager: loaded ", dbc->getRecordCount(),
|
||||
" faction templates (playerFriendGroup=0x", std::hex, playerFriendGroup, std::dec, ")");
|
||||
} else {
|
||||
LOG_WARNING("NpcManager: FactionTemplate.dbc not available, all NPCs default to hostile");
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn each NPC instance
|
||||
for (const auto* sPtr : active) {
|
||||
const auto& s = *sPtr;
|
||||
|
|
@ -751,6 +797,12 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
|||
if (s.entry != 0) {
|
||||
unit->setEntry(s.entry);
|
||||
}
|
||||
unit->setNpcFlags(s.npcFlags);
|
||||
unit->setFactionTemplate(s.faction);
|
||||
|
||||
// Determine hostility from faction template
|
||||
auto fIt = factionHostile.find(s.faction);
|
||||
unit->setHostile(fIt != factionHostile.end() ? fIt->second : true);
|
||||
|
||||
// Store canonical WoW coordinates for targeting/server compatibility
|
||||
glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos);
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,10 @@ void Renderer::renderSelectionCircle(const glm::mat4& view, const glm::mat4& pro
|
|||
if (!selCircleVisible) return;
|
||||
initSelectionCircle();
|
||||
|
||||
glm::mat4 model = glm::translate(glm::mat4(1.0f), selCirclePos);
|
||||
// Small Z offset to prevent clipping under terrain
|
||||
glm::vec3 raisedPos = selCirclePos;
|
||||
raisedPos.z += 0.15f;
|
||||
glm::mat4 model = glm::translate(glm::mat4(1.0f), raisedPos);
|
||||
model = glm::scale(model, glm::vec3(selCircleRadius));
|
||||
|
||||
glm::mat4 mvp = projection * view * model;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "ui/game_screen.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/spawn_presets.hpp"
|
||||
|
|
@ -101,6 +102,28 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
// Spellbook (P key toggle handled inside)
|
||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
||||
|
||||
// Set up inventory screen asset manager + player appearance (once)
|
||||
{
|
||||
static bool inventoryScreenInit = false;
|
||||
if (!inventoryScreenInit) {
|
||||
auto* am = core::Application::getInstance().getAssetManager();
|
||||
if (am) {
|
||||
inventoryScreen.setAssetManager(am);
|
||||
const auto* ch = gameHandler.getActiveCharacter();
|
||||
if (ch) {
|
||||
uint8_t skin = ch->appearanceBytes & 0xFF;
|
||||
uint8_t face = (ch->appearanceBytes >> 8) & 0xFF;
|
||||
uint8_t hairStyle = (ch->appearanceBytes >> 16) & 0xFF;
|
||||
uint8_t hairColor = (ch->appearanceBytes >> 24) & 0xFF;
|
||||
inventoryScreen.setPlayerAppearance(
|
||||
ch->race, ch->gender, skin, face,
|
||||
hairStyle, hairColor, ch->facialFeatures);
|
||||
inventoryScreenInit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set vendor mode before rendering inventory
|
||||
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
||||
|
||||
|
|
@ -113,7 +136,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||
|
||||
// Character screen (C key toggle handled inside render())
|
||||
inventoryScreen.renderCharacterScreen(gameHandler.getInventory());
|
||||
inventoryScreen.renderCharacterScreen(gameHandler);
|
||||
|
||||
if (inventoryScreen.consumeInventoryDirty()) {
|
||||
gameHandler.notifyInventoryChanged();
|
||||
|
|
@ -124,6 +147,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
updateCharacterTextures(gameHandler.getInventory());
|
||||
core::Application::getInstance().loadEquippedWeapons();
|
||||
gameHandler.notifyEquipmentChanged();
|
||||
inventoryScreen.markPreviewDirty();
|
||||
}
|
||||
|
||||
// Update renderer face-target position and selection circle
|
||||
|
|
@ -143,10 +167,10 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
||||
} else if (unit->isInteractable()) {
|
||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
||||
} else {
|
||||
} else if (unit->isHostile()) {
|
||||
circleColor = glm::vec3(1.0f, 0.2f, 0.2f); // red (hostile)
|
||||
} else {
|
||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
||||
}
|
||||
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
||||
|
|
@ -504,17 +528,21 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||
gameHandler.lootTarget(target->getGuid());
|
||||
} else if (gameHandler.isSinglePlayerMode()) {
|
||||
// Single-player: toggle auto-attack
|
||||
if (gameHandler.isAutoAttacking()) {
|
||||
gameHandler.stopAutoAttack();
|
||||
} else {
|
||||
gameHandler.startAutoAttack(target->getGuid());
|
||||
// Single-player: interact with friendly NPCs, attack hostiles
|
||||
if (!unit->isHostile() && unit->isInteractable()) {
|
||||
gameHandler.interactWithNpc(target->getGuid());
|
||||
} else if (unit->isHostile()) {
|
||||
if (gameHandler.isAutoAttacking()) {
|
||||
gameHandler.stopAutoAttack();
|
||||
} else {
|
||||
gameHandler.startAutoAttack(target->getGuid());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Online mode: interact with friendly NPCs, attack hostiles
|
||||
if (unit->isInteractable()) {
|
||||
if (!unit->isHostile() && unit->isInteractable()) {
|
||||
gameHandler.interactWithNpc(target->getGuid());
|
||||
} else {
|
||||
} else if (unit->isHostile()) {
|
||||
if (gameHandler.isAutoAttacking()) {
|
||||
gameHandler.stopAutoAttack();
|
||||
} else {
|
||||
|
|
@ -643,10 +671,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|||
auto u = std::static_pointer_cast<game::Unit>(target);
|
||||
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
} else if (u->isInteractable()) {
|
||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
} else {
|
||||
} else if (u->isHostile()) {
|
||||
hostileColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
} else {
|
||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
#include "ui/inventory_screen.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <imgui.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace ui {
|
||||
|
||||
InventoryScreen::~InventoryScreen() {
|
||||
// Clean up icon textures
|
||||
for (auto& [id, tex] : iconCache_) {
|
||||
if (tex) glDeleteTextures(1, &tex);
|
||||
}
|
||||
iconCache_.clear();
|
||||
}
|
||||
|
||||
ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
||||
switch (quality) {
|
||||
case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey
|
||||
|
|
@ -20,6 +36,272 @@ ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Item Icon Loading
|
||||
// ============================================================
|
||||
|
||||
GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
|
||||
if (displayInfoId == 0 || !assetManager_) return 0;
|
||||
|
||||
auto it = iconCache_.find(displayInfoId);
|
||||
if (it != iconCache_.end()) return it->second;
|
||||
|
||||
// Load ItemDisplayInfo.dbc
|
||||
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||
if (!displayInfoDbc) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||
if (recIdx < 0) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Field 5 = inventoryIcon_1
|
||||
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 5);
|
||||
if (iconName.empty()) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string iconPath = "Interface\\Icons\\" + iconName + ".blp";
|
||||
auto blpData = assetManager_->readFile(iconPath);
|
||||
if (blpData.empty()) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto image = pipeline::BLPLoader::load(blpData);
|
||||
if (!image.isValid()) {
|
||||
iconCache_[displayInfoId] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint texId = 0;
|
||||
glGenTextures(1, &texId);
|
||||
glBindTexture(GL_TEXTURE_2D, texId);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
iconCache_[displayInfoId] = texId;
|
||||
return texId;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Character Model Preview
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::setPlayerAppearance(game::Race race, game::Gender gender,
|
||||
uint8_t skin, uint8_t face,
|
||||
uint8_t hairStyle, uint8_t hairColor,
|
||||
uint8_t facialHair) {
|
||||
playerRace_ = race;
|
||||
playerGender_ = gender;
|
||||
playerSkin_ = skin;
|
||||
playerFace_ = face;
|
||||
playerHairStyle_ = hairStyle;
|
||||
playerHairColor_ = hairColor;
|
||||
playerFacialHair_ = facialHair;
|
||||
// Force preview reload on next render
|
||||
previewInitialized_ = false;
|
||||
}
|
||||
|
||||
void InventoryScreen::initPreview() {
|
||||
if (previewInitialized_ || !assetManager_) return;
|
||||
|
||||
if (!charPreview_) {
|
||||
charPreview_ = std::make_unique<rendering::CharacterPreview>();
|
||||
if (!charPreview_->initialize(assetManager_)) {
|
||||
LOG_WARNING("InventoryScreen: failed to init CharacterPreview");
|
||||
charPreview_.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
charPreview_->loadCharacter(playerRace_, playerGender_,
|
||||
playerSkin_, playerFace_,
|
||||
playerHairStyle_, playerHairColor_,
|
||||
playerFacialHair_);
|
||||
previewInitialized_ = true;
|
||||
previewDirty_ = true; // apply equipment on first load
|
||||
}
|
||||
|
||||
void InventoryScreen::updatePreview(float deltaTime) {
|
||||
if (charPreview_ && previewInitialized_) {
|
||||
charPreview_->update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryScreen::updatePreviewEquipment(game::Inventory& inventory) {
|
||||
if (!charPreview_ || !charPreview_->isModelLoaded() || !assetManager_) return;
|
||||
|
||||
auto* charRenderer = charPreview_->getCharacterRenderer();
|
||||
uint32_t instanceId = charPreview_->getInstanceId();
|
||||
if (!charRenderer || instanceId == 0) return;
|
||||
|
||||
// --- Geosets (mirroring GameScreen::updateCharacterGeosets) ---
|
||||
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||
|
||||
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
||||
if (!displayInfoDbc || displayInfoId == 0) return 0;
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||
if (recIdx < 0) return 0;
|
||||
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
||||
};
|
||||
|
||||
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!slot.empty()) {
|
||||
for (uint8_t t : types) {
|
||||
if (slot.item.inventoryType == t)
|
||||
return slot.item.displayInfoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!slot.empty()) {
|
||||
for (uint8_t t : types) {
|
||||
if (slot.item.inventoryType == t) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
std::unordered_set<uint16_t> geosets;
|
||||
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
|
||||
|
||||
// Hair geoset: group 1 = 100 + hairStyle + 1
|
||||
geosets.insert(static_cast<uint16_t>(100 + playerHairStyle_ + 1));
|
||||
// Facial hair geoset: group 2 = 200 + facialHair + 1
|
||||
geosets.insert(static_cast<uint16_t>(200 + playerFacialHair_ + 1));
|
||||
geosets.insert(701); // Ears
|
||||
|
||||
// Chest/Shirt
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : 501));
|
||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||
if (gg3 > 0) {
|
||||
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
||||
}
|
||||
}
|
||||
|
||||
// Legs
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({7});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
||||
}
|
||||
}
|
||||
|
||||
// Feet
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({8});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
||||
}
|
||||
|
||||
// Gloves
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({10});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
||||
}
|
||||
|
||||
// Cloak
|
||||
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
||||
|
||||
// Tabard
|
||||
if (hasEquippedType({19})) {
|
||||
geosets.insert(1201);
|
||||
}
|
||||
|
||||
charRenderer->setActiveGeosets(instanceId, geosets);
|
||||
|
||||
// --- Textures (mirroring GameScreen::updateCharacterTextures) ---
|
||||
auto& app = core::Application::getInstance();
|
||||
const auto& bodySkinPath = app.getBodySkinPath();
|
||||
const auto& underwearPaths = app.getUnderwearPaths();
|
||||
|
||||
if (bodySkinPath.empty() || !displayInfoDbc) return;
|
||||
|
||||
static const char* componentDirs[] = {
|
||||
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
||||
"TorsoUpperTexture", "TorsoLowerTexture",
|
||||
"LegUpperTexture", "LegLowerTexture", "FootTexture",
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, std::string>> regionLayers;
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
||||
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
||||
if (recIdx < 0) continue;
|
||||
|
||||
for (int region = 0; region < 8; region++) {
|
||||
uint32_t fieldIdx = 15 + region;
|
||||
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
||||
if (texName.empty()) continue;
|
||||
|
||||
std::string base = "Item\\TextureComponents\\" +
|
||||
std::string(componentDirs[region]) + "\\" + texName;
|
||||
std::string genderSuffix = (playerGender_ == game::Gender::FEMALE) ? "_F.blp" : "_M.blp";
|
||||
std::string genderPath = base + genderSuffix;
|
||||
std::string unisexPath = base + "_U.blp";
|
||||
std::string fullPath;
|
||||
if (assetManager_->fileExists(genderPath)) {
|
||||
fullPath = genderPath;
|
||||
} else if (assetManager_->fileExists(unisexPath)) {
|
||||
fullPath = unisexPath;
|
||||
} else {
|
||||
fullPath = base + ".blp";
|
||||
}
|
||||
regionLayers.emplace_back(region, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the skin texture slot index in the preview model
|
||||
// The preview model uses model ID PREVIEW_MODEL_ID; find slot for type-1 (body skin)
|
||||
const auto* modelData = charRenderer->getModelData(charPreview_->getModelId());
|
||||
uint32_t skinSlot = 0;
|
||||
if (modelData) {
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
if (modelData->textures[ti].type == 1) {
|
||||
skinSlot = static_cast<uint32_t>(ti);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
||||
if (newTex != 0) {
|
||||
charRenderer->setModelTexture(charPreview_->getModelId(), skinSlot, newTex);
|
||||
}
|
||||
|
||||
previewDirty_ = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Equip slot helpers
|
||||
// ============================================================
|
||||
|
||||
game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv) {
|
||||
switch (inventoryType) {
|
||||
case 1: return game::EquipSlot::HEAD;
|
||||
|
|
@ -191,19 +473,28 @@ void InventoryScreen::renderHeldItem() {
|
|||
ImVec4 qColor = getQualityColor(heldItem.quality);
|
||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
IM_COL32(40, 35, 30, 200));
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
// Try to show icon
|
||||
GLuint iconTex = getItemIcon(heldItem.displayInfoId);
|
||||
if (iconTex) {
|
||||
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
||||
ImVec2(pos.x + size, pos.y + size));
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
} else {
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
IM_COL32(40, 35, 30, 200));
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
|
||||
char abbr[4] = {};
|
||||
if (!heldItem.name.empty()) {
|
||||
abbr[0] = heldItem.name[0];
|
||||
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
|
||||
char abbr[4] = {};
|
||||
if (!heldItem.name.empty()) {
|
||||
abbr[0] = heldItem.name[0];
|
||||
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
|
||||
if (heldItem.stackCount > 1) {
|
||||
char countStr[16];
|
||||
|
|
@ -301,14 +592,32 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Character screen (C key) — standalone equipment window
|
||||
// Character screen (C key) — equipment + model preview + stats
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::renderCharacterScreen(game::Inventory& inventory) {
|
||||
void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
||||
if (!characterOpen) return;
|
||||
|
||||
auto& inventory = gameHandler.getInventory();
|
||||
|
||||
// Lazy-init the preview
|
||||
if (!previewInitialized_ && assetManager_) {
|
||||
initPreview();
|
||||
}
|
||||
|
||||
// Update preview equipment if dirty
|
||||
if (previewDirty_ && charPreview_ && previewInitialized_) {
|
||||
updatePreviewEquipment(inventory);
|
||||
}
|
||||
|
||||
// Update and render the preview FBO
|
||||
if (charPreview_ && previewInitialized_) {
|
||||
charPreview_->update(ImGui::GetIO().DeltaTime);
|
||||
charPreview_->render();
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(220.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(350.0f, 650.0f), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
||||
|
|
@ -316,8 +625,26 @@ void InventoryScreen::renderCharacterScreen(game::Inventory& inventory) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clamp window position within screen after resize
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImVec2 pos = ImGui::GetWindowPos();
|
||||
ImVec2 sz = ImGui::GetWindowSize();
|
||||
bool clamped = false;
|
||||
if (pos.x + sz.x > io.DisplaySize.x) { pos.x = std::max(0.0f, io.DisplaySize.x - sz.x); clamped = true; }
|
||||
if (pos.y + sz.y > io.DisplaySize.y) { pos.y = std::max(0.0f, io.DisplaySize.y - sz.y); clamped = true; }
|
||||
if (pos.x < 0.0f) { pos.x = 0.0f; clamped = true; }
|
||||
if (pos.y < 0.0f) { pos.y = 0.0f; clamped = true; }
|
||||
if (clamped) ImGui::SetWindowPos(pos);
|
||||
}
|
||||
|
||||
renderEquipmentPanel(inventory);
|
||||
|
||||
// Stats panel
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel());
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// If both bags and character are open, allow drag-and-drop between them
|
||||
|
|
@ -345,10 +672,17 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
};
|
||||
|
||||
constexpr float slotSize = 36.0f;
|
||||
constexpr float spacing = 4.0f;
|
||||
constexpr float previewW = 140.0f;
|
||||
|
||||
// Calculate column positions for the 3-column layout
|
||||
float contentStartX = ImGui::GetCursorPosX();
|
||||
float rightColX = contentStartX + slotSize + 8.0f + previewW + 8.0f;
|
||||
|
||||
int rows = 8;
|
||||
float previewStartY = ImGui::GetCursorScreenPos().y;
|
||||
|
||||
for (int r = 0; r < rows; r++) {
|
||||
// Left column
|
||||
{
|
||||
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
||||
const char* label = game::getEquipSlotName(leftSlots[r]);
|
||||
|
|
@ -360,8 +694,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::SameLine(slotSize + spacing + 60.0f);
|
||||
|
||||
// Right column
|
||||
ImGui::SameLine(rightColX);
|
||||
{
|
||||
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
||||
const char* label = game::getEquipSlotName(rightSlots[r]);
|
||||
|
|
@ -374,6 +708,44 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
}
|
||||
}
|
||||
|
||||
float previewEndY = ImGui::GetCursorScreenPos().y;
|
||||
|
||||
// Draw the 3D character preview in the center column
|
||||
if (charPreview_ && previewInitialized_ && charPreview_->getTextureId()) {
|
||||
float previewX = ImGui::GetWindowPos().x + contentStartX + slotSize + 8.0f;
|
||||
float previewH = previewEndY - previewStartY;
|
||||
// Maintain aspect ratio
|
||||
float texAspect = static_cast<float>(charPreview_->getWidth()) / static_cast<float>(charPreview_->getHeight());
|
||||
float displayW = previewW;
|
||||
float displayH = displayW / texAspect;
|
||||
if (displayH > previewH) {
|
||||
displayH = previewH;
|
||||
displayW = displayH * texAspect;
|
||||
}
|
||||
float offsetX = previewX + (previewW - displayW) * 0.5f;
|
||||
float offsetY = previewStartY + (previewH - displayH) * 0.5f;
|
||||
|
||||
ImVec2 pMin(offsetX, offsetY);
|
||||
ImVec2 pMax(offsetX + displayW, offsetY + displayH);
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
// Background for preview area
|
||||
drawList->AddRectFilled(pMin, pMax, IM_COL32(13, 13, 25, 255));
|
||||
drawList->AddImage(
|
||||
(ImTextureID)(uintptr_t)charPreview_->getTextureId(),
|
||||
pMin, pMax,
|
||||
ImVec2(0, 1), ImVec2(1, 0)); // flip Y for GL
|
||||
drawList->AddRect(pMin, pMax, IM_COL32(60, 60, 80, 200));
|
||||
|
||||
// Drag-to-rotate: detect mouse drag over the preview image
|
||||
ImGui::SetCursorScreenPos(pMin);
|
||||
ImGui::InvisibleButton("##charPreviewDrag", ImVec2(displayW, displayH));
|
||||
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||
float dx = ImGui::GetIO().MouseDelta.x;
|
||||
charPreview_->rotate(dx * 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Weapon row
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
|
@ -396,6 +768,60 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Stats Panel
|
||||
// ============================================================
|
||||
|
||||
void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel) {
|
||||
// Sum equipment stats
|
||||
int32_t totalArmor = 0;
|
||||
int32_t totalStr = 0, totalAgi = 0, totalSta = 0, totalInt = 0, totalSpi = 0;
|
||||
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (slot.empty()) continue;
|
||||
totalArmor += slot.item.armor;
|
||||
totalStr += slot.item.strength;
|
||||
totalAgi += slot.item.agility;
|
||||
totalSta += slot.item.stamina;
|
||||
totalInt += slot.item.intellect;
|
||||
totalSpi += slot.item.spirit;
|
||||
}
|
||||
|
||||
// Base stats: 20 + level
|
||||
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
||||
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
||||
ImVec4 gray(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
|
||||
// Armor (no base)
|
||||
if (totalArmor > 0) {
|
||||
ImGui::TextColored(gold, "Armor: %d", totalArmor);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "Armor: 0");
|
||||
}
|
||||
|
||||
// Helper to render a stat line
|
||||
auto renderStat = [&](const char* name, int32_t equipBonus) {
|
||||
int32_t total = baseStat + equipBonus;
|
||||
if (equipBonus > 0) {
|
||||
ImGui::TextColored(white, "%s: %d", name, total);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(green, "(+%d)", equipBonus);
|
||||
} else {
|
||||
ImGui::TextColored(gray, "%s: %d", name, total);
|
||||
}
|
||||
};
|
||||
|
||||
renderStat("Strength", totalStr);
|
||||
renderStat("Agility", totalAgi);
|
||||
renderStat("Stamina", totalSta);
|
||||
renderStat("Intellect", totalInt);
|
||||
renderStat("Spirit", totalSpi);
|
||||
}
|
||||
|
||||
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
||||
ImGui::Separator();
|
||||
|
|
@ -511,18 +937,27 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
borderCol = IM_COL32(0, 200, 0, 220);
|
||||
}
|
||||
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
// Try to show icon
|
||||
GLuint iconTex = getItemIcon(item.displayInfoId);
|
||||
if (iconTex) {
|
||||
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
||||
ImVec2(pos.x + size, pos.y + size));
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
} else {
|
||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
||||
borderCol, 0.0f, 0, 2.0f);
|
||||
|
||||
char abbr[4] = {};
|
||||
if (!item.name.empty()) {
|
||||
abbr[0] = item.name[0];
|
||||
if (item.name.size() > 1) abbr[1] = item.name[1];
|
||||
char abbr[4] = {};
|
||||
if (!item.name.empty()) {
|
||||
abbr[0] = item.name[0];
|
||||
if (item.name.size() > 1) abbr[1] = item.name[1];
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
}
|
||||
float textW = ImGui::CalcTextSize(abbr).x;
|
||||
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
|
||||
|
||||
if (item.stackCount > 1) {
|
||||
char countStr[16];
|
||||
|
|
@ -654,12 +1089,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
|||
ImGui::Text("%d Armor", item.armor);
|
||||
}
|
||||
|
||||
// Stats
|
||||
if (item.stamina != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Stamina", item.stamina);
|
||||
if (item.strength != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Strength", item.strength);
|
||||
if (item.agility != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Agility", item.agility);
|
||||
if (item.intellect != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Intellect", item.intellect);
|
||||
if (item.spirit != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Spirit", item.spirit);
|
||||
// Stats with "Equip:" prefix style
|
||||
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
ImVec4 red(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
|
||||
auto renderStat = [&](int32_t val, const char* name) {
|
||||
if (val > 0) {
|
||||
ImGui::TextColored(green, "+%d %s", val, name);
|
||||
} else if (val < 0) {
|
||||
ImGui::TextColored(red, "%d %s", val, name);
|
||||
}
|
||||
};
|
||||
|
||||
renderStat(item.stamina, "Stamina");
|
||||
renderStat(item.strength, "Strength");
|
||||
renderStat(item.agility, "Agility");
|
||||
renderStat(item.intellect, "Intellect");
|
||||
renderStat(item.spirit, "Spirit");
|
||||
|
||||
// Stack info
|
||||
if (item.maxStack > 1) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue