//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
///
/// Handles item and zen pickup for the offline player.
///
public sealed class ItemPickupHandler
{
private const byte MinPickupRange = 1;
private static readonly PickupItemAction PickupAction = new();
private static readonly HashSet Jewels =
[
ItemConstants.JewelOfChaos,
ItemConstants.JewelOfBless,
ItemConstants.JewelOfSoul,
ItemConstants.JewelOfLife,
ItemConstants.JewelOfCreation,
ItemConstants.JewelOfGuardian,
ItemConstants.Gemstone,
ItemConstants.JewelOfHarmony,
ItemConstants.LowerRefineStone,
ItemConstants.HigherRefineStone,
];
private readonly OfflinePlayer _player;
private readonly IMuHelperSettings? _config;
///
/// Initializes a new instance of the class.
///
/// The offline player.
/// The MU Helper configuration.
public ItemPickupHandler(OfflinePlayer player, IMuHelperSettings? config)
{
this._player = player;
this._config = config;
}
///
/// Scans for and picks up items within a configurable range.
///
public async ValueTask PickupItemsAsync()
{
if (this._config is null || this._player.CurrentMap is not { } map)
{
return;
}
if (!this._config.PickAllItems && !this._config.PickSelectItems)
{
return;
}
byte range = (byte)Math.Max(this._config.ObtainRange, MinPickupRange);
var drops = map.GetDropsInRange(this._player.Position, range);
foreach (var drop in drops)
{
if (this.ShouldPickUpDrop(drop))
{
await PickupAction.PickupItemAsync(this._player, drop.Id).ConfigureAwait(false);
}
}
}
private static bool IsJewel(Item item)
{
if (item.Definition is not { } definition)
{
return false;
}
return Jewels.Contains(new(definition.Number, definition.Group));
}
private bool ShouldPickUpDrop(IIdentifiable drop)
{
if (this._config!.PickAllItems)
{
return true;
}
if (!this._config.PickSelectItems)
{
return false;
}
if (drop is DroppedMoney && this._config.PickZen)
{
return true;
}
if (drop is DroppedItem droppedItem)
{
return this.ShouldPickUp(droppedItem.Item);
}
return false;
}
private bool ShouldPickUp(Item item)
{
if (this._config is null)
{
return false;
}
if (this._config.PickJewel && IsJewel(item))
{
// A human's helper takes every jewel - its owner trades, crafts or hoards them later. A bot
// has no later: it can only spend Bless, Soul and Life on its own gear, so a Jewel of Chaos
// or a stock-exceeding Soul is a backpack slot it never gets back.
return this._player.Account?.IsBot != true
|| Bots.BotJewelHandler.WantsMoreOf(this._player, item);
}
var isAncient = item.ItemSetGroups.Any(s => s.AncientSetDiscriminator != 0);
var isExcellent = item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Excellent);
if ((this._config.PickAncient && isAncient) || (this._config.PickExcellent && isExcellent))
{
// A human's helper hoards every excellent/ancient piece - its owner sorts the treasure
// out later. A bot has no later: it cannot trade, so it only takes what it can actually
// wear as an upgrade; everything else would silt up its backpack until the loot pickup
// stops.
return this._player.Account?.IsBot != true
|| Bots.BotEquipmentHandler.IsUpgradeFor(this._player, item);
}
if (this._config.PickUpgradeItems && Bots.BotEquipmentHandler.IsUpgradeFor(this._player, item))
{
// The item is class-qualified gear which beats what the bot currently wears - worth picking
// up; the BotEquipmentHandler will equip it on one of its next passes.
return true;
}
if (this._config.PickExtraItems && item.Definition is { } definition)
{
return this._config.ExtraItemNames.Any(name => definition.Name.ToString()?.Contains(name, StringComparison.OrdinalIgnoreCase) ?? false);
}
return false;
}
}