//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.Tests.Offline;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.MuHelper;
using MUnique.OpenMU.GameLogic.Offline;
using MUnique.OpenMU.GameServer.RemoteView.MuHelper;
using Item = MUnique.OpenMU.Persistence.BasicModel.Item;
using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition;
using ItemSlotType = MUnique.OpenMU.Persistence.BasicModel.ItemSlotType;
///
/// Tests for .
///
[TestFixture]
public class RepairHandlerTests
{
private IGameContext _gameContext = null!;
///
/// Sets up a fresh game context before each test.
///
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
///
/// Items at or below the 50% durability threshold should be repaired when the player
/// has enough Zen. Uses 10% (well below threshold) to confirm the happy path.
///
[Test]
public async ValueTask RepairsItemWhenSufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
///
/// Auto-repair does nothing when disabled in the configuration, regardless of durability.
///
[Test]
public async ValueTask DoesNothingWhenDisabledAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = false };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
///
/// Auto-repair does not repair when the player has insufficient Zen.
///
[Test]
public async ValueTask DoesNotRepairWhenInsufficientZenAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 10);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(10));
}
///
/// Fully durable items are skipped and no Zen is spent.
///
[Test]
public async ValueTask SkipsFullyDurableItemsAsync()
{
// Arrange
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 100);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
///
/// An item at exactly 50% durability (the threshold boundary) must be repaired.
/// Mirrors the client check: iHealth <= DEFAULT_DURABILITY_THRESHOLD (50).
///
[Test]
public async ValueTask RepairsItemAtExactlyFiftyPercentThresholdAsync()
{
// Arrange — 50 / 100 = 50% (ceiling-integer: (50*100 + 99) / 100 = 50)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 50);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
Assert.That(item.Durability, Is.EqualTo(100));
}
///
/// An item at 51% durability is above the threshold and must NOT be repaired,
/// so no Zen is spent. Mirrors the client check: iHealth <= 50.
///
[Test]
public async ValueTask SkipsItemAboveFiftyPercentThresholdAsync()
{
// Arrange — 51 / 100 = 51% (ceiling-integer: (51*100 + 99) / 100 = 51)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 100, currentDurability: 51);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
var initialMoney = 1_000_000;
player.TryAddMoney(initialMoney);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert — durability unchanged, no money spent
Assert.That(item.Durability, Is.EqualTo(51));
Assert.That(player.Money, Is.EqualTo(initialMoney));
}
///
/// Verifies that the ceiling-integer formula handles non-round max-durability values
/// correctly: 13 / 25 = 52% (above threshold → skip), but 12 / 25 = 48% (≤ 50% → repair).
///
[TestCase(13, false, TestName = "NonRoundMax_52pct_Skipped")]
[TestCase(12, true, TestName = "NonRoundMax_48pct_Repaired")]
public async ValueTask ThresholdWithNonRoundMaxDurabilityAsync(byte currentDurability, bool expectRepair)
{
// Arrange — max = 25; ceiling(d*100/25) = ceiling(d*4)
var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false);
var item = this.CreateDamagedItem(maxDurability: 25, currentDurability: currentDurability);
await player.Inventory!.AddItemAsync(InventoryConstants.ArmorSlot, item).ConfigureAwait(false);
player.TryAddMoney(1_000_000);
var config = new MuHelperSettings { RepairItem = true };
var handler = new RepairHandler(player, config);
// Act
await handler.PerformRepairsAsync().ConfigureAwait(false);
// Assert
if (expectRepair)
{
Assert.That(item.Durability, Is.EqualTo(25));
}
else
{
Assert.That(item.Durability, Is.EqualTo(currentDurability));
}
}
private async ValueTask CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private Item CreateDamagedItem(byte maxDurability, byte currentDurability)
{
return new Item
{
Definition = new ItemDefinition
{
Durability = maxDurability,
ItemSlot = new ItemSlotType(),
Width = 1,
Height = 1,
Value = 1000,
},
Durability = currentDurability,
};
}
}