mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +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;
|
y = py;
|
||||||
z = pz;
|
z = pz;
|
||||||
orientation = o;
|
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
|
// Object type
|
||||||
ObjectType getType() const { return type; }
|
ObjectType getType() const { return type; }
|
||||||
void setType(ObjectType t) { type = t; }
|
void setType(ObjectType t) { type = t; }
|
||||||
|
|
@ -108,6 +139,13 @@ protected:
|
||||||
|
|
||||||
// Update fields (dynamic values)
|
// Update fields (dynamic values)
|
||||||
std::map<uint16_t, uint32_t> fields;
|
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)
|
// Returns true if NPC has interaction flags (gossip/vendor/quest/trainer)
|
||||||
bool isInteractable() const { return npcFlags != 0; }
|
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:
|
protected:
|
||||||
std::string name;
|
std::string name;
|
||||||
uint32_t health = 0;
|
uint32_t health = 0;
|
||||||
|
|
@ -174,6 +218,8 @@ protected:
|
||||||
uint32_t displayId = 0;
|
uint32_t displayId = 0;
|
||||||
uint32_t unitFlags = 0;
|
uint32_t unitFlags = 0;
|
||||||
uint32_t npcFlags = 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)>;
|
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||||||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
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)
|
// Creature move callback (online mode - triggered by SMSG_MONSTER_MOVE)
|
||||||
// Parameters: guid, x, y, z (canonical), duration_ms (0 = instant)
|
// 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)>;
|
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
|
||||||
|
|
@ -644,6 +647,13 @@ private:
|
||||||
// Quest log
|
// Quest log
|
||||||
std::vector<QuestLogEntry> questLog_;
|
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
|
// Vendor
|
||||||
bool vendorWindowOpen = false;
|
bool vendorWindowOpen = false;
|
||||||
ListInventoryData currentVendorItems;
|
ListInventoryData currentVendorItems;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ struct NpcSpawnDef {
|
||||||
float rotation; // radians around Z
|
float rotation; // radians around Z
|
||||||
float scale;
|
float scale;
|
||||||
bool isCritter; // critters don't do humanoid emotes
|
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 {
|
struct NpcInstance {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ public:
|
||||||
int getWidth() const { return fboWidth_; }
|
int getWidth() const { return fboWidth_; }
|
||||||
int getHeight() const { return fboHeight_; }
|
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:
|
private:
|
||||||
void createFBO();
|
void createFBO();
|
||||||
void destroyFBO();
|
void destroyFBO();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
#include "rendering/world_map.hpp"
|
#include "rendering/world_map.hpp"
|
||||||
|
#include "rendering/character_preview.hpp"
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
#include "ui/quest_log_screen.hpp"
|
#include "ui/quest_log_screen.hpp"
|
||||||
#include "ui/spellbook_screen.hpp"
|
#include "ui/spellbook_screen.hpp"
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
|
#include "game/character.hpp"
|
||||||
#include "game/world_packets.hpp"
|
#include "game/world_packets.hpp"
|
||||||
|
#include <GL/glew.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
namespace pipeline { class AssetManager; }
|
||||||
|
namespace rendering { class CharacterPreview; class CharacterRenderer; }
|
||||||
namespace game { class GameHandler; }
|
namespace game { class GameHandler; }
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class InventoryScreen {
|
class InventoryScreen {
|
||||||
public:
|
public:
|
||||||
|
~InventoryScreen();
|
||||||
|
|
||||||
/// Render bags window (B key). Positioned at bottom of screen.
|
/// Render bags window (B key). Positioned at bottom of screen.
|
||||||
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
void render(game::Inventory& inventory, uint64_t moneyCopper);
|
||||||
|
|
||||||
/// Render character screen (C key). Standalone equipment window.
|
/// Render character screen (C key). Standalone equipment window.
|
||||||
void renderCharacterScreen(game::Inventory& inventory);
|
void renderCharacterScreen(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
bool isOpen() const { return open; }
|
bool isOpen() const { return open; }
|
||||||
void toggle() { open = !open; }
|
void toggle() { open = !open; }
|
||||||
|
|
@ -31,6 +39,21 @@ public:
|
||||||
gameHandler_ = handler;
|
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.
|
/// Returns true if equipment changed since last call, and clears the flag.
|
||||||
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
bool consumeEquipmentDirty() { bool d = equipmentDirty; equipmentDirty = false; return d; }
|
||||||
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
/// Returns true if any inventory slot changed since last call, and clears the flag.
|
||||||
|
|
@ -48,6 +71,30 @@ private:
|
||||||
bool vendorMode_ = false;
|
bool vendorMode_ = false;
|
||||||
game::GameHandler* gameHandler_ = nullptr;
|
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
|
// Drag-and-drop held item state
|
||||||
bool holdingItem = false;
|
bool holdingItem = false;
|
||||||
game::ItemDef heldItem;
|
game::ItemDef heldItem;
|
||||||
|
|
@ -58,6 +105,7 @@ private:
|
||||||
|
|
||||||
void renderEquipmentPanel(game::Inventory& inventory);
|
void renderEquipmentPanel(game::Inventory& inventory);
|
||||||
void renderBackpackPanel(game::Inventory& inventory);
|
void renderBackpackPanel(game::Inventory& inventory);
|
||||||
|
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel);
|
||||||
|
|
||||||
// Slot rendering with interaction support
|
// Slot rendering with interaction support
|
||||||
enum class SlotKind { BACKPACK, EQUIPMENT };
|
enum class SlotKind { BACKPACK, EQUIPMENT };
|
||||||
|
|
|
||||||
|
|
@ -626,6 +626,30 @@ void Application::setupUICallbacks() {
|
||||||
loadOnlineWorldTerrain(mapId, x, y, z);
|
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
|
// 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) {
|
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
|
// 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)
|
// Update combat text (Phase 2)
|
||||||
updateCombatText(deltaTime);
|
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
|
// Single-player local combat
|
||||||
if (singlePlayerMode_) {
|
if (singlePlayerMode_) {
|
||||||
updateLocalCombat(deltaTime);
|
updateLocalCombat(deltaTime);
|
||||||
|
|
@ -2480,6 +2485,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (it != block.fields.end() && it->second != 0) {
|
if (it != block.fields.end() && it->second != 0) {
|
||||||
auto unit = std::static_pointer_cast<Unit>(entity);
|
auto unit = std::static_pointer_cast<Unit>(entity);
|
||||||
unit->setEntry(it->second);
|
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);
|
queryCreatureInfo(it->second, block.guid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2493,6 +2503,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
case 25: unit->setPower(val); break;
|
case 25: unit->setPower(val); break;
|
||||||
case 32: unit->setMaxHealth(val); break;
|
case 32: unit->setMaxHealth(val); break;
|
||||||
case 33: unit->setMaxPower(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 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||||
case 54: unit->setLevel(val); break;
|
case 54: unit->setLevel(val); break;
|
||||||
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID
|
||||||
|
|
@ -2500,6 +2511,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
default: break;
|
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
|
// Trigger creature spawn callback for units with displayId
|
||||||
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() != 0) {
|
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() != 0) {
|
||||||
if (creatureSpawnCallback_) {
|
if (creatureSpawnCallback_) {
|
||||||
|
|
@ -2591,6 +2606,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
case 33: unit->setMaxPower(val); break;
|
case 33: unit->setMaxPower(val); break;
|
||||||
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS
|
||||||
case 54: unit->setLevel(val); break;
|
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
|
case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
@ -3191,8 +3210,9 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set entity to destination for targeting/logic; renderer interpolates visually
|
// Interpolate entity position alongside renderer (so targeting matches visual)
|
||||||
entity->setPosition(destCanonical.x, destCanonical.y, destCanonical.z, orientation);
|
entity->startMoveTo(destCanonical.x, destCanonical.y, destCanonical.z,
|
||||||
|
orientation, data.duration / 1000.0f);
|
||||||
|
|
||||||
// Notify renderer to smoothly move the creature
|
// Notify renderer to smoothly move the creature
|
||||||
if (creatureMoveCallback_) {
|
if (creatureMoveCallback_) {
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,8 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
||||||
uint32_t level = 1;
|
uint32_t level = 1;
|
||||||
uint32_t health = 100;
|
uint32_t health = 100;
|
||||||
std::string m2Path;
|
std::string m2Path;
|
||||||
|
uint32_t faction = 0;
|
||||||
|
uint32_t npcFlags = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<uint32_t, TemplateRow> templates;
|
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);
|
std::ifstream in(tmplPath);
|
||||||
processInsertStatements(in, [&](const std::vector<std::string>& cols) {
|
processInsertStatements(in, [&](const std::vector<std::string>& cols) {
|
||||||
if (cols.size() < 16) return true;
|
if (cols.size() < 19) return true;
|
||||||
try {
|
try {
|
||||||
uint32_t entry = static_cast<uint32_t>(std::stoul(cols[0]));
|
uint32_t entry = static_cast<uint32_t>(std::stoul(cols[0]));
|
||||||
uint32_t displayId = static_cast<uint32_t>(std::stoul(cols[6]));
|
uint32_t displayId = static_cast<uint32_t>(std::stoul(cols[6]));
|
||||||
std::string name = unquoteSqlString(cols[10]);
|
std::string name = unquoteSqlString(cols[10]);
|
||||||
uint32_t minLevel = static_cast<uint32_t>(std::stoul(cols[14]));
|
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;
|
TemplateRow tr;
|
||||||
tr.name = name.empty() ? ("Creature " + std::to_string(entry)) : name;
|
tr.name = name.empty() ? ("Creature " + std::to_string(entry)) : name;
|
||||||
tr.level = std::max(1u, minLevel);
|
tr.level = std::max(1u, minLevel);
|
||||||
tr.health = 150 + tr.level * 35;
|
tr.health = 150 + tr.level * 35;
|
||||||
|
tr.faction = faction;
|
||||||
|
tr.npcFlags = npcflag;
|
||||||
auto itModel = displayToModel.find(displayId);
|
auto itModel = displayToModel.find(displayId);
|
||||||
if (itModel != displayToModel.end()) {
|
if (itModel != displayToModel.end()) {
|
||||||
auto itPath = modelToPath.find(itModel->second);
|
auto itPath = modelToPath.find(itModel->second);
|
||||||
|
|
@ -604,6 +610,8 @@ std::vector<NpcSpawnDef> NpcManager::loadSpawnDefsFromAzerothCoreDb(
|
||||||
def.level = it->second.level;
|
def.level = it->second.level;
|
||||||
def.health = std::max(it->second.health, curhealth);
|
def.health = std::max(it->second.health, curhealth);
|
||||||
def.m2Path = it->second.m2Path;
|
def.m2Path = it->second.m2Path;
|
||||||
|
def.faction = it->second.faction;
|
||||||
|
def.npcFlags = it->second.npcFlags;
|
||||||
} else {
|
} else {
|
||||||
def.entry = entry;
|
def.entry = entry;
|
||||||
def.name = "Creature " + std::to_string(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
|
// Spawn each NPC instance
|
||||||
for (const auto* sPtr : active) {
|
for (const auto* sPtr : active) {
|
||||||
const auto& s = *sPtr;
|
const auto& s = *sPtr;
|
||||||
|
|
@ -751,6 +797,12 @@ void NpcManager::initialize(pipeline::AssetManager* am,
|
||||||
if (s.entry != 0) {
|
if (s.entry != 0) {
|
||||||
unit->setEntry(s.entry);
|
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
|
// Store canonical WoW coordinates for targeting/server compatibility
|
||||||
glm::vec3 spawnCanonical = core::coords::renderToCanonical(glPos);
|
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;
|
if (!selCircleVisible) return;
|
||||||
initSelectionCircle();
|
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));
|
model = glm::scale(model, glm::vec3(selCircleRadius));
|
||||||
|
|
||||||
glm::mat4 mvp = projection * view * model;
|
glm::mat4 mvp = projection * view * model;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/game_screen.hpp"
|
#include "ui/game_screen.hpp"
|
||||||
|
#include "rendering/character_preview.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "core/coordinates.hpp"
|
#include "core/coordinates.hpp"
|
||||||
#include "core/spawn_presets.hpp"
|
#include "core/spawn_presets.hpp"
|
||||||
|
|
@ -101,6 +102,28 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
// Spellbook (P key toggle handled inside)
|
// Spellbook (P key toggle handled inside)
|
||||||
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
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
|
// Set vendor mode before rendering inventory
|
||||||
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
||||||
|
|
||||||
|
|
@ -113,7 +136,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
||||||
|
|
||||||
// Character screen (C key toggle handled inside render())
|
// Character screen (C key toggle handled inside render())
|
||||||
inventoryScreen.renderCharacterScreen(gameHandler.getInventory());
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
||||||
|
|
||||||
if (inventoryScreen.consumeInventoryDirty()) {
|
if (inventoryScreen.consumeInventoryDirty()) {
|
||||||
gameHandler.notifyInventoryChanged();
|
gameHandler.notifyInventoryChanged();
|
||||||
|
|
@ -124,6 +147,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
updateCharacterTextures(gameHandler.getInventory());
|
updateCharacterTextures(gameHandler.getInventory());
|
||||||
core::Application::getInstance().loadEquippedWeapons();
|
core::Application::getInstance().loadEquippedWeapons();
|
||||||
gameHandler.notifyEquipmentChanged();
|
gameHandler.notifyEquipmentChanged();
|
||||||
|
inventoryScreen.markPreviewDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update renderer face-target position and selection circle
|
// 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);
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
||||||
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||||
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
||||||
} else if (unit->isInteractable()) {
|
} else if (unit->isHostile()) {
|
||||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
|
||||||
} else {
|
|
||||||
circleColor = glm::vec3(1.0f, 0.2f, 0.2f); // red (hostile)
|
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) {
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
||||||
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (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) {
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
||||||
gameHandler.lootTarget(target->getGuid());
|
gameHandler.lootTarget(target->getGuid());
|
||||||
} else if (gameHandler.isSinglePlayerMode()) {
|
} else if (gameHandler.isSinglePlayerMode()) {
|
||||||
// Single-player: toggle auto-attack
|
// Single-player: interact with friendly NPCs, attack hostiles
|
||||||
if (gameHandler.isAutoAttacking()) {
|
if (!unit->isHostile() && unit->isInteractable()) {
|
||||||
gameHandler.stopAutoAttack();
|
gameHandler.interactWithNpc(target->getGuid());
|
||||||
} else {
|
} else if (unit->isHostile()) {
|
||||||
gameHandler.startAutoAttack(target->getGuid());
|
if (gameHandler.isAutoAttacking()) {
|
||||||
|
gameHandler.stopAutoAttack();
|
||||||
|
} else {
|
||||||
|
gameHandler.startAutoAttack(target->getGuid());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Online mode: interact with friendly NPCs, attack hostiles
|
// Online mode: interact with friendly NPCs, attack hostiles
|
||||||
if (unit->isInteractable()) {
|
if (!unit->isHostile() && unit->isInteractable()) {
|
||||||
gameHandler.interactWithNpc(target->getGuid());
|
gameHandler.interactWithNpc(target->getGuid());
|
||||||
} else {
|
} else if (unit->isHostile()) {
|
||||||
if (gameHandler.isAutoAttacking()) {
|
if (gameHandler.isAutoAttacking()) {
|
||||||
gameHandler.stopAutoAttack();
|
gameHandler.stopAutoAttack();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -643,10 +671,10 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
||||||
auto u = std::static_pointer_cast<game::Unit>(target);
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
||||||
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
||||||
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||||
} else if (u->isInteractable()) {
|
} else if (u->isHostile()) {
|
||||||
hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
|
||||||
} else {
|
|
||||||
hostileColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
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 "ui/inventory_screen.hpp"
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
|
#include "core/application.hpp"
|
||||||
#include "core/input.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 <imgui.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace ui {
|
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) {
|
ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
|
||||||
switch (quality) {
|
switch (quality) {
|
||||||
case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey
|
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) {
|
game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv) {
|
||||||
switch (inventoryType) {
|
switch (inventoryType) {
|
||||||
case 1: return game::EquipSlot::HEAD;
|
case 1: return game::EquipSlot::HEAD;
|
||||||
|
|
@ -191,19 +473,28 @@ void InventoryScreen::renderHeldItem() {
|
||||||
ImVec4 qColor = getQualityColor(heldItem.quality);
|
ImVec4 qColor = getQualityColor(heldItem.quality);
|
||||||
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
|
||||||
|
|
||||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
|
// Try to show icon
|
||||||
IM_COL32(40, 35, 30, 200));
|
GLuint iconTex = getItemIcon(heldItem.displayInfoId);
|
||||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
if (iconTex) {
|
||||||
borderCol, 0.0f, 0, 2.0f);
|
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] = {};
|
char abbr[4] = {};
|
||||||
if (!heldItem.name.empty()) {
|
if (!heldItem.name.empty()) {
|
||||||
abbr[0] = heldItem.name[0];
|
abbr[0] = heldItem.name[0];
|
||||||
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
|
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) {
|
if (heldItem.stackCount > 1) {
|
||||||
char countStr[16];
|
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;
|
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::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;
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||||
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
if (!ImGui::Begin("Character", &characterOpen, flags)) {
|
||||||
|
|
@ -316,8 +625,26 @@ void InventoryScreen::renderCharacterScreen(game::Inventory& inventory) {
|
||||||
return;
|
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);
|
renderEquipmentPanel(inventory);
|
||||||
|
|
||||||
|
// Stats panel
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
renderStatsPanel(inventory, gameHandler.getPlayerLevel());
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
// If both bags and character are open, allow drag-and-drop between them
|
// 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 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;
|
int rows = 8;
|
||||||
|
float previewStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
|
||||||
for (int r = 0; r < rows; r++) {
|
for (int r = 0; r < rows; r++) {
|
||||||
|
// Left column
|
||||||
{
|
{
|
||||||
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
|
||||||
const char* label = game::getEquipSlotName(leftSlots[r]);
|
const char* label = game::getEquipSlotName(leftSlots[r]);
|
||||||
|
|
@ -360,8 +694,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine(slotSize + spacing + 60.0f);
|
// Right column
|
||||||
|
ImGui::SameLine(rightColX);
|
||||||
{
|
{
|
||||||
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
|
||||||
const char* label = game::getEquipSlotName(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
|
// Weapon row
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
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) {
|
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
@ -511,18 +937,27 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
borderCol = IM_COL32(0, 200, 0, 220);
|
borderCol = IM_COL32(0, 200, 0, 220);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
|
// Try to show icon
|
||||||
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
|
GLuint iconTex = getItemIcon(item.displayInfoId);
|
||||||
borderCol, 0.0f, 0, 2.0f);
|
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] = {};
|
char abbr[4] = {};
|
||||||
if (!item.name.empty()) {
|
if (!item.name.empty()) {
|
||||||
abbr[0] = item.name[0];
|
abbr[0] = item.name[0];
|
||||||
if (item.name.size() > 1) abbr[1] = item.name[1];
|
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) {
|
if (item.stackCount > 1) {
|
||||||
char countStr[16];
|
char countStr[16];
|
||||||
|
|
@ -654,12 +1089,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
|
||||||
ImGui::Text("%d Armor", item.armor);
|
ImGui::Text("%d Armor", item.armor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
// Stats with "Equip:" prefix style
|
||||||
if (item.stamina != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Stamina", item.stamina);
|
ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
if (item.strength != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Strength", item.strength);
|
ImVec4 red(1.0f, 0.2f, 0.2f, 1.0f);
|
||||||
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);
|
auto renderStat = [&](int32_t val, const char* name) {
|
||||||
if (item.spirit != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Spirit", item.spirit);
|
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
|
// Stack info
|
||||||
if (item.maxStack > 1) {
|
if (item.maxStack > 1) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue