mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: implement item durability tracking and vendor repair
- Add ITEM_FIELD_DURABILITY (60) and ITEM_FIELD_MAXDURABILITY (61) to update_field_table.hpp enum and wotlk/update_fields.json - Add curDurability/maxDurability to OnlineItemInfo and ItemDef structs - Parse durability fields in OBJECT_CREATE and OBJECT_VALUES handlers; preserve existing values on partial updates (fixes stale durability being reset to 0 on stack-count-only updates) - Propagate durability to ItemDef in all 5 rebuildOnlineInventory() paths - Implement GameHandler::repairItem() and repairAll() via CMSG_REPAIR_ITEM (itemGuid=0 repairs all equipped items per WotLK protocol) - Add canRepair flag to ListInventoryData; set it when player selects GOSSIP_OPTION_ARMORER in gossip window - Show "Repair All" button in vendor window header when canRepair=true - Display color-coded durability in item tooltip (green >50%, yellow >25%, red <=25%)
This commit is contained in:
parent
094ef88e57
commit
0afa41e908
9 changed files with 96 additions and 10 deletions
|
|
@ -33,6 +33,8 @@
|
|||
"PLAYER_EXPLORED_ZONES_START": 1041,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14,
|
||||
"ITEM_FIELD_DURABILITY": 60,
|
||||
"ITEM_FIELD_MAXDURABILITY": 61,
|
||||
"CONTAINER_FIELD_NUM_SLOTS": 64,
|
||||
"CONTAINER_FIELD_SLOT_1": 66
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1258,6 +1258,8 @@ public:
|
|||
uint32_t count = 1;
|
||||
};
|
||||
void buyBackItem(uint32_t buybackSlot);
|
||||
void repairItem(uint64_t vendorGuid, uint64_t itemGuid);
|
||||
void repairAll(uint64_t vendorGuid, bool useGuildBank = false);
|
||||
const std::deque<BuybackItem>& getBuybackItems() const { return buybackItems_; }
|
||||
void autoEquipItemBySlot(int backpackIndex);
|
||||
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
||||
|
|
@ -1269,6 +1271,7 @@ public:
|
|||
void useItemById(uint32_t itemId);
|
||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||
void setVendorCanRepair(bool v) { currentVendorItems.canRepair = v; }
|
||||
|
||||
// Mail
|
||||
bool isMailboxOpen() const { return mailboxOpen_; }
|
||||
|
|
@ -1831,6 +1834,8 @@ private:
|
|||
struct OnlineItemInfo {
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackCount = 1;
|
||||
uint32_t curDurability = 0;
|
||||
uint32_t maxDurability = 0;
|
||||
};
|
||||
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
||||
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ struct ItemDef {
|
|||
int32_t spirit = 0;
|
||||
uint32_t displayInfoId = 0;
|
||||
uint32_t sellPrice = 0;
|
||||
uint32_t curDurability = 0;
|
||||
uint32_t maxDurability = 0;
|
||||
};
|
||||
|
||||
struct ItemSlot {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ enum class UF : uint16_t {
|
|||
|
||||
// Item fields
|
||||
ITEM_FIELD_STACK_COUNT,
|
||||
ITEM_FIELD_DURABILITY,
|
||||
ITEM_FIELD_MAXDURABILITY,
|
||||
|
||||
// Container fields
|
||||
CONTAINER_FIELD_NUM_SLOTS,
|
||||
|
|
|
|||
|
|
@ -2179,6 +2179,7 @@ struct VendorItem {
|
|||
struct ListInventoryData {
|
||||
uint64_t vendorGuid = 0;
|
||||
std::vector<VendorItem> items;
|
||||
bool canRepair = false; // Set when vendor was opened via GOSSIP_OPTION_ARMORER
|
||||
|
||||
bool isValid() const { return true; }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1426,17 +1426,20 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// Use getLatestX/Y/Z (server-authoritative destination) for position sync
|
||||
// rather than getX/Y/Z (interpolated), which may be stale for entities
|
||||
// outside the 150-unit updateMovement() culling radius in GameHandler.
|
||||
glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||
// Distance check uses getLatestX/Y/Z (server-authoritative destination) to
|
||||
// avoid false-culling entities that moved while getX/Y/Z was stale.
|
||||
// Position sync still uses getX/Y/Z to preserve smooth interpolation for
|
||||
// nearby entities; distant entities (> 150u) have planarDist≈0 anyway
|
||||
// so the renderer remains driven correctly by creatureMoveCallback_.
|
||||
glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||
float canonDistSq = 0.0f;
|
||||
if (havePlayerPos) {
|
||||
glm::vec3 d = canonical - playerPos;
|
||||
glm::vec3 d = latestCanonical - playerPos;
|
||||
canonDistSq = glm::dot(d, d);
|
||||
if (canonDistSq > syncRadiusSq) continue;
|
||||
}
|
||||
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Visual collision guard: keep hostile melee units from rendering inside the
|
||||
|
|
|
|||
|
|
@ -8023,10 +8023,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) {
|
||||
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
||||
auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY));
|
||||
auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY));
|
||||
if (entryIt != block.fields.end() && entryIt->second != 0) {
|
||||
OnlineItemInfo info;
|
||||
// Preserve existing info when doing partial updates
|
||||
OnlineItemInfo info = onlineItems_.count(block.guid)
|
||||
? onlineItems_[block.guid] : OnlineItemInfo{};
|
||||
info.entry = entryIt->second;
|
||||
info.stackCount = (stackIt != block.fields.end()) ? stackIt->second : 1;
|
||||
if (stackIt != block.fields.end()) info.stackCount = stackIt->second;
|
||||
if (durIt != block.fields.end()) info.curDurability = durIt->second;
|
||||
if (maxDurIt!= block.fields.end()) info.maxDurability = maxDurIt->second;
|
||||
bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end());
|
||||
onlineItems_[block.guid] = info;
|
||||
if (isNew) newItemCreated = true;
|
||||
|
|
@ -8427,19 +8433,31 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
extractExploredZoneFields(lastPlayerFields_);
|
||||
}
|
||||
|
||||
// Update item stack count for online items
|
||||
// Update item stack count / durability for online items
|
||||
if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) {
|
||||
bool inventoryChanged = false;
|
||||
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
|
||||
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
|
||||
const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY);
|
||||
const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY);
|
||||
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
|
||||
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
|
||||
for (const auto& [key, val] : block.fields) {
|
||||
auto it = onlineItems_.find(block.guid);
|
||||
if (key == itemStackField) {
|
||||
auto it = onlineItems_.find(block.guid);
|
||||
if (it != onlineItems_.end() && it->second.stackCount != val) {
|
||||
it->second.stackCount = val;
|
||||
inventoryChanged = true;
|
||||
}
|
||||
} else if (key == itemDurField) {
|
||||
if (it != onlineItems_.end() && it->second.curDurability != val) {
|
||||
it->second.curDurability = val;
|
||||
inventoryChanged = true;
|
||||
}
|
||||
} else if (key == itemMaxDurField) {
|
||||
if (it != onlineItems_.end() && it->second.maxDurability != val) {
|
||||
it->second.maxDurability = val;
|
||||
inventoryChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update container slot GUIDs on bag content changes
|
||||
|
|
@ -10719,6 +10737,8 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.curDurability = itemIt->second.curDurability;
|
||||
def.maxDurability = itemIt->second.maxDurability;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
|
|
@ -10757,6 +10777,8 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.curDurability = itemIt->second.curDurability;
|
||||
def.maxDurability = itemIt->second.maxDurability;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
|
|
@ -10830,6 +10852,8 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.curDurability = itemIt->second.curDurability;
|
||||
def.maxDurability = itemIt->second.maxDurability;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
|
|
@ -10870,6 +10894,8 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.curDurability = itemIt->second.curDurability;
|
||||
def.maxDurability = itemIt->second.maxDurability;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
|
|
@ -10951,6 +10977,8 @@ void GameHandler::rebuildOnlineInventory() {
|
|||
ItemDef def;
|
||||
def.itemId = itemIt->second.entry;
|
||||
def.stackCount = itemIt->second.stackCount;
|
||||
def.curDurability = itemIt->second.curDurability;
|
||||
def.maxDurability = itemIt->second.maxDurability;
|
||||
def.maxStack = 1;
|
||||
|
||||
auto infoIt = itemInfoCache_.find(itemIt->second.entry);
|
||||
|
|
@ -14866,6 +14894,26 @@ void GameHandler::buyBackItem(uint32_t buybackSlot) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// CMSG_REPAIR_ITEM: npcGuid(8) + itemGuid(8) + useGuildBank(uint8)
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt8(0); // do not use guild bank
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
// itemGuid = 0 signals "repair all equipped" to the server
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(0);
|
||||
packet.writeUInt8(useGuildBank ? 1 : 0);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
LOG_INFO("Sell request: vendorGuid=0x", std::hex, vendorGuid,
|
||||
|
|
|
|||
|
|
@ -6538,6 +6538,9 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
|||
std::string processedText = replaceGenderPlaceholders(displayText, gameHandler);
|
||||
std::string label = std::string(icon) + " " + processedText;
|
||||
if (ImGui::Selectable(label.c_str())) {
|
||||
if (opt.text == "GOSSIP_OPTION_ARMORER") {
|
||||
gameHandler.setVendorCanRepair(true);
|
||||
}
|
||||
gameHandler.selectGossipOption(opt.id);
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
|
@ -6936,6 +6939,17 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|||
uint32_t ms = static_cast<uint32_t>((money / 100) % 100);
|
||||
uint32_t mc = static_cast<uint32_t>(money % 100);
|
||||
ImGui::Text("Your money: %ug %us %uc", mg, ms, mc);
|
||||
|
||||
if (vendor.canRepair) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f);
|
||||
if (ImGui::SmallButton("Repair All")) {
|
||||
gameHandler.repairAll(vendor.vendorGuid, false);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Repair all equipped items");
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell");
|
||||
|
|
|
|||
|
|
@ -1805,6 +1805,15 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
|
|||
if (!bonusLine.empty()) {
|
||||
ImGui::TextColored(green, "%s", bonusLine.c_str());
|
||||
}
|
||||
if (item.maxDurability > 0) {
|
||||
float durPct = static_cast<float>(item.curDurability) / static_cast<float>(item.maxDurability);
|
||||
ImVec4 durColor;
|
||||
if (durPct > 0.5f) durColor = ImVec4(0.1f, 1.0f, 0.1f, 1.0f); // green
|
||||
else if (durPct > 0.25f) durColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // yellow
|
||||
else durColor = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); // red
|
||||
ImGui::TextColored(durColor, "Durability %u / %u",
|
||||
item.curDurability, item.maxDurability);
|
||||
}
|
||||
if (item.sellPrice > 0) {
|
||||
uint32_t g = item.sellPrice / 10000;
|
||||
uint32_t s = (item.sellPrice / 100) % 100;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue