~ruther/NosSmooth

a76434f366210995ac509483a5726863388fcb1e — Rutherther 3 years ago 1b78257
feat(combat): add enemy, item, skill selectors
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

Do not follow this link