//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic.PlayerActions.Skills;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.PlugIns;
///
/// Action to perform a skill which is explicitly aimed to a target.
///
[PlugIn]
[Display(Name = nameof(PlugInResources.TargetedSkillDefaultPlugin_Name), Description = nameof(PlugInResources.TargetedSkillDefaultPlugin_Description), ResourceType = typeof(PlugInResources))]
[Guid("eb2949fb-5ed2-407e-a4e8-e3015ed5692b")]
public class TargetedSkillDefaultPlugin : TargetedSkillPluginBase
{
private static readonly Dictionary SummonSkillToMonsterMapping = new()
{
{ 30, 26 }, // Goblin
{ 31, 32 }, // Stone Golem
{ 32, 21 }, // Assassin
{ 33, 20 }, // Elite Yeti
{ 34, 10 }, // Dark Knight
{ 35, 150 }, // Bali
{ 36, 151 }, // Soldier
};
///
public override short Key => 0;
///
/// Performs the skill.
///
/// The player.
/// The target.
/// The skill identifier.
public override async ValueTask PerformSkillAsync(Player player, IAttackable target, ushort skillId)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (target is null)
{
return;
}
if (player.Attributes is not { } attributes)
{
return;
}
if (attributes[Stats.IsStunned] > 0)
{
player.Logger.LogWarning("Probably Hacker - player {Player} is attacking in stunned state", player);
return;
}
if (attributes[Stats.IsAsleep] > 0)
{
player.Logger.LogWarning("Probably Hacker - player {Player} is attacking in asleep state", player);
return;
}
var skillEntry = player.SkillList?.GetSkill(skillId);
var skill = skillEntry?.Skill;
if (skill is null || skill.SkillType == SkillType.PassiveBoost)
{
return;
}
if (skill.SkillType != SkillType.Buff && skill.SkillType != SkillType.Regeneration && skill.SkillType != SkillType.SummonMonster)
{
if (player.GameContext.PlugInManager.GetPlugInPoint() is { } speedCheck)
{
var eventArgs = new SpeedHackCheckEventArgs();
await speedCheck.AttackCheatCheckAsync(player, eventArgs).ConfigureAwait(false);
if (eventArgs.IsCheatDetected)
{
return;
}
}
}
var miniGame = player.CurrentMiniGame;
var inMiniGame = miniGame is { };
var isBuff = skill.SkillType is SkillType.Buff or SkillType.Regeneration;
if (player.IsAtSafezone() && !(inMiniGame && isBuff))
{
return;
}
if (inMiniGame && !miniGame!.IsSkillAllowed(skill, player, target))
{
return;
}
if (!target.IsActive())
{
return;
}
if (!target.CheckSkillTargetRestrictions(player, skill))
{
return;
}
if (!player.IsInRange(target.Position, skill.Range + 2))
{
// target position might be out of sync so we send the current coordinates to the client again.
if (!(target is ISupportWalk { IsWalking: true }))
{
await player.InvokeViewPlugInAsync(p => p.ObjectMovedAsync(target, MoveType.Instant)).ConfigureAwait(false);
}
return;
}
if (skill.SkillType == SkillType.SummonMonster && player.Summon is { })
{
await player.RemoveSummonAsync().ConfigureAwait(false);
return;
}
// enough mana, ag etc?
if (!await player.TryConsumeForSkillAsync(skill).ConfigureAwait(false))
{
return;
}
if (skill.MovesToTarget)
{
await player.MoveAsync(target.Position).ConfigureAwait(false);
}
if (skill.MovesTarget)
{
await target.MoveRandomlyAsync().ConfigureAwait(false);
}
var effectApplied = false;
if (skill.SkillType == SkillType.SummonMonster)
{
if (SummonSkillToMonsterMapping.TryGetValue(skill.Number, out var monsterNumber)
&& player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == monsterNumber) is { } monsterDefinition)
{
await player.CreateSummonedMonsterAsync(monsterDefinition).ConfigureAwait(false);
}
}
else
{
effectApplied = await this.ApplySkillAsync(player, target, skillEntry!).ConfigureAwait(false);
}
await player.ForEachWorldObserverAsync(p => p.ShowSkillAnimationAsync(player, target, skill, effectApplied), true).ConfigureAwait(false);
}
///
/// Determines the targets of the skill. It can be overridden by derived classes to provide custom target selection logic.
///
/// The player.
/// The skill target.
/// The skill.
/// The list of targets.
protected virtual IEnumerable DetermineTargets(Player player, IAttackable targetedTarget, Skill skill)
{
if (skill.Target == SkillTarget.ImplicitPlayer)
{
return player.GetAsEnumerable();
}
if (skill.Target == SkillTarget.ImplicitParty)
{
if (player.Party != null)
{
return player.Party.PartyList.OfType().Where(p => player.Observers.Contains((IWorldObserver)p));
}
return player.GetAsEnumerable();
}
if (skill.Target == SkillTarget.Explicit)
{
return targetedTarget.GetAsEnumerable();
}
if (skill.Target == SkillTarget.ExplicitWithImplicitInRange)
{
if (player.GameContext.PlugInManager.GetStrategy(skill.Number) is { } filterPlugin)
{
var rotationToTarget = (byte)(player.Position.GetAngleDegreeTo(targetedTarget.Position) / 360.0 * 255.0);
var attackablesInRange =
player.CurrentMap?
.GetAttackablesInRange(player.Position, skill.Range)
.Where(a => a != player)
.Where(a => player.GameContext.Configuration.AreaSkillHitsPlayer || a is NonPlayerCharacter)
.Where(a => !a.IsAtSafezone())
.Where(a => filterPlugin.IsTargetWithinBounds(player, a, player.Position, rotationToTarget))
.ToList();
if (attackablesInRange is not null)
{
if (!attackablesInRange.Contains(targetedTarget))
{
attackablesInRange.Add(targetedTarget);
}
return attackablesInRange;
}
}
else if (skill.ImplicitTargetRange > 0)
{
var targetsOfTarget = targetedTarget.CurrentMap?.GetAttackablesInRange(targetedTarget.Position, skill.ImplicitTargetRange) ?? Enumerable.Empty();
if (!player.GameContext.Configuration.AreaSkillHitsPlayer && targetedTarget is Monster)
{
return targetsOfTarget.OfType();
}
return targetsOfTarget;
}
else
{
// do nothing.
}
return targetedTarget.GetAsEnumerable();
}
var targets = player.CurrentMap?.GetAttackablesInRange(player.Position, skill.ImplicitTargetRange) ?? Enumerable.Empty();
if (skill.Target == SkillTarget.ImplicitAllInRange)
{
return targets;
}
if (skill.Target == SkillTarget.ImplicitPlayersInRange)
{
return targets.OfType();
}
if (skill.Target == SkillTarget.ImplicitNpcsInRange)
{
return targets.OfType();
}
return Enumerable.Empty();
}
private async ValueTask ApplySkillAsync(Player player, IAttackable targetedTarget, SkillEntry skillEntry)
{
skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));
var skill = skillEntry.Skill;
var success = false;
var targets = this.DetermineTargets(player, targetedTarget, skill);
bool isCombo = false;
if (skill.SkillType is SkillType.DirectHit or SkillType.CastleSiegeSkill
&& player.ComboState is { } comboState
&& !targetedTarget.IsAtSafezone()
&& !player.IsAtSafezone()
&& targetedTarget.IsActive()
&& player.IsActive())
{
isCombo = await comboState.RegisterSkillAsync(skill).ConfigureAwait(false);
}
foreach (var target in targets)
{
if (skill.SkillType == SkillType.DirectHit || skill.SkillType == SkillType.CastleSiegeSkill)
{
if (player.Attributes![Stats.AmmunitionConsumptionRate] > player.Attributes[Stats.AmmunitionAmount])
{
break;
}
if (!target.IsAtSafezone() && !player.IsAtSafezone() && target != player)
{
var hitInfo = await target.AttackByAsync(player, skillEntry, isCombo, 1, skill.NumberOfHitsPerAttack > 1 ? false : null).ConfigureAwait(false);
player.LastAttackedTarget.SetTarget(target);
success = await target.TryApplyElementalEffectsAsync(player, skillEntry, hitInfo).ConfigureAwait(false) || success;
for (int hit = 2; hit <= skill.NumberOfHitsPerAttack; hit++)
{
await target.AttackByAsync(player, skillEntry, isCombo, 1, hit == skill.NumberOfHitsPerAttack).ConfigureAwait(false);
}
}
}
else if (skill.MagicEffectDef != null)
{
// Buffs are allowed in the Safezone of Blood Castle.
var canDoBuff = !player.IsAtSafezone() || player.CurrentMiniGame is { };
if (!canDoBuff)
{
player.Logger.LogWarning("Can't apply magic effect when being in the safe-zone. skill: {SkillName} ({SkillNumber}), skillType: {SkillType}.", skill.Name, skill.Number, skill.SkillType);
break;
}
if (skill.SkillType == SkillType.Buff)
{
await target.ApplyMagicEffectAsync(player, skillEntry).ConfigureAwait(false);
success = true;
}
else if (skill.SkillType == SkillType.Regeneration)
{
await target.ApplyRegenerationAsync(player, skillEntry).ConfigureAwait(false);
success = true;
}
else
{
player.Logger.LogWarning("Skill.MagicEffectDef isn't null, but it's not a buff or regeneration skill. skill: {SkillName} ({SkillNumber}), skillType: {SkillType}.", skill.Name, skill.Number, skill.SkillType);
}
}
else
{
player.Logger.LogWarning("Skill.MagicEffectDef is null, skill: {SkillName} ({SkillNumber}), skillType: {SkillType}.", skill.Name, skill.Number, skill.SkillType);
}
}
if (isCombo)
{
await player.ForEachWorldObserverAsync(p => p.ShowComboAnimationAsync(player, targetedTarget), true).ConfigureAwait(false);
}
return success;
}
}