//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.Tests;
using Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Resets;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;
///
/// Tests for .
///
[TestFixture]
public class ResetCharacterActionTest
{
///
/// Verifies that reset is rejected when required items are missing, without charging zen.
///
[Test]
public async Task NotEnoughItemsRejectsResetAndKeepsZenAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 1).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(0));
Assert.That(player.Money, Is.EqualTo(1_000));
Assert.That(player.Inventory!.Items.Count(i => i.Definition == requiredItem), Is.EqualTo(1));
}
///
/// Verifies that reset consumes configured zen and required items when all conditions are met.
///
[Test]
public async Task EnoughItemsAndZenConsumesCostsOnceAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 2).ConfigureAwait(false);
Assert.That(player.Level, Is.EqualTo(400));
Assert.That(player.Money, Is.EqualTo(1_000));
Assert.That(
player.Inventory!.Items.Count(i => i.Definition is { } definition && definition.Group == requiredItem.Group && definition.Number == requiredItem.Number),
Is.EqualTo(2));
var progression = ResetProgressionCalculator.Calculate((int)player.Attributes[Stats.Resets], (int)player.Attributes[Stats.PointsPerReset], configuration);
Assert.That(progression.RequiredItemAmount, Is.EqualTo(2));
Assert.That(progression.RequiredZen, Is.EqualTo(500));
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.Money, Is.EqualTo(500));
Assert.That(player.Inventory!.Items.Count(i => i.Definition == requiredItem), Is.EqualTo(0));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(800));
Assert.That((int)player.Attributes[Stats.Level], Is.EqualTo(1));
}
///
/// Verifies cumulative point replacement across multiple resets when point tiers are configured.
///
[Test]
public async Task ReplacePointsUsesCumulativeTierTotalAcrossResetsAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 2_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 4).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(800));
Assert.That(player.Money, Is.EqualTo(1_500));
player.Attributes[Stats.Level] = 400;
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(2));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_600));
Assert.That(player.Money, Is.EqualTo(500));
}
///
/// Verifies add mode behavior when replace mode is disabled.
///
[Test]
public async Task TieredPointsAreAddedWhenReplaceIsDisabledAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
player.SelectedCharacter!.LevelUpPoints = 1_000;
var requiredItem = new ItemDefinition { Name = "Jewel of Creation", Group = 14, Number = 22, Width = 1, Height = 1 };
var configuration = this.CreateConfiguration(requiredItem);
configuration.ReplacePointsPerReset = false;
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
await AddRequiredItemsAsync(player, requiredItem, 2).ConfigureAwait(false);
var action = new ResetCharacterAction(player, await CreateResetNpcAsync(player).ConfigureAwait(false));
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_800));
}
///
/// Verifies that a reset with moves the player through
/// the regular warp path, so the client is told about the new position.
/// Writing the character's position fields directly moves the player without notifying anyone,
/// which makes the character slide across the map on the client instead of respawning.
///
[Test]
public async Task MoveHomeWarpsPlayerSoTheClientIsNotifiedAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
player.Money = 1_000;
// The home map has to be one whose GameMap instance does not exist yet: the spawn gate is
// resolved once, in the GameMap constructor, so a map created earlier would ignore it.
var homeMap = CreateMapDefinitionWithSpawnGate(1, 125);
player.GameContext.Configuration.Maps.Add(homeMap);
Mock.Get(player.SelectedCharacter!.CharacterClass!).Setup(c => c.HomeMap).Returns(homeMap);
var configuration = this.CreateConfiguration(null);
configuration.MoveHome = true;
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
var mapChangeView = Mock.Get(player.ViewPlugIns.GetPlugIn()!);
var action = new ResetCharacterAction(player);
await action.ResetCharacterAsync().ConfigureAwait(false);
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
Assert.That(player.Position, Is.EqualTo(new Point(125, 125)));
mapChangeView.Verify(v => v.MapChangeAsync(), Times.Once);
}
private static GameMapDefinition CreateMapDefinitionWithSpawnGate(short number, byte spawnCoordinate)
{
var mapMock = new Mock();
mapMock.SetupAllProperties();
mapMock.Setup(m => m.DropItemGroups).Returns(new List());
mapMock.Setup(m => m.MonsterSpawns).Returns(new List());
mapMock.Object.TerrainData = new byte[ushort.MaxValue + 3];
mapMock.Object.Number = number;
var spawnGate = new ExitGate
{
IsSpawnGate = true,
X1 = spawnCoordinate,
X2 = spawnCoordinate,
Y1 = spawnCoordinate,
Y2 = spawnCoordinate,
Map = mapMock.Object,
};
mapMock.Setup(m => m.ExitGates).Returns(new List { spawnGate });
return mapMock.Object;
}
private static async ValueTask AddRequiredItemsAsync(Player player, ItemDefinition requiredItem, int count)
{
for (byte i = 0; i < count; i++)
{
var item = player.PersistenceContext.CreateNew- ();
item.Definition = requiredItem;
item.Durability = 1;
var added = await player.Inventory!.AddItemAsync(item).ConfigureAwait(false);
Assert.That(added, Is.True);
}
}
private static async ValueTask CreateResetNpcAsync(Player player)
{
var spawnArea = new MonsterSpawnArea
{
X1 = 125,
X2 = 125,
Y1 = 125,
Y2 = 125,
};
var definition = new MonsterDefinition
{
Number = ResetCharacterNpcPlugin.ResetNpcNumber,
ObjectKind = NpcObjectKind.PassiveNpc,
Designation = "Reset Helper",
};
var map = await player.GameContext.GetMapAsync(0).ConfigureAwait(false)
?? throw new InvalidOperationException("Could not resolve map 0 for NPC test setup.");
return new NonPlayerCharacter(spawnArea, definition, map);
}
private ResetConfiguration CreateConfiguration(ItemDefinition? requiredItem)
{
return new ResetConfiguration
{
RequiredLevel = 400,
LevelAfterReset = 1,
RequiredMoney = 500,
MultiplyRequiredMoneyByResetCount = true,
RequiredResetItem = requiredItem,
ItemCostTiers =
[
new() { MinimumResetCount = 1, RequiredItemAmount = 2 },
],
PointsTiers =
[
new() { MinimumResetCount = 1, PointsGranted = 800 },
],
MoveHome = false,
LogOut = false,
ResetStats = false,
ReplacePointsPerReset = true,
};
}
}