* Core/Misc: Improvement item durability repair logic

* Move item repair cost calculation logic from Player::DurabilityRepair to Item::CalculateDurabilityRepairCost
* Make item repair cost calculation more congruent with client logic (std::round)
* Do not repair items if player does not have enough money to pay for the repair of all items at once
* Withdrawing money from guild bank only once to avoid spam in the guild bank log and reduce number of transactions in the database
This commit is contained in:
Mikhail Redko authored and Ovahlord committed 2022-06-15 14:24:35 +02:00
1 parent b7d6959816
commit c0a1bc8c8d
7 files changed
+169 -83

No files matched your search

+48
View File
@@ -34,6 +34,7 @@
#include "SpellMgr.h"
#include "TradeData.h"
#include "UpdateData.h"
#include "World.h"
#include "WorldPacket.h"
#include "WorldSession.h"
@@ -702,6 +703,53 @@ bool Item::CanBeTraded(bool mail, bool trade) const
return true;
}
uint32 Item::CalculateDurabilityRepairCost(float discount) const
{
uint32 maxDurability = GetUInt32Value(ITEM_FIELD_MAXDURABILITY);
if (!maxDurability)
return 0;
uint32 curDurability = GetUInt32Value(ITEM_FIELD_DURABILITY);
ASSERT(maxDurability >= curDurability);
uint32 lostDurability = maxDurability - curDurability;
if (!lostDurability)
return 0;
ItemTemplate const* itemTemplate = GetTemplate();
DurabilityCostsEntry const* durabilityCost = sDurabilityCostsStore.LookupEntry(itemTemplate->GetBaseItemLevel());
if (!durabilityCost)
return 0;
uint32 durabilityQualityEntryId = (itemTemplate->GetQuality() + 1) * 2;
DurabilityQualityEntry const* durabilityQualityEntry = sDurabilityQualityStore.LookupEntry(durabilityQualityEntryId);
if (!durabilityQualityEntry)
return 0;
uint32 dmultiplier;
switch (itemTemplate->GetClass())
{
case ITEM_CLASS_WEAPON:
dmultiplier = durabilityCost->Multiplier[ItemSubClassToDurabilityMultiplierId(itemTemplate->GetClass(), itemTemplate->GetSubClass())];
break;
case ITEM_CLASS_ARMOR:
dmultiplier = durabilityCost->Multiplier[ItemSubClassToDurabilityMultiplierId(itemTemplate->GetClass(), itemTemplate->GetSubClass())];
break;
default:
dmultiplier = 0;
break;
}
uint32 cost = static_cast<uint32>(std::round(lostDurability * dmultiplier * double(durabilityQualityEntry->Data)));
cost = uint32(cost * discount * sWorld->getRate(RATE_REPAIRCOST));
if (cost == 0) // Fix for ITEM_QUALITY_ARTIFACT
cost = 1;
return cost;
}
bool Item::HasEnchantRequiredSkill(Player const* player) const
{
// Check all enchants for required skill
+2
View File
@@ -105,6 +105,8 @@ class TC_GAME_API Item : public Object
void SetInTrade(bool b = true) { mb_in_trade = b; }
bool IsInTrade() const { return mb_in_trade; }
uint32 CalculateDurabilityRepairCost(float discount) const;
bool HasEnchantRequiredSkill(Player const* player) const;
uint32 GetEnchantRequiredLevel() const;
+106 -80
View File
@@ -4417,104 +4417,130 @@ void Player::DurabilityPointLossForEquipSlot(EquipmentSlots slot)
DurabilityPointsLoss(pItem, 1);
}
uint32 Player::DurabilityRepairAll(bool cost, float discountMod, bool guildBank)
void Player::DurabilityRepairAll(bool takeCost, float discountMod, bool guildBank)
{
uint32 TotalCost = 0;
// Collecting all items that can be repaired and repair costs
std::list<std::pair<Item*, uint32>> itemRepairCostStore;
// equipped, backpack, bags itself
for (uint8 i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; i++)
TotalCost += DurabilityRepair(((INVENTORY_SLOT_BAG_0 << 8) | i), cost, discountMod, guildBank);
if (Item* item = GetItemByPos(((INVENTORY_SLOT_BAG_0 << 8) | i)))
if (uint32 cost = item->CalculateDurabilityRepairCost(discountMod))
itemRepairCostStore.push_back(std::make_pair(item, cost));
// bank, buyback and keys not repaired
// items in inventory bags
for (uint8 j = INVENTORY_SLOT_BAG_START; j < INVENTORY_SLOT_BAG_END; j++)
for (uint8 i = 0; i < MAX_BAG_SIZE; i++)
TotalCost += DurabilityRepair(((j << 8) | i), cost, discountMod, guildBank);
return TotalCost;
}
if (Item* item = GetItemByPos(((j << 8) | i)))
if (uint32 cost = item->CalculateDurabilityRepairCost(discountMod))
itemRepairCostStore.push_back(std::make_pair(item, cost));
uint32 Player::DurabilityRepair(uint16 pos, bool cost, float discountMod, bool guildBank)
{
Item* item = GetItemByPos(pos);
uint32 TotalCost = 0;
if (!item)
return TotalCost;
uint32 maxDurability = item->GetUInt32Value(ITEM_FIELD_MAXDURABILITY);
if (!maxDurability)
return TotalCost;
uint32 curDurability = item->GetUInt32Value(ITEM_FIELD_DURABILITY);
if (cost)
// Handling a free repair case - just repair every item without taking cost.
if (!takeCost)
{
uint32 LostDurability = maxDurability - curDurability;
if (LostDurability>0)
{
ItemTemplate const* ditemProto = item->GetTemplate();
for (auto const& [item, cost] : itemRepairCostStore)
DurabilityRepair(item->GetPos(), false, 0.f);
DurabilityCostsEntry const* dcost = sDurabilityCostsStore.LookupEntry(ditemProto->GetBaseItemLevel());
if (!dcost)
{
TC_LOG_ERROR("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) tried to repair an item (ItemID: %u) with invalid item level %u",
GetName().c_str(), GetGUID().ToString().c_str(), ditemProto->GetId(), ditemProto->GetBaseItemLevel());
return TotalCost;
}
uint32 dQualitymodEntryId = (ditemProto->GetQuality() + 1) * 2;
DurabilityQualityEntry const* dQualitymodEntry = sDurabilityQualityStore.LookupEntry(dQualitymodEntryId);
if (!dQualitymodEntry)
{
TC_LOG_ERROR("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) tried to repair an item (ItemID: %u) with invalid QualitymodEntry %u",
GetName().c_str(), GetGUID().ToString().c_str(), ditemProto->GetId(), dQualitymodEntryId);
return TotalCost;
}
uint32 dmultiplier = dcost->Multiplier[ItemSubClassToDurabilityMultiplierId(ditemProto->GetClass(), ditemProto->GetSubClass())];
uint32 costs = uint32(LostDurability*dmultiplier*double(dQualitymodEntry->Data));
costs = uint32(costs * discountMod * sWorld->getRate(RATE_REPAIRCOST));
if (costs == 0) //fix for ITEM_QUALITY_ARTIFACT
costs = 1;
if (guildBank)
{
if (GetGuildId() == 0)
{
TC_LOG_DEBUG("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) tried to repair item in a guild bank but is not member of a guild",
GetName().c_str(), GetGUID().ToString().c_str());
return TotalCost;
}
Guild* guild = sGuildMgr->GetGuildById(GetGuildId());
if (!guild)
return TotalCost;
if (!guild->HandleMemberWithdrawMoney(GetSession(), costs, true))
return TotalCost;
TotalCost = costs;
}
else if (!HasEnoughMoney(uint64(costs)))
{
TC_LOG_DEBUG("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) has not enough money to repair item",
GetName().c_str(), GetGUID().ToString().c_str());
return TotalCost;
}
else
ModifyMoney(-int64(costs));
}
return;
}
item->SetUInt32Value(ITEM_FIELD_DURABILITY, maxDurability);
// Handling a free repair case - just repair every item without taking cost.
if (!takeCost)
{
for (auto const& [item, cost] : itemRepairCostStore)
DurabilityRepair(item->GetPos(), false, 0.f);
return;
}
if (guildBank)
{
// Handling a repair for guild money case.
// We have to repair items one by one until the guild bank has enough money available for withdrawal or until all items are repaired.
Guild* guild = GetGuild();
if (!guild)
return; // silent return, client shouldn't display this button for players without guild.
uint64 const availableGuildMoney = guild->GetMemberAvailableMoneyForRepairItems(GetGUID());
if (availableGuildMoney == 0)
return;
// Sort the items by repair cost from lowest to highest
itemRepairCostStore.sort([](auto const& a, auto const& b) -> bool
{
return a.second < b.second;
});
// We must calculate total repair cost and take money once to avoid spam in the guild bank log and reduce number of transactions in the database
uint32 totalCost = 0;
for (auto const& [item, cost] : itemRepairCostStore)
{
uint64 newTotalCost = totalCost + cost;
if (newTotalCost > availableGuildMoney || newTotalCost > MAX_MONEY_AMOUNT)
break;
totalCost = static_cast<uint32>(newTotalCost);
// Repair item without taking cost. We'll do it later.
DurabilityRepair(item->GetPos(), false, 0.f);
}
// Take money for repairs from the guild bank
guild->HandleMemberWithdrawMoney(GetSession(), totalCost, true);
}
else
{
// Handling a repair for player's money case.
// Unlike repairing for guild money, in this case we must first check if player has enough money to repair all the items at once.
uint32 totalCost = 0;
for (auto const& [item, cost] : itemRepairCostStore)
totalCost += cost;
if (!HasEnoughMoney(uint64(totalCost)))
return; // silent return, client should display error by itself and not send opcode.
ModifyMoney(-int32(totalCost));
// Payment for repair has already been taken, so just repair every item without taking cost.
for (auto const& [item, cost] : itemRepairCostStore)
DurabilityRepair(item->GetPos(), false, 0.f);
}
}
void Player::DurabilityRepair(uint16 pos, bool takeCost, float discountMod)
{
Item* item = GetItemByPos(pos);
if (!item)
return;
if (takeCost)
{
uint32 cost = item->CalculateDurabilityRepairCost(discountMod);
if (!HasEnoughMoney(uint64(cost)))
{
TC_LOG_DEBUG("entities.player.items", "Player::DurabilityRepair: Player '%s' (%s) has not enough money to repair item",
GetName().c_str(), GetGUID().ToString().c_str());
return;
}
ModifyMoney(-int32(cost));
}
bool isBroken = item->IsBroken();
item->SetUInt32Value(ITEM_FIELD_DURABILITY, item->GetUInt32Value(ITEM_FIELD_MAXDURABILITY));
item->SetState(ITEM_CHANGED, this);
// reapply mods for total broken and repaired item if equipped
if (IsEquipmentPos(pos) && !curDurability)
if (IsEquipmentPos(pos) && isBroken)
_ApplyItemMods(item, pos & 255, true);
return TotalCost;
}
void Player::RepopAtGraveyard()
+2 -2
View File
@@ -1883,8 +1883,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void DurabilityPointsLossAll(int32 points, bool inventory);
void DurabilityPointsLoss(Item* item, int32 points);
void DurabilityPointLossForEquipSlot(EquipmentSlots slot);
uint32 DurabilityRepairAll(bool cost, float discountMod, bool guildBank);
uint32 DurabilityRepair(uint16 pos, bool cost, float discountMod, bool guildBank);
void DurabilityRepairAll(bool takeCost, float discountMod, bool guildBank);
void DurabilityRepair(uint16 pos, bool takeCost, float discountMod);
void UpdateMirrorTimers();
void StopMirrorTimers();
+9
View File
@@ -3073,6 +3073,15 @@ bool Guild::ChangeMemberRank(CharacterDatabaseTransaction& trans, ObjectGuid gui
return false;
}
uint64 Guild::GetMemberAvailableMoneyForRepairItems(ObjectGuid guid) const
{
Member const* member = GetMember(guid);
if (!member)
return 0;
return std::min(m_bankMoney, static_cast<uint64>(_GetMemberRemainingMoney(member)));
}
bool Guild::IsMember(ObjectGuid guid) const
{
Members::const_iterator itr = m_members.find(guid.GetCounter());
+1
View File
@@ -862,6 +862,7 @@ public:
bool AddMember(CharacterDatabaseTransaction& trans, ObjectGuid guid, uint8 rankId = GUILD_RANK_NONE);
void DeleteMember(CharacterDatabaseTransaction& trans, ObjectGuid guid, bool isDisbanding = false, bool isKicked = false, bool canDeleteGuild = false);
bool ChangeMemberRank(CharacterDatabaseTransaction& trans, ObjectGuid guid, uint8 newRank);
uint64 GetMemberAvailableMoneyForRepairItems(ObjectGuid guid) const;
bool IsMember(ObjectGuid guid) const;
uint32 GetMembersCount() { return m_members.size(); }
+1 -1
View File
@@ -472,7 +472,7 @@ void WorldSession::HandleRepairItemOpcode(WorldPacket& recvData)
Item* item = _player->GetItemByGuid(itemGUID);
if (item)
_player->DurabilityRepair(item->GetPos(), true, discountMod, guildBank != 0);
_player->DurabilityRepair(item->GetPos(), true, discountMod);
}
else
{