Files
AdamuSw/tests/MUnique.OpenMU.Tests/Offline/CombatHandlerTests.cs
sven-n d04cbecae3 Merge pull request #820 from nolt/feature-bots
(cherry picked from commit b10de0645a869485fbb5771a739abd06b5708c2d)
2026-07-23 11:38:08 +03:00

168 lines
6.0 KiB
C#

// <copyright file="CombatHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
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;
/// <summary>
/// Tests for <see cref="CombatHandler"/>.
/// </summary>
[TestFixture]
public class CombatHandlerTests
{
private IGameContext _gameContext = null!;
private readonly Point _origin = new(100, 100);
/// <summary>
/// Sets up a fresh game context before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformAttackAsync"/> moves closer to the target if out of range.
/// </summary>
[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);
}
/// <summary>
/// Tests that <see cref="CombatHandler.PerformDrainLifeRecoveryAsync"/> uses Drain Life when HP is low.
/// </summary>
[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<OfflinePlayer> CreateOfflinePlayerAsync()
{
return await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
}
private async ValueTask<Monster> 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<INpcIntelligence>().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<AttributeRequirement>();
this.ConsumeRequirements = new List<AttributeRequirement>();
this.Range = 1;
this.SkillType = SkillType.DirectHit;
this.DamageType = DamageType.Curse;
}
}
}