// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Tests.Offline; using Moq; using MUnique.OpenMU.AttributeSystem; 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.Offline; using MUnique.OpenMU.GameServer.RemoteView.MuHelper; using MUnique.OpenMU.Pathfinding; using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition; using MonsterAttribute = MUnique.OpenMU.Persistence.BasicModel.MonsterAttribute; /// /// Tests for . /// [TestFixture] public class CombatHandlerTests { private IGameContext _gameContext = null!; private readonly Point _origin = new(100, 100); /// /// Sets up a fresh game context before each test. /// [SetUp] public void SetUp() { this._gameContext = GameContextTestHelper.CreateGameContext(); } /// /// Tests that moves closer to the target if out of range. /// [Test] public async ValueTask PerformAttackAsync_MovesCloserToTargetAsync() { // Arrange var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false); player.Position = this._origin; var monster = await this.CreateMonsterAsync(new Point(105, 105)).ConfigureAwait(false); await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false); var config = new MuHelperSettings { HuntingRange = 10 }; player.HuntingOrigin = this._origin; var movementHandler = new MovementHandler(player, config); var handler = new CombatHandler(player, config, movementHandler); // Act // The first call only acquires the target and turns towards it: a fresh target gets a small // randomized human-like reaction delay (up to 900 ms) before the bot engages, so we call // again after the delay has certainly elapsed. await handler.PerformAttackAsync().ConfigureAwait(false); await Task.Delay(1000).ConfigureAwait(false); await handler.PerformAttackAsync().ConfigureAwait(false); // Assert // Should have initiated a walk closer to the monster Assert.That(player.IsWalking, Is.True); } /// /// Tests that uses Drain Life when HP is low. /// [Test] public async ValueTask PerformDrainLifeRecoveryAsync_UsesDrainLifeWhenLowHpAsync() { // Arrange var player = await this.CreateOfflinePlayerAsync().ConfigureAwait(false); player.Position = this._origin; player.Attributes![Stats.CurrentHealth] = 10; // Low HP player.Attributes[Stats.MaximumHealth] = 100; // Place monster slightly away from player so GetDirectionTo doesn't return Undefined var monsterPosition = new Point((byte)(this._origin.X + 1), this._origin.Y); var monster = await this.CreateMonsterAsync(monsterPosition).ConfigureAwait(false); await player.CurrentMap!.AddAsync(monster).ConfigureAwait(false); var config = new MuHelperSettings { UseDrainLife = true, HealThresholdPercent = 50, HuntingRange = 10 }; // Add Drain Life skill to player var drainSkill = new TestSkill { Number = 214, }; await player.SkillList!.AddLearnedSkillAsync(drainSkill).ConfigureAwait(false); player.HuntingOrigin = this._origin; var movementHandler = new MovementHandler(player, config); var handler = new CombatHandler(player, config, movementHandler); // Act await handler.PerformDrainLifeRecoveryAsync().ConfigureAwait(false); // Assert // We verify that rotation was updated, which happens during ExecuteAttackAsync Assert.That(player.Rotation, Is.Not.EqualTo(default(Direction))); } private async ValueTask CreateOfflinePlayerAsync() { return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false); } private async ValueTask CreateMonsterAsync(Point position) { var monsterDefinition = new MonsterDefinition { ObjectKind = NpcObjectKind.Monster, }; monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.MaximumHealth, Value = 1000 }); monsterDefinition.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.DefenseBase, Value = 100 }); var map = await this._gameContext.GetMapAsync(0).ConfigureAwait(false)!; var spawnArea = new MonsterSpawnArea { MonsterDefinition = monsterDefinition, GameMap = map!.Definition, X1 = position.X, Y1 = position.Y, X2 = position.X, Y2 = position.Y, Quantity = 1, }; var monster = new Monster( spawnArea, monsterDefinition, map, NullDropGenerator.Instance, new Mock().Object, this._gameContext.PlugInManager, this._gameContext.PathFinderPool); monster.Initialize(); monster.Attributes[Stats.CurrentHealth] = 100; return monster; } private class TestSkill : Skill { public TestSkill() { this.Requirements = new List(); this.ConsumeRequirements = new List(); this.Range = 1; this.SkillType = SkillType.DirectHit; this.DamageType = DamageType.Curse; } } }