// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.Views.World; using MUnique.OpenMU.PlugIns; using static MUnique.OpenMU.DataModel.InventoryConstants; /// /// The storage of an inventory of a player, which also contains equippable slots. This class also manages the powerups which get created by equipped items. /// public class InventoryStorage : Storage, IInventoryStorage { private readonly IGameContext _gameContext; private readonly Player _player; /// /// Initializes a new instance of the class. /// /// The player. /// The game context. public InventoryStorage(Player player, IGameContext context) : base( GetInventorySize(0), EquippableSlotsCount, 0, new ItemStorageAdapter(player.SelectedCharacter?.Inventory ?? throw Error.NotInitializedProperty(player, "SelectedCharacter.Inventory"), FirstEquippableItemSlotIndex, player.InventorySize)) { this._player = player; this.EquippedItemsChanged += async eventArgs => await this.UpdateItemsOnChangeAsync(eventArgs.Item, eventArgs.IsEquipped).ConfigureAwait(false); this._gameContext = context; if (player.SelectedCharacter.InventoryExtensions > 0) { var extensions = new List(player.SelectedCharacter.InventoryExtensions); var sizePerExtension = RowsOfOneExtension * RowSize; for (int i = 0; i < player.SelectedCharacter.InventoryExtensions; i++) { var offset = FirstExtensionItemSlotIndex + (i * sizePerExtension); var extension = new Storage(sizePerExtension, 0, offset, this.ItemStorage); extensions.Add(extension); } this.Extensions = extensions; } this.InitializePowerUps(); } /// public event AsyncEventHandler? EquippedItemsChanged; /// public IEnumerable EquippedItems { get { for (int i = FirstEquippableItemSlotIndex; i <= LastEquippableItemSlotIndex; i++) { if (this.ItemArray[i] is not null) { yield return this.ItemArray[i]!; } } } } /// public Item? EquippedAmmunitionItem { get { if (this.ItemArray[LeftHandSlot] is { } leftItem && (leftItem.Definition?.IsAmmunition ?? false)) { return leftItem; } if (this.ItemArray[RightHandSlot] is { } rightItem && (rightItem.Definition?.IsAmmunition ?? false)) { return rightItem; } return null; } } /// /// Additionally we need to make a temporary item persistent with the context of the player. public override async ValueTask AddItemAsync(byte slot, Item item) { Item? convertedItem = null; if (item is TemporaryItem temporaryItem) { convertedItem = temporaryItem.MakePersistent(this._player.PersistenceContext); } var success = await base.AddItemAsync(slot, convertedItem ?? item).ConfigureAwait(false); if (!success && convertedItem != null) { this._player.PersistenceContext.Detach(convertedItem); } if (success) { var isEquippedItem = this.IsWearingSlot(slot); if (isEquippedItem && this.EquippedItemsChanged is { } eventHandler) { await eventHandler(new ItemEventArgs(convertedItem ?? item, true)).ConfigureAwait(false); } } return success; } /// public override async ValueTask RemoveItemAsync(Item item) { var isEquippedItem = this.IsWearingSlot(item.ItemSlot); await base.RemoveItemAsync(item).ConfigureAwait(false); if (isEquippedItem && this.EquippedItemsChanged is { } eventHandler) { await eventHandler(new ItemEventArgs(item, false)).ConfigureAwait(false); } } private bool IsWearingSlot(int slot) { return slot >= FirstEquippableItemSlotIndex && slot <= LastEquippableItemSlotIndex; } private async ValueTask UpdateItemsOnChangeAsync(Item item, bool isEquipped) { this._player.OnAppearanceChanged(); await this._player.ForEachWorldObserverAsync( p => p.AppearanceChangedAsync(this._player, item, isEquipped), false).ConfigureAwait(false); // in my tests it was not needed to send the appearance to the own players client. if (this._player.Attributes is null) { return; } if (this._player.Attributes.ItemPowerUps.Remove(item, out var itemPowerUps)) { foreach (var powerUp in itemPowerUps) { powerUp.Dispose(); } } this.UpdateSetPowerUps(); var itemAdded = this.EquippedItems.Contains(item); if (itemAdded) { var factory = this._gameContext.ItemPowerUpFactory; this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList()); // reset player equipped ammunition amount if (this.EquippedAmmunitionItem is { } ammoItem) { this._player.Attributes[Stats.AmmunitionAmount] = (float)ammoItem.Durability; } } } private void InitializePowerUps() { if (this._player.Attributes is null) { throw new InvalidOperationException("The player's AttributeSystem is not set yet."); } foreach (var powerUp in this._player.Attributes.ItemPowerUps.Values.SelectMany(p => p).ToList()) { powerUp.Dispose(); } var factory = this._gameContext?.ItemPowerUpFactory; if (factory != null) { foreach (var item in this.EquippedItems) { this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList()); } this.UpdateSetPowerUps(); } else { this._player.Logger.LogError("item power up factory not available during initialization of the inventory."); } } private void UpdateSetPowerUps() { if (this._player.Attributes is null) { throw new InvalidOperationException("The players AttributeSystem is not set yet."); } if (this._player.Attributes.ItemSetPowerUps is not null) { foreach (var powerUp in this._player.Attributes.ItemSetPowerUps) { powerUp.Dispose(); } } var factory = this._gameContext.ItemPowerUpFactory; this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList(); } }