mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 00:23:50 +00:00
feat(repair): DBC-based repair cost estimation and UI display
Calculate repair costs client-side using DurabilityCosts.dbc and DurabilityQuality.dbc. Block repair when player can't afford it and only apply optimistic durability/gold updates when cost is verified. Show repair cost next to the Repair All button in the vendor window.
This commit is contained in:
parent
3dec33ecf1
commit
53244d025c
7 changed files with 160 additions and 14 deletions
|
|
@ -1953,6 +1953,7 @@ public:
|
|||
void buyBackItem(uint32_t buybackSlot);
|
||||
void repairItem(uint64_t vendorGuid, uint64_t itemGuid);
|
||||
void repairAll(uint64_t vendorGuid, bool useGuildBank = false);
|
||||
uint32_t estimateRepairAllCost() const;
|
||||
const std::deque<BuybackItem>& getBuybackItems() const;
|
||||
void autoEquipItemBySlot(int backpackIndex);
|
||||
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ public:
|
|||
void buyBackItem(uint32_t buybackSlot);
|
||||
void repairItem(uint64_t vendorGuid, uint64_t itemGuid);
|
||||
void repairAll(uint64_t vendorGuid, bool useGuildBank = false);
|
||||
uint32_t estimateRepairAllCost() const;
|
||||
const std::deque<BuybackItem>& getBuybackItems() const { return buybackItems_; }
|
||||
void autoEquipItemBySlot(int backpackIndex);
|
||||
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
||||
|
|
@ -397,6 +398,15 @@ private:
|
|||
std::string pendingSaveSetName_;
|
||||
std::string pendingSaveSetIcon_;
|
||||
std::vector<EquipmentSetInfo> equipmentSetInfo_;
|
||||
|
||||
// ---- Repair cost DBC cache ----
|
||||
mutable bool repairDbcLoaded_ = false;
|
||||
// DurabilityCosts.dbc: [itemLevel] -> multiplier[29] (weapon subclass 0-20, armor subclass+21)
|
||||
mutable std::unordered_map<uint32_t, std::array<uint32_t, 29>> durabilityCosts_;
|
||||
// DurabilityQuality.dbc: [id] -> quality_mod float
|
||||
mutable std::unordered_map<uint32_t, float> durabilityQuality_;
|
||||
void loadRepairDbc() const;
|
||||
uint32_t estimateItemRepairCost(uint64_t itemGuid) const;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,8 @@ void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
|
|||
const uint32_t prevDur = it->second.curDurability;
|
||||
it->second.curDurability = val;
|
||||
inventoryChanged = true;
|
||||
LOG_DEBUG("Item durability update: guid=0x", std::hex, block.guid,
|
||||
std::dec, " dur ", prevDur, "->", val, "/", it->second.maxDurability);
|
||||
// Warn once when durability drops below 20% for an equipped item.
|
||||
const uint32_t maxDur = it->second.maxDurability;
|
||||
if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) {
|
||||
|
|
|
|||
|
|
@ -5936,6 +5936,11 @@ void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) {
|
|||
if (inventoryHandler_) inventoryHandler_->repairAll(vendorGuid, useGuildBank);
|
||||
}
|
||||
|
||||
uint32_t GameHandler::estimateRepairAllCost() const {
|
||||
if (inventoryHandler_) return inventoryHandler_->estimateRepairAllCost();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) {
|
||||
if (inventoryHandler_) inventoryHandler_->sellItem(vendorGuid, itemGuid, count);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
#include "core/logger.hpp"
|
||||
#include "network/world_socket.hpp"
|
||||
#include "network/packet.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -697,9 +699,8 @@ void InventoryHandler::handleLootResponse(network::Packet& packet) {
|
|||
const bool wotlkLoot = isActiveExpansion("wotlk");
|
||||
if (!LootResponseParser::parse(packet, currentLoot_, wotlkLoot)) return;
|
||||
const bool hasLoot = !currentLoot_.items.empty() || currentLoot_.gold > 0;
|
||||
LOG_WARNING("[GO-DIAG] SMSG_LOOT_RESPONSE: guid=0x", std::hex, currentLoot_.lootGuid, std::dec,
|
||||
" items=", currentLoot_.items.size(), " gold=", currentLoot_.gold,
|
||||
" hasLoot=", hasLoot);
|
||||
LOG_DEBUG("SMSG_LOOT_RESPONSE: guid=0x", std::hex, currentLoot_.lootGuid, std::dec,
|
||||
" items=", currentLoot_.items.size(), " gold=", currentLoot_.gold);
|
||||
if (!hasLoot && owner_.isCasting() && owner_.getCurrentCastSpellId() != 0 && lastInteractedGoGuid_ != 0) {
|
||||
LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast");
|
||||
return;
|
||||
|
|
@ -1029,22 +1030,58 @@ void InventoryHandler::buyBackItem(uint32_t buybackSlot) {
|
|||
|
||||
void InventoryHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
|
||||
uint32_t cost = estimateItemRepairCost(itemGuid);
|
||||
if (cost > 0 && owner_.getMoneyCopper() < cost) {
|
||||
owner_.addUIError("Not enough money");
|
||||
return;
|
||||
}
|
||||
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt8(0);
|
||||
if (!isClassicLikeExpansion()) packet.writeUInt8(0);
|
||||
owner_.socket->send(packet);
|
||||
|
||||
// Only do optimistic update if we verified the player can afford it
|
||||
if (cost > 0) {
|
||||
owner_.playerMoneyCopper_ -= cost;
|
||||
auto it = owner_.onlineItems_.find(itemGuid);
|
||||
if (it != owner_.onlineItems_.end()) {
|
||||
it->second.curDurability = it->second.maxDurability;
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) {
|
||||
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
|
||||
|
||||
uint32_t totalCost = estimateRepairAllCost();
|
||||
|
||||
if (!useGuildBank && totalCost > 0 && owner_.getMoneyCopper() < totalCost) {
|
||||
owner_.addUIError("Not enough money");
|
||||
return;
|
||||
}
|
||||
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM));
|
||||
packet.writeUInt64(vendorGuid);
|
||||
packet.writeUInt64(0);
|
||||
packet.writeUInt8(useGuildBank ? 1 : 0);
|
||||
if (!isClassicLikeExpansion()) packet.writeUInt8(useGuildBank ? 1 : 0);
|
||||
owner_.socket->send(packet);
|
||||
LOG_INFO("Sent CMSG_REPAIR_ITEM repairAll vendor=0x", std::hex, vendorGuid,
|
||||
std::dec, " guildBank=", useGuildBank ? 1 : 0);
|
||||
|
||||
// Only do optimistic update if we verified the player can afford it
|
||||
if (totalCost > 0) {
|
||||
if (!useGuildBank) {
|
||||
owner_.playerMoneyCopper_ -= totalCost;
|
||||
}
|
||||
for (auto& [guid, info] : owner_.onlineItems_) {
|
||||
if (info.maxDurability > 0 && info.curDurability < info.maxDurability) {
|
||||
info.curDurability = info.maxDurability;
|
||||
}
|
||||
}
|
||||
rebuildOnlineInventory();
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryHandler::autoEquipItemBySlot(int backpackIndex) {
|
||||
|
|
@ -1088,9 +1125,8 @@ void InventoryHandler::useItemBySlot(int backpackIndex) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
LOG_WARNING("useItemBySlot: item='", slot.item.name, "' entry=", slot.item.itemId,
|
||||
" guid=0x", std::hex, itemGuid, std::dec,
|
||||
" spellId=", useSpellId, " spellCount=", info->spells.size());
|
||||
LOG_DEBUG("useItemBySlot: entry=", slot.item.itemId,
|
||||
" spellId=", useSpellId);
|
||||
}
|
||||
auto packet = owner_.packetParsers_
|
||||
? owner_.packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId)
|
||||
|
|
@ -1300,7 +1336,6 @@ void InventoryHandler::handleListInventory(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
vendorWindowOpen_ = true;
|
||||
owner_.closeGossip();
|
||||
if (owner_.addonEventCallback_) owner_.addonEventCallback_("MERCHANT_SHOW", {});
|
||||
|
|
@ -3255,5 +3290,94 @@ void InventoryHandler::addMoneyCopper(uint32_t amount) {
|
|||
owner_.fireAddonEvent("CHAT_MSG_MONEY", {msg});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Repair cost estimation from DBC data
|
||||
// ============================================================
|
||||
|
||||
void InventoryHandler::loadRepairDbc() const {
|
||||
if (repairDbcLoaded_) return;
|
||||
repairDbcLoaded_ = true;
|
||||
|
||||
auto* am = owner_.services().assetManager;
|
||||
if (!am || !am->isInitialized()) return;
|
||||
|
||||
// DurabilityCosts.dbc: field 0 = itemLevel (key), fields 1-29 = cost multipliers
|
||||
// Columns 1-21 = weapon subclass (0-20), columns 22-29 = armor subclass (0-7)
|
||||
auto costsDbc = am->loadDBC("DurabilityCosts.dbc");
|
||||
if (costsDbc && costsDbc->isLoaded()) {
|
||||
uint32_t count = costsDbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t itemLevel = costsDbc->getUInt32(i, 0);
|
||||
std::array<uint32_t, 29> mults{};
|
||||
for (uint32_t f = 0; f < 29; ++f)
|
||||
mults[f] = costsDbc->getUInt32(i, f + 1);
|
||||
durabilityCosts_[itemLevel] = mults;
|
||||
}
|
||||
}
|
||||
|
||||
// DurabilityQuality.dbc: field 0 = id (key), field 1 = quality_mod (float)
|
||||
auto qualDbc = am->loadDBC("DurabilityQuality.dbc");
|
||||
if (qualDbc && qualDbc->isLoaded()) {
|
||||
uint32_t count = qualDbc->getRecordCount();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint32_t id = qualDbc->getUInt32(i, 0);
|
||||
float mod = qualDbc->getFloat(i, 1);
|
||||
durabilityQuality_[id] = mod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t InventoryHandler::estimateItemRepairCost(uint64_t itemGuid) const {
|
||||
auto itemIt = owner_.onlineItems_.find(itemGuid);
|
||||
if (itemIt == owner_.onlineItems_.end()) return 0;
|
||||
const auto& item = itemIt->second;
|
||||
|
||||
if (item.maxDurability == 0 || item.curDurability >= item.maxDurability) return 0;
|
||||
uint32_t lostDur = item.maxDurability - item.curDurability;
|
||||
|
||||
auto infoIt = owner_.itemInfoCache_.find(item.entry);
|
||||
if (infoIt == owner_.itemInfoCache_.end()) return 0;
|
||||
const auto& info = infoIt->second;
|
||||
|
||||
loadRepairDbc();
|
||||
|
||||
// Look up DurabilityCosts multiplier for this itemLevel
|
||||
auto costIt = durabilityCosts_.find(info.itemLevel);
|
||||
if (costIt == durabilityCosts_.end()) return 0;
|
||||
|
||||
// Determine column index: weapon (class=2) uses subClass directly,
|
||||
// armor (class=4) uses subClass + 21
|
||||
uint32_t colIndex = 0;
|
||||
if (info.itemClass == 2) { // ITEM_CLASS_WEAPON
|
||||
colIndex = info.subClass;
|
||||
} else if (info.itemClass == 4) { // ITEM_CLASS_ARMOR
|
||||
colIndex = info.subClass + 21;
|
||||
} else {
|
||||
return 0; // only weapons and armor have durability
|
||||
}
|
||||
if (colIndex >= 29) return 0;
|
||||
|
||||
uint32_t dmultiplier = costIt->second[colIndex];
|
||||
if (dmultiplier == 0) return 0;
|
||||
|
||||
// Quality modifier lookup: index is (quality + 1) * 2
|
||||
uint32_t qualIndex = (info.quality + 1) * 2;
|
||||
auto qualIt = durabilityQuality_.find(qualIndex);
|
||||
if (qualIt == durabilityQuality_.end()) return 0;
|
||||
float qualMod = qualIt->second;
|
||||
|
||||
uint32_t cost = static_cast<uint32_t>(lostDur * dmultiplier * qualMod);
|
||||
if (cost == 0 && lostDur > 0) cost = 1; // minimum 1 copper
|
||||
return cost;
|
||||
}
|
||||
|
||||
uint32_t InventoryHandler::estimateRepairAllCost() const {
|
||||
uint32_t total = 0;
|
||||
for (const auto& [guid, info] : owner_.onlineItems_) {
|
||||
total += estimateItemRepairCost(guid);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -948,8 +948,7 @@ void QuestHandler::selectGossipOption(uint32_t optionId) {
|
|||
}
|
||||
auto pkt = ListInventoryPacket::build(currentGossip_.npcGuid);
|
||||
owner_.socket->send(pkt);
|
||||
LOG_INFO("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec,
|
||||
" vendor=", (int)isVendor, " repair=", (int)isArmorer);
|
||||
LOG_DEBUG("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip_.npcGuid, std::dec);
|
||||
}
|
||||
|
||||
if (textLower.find("make this inn your home") != std::string::npos ||
|
||||
|
|
|
|||
|
|
@ -896,11 +896,16 @@ void WindowManager::renderVendorWindow(game::GameHandler& gameHandler,
|
|||
renderCoinsFromCopper(money);
|
||||
|
||||
if (vendor.canRepair) {
|
||||
uint32_t repairCost = gameHandler.estimateRepairAllCost();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f);
|
||||
if (ImGui::SmallButton("Repair All")) {
|
||||
gameHandler.repairAll(vendor.vendorGuid, false);
|
||||
}
|
||||
if (repairCost > 0) {
|
||||
ImGui::SameLine(0, 4);
|
||||
renderCoinsFromCopper(repairCost);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
// Show durability summary of all equipment
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
|
|
@ -925,7 +930,7 @@ void WindowManager::renderVendorWindow(game::GameHandler& gameHandler,
|
|||
gameHandler.repairAll(vendor.vendorGuid, true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Repair all equipped items using guild bank funds");
|
||||
ImGui::SetTooltip("Repair all items using guild bank funds");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue