diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 28a83f695..d64fb995e 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -200,6 +200,7 @@ namespace Framework.Constants public const int BoundaryVisualizeFailsafeLimit = 750; public const int BoundaryVisualizeSpawnHeight = 5; public const uint AIDefaultCooldown = 5000; + public const uint CreatureTappersSoftCap = 5; /// /// GameObject Const diff --git a/Source/Framework/Constants/UnitConst.cs b/Source/Framework/Constants/UnitConst.cs index 55bdb49bb..9aed05b69 100644 --- a/Source/Framework/Constants/UnitConst.cs +++ b/Source/Framework/Constants/UnitConst.cs @@ -578,7 +578,7 @@ namespace Framework.Constants TrackUnit = 0x08, Tapped = 0x10, // Lua_UnitIsTapped SpecialInfo = 0x20, - Unused = 0x40, // previously UNIT_DYNFLAG_DEAD + CanSkin = 0x40, // previously UNIT_DYNFLAG_DEAD ReferAFriend = 0x80 } } diff --git a/Source/Game/AI/CoreAI/CreatureAI.cs b/Source/Game/AI/CoreAI/CreatureAI.cs index e727b2a40..6cb9bc517 100644 --- a/Source/Game/AI/CoreAI/CreatureAI.cs +++ b/Source/Game/AI/CoreAI/CreatureAI.cs @@ -309,7 +309,7 @@ namespace Game.AI // sometimes bosses stuck in combat? me.CombatStop(true); - me.SetLootRecipient(null); + me.SetTappedBy(null); me.ResetPlayerDamageReq(); me.SetLastDamagedTime(0); me.SetCannotReachTarget(false); diff --git a/Source/Game/AI/ScriptedAI/ScriptedAI.cs b/Source/Game/AI/ScriptedAI/ScriptedAI.cs index 8d91c20d9..f5bb2d2ac 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedAI.cs @@ -211,7 +211,7 @@ namespace Game.AI if (reset) { who.LoadCreaturesAddon(); - who.SetLootRecipient(null); + who.SetTappedBy(null); who.ResetPlayerDamageReq(); who.SetLastDamagedTime(0); who.SetCannotReachTarget(false); @@ -596,7 +596,7 @@ namespace Game.AI if (reset) { creature.LoadCreaturesAddon(); - creature.SetLootRecipient(null); + creature.SetTappedBy(null); creature.ResetPlayerDamageReq(); creature.SetLastDamagedTime(0); creature.SetCannotReachTarget(false); diff --git a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs index 1070b3683..553aebd27 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs @@ -135,7 +135,7 @@ namespace Game.AI { me.RemoveAllAuras(); me.CombatStop(true); - me.SetLootRecipient(null); + me.SetTappedBy(null); EngagementOver(); diff --git a/Source/Game/AI/SmartScripts/SmartScript.cs b/Source/Game/AI/SmartScripts/SmartScript.cs index 48a489c26..aa4325cdd 100644 --- a/Source/Game/AI/SmartScripts/SmartScript.cs +++ b/Source/Game/AI/SmartScripts/SmartScript.cs @@ -826,12 +826,14 @@ namespace Game.AI if (_me == null) break; - Player player = _me.GetLootRecipient(); - if (player != null) + foreach (ObjectGuid tapperGuid in _me.GetTapList()) { - player.RewardPlayerAndGroupAtEvent(e.Action.killedMonster.creature, player); - Log.outDebug(LogFilter.ScriptsAi, "SmartScript.ProcessAction: SMART_ACTION_CALL_KILLEDMONSTER: Player {0}, Killcredit: {1}", - player.GetGUID().ToString(), e.Action.killedMonster.creature); + Player tapper = Global.ObjAccessor.GetPlayer(_me, tapperGuid); + if (tapper != null) + { + tapper.KilledMonsterCredit(e.Action.killedMonster.creature, _me.GetGUID()); + Log.outDebug(LogFilter.ScriptsAi, $"SmartScript::ProcessAction: SMART_ACTION_CALL_KILLEDMONSTER: Player {tapper.GetGUID()}, Killcredit: {e.Action.killedMonster.creature}"); + } } } else // Specific target type @@ -2984,22 +2986,11 @@ namespace Game.AI { if (_me) { - Group lootGroup = _me.GetLootRecipientGroup(); - if (lootGroup) + foreach (ObjectGuid tapperGuid in _me.GetTapList()) { - for (GroupReference refe = lootGroup.GetFirstMember(); refe != null; refe = refe.Next()) - { - Player recipient = refe.GetSource(); - if (recipient) - if (recipient.IsInMap(_me)) - targets.Add(recipient); - } - } - else - { - Player recipient = _me.GetLootRecipient(); - if (recipient) - targets.Add(recipient); + Player tapper = Global.ObjAccessor.GetPlayer(_me, tapperGuid); + if (tapper != null) + targets.Add(tapper); } } break; diff --git a/Source/Game/BattleGrounds/BattleGround.cs b/Source/Game/BattleGrounds/BattleGround.cs index 06d59d448..46ecd640e 100644 --- a/Source/Game/BattleGrounds/BattleGround.cs +++ b/Source/Game/BattleGrounds/BattleGround.cs @@ -1855,7 +1855,7 @@ namespace Game.BattleGrounds void RewardXPAtKill(Player killer, Player victim) { if (WorldConfig.GetBoolValue(WorldCfg.BgXpForKill) && killer && victim) - killer.RewardPlayerAndGroupAtKill(victim, true); + new KillRewarder(new[] { killer }, victim, true).Reward(); } public uint GetTeamScore(int teamIndex) diff --git a/Source/Game/Chat/Commands/DebugCommands.cs b/Source/Game/Chat/Commands/DebugCommands.cs index 8a7f18fe2..e949c773e 100644 --- a/Source/Game/Chat/Commands/DebugCommands.cs +++ b/Source/Game/Chat/Commands/DebugCommands.cs @@ -626,8 +626,13 @@ namespace Game.Chat if (!target) return false; - handler.SendSysMessage("Loot recipient for creature {0} (GUID {1}, SpawnID {2}) is {3}", target.GetName(), target.GetGUID().ToString(), target.GetSpawnId(), - target.HasLootRecipient() ? (target.GetLootRecipient() ? target.GetLootRecipient().GetName() : "offline") : "no loot recipient"); + handler.SendSysMessage($"Loot recipients for creature {target.GetName()} ({target.GetGUID()}, SpawnID {target.GetSpawnId()}) are:"); + + foreach (ObjectGuid tapperGuid in target.GetTapList()) + { + Player tapper = Global.ObjAccessor.GetPlayer(target, tapperGuid); + handler.SendSysMessage($"* {(tapper != null ? tapper.GetName() : "offline")}"); + } return true; } diff --git a/Source/Game/Chat/Commands/NPCCommands.cs b/Source/Game/Chat/Commands/NPCCommands.cs index e4eddc230..760e7011c 100644 --- a/Source/Game/Chat/Commands/NPCCommands.cs +++ b/Source/Game/Chat/Commands/NPCCommands.cs @@ -324,8 +324,8 @@ namespace Game.Chat return false; } - Loot loot = creatureTarget.loot; - if (!creatureTarget.IsDead() || loot == null || loot.Empty()) + Loot loot = creatureTarget._loot; + if (!creatureTarget.IsDead() || loot == null || loot.IsLooted()) { handler.SendSysMessage(CypherStrings.CommandNotDeadOrNoLoot, creatureTarget.GetName()); return false; diff --git a/Source/Game/Entities/Creature/Creature.Fields.cs b/Source/Game/Entities/Creature/Creature.Fields.cs index 5af7ac601..f47bde160 100644 --- a/Source/Game/Entities/Creature/Creature.Fields.cs +++ b/Source/Game/Entities/Creature/Creature.Fields.cs @@ -90,9 +90,9 @@ namespace Game.Entities // vendor items List m_vendorItemCounts = new(); - public Loot loot; - ObjectGuid m_lootRecipient; - ObjectGuid m_lootRecipientGroup; + internal Dictionary m_personalLoot = new(); + public Loot _loot; + List m_tapList = new(); } public enum ObjectCellMoveState diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 3adb76b27..f1ce8e90e 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -160,7 +160,7 @@ namespace Game.Entities SetDeathState(DeathState.Dead); RemoveAllAuras(); //DestroyForNearbyPlayers(); // old UpdateObjectVisibility() - loot = null; + _loot = null; uint respawnDelay = m_respawnDelay; CreatureAI ai = GetAI(); if (ai != null) @@ -507,7 +507,10 @@ namespace Game.Entities if (IsEngaged()) AIUpdateTick(diff); - loot?.Update(); + _loot?.Update(); + + foreach (var (playerOwner, loot) in m_personalLoot) + loot.Update(); if (m_corpseRemoveTime <= GameTime.GetGameTime()) { @@ -1165,24 +1168,8 @@ namespace Game.Entities } public void ResetPickPocketRefillTimer() { _pickpocketLootRestore = 0; } public bool CanGeneratePickPocketLoot() { return _pickpocketLootRestore <= GameTime.GetGameTime(); } - public ObjectGuid GetLootRecipientGUID() { return m_lootRecipient; } - public Player GetLootRecipient() - { - if (m_lootRecipient.IsEmpty()) - return null; - return Global.ObjAccessor.FindPlayer(m_lootRecipient); - } - - public Group GetLootRecipientGroup() - { - if (m_lootRecipientGroup.IsEmpty()) - return null; - - return Global.GroupMgr.GetGroupByGUID(m_lootRecipientGroup); - } - - public void SetLootRecipient(Unit unit, bool withGroup = true) + public void SetTappedBy(Unit unit, bool withGroup = true) { // set the player whose group should receive the right // to loot the creature after it dies @@ -1190,12 +1177,14 @@ namespace Game.Entities if (unit == null) { - m_lootRecipient.Clear(); - m_lootRecipientGroup.Clear(); + m_tapList.Clear(); RemoveDynamicFlag(UnitDynFlags.Lootable | UnitDynFlags.Tapped); return; } + if (m_tapList.Count >= SharedConst.CreatureTappersSoftCap) + return; + if (!unit.IsTypeId(TypeId.Player) && !unit.IsVehicle()) return; @@ -1203,31 +1192,62 @@ namespace Game.Entities if (player == null) // normal creature, no player involved return; - m_lootRecipient = player.GetGUID(); + m_tapList.Add(player.GetGUID()); if (withGroup) { Group group = player.GetGroup(); - if (group) - m_lootRecipientGroup = group.GetGUID(); + if (group != null) + for (var itr = group.GetFirstMember(); itr != null; itr = itr.Next()) + if (GetMap().IsRaid() || group.SameSubGroup(player, itr.GetSource())) + m_tapList.Add(itr.GetSource().GetGUID()); } - else - m_lootRecipientGroup = ObjectGuid.Empty; - SetDynamicFlag(UnitDynFlags.Tapped); + if (m_tapList.Count >= SharedConst.CreatureTappersSoftCap) + SetDynamicFlag(UnitDynFlags.Tapped); } public bool IsTappedBy(Player player) { - if (player.GetGUID() == m_lootRecipient) - return true; + return m_tapList.Contains(player.GetGUID()); + } - Group playerGroup = player.GetGroup(); - if (!playerGroup || playerGroup != GetLootRecipientGroup()) // if we dont have a group we arent the recipient - return false; // if creature doesnt have group bound it means it was solo killed by someone else + public override Loot GetLootForPlayer(Player player) + { + if (m_personalLoot.Empty()) + return _loot; + + var loot = m_personalLoot.LookupByKey(player.GetGUID()); + if (loot != null) + return loot; + + return null; + } + + public bool IsFullyLooted() + { + if (_loot != null && !_loot.IsLooted()) + return false; + + foreach (var (_, loot) in m_personalLoot) + if (!loot.IsLooted()) + return false; return true; } + public bool IsSkinnedBy(Player player) + { + Loot loot = GetLootForPlayer(player); + if (loot != null) + return loot.loot_type == LootType.Skinning; + + return false; + } + + public List GetTapList() { return m_tapList; } + public void SetTapList(List tapList) { m_tapList = tapList; } + public bool HasLootRecipient() { return !m_tapList.Empty(); } + public void SaveToDB() { // this should only be used when the creature has already been loaded @@ -1867,7 +1887,7 @@ namespace Game.Entities else SetSpawnHealth(); - SetLootRecipient(null); + SetTappedBy(null); ResetPlayerDamageReq(); SetCannotReachTarget(false); @@ -1926,7 +1946,7 @@ namespace Game.Entities Log.outDebug(LogFilter.Unit, "Respawning creature {0} ({1})", GetName(), GetGUID().ToString()); m_respawnTime = 0; ResetPickPocketRefillTimer(); - loot = null; + _loot = null; if (m_originalEntry != GetEntry()) UpdateEntry(m_originalEntry); @@ -2560,13 +2580,9 @@ namespace Game.Entities { return !_isMissingCanSwimFlagOutOfCombat; } - + public void AllLootRemovedFromCorpse() { - if ((loot == null || loot.loot_type != LootType.Skinning) && !IsPet() && GetCreatureTemplate().SkinLootId != 0 && HasLootRecipient()) - if (LootStorage.Skinning.HaveLootFor(GetCreatureTemplate().SkinLootId)) - SetUnitFlag(UnitFlags.Skinnable); - long now = GameTime.GetGameTime(); // Do not reset corpse remove time if corpse is already removed if (m_corpseRemoveTime <= now) @@ -2576,7 +2592,19 @@ namespace Game.Entities float decayRate = m_ignoreCorpseDecayRatio ? 1.0f : WorldConfig.GetFloatValue(WorldCfg.RateCorpseDecayLooted); // corpse skinnable, but without skinning flag, and then skinned, corpse will despawn next update - if (loot != null && loot.loot_type == LootType.Skinning) + bool isFullySkinned() + { + if (_loot != null && _loot.loot_type == LootType.Skinning && _loot.IsLooted()) + return true; + + foreach (var (_, loot) in m_personalLoot) + if (loot.loot_type != LootType.Skinning || !loot.IsLooted()) + return false; + + return true; + } + + if (isFullySkinned()) m_corpseRemoveTime = now; else m_corpseRemoveTime = now + (uint)(m_corpseDelay * decayRate); @@ -3269,10 +3297,6 @@ namespace Game.Entities return false; return true; } - - public bool HasLootRecipient() { return !m_lootRecipient.IsEmpty() || !m_lootRecipientGroup.IsEmpty(); } - - public override Loot GetLootForPlayer(Player player) { return loot; } public LootModes GetLootMode() { return m_LootMode; } public bool HasLootMode(LootModes lootMode) { return Convert.ToBoolean(m_LootMode & lootMode); } @@ -3343,7 +3367,6 @@ namespace Game.Entities void SetDisableReputationGain(bool disable) { DisableReputationGain = disable; } public bool IsReputationGainDisabled() { return DisableReputationGain; } - public bool IsDamageEnoughForLootingAndReward() { return m_creatureInfo.FlagsExtra.HasAnyFlag(CreatureFlagsExtra.NoPlayerDamageReq) || m_PlayerDamageReq == 0; } // Part of Evade mechanics long GetLastDamagedTime() { return _lastDamagedTime; } diff --git a/Source/Game/Entities/GameObject/GameObject.cs b/Source/Game/Entities/GameObject/GameObject.cs index 455851f16..03ddbda2f 100644 --- a/Source/Game/Entities/GameObject/GameObject.cs +++ b/Source/Game/Entities/GameObject/GameObject.cs @@ -956,45 +956,47 @@ namespace Game.Entities public Loot GetFishLoot(Player lootOwner) { - uint zone, subzone; uint defaultzone = 1; - GetZoneAndAreaId(out zone, out subzone); Loot fishLoot = new(GetMap(), GetGUID(), LootType.Fishing, null); - // if subzone loot exist use it - fishLoot.FillLoot(subzone, LootStorage.Fishing, lootOwner, true, true); - if (fishLoot.Empty()) + uint areaId = GetAreaId(); + AreaTableRecord areaEntry; + while ((areaEntry = CliDB.AreaTableStorage.LookupByKey(areaId)) != null) { - //subzone no result,use zone loot - fishLoot.FillLoot(zone, LootStorage.Fishing, lootOwner, true); - //use zone 1 as default, somewhere fishing got nothing,becase subzone and zone not set, like Off the coast of Storm Peaks. - if (fishLoot.Empty()) - fishLoot.FillLoot(defaultzone, LootStorage.Fishing, lootOwner, true, true); + fishLoot.FillLoot(areaId, LootStorage.Fishing, lootOwner, true, true); + if (!fishLoot.IsLooted()) + break; + + areaId = areaEntry.ParentAreaID; } + if (fishLoot.IsLooted()) + fishLoot.FillLoot(defaultzone, LootStorage.Fishing, lootOwner, true, true); + return fishLoot; } public Loot GetFishLootJunk(Player lootOwner) { - uint zone, subzone; uint defaultzone = 1; - GetZoneAndAreaId(out zone, out subzone); Loot fishLoot = new(GetMap(), GetGUID(), LootType.FishingJunk, null); - // if subzone loot exist use it - fishLoot.FillLoot(subzone, LootStorage.Fishing, lootOwner, true, true, LootModes.JunkFish); - if (fishLoot.Empty()) //use this becase if zone or subzone has normal mask drop, then fishloot.FillLoot return true. + uint areaId = GetAreaId(); + AreaTableRecord areaEntry; + while ((areaEntry = CliDB.AreaTableStorage.LookupByKey(areaId)) != null) { - //use zone loot - fishLoot.FillLoot(zone, LootStorage.Fishing, lootOwner, true, true, LootModes.JunkFish); - if (fishLoot.Empty()) - //use zone 1 as default - fishLoot.FillLoot(defaultzone, LootStorage.Fishing, lootOwner, true, true, LootModes.JunkFish); + fishLoot.FillLoot(areaId, LootStorage.Fishing, lootOwner, true, true, LootModes.JunkFish); + if (!fishLoot.IsLooted()) + break; + + areaId = areaEntry.ParentAreaID; } + if (fishLoot.IsLooted()) + fishLoot.FillLoot(defaultzone, LootStorage.Fishing, lootOwner, true, true, LootModes.JunkFish); + return fishLoot; } @@ -2886,51 +2888,6 @@ namespace Game.Entities GetMap().InsertGameObjectModel(m_model); } - Player GetLootRecipient() - { - if (m_lootRecipient.IsEmpty()) - return null; - return Global.ObjAccessor.FindPlayer(m_lootRecipient); - } - - Group GetLootRecipientGroup() - { - if (m_lootRecipientGroup.IsEmpty()) - return Global.GroupMgr.GetGroupByGUID(m_lootRecipientGroup); - - return null; - } - - public void SetLootRecipient(Unit unit, Group group) - { - // set the player whose group should receive the right - // to loot the creature after it dies - // should be set to null after the loot disappears - - if (unit == null) - { - m_lootRecipient.Clear(); - m_lootRecipientGroup = group ? group.GetGUID() : ObjectGuid.Empty; - return; - } - - if (!unit.IsTypeId(TypeId.Player) && !unit.IsVehicle()) - return; - - Player player = unit.GetCharmerOrOwnerPlayerOrPlayerItself(); - if (player == null) // normal creature, no player involved - return; - - m_lootRecipient = player.GetGUID(); - - // either get the group from the passed parameter or from unit's one - Group unitGroup = player.GetGroup(); - if (group) - m_lootRecipientGroup = group.GetGUID(); - else if (unitGroup) - m_lootRecipientGroup = unitGroup.GetGUID(); - } - public bool IsLootAllowedFor(Player player) { Loot loot = GetLootForPlayer(player); @@ -2942,15 +2899,8 @@ namespace Game.Entities return false; } - if (m_lootRecipient.IsEmpty() && m_lootRecipientGroup.IsEmpty()) - return true; - - if (player.GetGUID() == m_lootRecipient) - return true; - - Group playerGroup = player.GetGroup(); - if (!playerGroup || playerGroup != GetLootRecipientGroup()) // if we dont have a group we arent the recipient - return false; // if go doesnt have group bound it means it was solo killed by someone else + if (HasLootRecipient()) + return m_tapList.Contains(player.GetGUID()); // if go doesnt have group bound it means it was solo killed by someone else return true; } @@ -3444,7 +3394,10 @@ namespace Game.Entities public uint GetUseCount() { return m_usetimes; } uint GetUniqueUseCount() { return (uint)m_unique_users.Count; } - bool HasLootRecipient() { return !m_lootRecipient.IsEmpty() || !m_lootRecipientGroup.IsEmpty(); } + List GetTapList() { return m_tapList; } + void SetTapList(List tapList) { m_tapList = tapList; } + + bool HasLootRecipient() { return !m_tapList.Empty(); } public override uint GetLevelForTarget(WorldObject target) { @@ -3552,8 +3505,7 @@ namespace Game.Entities List m_unique_users = new(); uint m_usetimes; - ObjectGuid m_lootRecipient; - ObjectGuid m_lootRecipientGroup; + List m_tapList = new(); LootModes m_LootMode; // bitmask, default LOOT_MODE_DEFAULT, determines what loot will be lootable long m_packedRotation; Quaternion m_localRotation; diff --git a/Source/Game/Entities/Object/Update/UpdateFields.cs b/Source/Game/Entities/Object/Update/UpdateFields.cs index 776450389..228ddae33 100644 --- a/Source/Game/Entities/Object/Update/UpdateFields.cs +++ b/Source/Game/Entities/Object/Update/UpdateFields.cs @@ -99,8 +99,6 @@ namespace Game.Entities Unit unit = obj.ToUnit(); if (unit != null) { - unitDynFlags &= ~(uint)UnitDynFlags.Tapped; - Creature creature = obj.ToCreature(); if (creature != null) { @@ -109,6 +107,9 @@ namespace Game.Entities if (!receiver.IsAllowedToLoot(creature)) unitDynFlags &= ~(uint)UnitDynFlags.Lootable; + + if ((unitDynFlags & (uint)UnitDynFlags.CanSkin) != 0 && creature.IsSkinnedBy(receiver)) + unitDynFlags &= ~(uint)UnitDynFlags.CanSkin; } // unit UNIT_DYNFLAG_TRACK_UNIT should only be sent to caster of SPELL_AURA_MOD_STALKED auras @@ -1327,7 +1328,7 @@ namespace Game.Entities data.WriteUInt32(GetViewerDependentFlags(this, owner, receiver)); data.WriteUInt32(Flags2); - data.WriteUInt32(Flags3); + data.WriteUInt32(GetViewerDependentFlags3(this, owner, receiver)); data.WriteUInt32(GetViewerDependentAuraState(this, owner, receiver)); for (int i = 0; i < 2; ++i) data.WriteUInt32(AttackRoundBaseTime[i]); @@ -1712,7 +1713,7 @@ namespace Game.Entities } if (changesMask[45]) { - data.WriteUInt32(Flags3); + data.WriteUInt32(GetViewerDependentFlags3(this, owner, receiver)); } if (changesMask[46]) { @@ -2308,6 +2309,14 @@ namespace Game.Entities return flags; } + uint GetViewerDependentFlags3(UnitData unitData, Unit unit, Player receiver) + { + uint flags = unitData.Flags3; + if ((flags & (uint)UnitFlags3.AlreadySkinned) != 0 && unit.IsCreature() && !unit.ToCreature().IsSkinnedBy(receiver)) + flags &= ~(uint)UnitFlags3.AlreadySkinned; + + return flags; + } uint GetViewerDependentAuraState(UnitData unitData, Unit unit, Player receiver) { // Check per caster aura states to not enable using a spell in client if specified aura is not by target diff --git a/Source/Game/Entities/Player/KillRewarder.cs b/Source/Game/Entities/Player/KillRewarder.cs index 9832ba74a..7a44535ea 100644 --- a/Source/Game/Entities/Player/KillRewarder.cs +++ b/Source/Game/Entities/Player/KillRewarder.cs @@ -21,14 +21,14 @@ using Game.Groups; using Game.Maps; using Game.Scenarios; using System.Collections.Generic; +using System.Linq; namespace Game.Entities { public class KillRewarder { - Player _killer; + Player[] _killers; Unit _victim; - Group _group; float _groupRate; Player _maxNotGrayMember; uint _count; @@ -39,11 +39,10 @@ namespace Game.Entities bool _isBattleground; bool _isPvP; - public KillRewarder(Player killer, Unit victim, bool isBattleground) + public KillRewarder(Player[] killers, Unit victim, bool isBattleground) { - _killer = killer; + _killers = killers; _victim = victim; - _group = killer.GetGroup(); _groupRate = 1.0f; _maxNotGrayMember = null; _count = 0; @@ -60,27 +59,37 @@ namespace Game.Entities // or if its owned by player and its not a vehicle else if (victim.GetCharmerOrOwnerGUID().IsPlayer()) _isPvP = !victim.IsVehicle(); - - _InitGroupData(); } public void Reward() { - // 3. Reward killer (and group, if necessary). - if (_group) - // 3.1. If killer is in group, reward group. - _RewardGroup(); - else + SortedSet processedGroups = new(); + foreach (Player killer in _killers) { - // 3.2. Reward single killer (not group case). - // 3.2.1. Initialize initial XP amount based on killer's level. - _InitXP(_killer); - // To avoid unnecessary calculations and calls, - // proceed only if XP is not ZERO or player is not on Battleground - // (Battlegroundrewards only XP, that's why). - if (!_isBattleground || _xp != 0) - // 3.2.2. Reward killer. - _RewardPlayer(_killer, false); + _InitGroupData(killer); + + // 3. Reward killer (and group, if necessary). + Group group = killer.GetGroup(); + if (group != null) + { + if (!processedGroups.Add(group)) + continue; + + // 3.1. If killer is in group, reward group. + _RewardGroup(group, killer); + } + else + { + // 3.2. Reward single killer (not group case). + // 3.2.1. Initialize initial XP amount based on killer's level. + _InitXP(killer, killer); + // To avoid unnecessary calculations and calls, + // proceed only if XP is not ZERO or player is not on battleground + // (battleground rewards only XP, that's why). + if (!_isBattleground || _xp != 0) + // 3.2.2. Reward killer. + _RewardPlayer(killer, false); + } } // 5. Credit instance encounter. @@ -99,25 +108,26 @@ namespace Game.Entities uint guildId = victim.GetMap().GetOwnerGuildId(); var guild = Global.GuildMgr.GetGuildById(guildId); if (guild != null) - guild.UpdateCriteria(CriteriaType.KillCreature, victim.GetEntry(), 1, 0, victim, _killer); + guild.UpdateCriteria(CriteriaType.KillCreature, victim.GetEntry(), 1, 0, victim, _killers.First()); Scenario scenario = victim.GetScenario(); if (scenario != null) - scenario.UpdateCriteria(CriteriaType.KillCreature, victim.GetEntry(), 1, 0, victim, _killer); + scenario.UpdateCriteria(CriteriaType.KillCreature, victim.GetEntry(), 1, 0, victim, _killers.First()); } } - void _InitGroupData() + void _InitGroupData(Player killer) { - if (_group) + Group group = killer.GetGroup(); + if (group != null) { // 2. In case when player is in group, initialize variables necessary for group calculations: - for (GroupReference refe = _group.GetFirstMember(); refe != null; refe = refe.Next()) + for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) { Player member = refe.GetSource(); - if (member) + if (member != null) { - if (_killer == member || (member.IsAtGroupRewardDistance(_victim) && member.IsAlive())) + if (killer == member || (member.IsAtGroupRewardDistance(_victim) && member.IsAlive())) { uint lvl = member.GetLevel(); // 2.1. _count - number of alive group members within reward distance; @@ -143,14 +153,14 @@ namespace Game.Entities _count = 1; } - void _InitXP(Player player) + void _InitXP(Player player, Player killer) { // Get initial value of XP for kill. // XP is given: // * on Battlegrounds; // * otherwise, not in PvP; // * not if killer is on vehicle. - if (_isBattleground || (!_isPvP && _killer.GetVehicle() == null)) + if (_isBattleground || (!_isPvP && killer.GetVehicle() == null)) _xp = Formulas.XPGain(player, _victim, _isBattleground); } @@ -164,7 +174,7 @@ namespace Game.Entities void _RewardXP(Player player, float rate) { uint xp = _xp; - if (_group) + if (player.GetGroup() != null) { // 4.2.1. If player is in group, adjust XP: // * set to 0 if player's level is more than maximum level of not gray member; @@ -188,7 +198,7 @@ namespace Game.Entities Pet pet = player.GetPet(); if (pet) // 4.2.4. If player has pet, reward pet with XP (100% for single player, 50% for group case). - pet.GivePetXP(_group ? xp / 2 : xp); + pet.GivePetXP(player.GetGroup() != null ? xp / 2 : xp); } } @@ -202,7 +212,7 @@ namespace Game.Entities void _RewardKillCredit(Player player) { // 4.4. Give kill credit (player must not be in group, or he must be alive or without corpse). - if (!_group || player.IsAlive() || player.GetCorpse() == null) + if (player.GetGroup() == null || player.IsAlive() || player.GetCorpse() == null) { Creature target = _victim.ToCreature(); if (target != null) @@ -228,7 +238,7 @@ namespace Game.Entities // Give reputation and kill credit only in PvE. if (!_isPvP || _isBattleground) { - float rate = _group ? _groupRate * player.GetLevel() / _sumLevel : 1.0f; + float rate = player.GetGroup() != null ? _groupRate * player.GetLevel() / _sumLevel : 1.0f; if (_xp != 0) // 4.2. Give XP. _RewardXP(player, rate); @@ -241,34 +251,34 @@ namespace Game.Entities } } - void _RewardGroup() + void _RewardGroup(Group group, Player killer) { if (_maxLevel != 0) { if (_maxNotGrayMember != null) // 3.1.1. Initialize initial XP amount based on maximum level of group member, // for whom victim is not gray. - _InitXP(_maxNotGrayMember); + _InitXP(_maxNotGrayMember, killer); // To avoid unnecessary calculations and calls, // proceed only if XP is not ZERO or player is not on Battleground // (Battlegroundrewards only XP, that's why). if (!_isBattleground || _xp != 0) { - bool isDungeon = !_isPvP && CliDB.MapStorage.LookupByKey(_killer.GetMapId()).IsDungeon(); + bool isDungeon = !_isPvP && CliDB.MapStorage.LookupByKey(killer.GetMapId()).IsDungeon(); if (!_isBattleground) { // 3.1.2. Alter group rate if group is in raid (not for Battlegrounds). - bool isRaid = !_isPvP && CliDB.MapStorage.LookupByKey(_killer.GetMapId()).IsRaid() && _group.IsRaidGroup(); + bool isRaid = !_isPvP && CliDB.MapStorage.LookupByKey(killer.GetMapId()).IsRaid() && group.IsRaidGroup(); _groupRate = Formulas.XPInGroupRate(_count, isRaid); } // 3.1.3. Reward each group member (even dead or corpse) within reward distance. - for (GroupReference refe = _group.GetFirstMember(); refe != null; refe = refe.Next()) + for (GroupReference refe = group.GetFirstMember(); refe != null; refe = refe.Next()) { Player member = refe.GetSource(); if (member) { // Killer may not be at reward distance, check directly - if (_killer == member || member.IsAtGroupRewardDistance(_victim)) + if (killer == member || member.IsAtGroupRewardDistance(_victim)) _RewardPlayer(member, isDungeon); } } diff --git a/Source/Game/Entities/Player/Player.Combat.cs b/Source/Game/Entities/Player/Player.Combat.cs index a6e233da8..ef485553c 100644 --- a/Source/Game/Entities/Player/Player.Combat.cs +++ b/Source/Game/Entities/Player/Player.Combat.cs @@ -44,11 +44,6 @@ namespace Game.Entities } } - public void RewardPlayerAndGroupAtKill(Unit victim, bool isBattleground) - { - new KillRewarder(this, victim, isBattleground).Reward(); - } - public void RewardPlayerAndGroupAtEvent(uint creature_id, WorldObject pRewardSource) { if (pRewardSource == null) diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index 32d022830..88a82521d 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -3126,7 +3126,7 @@ namespace Game.Entities public bool IsAllowedToLoot(Creature creature) { - if (!creature.IsDead() || !creature.IsDamageEnoughForLootingAndReward()) + if (!creature.IsDead()) return false; if (HasPendingBind()) @@ -3139,15 +3139,6 @@ namespace Game.Entities if (!loot.HasAllowedLooter(GetGUID()) || (!loot.HasItemForAll() && !loot.HasItemFor(this))) // no loot in creature for this player return false; - if (loot.loot_type == LootType.Skinning) - return creature.GetLootRecipientGUID() == GetGUID(); - - Group thisGroup = GetGroup(); - if (!thisGroup) - return this == creature.GetLootRecipient(); - else if (thisGroup != creature.GetLootRecipientGroup()) - return false; - switch (loot.GetLootMethod()) { case LootMethod.PersonalLoot:// @todo implement personal loot (http://wow.gamepedia.com/Loot#Personal_Loot) diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index e0df1e954..a3acb1b68 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -706,36 +706,16 @@ namespace Game.Entities bool isRewardAllowed = true; if (creature != null) - { - isRewardAllowed = creature.IsDamageEnoughForLootingAndReward(); - if (!isRewardAllowed) - creature.SetLootRecipient(null); - } + isRewardAllowed = !creature.GetTapList().Empty(); + List tappers = new(); if (isRewardAllowed && creature) { - Player lootRecipient = creature.GetLootRecipient(); - if (lootRecipient != null) + foreach (ObjectGuid tapperGuid in creature.GetTapList()) { - // Loot recipient can be in a different map - if (!creature.IsInMap(lootRecipient)) - { - Group group = creature.GetLootRecipientGroup(); - if (group != null) - { - for (GroupReference itr = group.GetFirstMember(); itr != null; itr = itr.Next()) - { - Player member = itr.GetSource(); - if (!member || !creature.IsInMap(member)) - continue; - - player = member; - break; - } - } - } - else - player = creature.GetLootRecipient(); + Player tapper = Global.ObjAccessor.GetPlayer(creature, tapperGuid); + if (tapper != null) + tappers.Add(tapper); } } @@ -745,64 +725,92 @@ namespace Game.Entities // Reward player, his pets, and group/raid members // call kill spell proc event (before real die and combat stop to triggering auras removed at death/combat stop) - if (isRewardAllowed && player != null && player != victim) + if (isRewardAllowed) { PartyKillLog partyKillLog = new(); partyKillLog.Player = player.GetGUID(); partyKillLog.Victim = victim.GetGUID(); + partyKillLog.Write(); - Player looter = player; - var group = player.GetGroup(); - if (group) + HashSet groups = new(); + foreach (Player tapper in tappers) { - group.BroadcastPacket(partyKillLog, group.GetMemberGroup(player.GetGUID()) != 0); - - if (creature) + Group tapperGroup = tapper.GetGroup(); + if (tapperGroup != null) { - group.UpdateLooterGuid(creature, true); - if (!group.GetLooterGuid().IsEmpty()) + if (groups.Add(tapperGroup)) { - looter = Global.ObjAccessor.FindPlayer(group.GetLooterGuid()); - if (looter) - creature.SetLootRecipient(looter); // update creature loot recipient to the allowed looter. + tapperGroup.BroadcastPacket(partyKillLog, tapperGroup.GetMemberGroup(tapper.GetGUID()) != 0); + + if (creature) + tapperGroup.UpdateLooterGuid(creature, true); } } + else + tapper.SendPacket(partyKillLog); } - else - player.SendPacket(partyKillLog); // Generate loot before updating looter if (creature) { - creature.loot = new Loot(creature.GetMap(), creature.GetGUID(), LootType.Corpse, group); - Loot loot = creature.loot; - if (creature.GetMap().Is25ManRaid()) - loot.maxDuplicates = 3; - + DungeonEncounterRecord dungeonEncounter = null; InstanceScript instance = creature.GetInstanceScript(); if (instance != null) + dungeonEncounter = instance.GetBossDungeonEncounter(creature); + + if (creature.GetMap().IsDungeon()) { - DungeonEncounterRecord dungeonEncounter = instance.GetBossDungeonEncounter(creature); + Group group = !groups.Empty() ? groups.First() : null; + Player looter = group ? Global.ObjAccessor.GetPlayer(creature, group.GetLooterGuid()) : tappers[0]; + + Loot loot = new(creature.GetMap(), creature.GetGUID(), LootType.Corpse, dungeonEncounter != null ? group : null); + if (dungeonEncounter != null) loot.SetDungeonEncounterId(dungeonEncounter.Id); + + uint lootid = creature.GetCreatureTemplate().LootId; + if (lootid != 0) + loot.FillLoot(lootid, LootStorage.Creature, looter, dungeonEncounter != null, false, creature.GetLootMode(), creature.GetMap().GetDifficultyLootItemContext()); + + if (creature.GetLootMode() > 0) + loot.GenerateMoneyLoot(creature.GetCreatureTemplate().MinGold, creature.GetCreatureTemplate().MaxGold); + + if (group) + loot.NotifyLootList(creature.GetMap()); + + if (dungeonEncounter != null || groups.Empty()) + creature._loot = loot; // TODO: personal boss loot + else + creature.m_personalLoot[looter.GetGUID()] = loot; // trash mob loot is personal, generated with round robin rules + + // Update round robin looter only if the creature had loot + if (!loot.IsLooted()) + foreach (Group tapperGroup in groups) + tapperGroup.UpdateLooterGuid(creature); } + else + { + foreach (Player tapper in tappers) + { + Loot loot = new(creature.GetMap(), creature.GetGUID(), LootType.Corpse, null); - uint lootid = creature.GetCreatureTemplate().LootId; - if (lootid != 0) - loot.FillLoot(lootid, LootStorage.Creature, looter, false, false, creature.GetLootMode(), creature.GetMap().GetDifficultyLootItemContext()); + if (dungeonEncounter != null) + loot.SetDungeonEncounterId(dungeonEncounter.Id); - if (creature.GetLootMode() > 0) - loot.GenerateMoneyLoot(creature.GetCreatureTemplate().MinGold, creature.GetCreatureTemplate().MaxGold); + uint lootid = creature.GetCreatureTemplate().LootId; + if (lootid != 0) + loot.FillLoot(lootid, LootStorage.Creature, tapper, true, false, creature.GetLootMode(), creature.GetMap().GetDifficultyLootItemContext()); - loot.NotifyLootList(creature.GetMap()); + if (creature.GetLootMode() > 0) + loot.GenerateMoneyLoot(creature.GetCreatureTemplate().MinGold, creature.GetCreatureTemplate().MaxGold); - // Update round robin looter only if the creature had loot - if (group != null && !loot.Empty()) - group.UpdateLooterGuid(creature); + creature.m_personalLoot[tapper.GetGUID()] = loot; + } + } } - player.RewardPlayerAndGroupAtKill(victim, false); - } + new KillRewarder(tappers.ToArray(), victim, false).Reward(); + } // Do KILL and KILLED procs. KILL proc is called only for the unit who landed the killing blow (and its owner - for pets and totems) regardless of who tapped the victim if (attacker != null && (attacker.IsPet() || attacker.IsTotem())) @@ -816,16 +824,10 @@ namespace Game.Entities if (!victim.IsCritter()) { ProcSkillsAndAuras(attacker, victim, new ProcFlagsInit(ProcFlags.Kill), new ProcFlagsInit(ProcFlags.Heartbeat), ProcFlagsSpellType.MaskAll, ProcFlagsSpellPhase.None, ProcFlagsHit.None, null, null, null); - if (player != null && player.GetGroup() != null) - { - for (GroupReference itr = player.GetGroup().GetFirstMember(); itr != null; itr = itr.Next()) - { - Player member = itr.GetSource(); - if (member != null) - if (member.IsAtGroupRewardDistance(victim)) - Unit.ProcSkillsAndAuras(member, victim, new ProcFlagsInit(ProcFlags.None, ProcFlags2.TargetDies), new ProcFlagsInit(ProcFlags.None), ProcFlagsSpellType.MaskAll, ProcFlagsSpellPhase.None, ProcFlagsHit.None, null, null, null); - } - } + + foreach (Player tapper in tappers) + if (tapper.IsAtGroupRewardDistance(victim)) + Unit.ProcSkillsAndAuras(tapper, victim, new ProcFlagsInit(ProcFlags.None, ProcFlags2.TargetDies), new ProcFlagsInit(), ProcFlagsSpellType.MaskAll, ProcFlagsSpellPhase.None, ProcFlagsHit.None, null, null, null); } // Proc auras on death - must be before aura/combat remove @@ -849,9 +851,9 @@ namespace Game.Entities // Inform pets (if any) when player kills target) // MUST come after victim.setDeathState(JUST_DIED); or pet next target // selection will get stuck on same target and break pet react state - if (player != null) + foreach (Player tapper in tappers) { - Pet pet = player.GetPet(); + Pet pet = tapper.GetPet(); if (pet != null && pet.IsAlive() && pet.IsControlled()) { if (pet.IsAIEnabled()) @@ -899,10 +901,16 @@ namespace Game.Entities if (!creature.IsPet()) { // must be after setDeathState which resets dynamic flags - if (creature.loot != null && !creature.loot.IsLooted()) + if (!creature.IsFullyLooted()) creature.SetDynamicFlag(UnitDynFlags.Lootable); else creature.AllLootRemovedFromCorpse(); + + if (LootStorage.Skinning.HaveLootFor(creature.GetCreatureTemplate().SkinLootId)) + { + creature.SetDynamicFlag(UnitDynFlags.CanSkin); + creature.SetUnitFlag(UnitFlags.Skinnable); + } } // Call KilledUnit for creatures, this needs to be called after the lootable flag is set diff --git a/Source/Game/Entities/Unit/Unit.cs b/Source/Game/Entities/Unit/Unit.cs index b1ce7d736..da6dd6cb3 100644 --- a/Source/Game/Entities/Unit/Unit.cs +++ b/Source/Game/Entities/Unit/Unit.cs @@ -2574,8 +2574,7 @@ namespace Game.Entities if (victim.GetTypeId() != TypeId.Player && (!victim.IsControlledByPlayer() || victim.IsVehicle())) { - if (!victim.ToCreature().HasLootRecipient()) - victim.ToCreature().SetLootRecipient(attacker); + victim.ToCreature().SetTappedBy(attacker); if (attacker == null || attacker.IsControlledByPlayer()) victim.ToCreature().LowerPlayerDamageReq(health < damage ? health : damage); diff --git a/Source/Game/Handlers/LootHandler.cs b/Source/Game/Handlers/LootHandler.cs index be9ec33f1..3e158f543 100644 --- a/Source/Game/Handlers/LootHandler.cs +++ b/Source/Game/Handlers/LootHandler.cs @@ -355,11 +355,14 @@ namespace Game if (loot.IsLooted()) { - creature.RemoveDynamicFlag(UnitDynFlags.Lootable); + if (creature.IsFullyLooted()) + { + creature.RemoveDynamicFlag(UnitDynFlags.Lootable); - // skip pickpocketing loot for speed, skinning timer reduction is no-op in fact - if (!creature.IsAlive()) - creature.AllLootRemovedFromCorpse(); + // skip pickpocketing loot for speed, skinning timer reduction is no-op in fact + if (!creature.IsAlive()) + creature.AllLootRemovedFromCorpse(); + } } else { @@ -369,11 +372,11 @@ namespace Game loot.roundRobinPlayer.Clear(); loot.NotifyLootList(creature.GetMap()); } - - // force dynflag update to update looter and lootable info - creature.m_values.ModifyValue(creature.m_objectData).ModifyValue(creature.m_objectData.DynamicFlags); - creature.ForceUpdateFieldChange(); } + + // force dynflag update to update looter and lootable info + creature.m_values.ModifyValue(creature.m_objectData).ModifyValue(creature.m_objectData.DynamicFlags); + creature.ForceUpdateFieldChange(); } } diff --git a/Source/Game/Loot/Loot.cs b/Source/Game/Loot/Loot.cs index 8bfca59cc..7486d4690 100644 --- a/Source/Game/Loot/Loot.cs +++ b/Source/Game/Loot/Loot.cs @@ -659,7 +659,6 @@ namespace Game.Loots public Loot(Map map, ObjectGuid owner, LootType type, Group group) { loot_type = type; - maxDuplicates = 1; _guid = map ? ObjectGuid.Create(HighGuid.LootObject, map.GetId(), 0, map.GenerateLowGuid(HighGuid.LootObject)) : ObjectGuid.Empty; _owner = owner; _itemContext = ItemContext.None; @@ -1052,26 +1051,6 @@ namespace Game.Loots } } - public void Clear() - { - PlayerFFAItems.Clear(); - - foreach (ObjectGuid playerGuid in PlayersLooting) - { - Player player = Global.ObjAccessor.FindConnectedPlayer(playerGuid); - if (player != null) - player.GetSession().DoLootRelease(this); - } - - PlayersLooting.Clear(); - items.Clear(); - gold = 0; - unlootedCount = 0; - roundRobinPlayer = ObjectGuid.Empty; - _itemContext = 0; - _rolls.Clear(); - } - public void NotifyLootList(Map map) { LootList lootList = new(); @@ -1095,7 +1074,6 @@ namespace Game.Loots } } - public bool Empty() { return items.Empty() && gold == 0; } public bool IsLooted() { return gold == 0 && unlootedCount == 0; } public void AddLooter(ObjectGuid guid) { PlayersLooting.Add(guid); } @@ -1117,7 +1095,6 @@ namespace Game.Loots public byte unlootedCount; public ObjectGuid roundRobinPlayer; // GUID of the player having the Round-Robin ownership for the loot. If 0, round robin owner has released. public LootType loot_type; // required for achievement system - public byte maxDuplicates; // Max amount of items with the same entry that can drop (default is 1; on 25 man raid mode 3) List PlayersLooting = new(); MultiMap PlayerFFAItems = new(); diff --git a/Source/Game/Loot/LootManager.cs b/Source/Game/Loot/LootManager.cs index f6cb5143f..28cf86abd 100644 --- a/Source/Game/Loot/LootManager.cs +++ b/Source/Game/Loot/LootManager.cs @@ -1100,12 +1100,6 @@ namespace Game.Loots if (!Convert.ToBoolean(item.lootmode & _lootMode)) return true; - byte foundDuplicates = 0; - foreach (var lootItem in _loot.items) - if (lootItem.itemid == item.itemid) - if (++foundDuplicates == _loot.maxDuplicates) - return true; - return false; } diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 0e114f9cb..2f8477545 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -1213,8 +1213,8 @@ namespace Game.Spells break; case Targets.UnitTargetTapList: Creature creatureCaster = m_caster.ToCreature(); - if (creatureCaster != null) - target = creatureCaster.GetLootRecipient(); + if (creatureCaster != null && !creatureCaster.GetTapList().Empty()) + target = Global.ObjAccessor.GetWorldObject(creatureCaster, creatureCaster.GetTapList().SelectRandom()); break; case Targets.UnitOwnCritter: { @@ -5188,7 +5188,7 @@ namespace Game.Spells Creature creature = m_targets.GetUnitTarget().ToCreature(); Loot loot = creature.GetLootForPlayer(m_caster.ToPlayer()); - if (loot != null && !loot.IsLooted()) + if (loot != null && (!loot.IsLooted() || loot.loot_type == LootType.Skinning)) return SpellCastResult.TargetNotLooted; SkillType skill = creature.GetCreatureTemplate().GetRequiredLootSkill(); @@ -8557,8 +8557,8 @@ namespace Game.Spells { Creature targetCreature = unit.ToCreature(); if (targetCreature != null) - if (!targetCreature.HasLootRecipient() && unitCaster.IsPlayer()) - targetCreature.SetLootRecipient(unitCaster); + if (unitCaster.IsPlayer()) + targetCreature.SetTappedBy(unitCaster); } } diff --git a/Source/Game/Spells/SpellEffects.cs b/Source/Game/Spells/SpellEffects.cs index 7ef27562a..af8991bf3 100644 --- a/Source/Game/Spells/SpellEffects.cs +++ b/Source/Game/Spells/SpellEffects.cs @@ -1844,25 +1844,25 @@ namespace Game.Spells { creature.StartPickPocketRefillTimer(); - creature.loot = new Loot(creature.GetMap(), creature.GetGUID(), LootType.Pickpocketing, null); + creature._loot = new Loot(creature.GetMap(), creature.GetGUID(), LootType.Pickpocketing, null); uint lootid = creature.GetCreatureTemplate().PickPocketId; if (lootid != 0) - creature.loot.FillLoot(lootid, LootStorage.Pickpocketing, player, true); + creature._loot.FillLoot(lootid, LootStorage.Pickpocketing, player, true); // Generate extra money for pick pocket loot uint a = RandomHelper.URand(0, creature.GetLevel() / 2); uint b = RandomHelper.URand(0, player.GetLevel() / 2); - creature.loot.gold = (uint)(10 * (a + b) * WorldConfig.GetFloatValue(WorldCfg.RateDropMoney)); + creature._loot.gold = (uint)(10 * (a + b) * WorldConfig.GetFloatValue(WorldCfg.RateDropMoney)); } - else if (creature.loot != null) + else if (creature._loot != null) { - if (creature.loot.loot_type == LootType.Pickpocketing && creature.loot.IsLooted()) - player.SendLootError(creature.loot.GetGUID(), creature.GetGUID(), LootError.AlreadPickPocketed); + if (creature._loot.loot_type == LootType.Pickpocketing && creature._loot.IsLooted()) + player.SendLootError(creature._loot.GetGUID(), creature.GetGUID(), LootError.AlreadPickPocketed); return; } - player.SendLoot(creature.loot); + player.SendLoot(creature._loot); } [SpellEffectHandler(SpellEffectName.AddFarsight)] @@ -3493,12 +3493,12 @@ namespace Game.Spells SkillType skill = creature.GetCreatureTemplate().GetRequiredLootSkill(); - creature.RemoveUnitFlag(UnitFlags.Skinnable); + creature.SetUnitFlag3(UnitFlags3.AlreadySkinned); creature.SetDynamicFlag(UnitDynFlags.Lootable); - creature.loot = new Loot(creature.GetMap(), creature.GetGUID(), LootType.Skinning, null); - creature.loot.FillLoot(creature.GetCreatureTemplate().SkinLootId, LootStorage.Skinning, player, true); - creature.SetLootRecipient(player, false); - player.SendLoot(creature.loot); + Loot loot = new(creature.GetMap(), creature.GetGUID(), LootType.Skinning, null); + creature.m_personalLoot[player.GetGUID()] = loot; + loot.FillLoot(creature.GetCreatureTemplate().SkinLootId, LootStorage.Skinning, player, true); + player.SendLoot(loot); if (skill == SkillType.Skinning) {