// // 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; } }