Repository navigation
feat(items): use figurines and charms inside the inventory #1470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| --- | ||
| title: Figurines and charms | ||
| sidebar_label: Figurines and charms | ||
| description: Figurines and charms give a bonus while used in the inventory. | ||
| --- | ||
|
|
||
| The figurines and charms are items of the in-game shop. They are not worn in | ||
| an equipment slot. Instead, the player right clicks them inside the inventory | ||
| to use them, and the bonus applies as long as they stay in use there. Another | ||
| right click stops the use. | ||
|
|
||
| | Item | Number | Bonus | | ||
| | ------------------------ | ------ | ------------------------------ | | ||
| | Hawk Figurine | 13/128 | +10 critical damage | | ||
| | Goat Figurine | 13/129 | +10 excellent damage | | ||
| | Oak Charm | 13/130 | +50 maximum health | | ||
| | Maple Charm | 13/131 | +50 maximum mana | | ||
| | Golden Oak Charm | 13/132 | +150 health, +50 shield | | ||
| | Golden Maple Charm | 13/133 | +150 mana, +50 ability (AG) | | ||
|
|
||
| The values are the ones which the game client shows in the item tooltip. They | ||
| are stored as power-ups of the item definitions, so they can be changed in the | ||
| admin panel. | ||
|
|
||
| ## Rules | ||
|
|
||
| * Only one item of the same kind can be in use at the same time. Using a | ||
| second one is refused with a message. | ||
| * The item keeps its state when it is moved inside the inventory. | ||
| * When the item leaves the inventory, for example when it is dropped or moved | ||
| into the vault, it is no longer in use, and the bonus is removed. | ||
| * Like in the original game, the state is stored in the durability of the | ||
| item: 254 means in use, 255 means not in use. A new item starts with 255 | ||
| when it is added to the inventory. The item definitions have a durability | ||
| of 1, so the items are not stackable. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The *Items used inside the inventory* feature plugin defines which items can | ||
| be used inside the inventory. By default, these are the items 13/128 to | ||
| 13/134. When the plugin is deactivated, no item can be used this way. | ||
|
|
||
| ## Update of existing databases | ||
|
|
||
| The update *Add the figurines and charms* adds the items, and links the | ||
| products of the cash shop to them. Their packages were not for sale before, | ||
| because their products had no item. The update leaves them that way, so the | ||
| administrator can put them on sale in the admin panel. | ||
|
|
||
| ## Protocol | ||
|
|
||
| The client sends the request `C1 BF 20` with the inventory slot and the | ||
| requested state. The server answers with the same packet and the new state of | ||
| the item. | ||
|
|
||
| :::note | ||
| The Worn Horseshoe (13/134) is not added yet, because there is no attribute | ||
| for the item drop rate yet. | ||
| ::: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // <copyright file="InventoryItemUseConfiguration.cs" company="MUnique"> | ||
| // Licensed under the MIT License. See LICENSE file in the project root for full license information. | ||
| // </copyright> | ||
|
|
||
| namespace MUnique.OpenMU.GameLogic; | ||
|
|
||
| using MUnique.OpenMU.DataModel.Configuration.Items; | ||
|
|
||
| /// <summary> | ||
| /// The configuration of the items which are used inside the inventory, like the figurines and charms. | ||
| /// </summary> | ||
| public class InventoryItemUseConfiguration | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the items which are used inside the inventory instead of being equipped. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// By default, these are the figurines (group 13, number 128 and 129), the charms (130 to 133) | ||
| /// and the worn horseshoe (134), which the game client requests to use with a right click in the inventory. | ||
| /// </remarks> | ||
| public IList<InventoryUsedItemIdentifier> Items { get; set; } = CreateDefaultItems(); | ||
|
|
||
| /// <summary> | ||
| /// Determines whether the item definition belongs to an item which is used inside the inventory. | ||
| /// </summary> | ||
| /// <param name="definition">The item definition.</param> | ||
| /// <returns><c>true</c>, if items of this definition are used inside the inventory; otherwise, <c>false</c>.</returns> | ||
| public bool Contains(ItemDefinition definition) | ||
| { | ||
| return this.Items.Any(id => id.Group == definition.Group && id.Number == definition.Number); | ||
| } | ||
|
|
||
| private static IList<InventoryUsedItemIdentifier> CreateDefaultItems() | ||
| { | ||
| return Enumerable.Range(128, 7) | ||
| .Select(number => new InventoryUsedItemIdentifier { Group = 13, Number = (short)number }) | ||
| .ToList(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // <copyright file="InventoryItemUseFeaturePlugIn.cs" company="MUnique"> | ||
| // Licensed under the MIT License. See LICENSE file in the project root for full license information. | ||
| // </copyright> | ||
|
|
||
| namespace MUnique.OpenMU.GameLogic; | ||
|
|
||
| using System.Runtime.InteropServices; | ||
| using MUnique.OpenMU.PlugIns; | ||
|
|
||
| /// <summary> | ||
| /// The use of items inside the inventory, like the figurines and charms. | ||
| /// While such an item is in use, its power-ups apply, although it isn't equipped. | ||
| /// When it's deactivated, no item can be used inside the inventory. | ||
| /// </summary> | ||
| [PlugIn] | ||
| [Display(Name = nameof(PlugInResources.InventoryItemUseFeaturePlugIn_Name), Description = nameof(PlugInResources.InventoryItemUseFeaturePlugIn_Description), ResourceType = typeof(PlugInResources))] | ||
| [Guid("0C9C3D52-7B0E-4E55-9D1B-2E6A35C4F1A8")] | ||
| public class InventoryItemUseFeaturePlugIn : IFeaturePlugIn, ISupportCustomConfiguration<InventoryItemUseConfiguration>, ISupportDefaultCustomConfiguration | ||
| { | ||
| /// <inheritdoc /> | ||
| public InventoryItemUseConfiguration? Configuration { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the configuration of the items which are used inside the inventory. | ||
| /// </summary> | ||
| /// <param name="gameContext">The game context.</param> | ||
| /// <returns>The configuration; <c>null</c>, if the feature is deactivated.</returns> | ||
| public static InventoryItemUseConfiguration? GetActiveConfiguration(IGameContext gameContext) | ||
| { | ||
| return gameContext.FeaturePlugIns.GetPlugIn<InventoryItemUseFeaturePlugIn>() is { } plugIn | ||
| ? plugIn.Configuration ?? new InventoryItemUseConfiguration() | ||
| : null; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public object CreateDefaultConfig() | ||
| { | ||
| return new InventoryItemUseConfiguration(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,8 @@ public class InventoryStorage : Storage, IInventoryStorage | |
|
|
||
| private readonly Player _player; | ||
|
|
||
| private Item? _itemMovedInsideInventory; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="InventoryStorage" /> class. | ||
| /// </summary> | ||
|
|
@@ -113,6 +115,22 @@ public override async ValueTask<bool> AddItemAsync(byte slot, Item item) | |
| { | ||
| await this.RaiseEquippedItemsChangedAsync(convertedItem ?? item, true).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| this.OnItemAddedToInventory(convertedItem ?? item); | ||
| } | ||
| } | ||
|
|
||
| return success; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override async ValueTask<bool> AddItemAsync(Item item) | ||
| { | ||
| var success = await base.AddItemAsync(item).ConfigureAwait(false); | ||
| if (success && !this.IsWearingSlot(item.ItemSlot)) | ||
| { | ||
| this.OnItemAddedToInventory(item); | ||
| } | ||
|
|
||
| return success; | ||
|
|
@@ -127,6 +145,23 @@ public override async ValueTask RemoveItemAsync(Item item) | |
| { | ||
| await this.RaiseEquippedItemsChangedAsync(item, false).ConfigureAwait(false); | ||
| } | ||
| else | ||
| { | ||
| this.OnItemRemovedFromInventory(item); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Keeps the use state and the power-ups of an item which is used inside the inventory, | ||
| /// while it's moved to another slot of the inventory, until the returned object is disposed. | ||
| /// Otherwise, removing the item would stop its use, like when it leaves the inventory. | ||
| /// </summary> | ||
| /// <param name="item">The item which is moved inside the inventory.</param> | ||
| /// <returns>The object which ends keeping the use state when it's disposed.</returns> | ||
| public IDisposable KeepInventoryUseWhileMoving(Item item) | ||
| { | ||
| this._itemMovedInsideInventory = item; | ||
| return new MoveInsideInventoryScope(this); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -151,6 +186,52 @@ public async ValueTask RaiseEquippedItemsChangedAsync(Item item, bool isEquipped | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles an item which was added to an inventory slot which isn't equippable. | ||
| /// An item which is used inside the inventory gets its power-ups while it's in use. | ||
| /// </summary> | ||
| /// <param name="item">The item.</param> | ||
| private void OnItemAddedToInventory(Item item) | ||
| { | ||
| if (this._player.Attributes is not { } attributes | ||
| || !item.IsUsedInsideInventory(this._gameContext)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (item.IsInUseInsideInventory(this._gameContext) && attributes.ItemPowerUps.ContainsKey(item)) | ||
| { | ||
| // It's still in use, e.g. after it was moved inside the inventory. | ||
| return; | ||
| } | ||
|
|
||
| if ((int)item.Durability != InventoryUsedItemExtensions.InUseDurability | ||
| || this.Items.Any(other => other != item && other.Definition == item.Definition && other.IsInUseInsideInventory(this._gameContext))) | ||
| { | ||
| // A new item starts without being used, and only one item of the same kind can be used at the same time. | ||
| item.Durability = InventoryUsedItemExtensions.NotInUseDurability; | ||
| } | ||
|
|
||
| attributes.UpdateInventoryUsedItemPowerUps(item, this._gameContext); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles an item which was removed from an inventory slot which isn't equippable. | ||
| /// An item which is used inside the inventory stops being used when it leaves the inventory, e.g. when it's dropped. | ||
| /// </summary> | ||
| /// <param name="item">The item.</param> | ||
| private void OnItemRemovedFromInventory(Item item) | ||
| { | ||
| if (item == this._itemMovedInsideInventory | ||
| || !item.IsUsedInsideInventory(this._gameContext)) | ||
| { | ||
| return; | ||
|
Comment on lines
+225
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If an operator disables the feature or removes an item from its configuration while a player has an active charm, the live plugin configuration makes Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| item.Durability = InventoryUsedItemExtensions.NotInUseDurability; | ||
| this._player.Attributes?.RemoveInventoryUsedItemPowerUps(item); | ||
| } | ||
|
|
||
| private bool IsWearingSlot(int slot) | ||
| { | ||
| return slot >= FirstEquippableItemSlotIndex && slot <= LastEquippableItemSlotIndex; | ||
|
|
@@ -205,14 +286,18 @@ private void InitializePowerUps() | |
| powerUp.Dispose(); | ||
| } | ||
|
|
||
| var factory = this._gameContext?.ItemPowerUpFactory; | ||
| if (factory != null) | ||
| if (this._gameContext?.ItemPowerUpFactory is { } factory) | ||
| { | ||
| foreach (var item in this.EquippedItems) | ||
| { | ||
| this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList()); | ||
| } | ||
|
|
||
| foreach (var item in this.Items.Where(item => item.IsInUseInsideInventory(this._gameContext))) | ||
| { | ||
| this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList()); | ||
| } | ||
|
|
||
| this.UpdateSetPowerUps(); | ||
| } | ||
| else | ||
|
|
@@ -239,4 +324,27 @@ private void UpdateSetPowerUps() | |
| var factory = this._gameContext.ItemPowerUpFactory; | ||
| this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The scope in which an item is moved inside the inventory, see <see cref="KeepInventoryUseWhileMoving"/>. | ||
| /// </summary> | ||
| private sealed class MoveInsideInventoryScope : IDisposable | ||
| { | ||
| private readonly InventoryStorage _inventory; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="MoveInsideInventoryScope"/> class. | ||
| /// </summary> | ||
| /// <param name="inventory">The inventory.</param> | ||
| public MoveInsideInventoryScope(InventoryStorage inventory) | ||
| { | ||
| this._inventory = inventory; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Dispose() | ||
| { | ||
| this._inventory._itemMovedInsideInventory = null; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a player picks up a GM-created charm,
GameMasterItemFactorysupplies aTemporaryItemwith durability 1. This new call normalizes only its persistent copy to 255.DroppedItem.TryPickUpAsynccopies only the slot back to the temporary item, andPickupItemAction.PickupItemAsyncpasses that original object toIItemAppearPlugIn, so the item-added packet contains durability 1 while the server's inventory contains 255. Normalize the source too, or serialize the inserted persistent item, so the pickup response carries the actual inventory-use state.Useful? React with 👍 / 👎.