baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
327
src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
Normal file
327
src/GameLogic/PlayerActions/Skills/TargetedSkillDefaultPlugin.cs
Normal file
@@ -0,0 +1,327 @@
|
||||
// <copyright file="TargetedSkillDefaultPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Action to perform a skill which is explicitly aimed to a target.
|
||||
/// </summary>
|
||||
[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<short, short> 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
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override short Key => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the skill.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="skillId">The skill identifier.</param>
|
||||
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<ISpeedHackCheatCheckPlugIn>() 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<IObjectMovedPlugIn>(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<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(player, target, skill, effectApplied), true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the targets of the skill. It can be overridden by derived classes to provide custom target selection logic.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="targetedTarget">The skill target.</param>
|
||||
/// <param name="skill">The skill.</param>
|
||||
/// <returns>The list of targets.</returns>
|
||||
protected virtual IEnumerable<IAttackable> 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<IAttackable>().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<short, IAreaSkillTargetFilter>(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<IAttackable>();
|
||||
if (!player.GameContext.Configuration.AreaSkillHitsPlayer && targetedTarget is Monster)
|
||||
{
|
||||
return targetsOfTarget.OfType<Monster>();
|
||||
}
|
||||
|
||||
return targetsOfTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
return targetedTarget.GetAsEnumerable();
|
||||
}
|
||||
|
||||
var targets = player.CurrentMap?.GetAttackablesInRange(player.Position, skill.ImplicitTargetRange) ?? Enumerable.Empty<IAttackable>();
|
||||
|
||||
if (skill.Target == SkillTarget.ImplicitAllInRange)
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
|
||||
if (skill.Target == SkillTarget.ImplicitPlayersInRange)
|
||||
{
|
||||
return targets.OfType<Player>();
|
||||
}
|
||||
|
||||
if (skill.Target == SkillTarget.ImplicitNpcsInRange)
|
||||
{
|
||||
return targets.OfType<Monster>();
|
||||
}
|
||||
|
||||
return Enumerable.Empty<IAttackable>();
|
||||
}
|
||||
|
||||
private async ValueTask<bool> 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<IShowSkillAnimationPlugIn>(p => p.ShowComboAnimationAsync(player, targetedTarget), true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user