Skip to content

Commit

Permalink
Merge pull request #513 from MUnique/dev/area-skill-config
Browse files Browse the repository at this point in the history
Added area skill configurations
  • Loading branch information
sven-n authored Oct 29, 2024
2 parents 1d4ef62 + c7d9c9d commit ec6410a
Show file tree
Hide file tree
Showing 26 changed files with 5,930 additions and 306 deletions.
82 changes: 82 additions & 0 deletions src/DataModel/Configuration/AreaSkillSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// <copyright file="AreaSkillSettings.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Configuration;

using MUnique.OpenMU.Annotations;

/// <summary>
/// Settings for area skills.
/// </summary>
[Cloneable]
public partial class AreaSkillSettings
{
/// <summary>
/// Gets or sets a value indicating whether to use a frustum to filter potential targets.
/// </summary>
public bool UseFrustumFilter { get; set; }

/// <summary>
/// Gets or sets the width of the frustum at the start.
/// </summary>
public float FrustumStartWidth { get; set; }

/// <summary>
/// Gets or sets the width of the frustum at the end.
/// </summary>
public float FrustumEndWidth { get; set; }

/// <summary>
/// Gets or sets the distance.
/// </summary>
public float FrustumDistance { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to consider the target area coordinate to filter potential targets.
/// </summary>
public bool UseTargetAreaFilter { get; set; }

/// <summary>
/// Gets or sets the target area diameter.
/// </summary>
public float TargetAreaDiameter { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use deferred hits,
/// for skills which take a while to visually arrive at the target.
/// </summary>
public bool UseDeferredHits { get; set; }

/// <summary>
/// Gets or sets the delay per one distance, when <see cref="UseDeferredHits"/> is active.
/// </summary>
public TimeSpan DelayPerOneDistance { get; set; }

/// <summary>
/// Gets or sets the delay between hits.
/// </summary>
public TimeSpan DelayBetweenHits { get; set; }

/// <summary>
/// Gets or sets the minimum number of hits per target.
/// </summary>
public int MinimumNumberOfHitsPerTarget { get; set; }

/// <summary>
/// Gets or sets the maximum number of hits per target.
/// </summary>
public int MaximumNumberOfHitsPerTarget { get; set; }

/// <summary>
/// Gets or sets the maximum number of hits per attack.
/// </summary>
public int MaximumNumberOfHitsPerAttack { get; set; }

/// <summary>
/// Gets or sets the hit chance per distance multiplier.
/// E.g. when set to 0.9 and the target is 5 steps away,
/// the chance to hit is 5^0.9 = 0.59.
/// </summary>
public float HitChancePerDistanceMultiplier { get; set; }
}
10 changes: 8 additions & 2 deletions src/DataModel/Configuration/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ public partial class Skill
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the attack damage. Only relevant for attack skills.
/// </summary>
public int AttackDamage { get; set; }

/// <summary>
/// Gets or sets the requirements to execute the skill.
/// </summary>
Expand Down Expand Up @@ -291,7 +296,8 @@ public partial class Skill
public virtual MasterSkillDefinition? MasterDefinition { get; set; }

/// <summary>
/// Gets or sets the attack damage. Only relevant for attack skills.
/// Gets or sets the area skill settings.
/// </summary>
public int AttackDamage { get; set; }
[MemberOfAggregate]
public virtual AreaSkillSettings? AreaSkillSettings { get; set; }
}
188 changes: 148 additions & 40 deletions src/GameLogic/PlayerActions/Skills/AreaSkillAttackAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;

using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views;
Expand All @@ -17,15 +21,18 @@ public class AreaSkillAttackAction
{
private const int UndefinedTarget = 0xFFFF;

private static readonly ConcurrentDictionary<AreaSkillSettings, FrustumBasedTargetFilter> FrustumFilters = new();

/// <summary>
/// Performs the skill by the player at the specified area. Additionally to the target area, a target object can be specified.
/// Performs the skill by the player at the specified area. Additionally, to the target area, a target object can be specified.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="extraTargetId">The extra target identifier.</param>
/// <param name="skillId">The skill identifier.</param>
/// <param name="targetAreaCenter">The coordinates of the center of the target area.</param>
/// <param name="rotation">The rotation in which the player is looking. It's not really relevant for the hitted objects yet, but for some directed skills in the future it might be.</param>
public async ValueTask AttackAsync(Player player, ushort extraTargetId, ushort skillId, Point targetAreaCenter, byte rotation)
/// <param name="hitImplicitlyForExplicitSkill">If set to <c>true</c>, hit implicitly for <see cref="SkillType.AreaSkillExplicitHits"/>.</param>
public async ValueTask AttackAsync(Player player, ushort extraTargetId, ushort skillId, Point targetAreaCenter, byte rotation, bool hitImplicitlyForExplicitSkill = false)
{
var skillEntry = player.SkillList?.GetSkill(skillId);
var skill = skillEntry?.Skill;
Expand All @@ -39,8 +46,11 @@ public async ValueTask AttackAsync(Player player, ushort extraTargetId, ushort s
return;
}

if (skill.SkillType is SkillType.AreaSkillAutomaticHits or SkillType.AreaSkillExplicitTarget)
if (skill.SkillType is SkillType.AreaSkillAutomaticHits or SkillType.AreaSkillExplicitTarget
|| (skill.SkillType is SkillType.AreaSkillExplicitHits && hitImplicitlyForExplicitSkill))
{
// todo: delayed automatic hits, like evil spirit, flame, triple shot... when hitImplicitlyForExplicitSkill = true.

await this.PerformAutomaticHitsAsync(player, extraTargetId, targetAreaCenter, skillEntry!, skill, rotation).ConfigureAwait(false);
}

Expand All @@ -66,77 +76,175 @@ private async ValueTask PerformAutomaticHitsAsync(Player player, ushort extraTar
return;
}

bool isExtraTargetDefined = extraTargetId != UndefinedTarget;
var extraTarget = isExtraTargetDefined ? player.GetObject(extraTargetId) as IAttackable : null;

var attackablesInRange =
skill.SkillType == SkillType.AreaSkillExplicitTarget
? null
: player.CurrentMap?
.GetAttackablesInRange(targetAreaCenter, skill.Range)
.Where(a => a != player)
.Where(a => !a.IsAtSafezone());

bool isCombo = false;
if (player.ComboState is { } comboState)
{
isCombo = await comboState.RegisterSkillAsync(skill).ConfigureAwait(false);
}

if (attackablesInRange is not null)
IAttackable? extraTarget = null;
var targets = GetTargets(player, targetAreaCenter, skill, rotation, extraTargetId);
if (skill.AreaSkillSettings is not { } areaSkillSettings
|| AreaSkillSettingsAreDefault(areaSkillSettings))
{
if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillTargetFilter>(skill.Number) is { } filterPlugin)
// Just hit all targets once.
foreach (var target in targets)
{
attackablesInRange = attackablesInRange.Where(a => filterPlugin.IsTargetWithinBounds(player, a, targetAreaCenter, rotation));
if (target.Id == extraTargetId)
{
extraTarget = target;
}

await this.ApplySkillAsync(player, skillEntry, target, targetAreaCenter, isCombo).ConfigureAwait(false);
}
}
else
{
extraTarget = await AttackTargetsAsync(player, extraTargetId, targetAreaCenter, skillEntry, areaSkillSettings, targets, isCombo).ConfigureAwait(false);
}

if (isCombo)
{
await player.ForEachWorldObserverAsync<IShowSkillAnimationPlugIn>(p => p.ShowComboAnimationAsync(player, extraTarget), true).ConfigureAwait(false);
}
}

if (!player.GameContext.Configuration.AreaSkillHitsPlayer)
private async Task<IAttackable?> AttackTargetsAsync(Player player, ushort extraTargetId, Point targetAreaCenter, SkillEntry skillEntry, AreaSkillSettings areaSkillSettings, IEnumerable<IAttackable> targets, bool isCombo)
{
IAttackable? extraTarget = null;
var attackCount = 0;
var maxAttacks = areaSkillSettings.MaximumNumberOfHitsPerAttack == 0 ? int.MaxValue : areaSkillSettings.MaximumNumberOfHitsPerAttack;
var currentDelay = TimeSpan.Zero;

for (int attackRound = 0; attackRound < areaSkillSettings.MaximumNumberOfHitsPerTarget; attackRound++)
{
if (attackCount > maxAttacks)
{
attackablesInRange = attackablesInRange.Where(a => a is not Player);
break;
}

foreach (var target in attackablesInRange)
foreach (var target in targets)
{
await this.ApplySkillAsync(player, skillEntry, target, targetAreaCenter, isCombo).ConfigureAwait(false);
if (target.Id == extraTargetId)
{
extraTarget = target;
}

if (target != extraTarget)
var hitChance = attackRound < areaSkillSettings.MinimumNumberOfHitsPerTarget
? 1.0
: Math.Min(areaSkillSettings.HitChancePerDistanceMultiplier, Math.Pow(areaSkillSettings.HitChancePerDistanceMultiplier, player.GetDistanceTo(target)));
if (hitChance < 1.0 && !Rand.NextRandomBool(hitChance))
{
continue;
}

isExtraTargetDefined = false;
extraTarget = null;
var distanceDelay = areaSkillSettings.DelayPerOneDistance * player.GetDistanceTo(target);
var attackDelay = currentDelay + distanceDelay;
attackCount++;

if (attackDelay == TimeSpan.Zero)
{
await this.ApplySkillAsync(player, skillEntry, target, targetAreaCenter, isCombo).ConfigureAwait(false);
}
else
{
// The most pragmatic approach is just spawning a Task for each hit.
// We have to see, how this works out in terms of performance.
_ = Task.Run(async () =>
{
await Task.Delay(attackDelay).ConfigureAwait(false);
if (!target.IsAtSafezone() && target.IsActive())
{
await this.ApplySkillAsync(player, skillEntry, target, targetAreaCenter, isCombo).ConfigureAwait(false);
}
});
}
}

currentDelay += areaSkillSettings.DelayBetweenHits;
}

return extraTarget;
}

private static bool AreaSkillSettingsAreDefault([NotNullWhen(true)] AreaSkillSettings? settings)
{
if (settings is null)
{
return true;
}

return !settings.UseDeferredHits
&& settings.DelayPerOneDistance <= TimeSpan.Zero
&& settings.MinimumNumberOfHitsPerTarget == 1
&& settings.MaximumNumberOfHitsPerTarget == 1
&& settings.MaximumNumberOfHitsPerAttack == 0
&& Math.Abs(settings.HitChancePerDistanceMultiplier - 1.0) <= 0.00001f;
}

private static IEnumerable<IAttackable> GetTargets(Player player, Point targetAreaCenter, Skill skill, byte rotation, ushort extraTargetId)
{
var isExtraTargetDefined = extraTargetId != UndefinedTarget;
var extraTarget = isExtraTargetDefined ? player.GetObject(extraTargetId) as IAttackable : null;

if (skill.SkillType == SkillType.AreaSkillExplicitTarget)
{
if (extraTarget?.CheckSkillTargetRestrictions(player, skill) is true
&& player.IsInRange(extraTarget.Position, skill.Range + 2)
&& !extraTarget.IsAtSafezone())
{
yield return extraTarget;
}

yield break;
}

if (isExtraTargetDefined
&& extraTarget is not null
&& player.IsInRange(extraTarget.Position, skill.Range + 2)
&& !player.IsAtSafezone())
foreach (var target in GetTargetsInRange(player, targetAreaCenter, skill, rotation).Where(t => t != extraTarget))
{
await this.ApplySkillAsync(player, skillEntry, extraTarget, targetAreaCenter, isCombo).ConfigureAwait(false);
yield return target;
}
}

if (isCombo)
private static IEnumerable<IAttackable> GetTargetsInRange(Player player, Point targetAreaCenter, Skill skill, byte rotation)
{
var targetsInRange = player.CurrentMap?
.GetAttackablesInRange(targetAreaCenter, skill.Range)
.Where(a => a != player)
.Where(a => !a.IsAtSafezone())
?? [];

if (skill.AreaSkillSettings is { UseFrustumFilter: true } areaSkillSettings)
{
await player.ForEachWorldObserverAsync<IShowSkillAnimationPlugIn>(p => p.ShowComboAnimationAsync(player, extraTarget), true).ConfigureAwait(false);
var filter = FrustumFilters.GetOrAdd(areaSkillSettings, static s => new FrustumBasedTargetFilter(s.FrustumStartWidth, s.FrustumEndWidth, s.FrustumDistance));
targetsInRange = targetsInRange.Where(a => filter.IsTargetWithinBounds(player, a, targetAreaCenter, rotation));
}

if (skill.AreaSkillSettings is { UseTargetAreaFilter: true })
{
targetsInRange = targetsInRange.Where(a => a.GetDistanceTo(targetAreaCenter) < skill.AreaSkillSettings.TargetAreaDiameter * 0.5f);
}

if (!player.GameContext.Configuration.AreaSkillHitsPlayer)
{
targetsInRange = targetsInRange.Where(a => a is not Player);
}

targetsInRange = targetsInRange.Where(target => target.CheckSkillTargetRestrictions(player, skill));

return targetsInRange;
}

private async ValueTask ApplySkillAsync(Player player, SkillEntry skillEntry, IAttackable target, Point targetAreaCenter, bool isCombo)
{
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));

if (target.CheckSkillTargetRestrictions(player, skillEntry.Skill))
{
var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false);
var baseSkill = skillEntry.GetBaseSkill();
var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(player, skillEntry).ConfigureAwait(false);
var baseSkill = skillEntry.GetBaseSkill();

if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(baseSkill.Number) is { } strategy)
{
await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
}
if (player.GameContext.PlugInManager.GetStrategy<short, IAreaSkillPlugIn>(baseSkill.Number) is { } strategy)
{
await strategy.AfterTargetGotAttackedAsync(player, target, skillEntry, targetAreaCenter, hitInfo).ConfigureAwait(false);
}
}
}

This file was deleted.

Loading

0 comments on commit ec6410a

Please sign in to comment.