mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix spell targeting, item query parsing, loot UI, hair/skin textures, and attack animations
Fix spell cast target fallback using selected target instead of no-op tautology. Fix SMSG_ITEM_QUERY_SINGLE_RESPONSE to always read 10 stat pairs (server sends all 10 regardless of statsCount), fixing misaligned armor/stat reads. Fix XP gain parser to read float groupRate + uint8 RAF instead of bogus uint32 groupBonus. Add item icons and quality-colored names to loot window. Use actual character appearance bytes for CharSections.dbc skin/face/hair lookups instead of hardcoded defaults. Add weapon-type-aware attack animation selection (2H prioritizes anim 18). Add readable spell cast failure messages and vendor sell hint.
This commit is contained in:
parent
f0aad5e97f
commit
2ddef93f52
9 changed files with 338 additions and 54 deletions
|
|
@ -65,5 +65,173 @@ struct SpellCooldownEntry {
|
|||
uint32_t categoryCooldownMs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get human-readable spell cast failure reason (WoW 3.3.5a SpellCastResult)
|
||||
*/
|
||||
inline const char* getSpellCastResultString(uint8_t result) {
|
||||
switch (result) {
|
||||
case 0: return "Spell failed";
|
||||
case 1: return "Affects dead target";
|
||||
case 2: return "Already at full health";
|
||||
case 3: return "Already at full mana";
|
||||
case 4: return "Already at full power";
|
||||
case 5: return "Already being tamed";
|
||||
case 6: return "Already have charm";
|
||||
case 7: return "Already have summon";
|
||||
case 8: return "Already open";
|
||||
case 9: return "Aura bounced";
|
||||
case 10: return "Autopilot in use";
|
||||
case 11: return "Bad implicit targets";
|
||||
case 12: return "Bad targets";
|
||||
case 13: return "Can't be charmed";
|
||||
case 14: return "Can't be disenchanted";
|
||||
case 15: return "Can't be disenchanted (skill)";
|
||||
case 16: return "Can't be milled";
|
||||
case 17: return "Can't be prospected";
|
||||
case 18: return "Can't cast on tapped";
|
||||
case 19: return "Can't duel while invisible";
|
||||
case 20: return "Can't duel while stealthed";
|
||||
case 21: return "Can't stealth";
|
||||
case 22: return "Caster aurastate";
|
||||
case 23: return "Caster dead";
|
||||
case 24: return "Charmed";
|
||||
case 25: return "Chest in use";
|
||||
case 26: return "Confused";
|
||||
case 27: return "Don't report";
|
||||
case 28: return "Equipped item";
|
||||
case 29: return "Equipped item (class)";
|
||||
case 30: return "Equipped item (class2)";
|
||||
case 31: return "Equipped item (level)";
|
||||
case 32: return "Error";
|
||||
case 33: return "Fizzle";
|
||||
case 34: return "Fleeing";
|
||||
case 35: return "Food too low level";
|
||||
case 36: return "Highlighted rune needed";
|
||||
case 37: return "Immune";
|
||||
case 38: return "Interrupted";
|
||||
case 39: return "Interrupted (combat)";
|
||||
case 40: return "Invalid item";
|
||||
case 41: return "Item already enchanted";
|
||||
case 42: return "Item gone";
|
||||
case 43: return "Item not found";
|
||||
case 44: return "Item not ready";
|
||||
case 45: return "Level requirement";
|
||||
case 46: return "Line of sight";
|
||||
case 47: return "Lowlevel";
|
||||
case 48: return "Low castlevel";
|
||||
case 49: return "Mainhand empty";
|
||||
case 50: return "Moving";
|
||||
case 51: return "Must be behind target";
|
||||
case 52: return "Need ammo";
|
||||
case 53: return "Need ammo pouch";
|
||||
case 54: return "Need exotic ammo";
|
||||
case 55: return "Need more items";
|
||||
case 56: return "No path";
|
||||
case 57: return "Not behind";
|
||||
case 58: return "Not fishable";
|
||||
case 59: return "Not flying";
|
||||
case 60: return "Not here";
|
||||
case 61: return "Not infront";
|
||||
case 62: return "Not in control";
|
||||
case 63: return "Not known";
|
||||
case 64: return "Not mounted";
|
||||
case 65: return "Not on taxi";
|
||||
case 66: return "Not on transport";
|
||||
case 67: return "Not ready";
|
||||
case 68: return "Not shapeshift";
|
||||
case 69: return "Not standing";
|
||||
case 70: return "Not tradeable";
|
||||
case 71: return "Not trading";
|
||||
case 72: return "Not unsheathed";
|
||||
case 73: return "Not while ghost";
|
||||
case 74: return "Not while looting";
|
||||
case 75: return "No charges remain";
|
||||
case 76: return "No champion";
|
||||
case 77: return "No combo points";
|
||||
case 78: return "No dueling";
|
||||
case 79: return "No endurance";
|
||||
case 80: return "No fish";
|
||||
case 81: return "No items while shapeshifted";
|
||||
case 82: return "No mounts allowed here";
|
||||
case 83: return "No pet";
|
||||
case 84: return "No power";
|
||||
case 85: return "Nothing to dispel";
|
||||
case 86: return "Nothing to steal";
|
||||
case 87: return "Only above water";
|
||||
case 88: return "Only daytime";
|
||||
case 89: return "Only indoors";
|
||||
case 90: return "Only mounted";
|
||||
case 91: return "Only nighttime";
|
||||
case 92: return "Only outdoors";
|
||||
case 93: return "Only shapeshift";
|
||||
case 94: return "Only stealthed";
|
||||
case 95: return "Only underwater";
|
||||
case 96: return "Out of range";
|
||||
case 97: return "Pacified";
|
||||
case 98: return "Possessed";
|
||||
case 99: return "Reagents";
|
||||
case 100: return "Requires area";
|
||||
case 101: return "Requires spell focus";
|
||||
case 102: return "Rooted";
|
||||
case 103: return "Silenced";
|
||||
case 104: return "Spell in progress";
|
||||
case 105: return "Spell learned";
|
||||
case 106: return "Spell unavailable";
|
||||
case 107: return "Stunned";
|
||||
case 108: return "Targets dead";
|
||||
case 109: return "Target not dead";
|
||||
case 110: return "Target not in party";
|
||||
case 111: return "Target not in raid";
|
||||
case 112: return "Target friendly";
|
||||
case 113: return "Target is player";
|
||||
case 114: return "Target is player controlled";
|
||||
case 115: return "Target not dead";
|
||||
case 116: return "Target not in party";
|
||||
case 117: return "Target not player";
|
||||
case 118: return "Target no pockets";
|
||||
case 119: return "Target no weapons";
|
||||
case 120: return "Target out of range";
|
||||
case 121: return "Target unskinnable";
|
||||
case 122: return "Thirst satiated";
|
||||
case 123: return "Too close";
|
||||
case 124: return "Too many of item";
|
||||
case 125: return "Totem category";
|
||||
case 126: return "Totems";
|
||||
case 127: return "Training points";
|
||||
case 128: return "Try again";
|
||||
case 129: return "Unit not behind";
|
||||
case 130: return "Unit not infront";
|
||||
case 131: return "Wrong pet food";
|
||||
case 132: return "Not while fatigued";
|
||||
case 133: return "Target not in instance";
|
||||
case 134: return "Not while trading";
|
||||
case 135: return "Target not in raid";
|
||||
case 136: return "Target feign dead";
|
||||
case 137: return "Disabled by power scaling";
|
||||
case 138: return "Quest players only";
|
||||
case 139: return "Not idle";
|
||||
case 140: return "Not inactive";
|
||||
case 141: return "Partial playtime";
|
||||
case 142: return "No playtime";
|
||||
case 143: return "Not in battleground";
|
||||
case 144: return "Not in raid instance";
|
||||
case 145: return "Only in arena";
|
||||
case 146: return "Target locked to raid instance";
|
||||
case 147: return "On use enchant";
|
||||
case 148: return "Not on ground";
|
||||
case 149: return "Custom error";
|
||||
case 150: return "Can't open lock";
|
||||
case 151: return "Wrong artifact equipped";
|
||||
case 173: return "Not enough mana";
|
||||
case 174: return "Not enough health";
|
||||
case 175: return "Not enough holy power";
|
||||
case 176: return "Not enough rage";
|
||||
case 177: return "Not enough energy";
|
||||
case 178: return "Not enough runes";
|
||||
case 179: return "Not enough runic power";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ public:
|
|||
void setTargetPosition(const glm::vec3* pos);
|
||||
bool isMoving() const;
|
||||
void triggerMeleeSwing();
|
||||
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
||||
|
||||
// Selection circle for targeted entity
|
||||
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
|
||||
|
|
@ -254,6 +255,7 @@ private:
|
|||
float meleeSwingCooldown = 0.0f;
|
||||
float meleeAnimDurationMs = 0.0f;
|
||||
uint32_t meleeAnimId = 0;
|
||||
uint32_t equippedWeaponInvType_ = 0;
|
||||
|
||||
bool terrainEnabled = true;
|
||||
bool terrainLoaded = false;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ private:
|
|||
|
||||
// Item icon cache: displayInfoId -> GL texture
|
||||
std::unordered_map<uint32_t, GLuint> iconCache_;
|
||||
public:
|
||||
GLuint getItemIcon(uint32_t displayInfoId);
|
||||
private:
|
||||
|
||||
// Character model preview
|
||||
std::unique_ptr<rendering::CharacterPreview> charPreview_;
|
||||
|
|
|
|||
|
|
@ -787,6 +787,21 @@ void Application::spawnPlayerCharacter() {
|
|||
std::string faceLowerTexturePath;
|
||||
std::vector<std::string> underwearPaths;
|
||||
|
||||
// Extract appearance bytes for texture lookups
|
||||
uint8_t charSkinId = 0, charFaceId = 0, charHairStyleId = 0, charHairColorId = 0;
|
||||
if (gameHandler) {
|
||||
const game::Character* activeChar = gameHandler->getActiveCharacter();
|
||||
if (activeChar) {
|
||||
charSkinId = activeChar->appearanceBytes & 0xFF;
|
||||
charFaceId = (activeChar->appearanceBytes >> 8) & 0xFF;
|
||||
charHairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF;
|
||||
charHairColorId = (activeChar->appearanceBytes >> 24) & 0xFF;
|
||||
LOG_INFO("Appearance: skin=", (int)charSkinId, " face=", (int)charFaceId,
|
||||
" hairStyle=", (int)charHairStyleId, " hairColor=", (int)charHairColorId);
|
||||
}
|
||||
}
|
||||
|
||||
std::string hairTexturePath;
|
||||
if (useCharSections) {
|
||||
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
||||
if (charSectionsDbc) {
|
||||
|
|
@ -794,6 +809,7 @@ void Application::spawnPlayerCharacter() {
|
|||
bool foundSkin = false;
|
||||
bool foundUnderwear = false;
|
||||
bool foundFaceLower = false;
|
||||
bool foundHair = false;
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||
|
|
@ -803,23 +819,37 @@ void Application::spawnPlayerCharacter() {
|
|||
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
|
||||
if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == 0) {
|
||||
// Section 0 = skin: match by colorIndex = skin byte
|
||||
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
if (!tex1.empty()) {
|
||||
bodySkinPath = tex1;
|
||||
foundSkin = true;
|
||||
LOG_INFO(" DBC body skin: ", bodySkinPath);
|
||||
LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", (int)charSkinId, ")");
|
||||
}
|
||||
} else if (baseSection == 3 && colorIndex == 0) {
|
||||
(void)variationIndex;
|
||||
} else if (baseSection == 1 && !foundFaceLower && variationIndex == 0 && colorIndex == 0) {
|
||||
}
|
||||
// Section 3 = hair: match variation=hairStyle, color=hairColor
|
||||
else if (baseSection == 3 && !foundHair &&
|
||||
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
|
||||
hairTexturePath = charSectionsDbc->getString(r, 4);
|
||||
if (!hairTexturePath.empty()) {
|
||||
foundHair = true;
|
||||
LOG_INFO(" DBC hair texture: ", hairTexturePath,
|
||||
" (style=", (int)charHairStyleId, " color=", (int)charHairColorId, ")");
|
||||
}
|
||||
}
|
||||
// Section 1 = face lower: match variation=faceId
|
||||
else if (baseSection == 1 && !foundFaceLower &&
|
||||
variationIndex == charFaceId && colorIndex == charSkinId) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, 4);
|
||||
if (!tex1.empty()) {
|
||||
faceLowerTexturePath = tex1;
|
||||
foundFaceLower = true;
|
||||
LOG_INFO(" DBC face texture: ", faceLowerTexturePath);
|
||||
}
|
||||
} else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == 0) {
|
||||
}
|
||||
// Section 4 = underwear
|
||||
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
||||
for (int f = 4; f <= 6; f++) {
|
||||
std::string tex = charSectionsDbc->getString(r, f);
|
||||
if (!tex.empty()) {
|
||||
|
|
@ -829,36 +859,19 @@ void Application::spawnPlayerCharacter() {
|
|||
}
|
||||
foundUnderwear = true;
|
||||
}
|
||||
|
||||
if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break;
|
||||
}
|
||||
|
||||
if (!foundHair) {
|
||||
LOG_WARNING("No DBC hair match for style=", (int)charHairStyleId,
|
||||
" color=", (int)charHairColorId,
|
||||
" race=", targetRaceId, " sex=", targetSexId);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
|
||||
}
|
||||
|
||||
// Look up hair texture from CharSections.dbc section 3
|
||||
std::string hairTexturePath;
|
||||
if (gameHandler) {
|
||||
const game::Character* activeChar = gameHandler->getActiveCharacter();
|
||||
if (activeChar) {
|
||||
uint8_t hairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF;
|
||||
uint8_t hairColorId = (activeChar->appearanceBytes >> 24) & 0xFF;
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
|
||||
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
|
||||
uint32_t section = charSectionsDbc->getUInt32(r, 3);
|
||||
uint32_t variation = charSectionsDbc->getUInt32(r, 8);
|
||||
uint32_t colorIdx = charSectionsDbc->getUInt32(r, 9);
|
||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||
if (section != 3) continue;
|
||||
if (variation != hairStyleId) continue;
|
||||
if (colorIdx != hairColorId) continue;
|
||||
hairTexturePath = charSectionsDbc->getString(r, 4);
|
||||
LOG_INFO(" DBC hair texture: ", hairTexturePath,
|
||||
" (style=", (int)hairStyleId, " color=", (int)hairColorId, ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& tex : model.textures) {
|
||||
if (tex.type == 1 && tex.filename.empty()) {
|
||||
tex.filename = bodySkinPath;
|
||||
|
|
|
|||
|
|
@ -3309,7 +3309,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
|||
|
||||
if (casting) return; // Already casting
|
||||
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : targetGuid;
|
||||
uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid;
|
||||
auto packet = CastSpellPacket::build(spellId, target, ++castCount);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
|
||||
|
|
@ -3389,11 +3389,16 @@ void GameHandler::handleCastFailed(network::Packet& packet) {
|
|||
currentCastSpellId = 0;
|
||||
castTimeRemaining = 0.0f;
|
||||
|
||||
// Add system message about failed cast
|
||||
// Add system message about failed cast with readable reason
|
||||
const char* reason = getSpellCastResultString(data.result);
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.language = ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
||||
if (reason) {
|
||||
msg.message = reason;
|
||||
} else {
|
||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
||||
}
|
||||
addLocalChatMessage(msg);
|
||||
}
|
||||
|
||||
|
|
@ -4310,8 +4315,8 @@ void GameHandler::simulateXpGain(uint64_t victimGuid, uint32_t totalXp) {
|
|||
packet.writeUInt64(victimGuid);
|
||||
packet.writeUInt32(totalXp);
|
||||
packet.writeUInt8(0); // kill XP
|
||||
packet.writeFloat(0.0f);
|
||||
packet.writeUInt32(0); // group bonus
|
||||
packet.writeFloat(1.0f); // group rate (1.0 = solo, no bonus)
|
||||
packet.writeUInt8(0); // RAF flag
|
||||
handleXpGain(packet);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1326,16 +1326,19 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
|
|||
data.containerSlots = packet.readUInt32();
|
||||
|
||||
uint32_t statsCount = packet.readUInt32();
|
||||
for (uint32_t i = 0; i < statsCount && i < 10; i++) {
|
||||
// Server always sends 10 stat pairs; statsCount tells how many are meaningful
|
||||
for (uint32_t i = 0; i < 10; i++) {
|
||||
uint32_t statType = packet.readUInt32();
|
||||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||||
switch (statType) {
|
||||
case 3: data.agility = statValue; break;
|
||||
case 4: data.strength = statValue; break;
|
||||
case 5: data.intellect = statValue; break;
|
||||
case 6: data.spirit = statValue; break;
|
||||
case 7: data.stamina = statValue; break;
|
||||
default: break;
|
||||
if (i < statsCount) {
|
||||
switch (statType) {
|
||||
case 3: data.agility = statValue; break;
|
||||
case 4: data.strength = statValue; break;
|
||||
case 5: data.intellect = statValue; break;
|
||||
case 6: data.spirit = statValue; break;
|
||||
case 7: data.stamina = statValue; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1585,9 +1588,13 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
|||
data.totalXp = packet.readUInt32();
|
||||
data.type = packet.readUInt8();
|
||||
if (data.type == 0) {
|
||||
// Kill XP: has group bonus float (unused) + group bonus uint32
|
||||
packet.readFloat();
|
||||
data.groupBonus = packet.readUInt32();
|
||||
// Kill XP: float groupRate (1.0 = solo) + uint8 RAF flag
|
||||
float groupRate = packet.readFloat();
|
||||
packet.readUInt8(); // RAF bonus flag
|
||||
// Group bonus = total - (total / rate); only if grouped (rate > 1)
|
||||
if (groupRate > 1.0f) {
|
||||
data.groupBonus = data.totalXp - static_cast<uint32_t>(data.totalXp / groupRate);
|
||||
}
|
||||
}
|
||||
LOG_INFO("XP gain: ", data.totalXp, " xp (type=", static_cast<int>(data.type), ")");
|
||||
return data.totalXp > 0;
|
||||
|
|
|
|||
|
|
@ -391,9 +391,26 @@ uint32_t Renderer::resolveMeleeAnimId() {
|
|||
return 0.0f;
|
||||
};
|
||||
|
||||
// Prefer weapon attacks (1H=17, 2H=18) over unarmed (16); 19-21 are other variants
|
||||
const uint32_t attackCandidates[] = {17, 18, 16, 19, 20, 21};
|
||||
for (uint32_t id : attackCandidates) {
|
||||
// Select animation priority based on equipped weapon type
|
||||
// WoW inventory types: 17 = 2H weapon, 13/21 = 1H, 0 = unarmed
|
||||
// WoW anim IDs: 16 = unarmed, 17 = 1H attack, 18 = 2H attack
|
||||
const uint32_t* attackCandidates;
|
||||
size_t candidateCount;
|
||||
static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21};
|
||||
static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21};
|
||||
static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21};
|
||||
if (equippedWeaponInvType_ == 17) { // INVTYPE_2HWEAPON
|
||||
attackCandidates = candidates2H;
|
||||
candidateCount = 6;
|
||||
} else if (equippedWeaponInvType_ == 0) {
|
||||
attackCandidates = candidatesUnarmed;
|
||||
candidateCount = 6;
|
||||
} else {
|
||||
attackCandidates = candidates1H;
|
||||
candidateCount = 6;
|
||||
}
|
||||
for (size_t ci = 0; ci < candidateCount; ci++) {
|
||||
uint32_t id = attackCandidates[ci];
|
||||
if (characterRenderer->hasAnimation(characterInstanceId, id)) {
|
||||
meleeAnimId = id;
|
||||
meleeAnimDurationMs = findDuration(id);
|
||||
|
|
|
|||
|
|
@ -149,6 +149,12 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
core::Application::getInstance().loadEquippedWeapons();
|
||||
gameHandler.notifyEquipmentChanged();
|
||||
inventoryScreen.markPreviewDirty();
|
||||
// Update renderer weapon type for animation selection
|
||||
auto* r = core::Application::getInstance().getRenderer();
|
||||
if (r) {
|
||||
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
||||
r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType);
|
||||
}
|
||||
}
|
||||
|
||||
// Update renderer face-target position and selection circle
|
||||
|
|
@ -1723,14 +1729,73 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::Separator();
|
||||
}
|
||||
|
||||
// Items
|
||||
// Items with icons and labels
|
||||
constexpr float iconSize = 32.0f;
|
||||
for (const auto& item : loot.items) {
|
||||
ImGui::PushID(item.slotIndex);
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "Item %u (x%u)", item.itemId, item.count);
|
||||
if (ImGui::Selectable(label)) {
|
||||
|
||||
// Get item info for name and quality
|
||||
const auto* info = gameHandler.getItemInfo(item.itemId);
|
||||
std::string itemName = info && !info->name.empty()
|
||||
? info->name
|
||||
: "Item #" + std::to_string(item.itemId);
|
||||
game::ItemQuality quality = info
|
||||
? static_cast<game::ItemQuality>(info->quality)
|
||||
: game::ItemQuality::COMMON;
|
||||
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
|
||||
|
||||
// Get item icon
|
||||
uint32_t displayId = item.displayInfoId;
|
||||
if (displayId == 0 && info) displayId = info->displayInfoId;
|
||||
GLuint iconTex = inventoryScreen.getItemIcon(displayId);
|
||||
|
||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
|
||||
|
||||
// Invisible selectable for click handling
|
||||
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
|
||||
gameHandler.lootItem(item.slotIndex);
|
||||
}
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
// Draw hover highlight
|
||||
if (hovered) {
|
||||
drawList->AddRectFilled(cursor,
|
||||
ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f,
|
||||
cursor.y + rowH),
|
||||
IM_COL32(255, 255, 255, 30));
|
||||
}
|
||||
|
||||
// Draw icon
|
||||
if (iconTex) {
|
||||
drawList->AddImage((ImTextureID)(uintptr_t)iconTex,
|
||||
cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize));
|
||||
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor));
|
||||
} else {
|
||||
drawList->AddRectFilled(cursor,
|
||||
ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
||||
IM_COL32(40, 40, 50, 200));
|
||||
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
||||
IM_COL32(80, 80, 80, 200));
|
||||
}
|
||||
|
||||
// Draw item name
|
||||
float textX = cursor.x + iconSize + 6.0f;
|
||||
float textY = cursor.y + 2.0f;
|
||||
drawList->AddText(ImVec2(textX, textY),
|
||||
ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str());
|
||||
|
||||
// Draw count if > 1
|
||||
if (item.count > 1) {
|
||||
char countStr[32];
|
||||
snprintf(countStr, sizeof(countStr), "x%u", item.count);
|
||||
float countY = textY + ImGui::GetTextLineHeight();
|
||||
drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
|
|
@ -1924,6 +1989,9 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell");
|
||||
ImGui::Separator();
|
||||
|
||||
if (vendor.items.empty()) {
|
||||
ImGui::TextDisabled("This vendor has nothing for sale.");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -642,7 +642,9 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
|
||||
// Stats panel
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
renderStatsPanel(inventory, gameHandler.getPlayerLevel());
|
||||
|
||||
ImGui::End();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue