A Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs +162 -0
@@ 0,0 1,162 @@
+//
+// EnemyPolicy.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Data.SqlTypes;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// An enemy selector policy that selects the monsters based on their vnums.
+/// </summary>
+/// <param name="SelectPolicy">The policy to select an enemy.</param>
+/// <param name="MonsterVNums">The vnums of the monsters to target.</param>
+/// <param name="CombatArea">The area in which to get enemies from.</param>
+public record EnemyPolicy
+(
+ EnemySelectPolicy SelectPolicy,
+ int[]? MonsterVNums = default,
+ CombatArea? CombatArea = default
+) : IEnemySelector
+{
+ /// <inheritdoc />
+ public Result<ILivingEntity> GetSelectedEntity(ICombatState combatState, ICollection<ILivingEntity> possibleTargets)
+ {
+ var targets = possibleTargets.OfType<Monster>();
+ if (MonsterVNums is not null)
+ {
+ targets = targets.Where(x => MonsterVNums.Contains(x.VNum));
+ }
+
+ if (!targets.Any())
+ {
+ return new EntityNotFoundError();
+ }
+
+ if (combatState.Game.Character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var position = combatState.Game.Character.Position;
+ if (position is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var characterPosition = position.Value;
+ ILivingEntity? target = null;
+ switch (SelectPolicy)
+ {
+ case EnemySelectPolicy.Aggressive:
+ throw new NotImplementedException(); // TODO: implement aggressive policy
+ case EnemySelectPolicy.Closest:
+ target = targets
+ .Where(x => x.Position is not null && (CombatArea?.IsInside(x.Position.Value) ?? true))
+ .MinBy(x => x.Position!.Value.DistanceSquared(characterPosition))!;
+ break;
+ case EnemySelectPolicy.LowestHealth:
+ target = targets.MinBy
+ (
+ x =>
+ {
+ if (x.Hp is null)
+ {
+ return int.MaxValue;
+ }
+
+ if (x.Hp.Amount is not null)
+ {
+ return x.Hp.Amount;
+ }
+
+ if (x.Hp.Percentage is not null && x.Level is not null)
+ {
+ return x.Hp.Percentage * 100 * x.Level;
+ }
+
+ if (x.Hp.Maximum is not null)
+ {
+ return x.Hp.Maximum; // Assume max health, best guess.
+ }
+
+ return int.MaxValue;
+ }
+ );
+ break;
+ }
+
+ if (target is null)
+ {
+ return new EntityNotFoundError();
+ }
+
+ return Result<ILivingEntity>.FromSuccess(target);
+ }
+}
+
+/// <summary>
+/// A policy enemy selector.
+/// </summary>
+public enum EnemySelectPolicy
+{
+ /// <summary>
+ /// Select the enemy with the lowest health.
+ /// </summary>
+ LowestHealth,
+
+ /// <summary>
+ /// Selects the enemy that targets the user.
+ /// </summary>
+ Aggressive,
+
+ /// <summary>
+ /// Selects the enemy that is the closest to the character.
+ /// </summary>
+ Closest
+}
+
+/// <summary>
+/// The combat area around which to find enemies.
+/// </summary>
+/// <param name="CenterX">The area center x coordinate.</param>
+/// <param name="CenterY">The area center y coordinate.</param>
+/// <param name="Range">The maximum range from the center.</param>
+public record CombatArea(short CenterX, short CenterY, short Range)
+{
+ /// <summary>
+ /// Create a combat area around a specified entity.
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <param name="range">The range.</param>
+ /// <returns>The combat area.</returns>
+ /// <exception cref="ArgumentException">If the entity does not have a position.</exception>
+ public static CombatArea CreateAroundEntity(IEntity entity, short range)
+ {
+ var position = entity.Position;
+ if (position is null)
+ {
+ throw new ArgumentException(nameof(entity));
+ }
+
+ return new CombatArea(position.Value.X, position.Value.Y, range);
+ }
+
+ /// <summary>
+ /// Gets whether the position is inside of the combat area.
+ /// </summary>
+ /// <param name="position">The position.</param>
+ /// <returns>Whether the position is inside.</returns>
+ public bool IsInside(Position position)
+ {
+ return position.DistanceSquared(new Position(CenterX, CenterY)) < Range * Range;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs +90 -0
@@ 0,0 1,90 @@
+//
+// UseItemPolicy.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// The policy to use an item.
+/// </summary>
+/// <param name="UseItems">Whether to use items.</param>
+/// <param name="UseBelowHealthPercentage">Use items below the given character's health percentage.</param>
+/// <param name="UseBelowManaPercentage">Use items below the given character's mana percentage.</param>
+/// <param name="UseHealthItemsVNums">The vnums of the items to use as health items.</param>
+/// <param name="UseManaItemsVNums">The vnums of the items to use as mana items.</param>
+public record UseItemPolicy
+(
+ bool UseItems,
+ int UseBelowHealthPercentage,
+ int UseBelowManaPercentage,
+ int[] UseHealthItemsVNums,
+ int[] UseManaItemsVNums
+) : IItemSelector
+{
+ /// <inheritdoc />
+ public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new ItemNotFoundError();
+ }
+
+ if (ShouldUseHpItem(character))
+ {
+ var item = possibleItems.FirstOrDefault(x => UseHealthItemsVNums.Contains(x.ItemVNum));
+ if (item is not null)
+ {
+ return item;
+ }
+ }
+
+ if (ShouldUseMpItem(character))
+ {
+ var item = possibleItems.FirstOrDefault(x => UseManaItemsVNums.Contains(x.ItemVNum));
+ if (item is not null)
+ {
+ return item;
+ }
+ }
+
+ return new ItemNotFoundError();
+ }
+
+ /// <inheritdoc />
+ public Result<bool> ShouldUseItem(ICombatState combatState)
+ {
+ if (!UseItems)
+ {
+ return false;
+ }
+
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return false;
+ }
+
+ return ShouldUseHpItem(character) || ShouldUseMpItem(character);
+ }
+
+ private bool ShouldUseHpItem(Character character)
+ {
+ return character.Hp is not null && character.Hp.Percentage is not null
+ && character.Hp.Percentage < UseBelowHealthPercentage;
+ }
+
+ private bool ShouldUseMpItem(Character character)
+ {
+ return character.Mp is not null && character.Mp.Percentage is not null
+ && character.Mp.Percentage < UseBelowManaPercentage;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Policies/UseSkillPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseSkillPolicy.cs +58 -0
@@ 0,0 1,58 @@
+//
+// UseSkillPolicy.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// The policy to use a skill.
+/// </summary>
+/// <param name="PreferTargetedSkills">Whether to prefer targeted skills (true) or area skills (false).</param>
+/// <param name="AllowedSkillVNums">The vnums of the skills that are allowed to be used.</param>
+public record UseSkillPolicy(bool PreferTargetedSkills, int[]? AllowedSkillVNums)
+ : ISkillSelector
+{
+ /// <inheritdoc />
+ public Result<Skill> GetSelectedSkill(IEnumerable<Skill> usableSkills)
+ {
+ var skills = usableSkills.Where(x => CanBeUsed(x))
+ .Reverse();
+
+ if (PreferTargetedSkills)
+ {
+ skills = skills.OrderBy(x => x.Info!.HitType == HitType.EnemiesInZone ? 1 : 0);
+ }
+
+ var skill = skills.FirstOrDefault();
+ if (skill is null)
+ {
+ return new SkillNotFoundError();
+ }
+
+ return skill;
+ }
+
+ private bool CanBeUsed(Skill skill)
+ {
+ if (AllowedSkillVNums is not null && !AllowedSkillVNums.Contains(skill.SkillVNum))
+ {
+ return false;
+ }
+
+ if (skill.Info is null)
+ {
+ return false;
+ }
+
+ return skill.Info.HitType is HitType.EnemiesInZone or HitType.TargetOnly
+ && skill.Info.TargetType is TargetType.Target or TargetType.NoTarget;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/IEnemySelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IEnemySelector.cs +24 -0
@@ 0,0 1,24 @@
+//
+// IEnemySelector.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects an enemy from the possible enemies.
+/// </summary>
+public interface IEnemySelector
+{
+ /// <summary>
+ /// Gets the entity to be currently selected.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="possibleTargets">The collection of possible targets.</param>
+ /// <returns>The selected entity, or an error.</returns>
+ public Result<ILivingEntity> GetSelectedEntity(ICombatState combatState, ICollection<ILivingEntity> possibleTargets);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs +31 -0
@@ 0,0 1,31 @@
+//
+// IItemSelector.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects an item to be used.
+/// </summary>
+public interface IItemSelector
+{
+ /// <summary>
+ /// Gets the entity to be currently selected.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="possibleItems">The items that may be used.</param>
+ /// <returns>The selected item, or an error.</returns>
+ public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems);
+
+ /// <summary>
+ /// Gets whether currently an item should be used.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <returns>Whether to use an item or an error.</returns>
+ public Result<bool> ShouldUseItem(ICombatState combatState);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/ISkillSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/ISkillSelector.cs +23 -0
@@ 0,0 1,23 @@
+//
+// ISkillSelector.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Game.Data.Characters;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects a skill to use from a possible skills.
+/// </summary>
+public interface ISkillSelector
+{
+ /// <summary>
+ /// Gets the skill to use.
+ /// </summary>
+ /// <param name="usableSkills">The skills that may be used. Won't contain skills the user doesn't have mana for and that are on cooldown.</param>
+ /// <returns>The skill to use, or an error.</returns>
+ public Result<Skill> GetSelectedSkill(IEnumerable<Skill> usableSkills);
+}<
\ No newline at end of file