using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using WingsAPI.Game.Extensions.ItemExtension.Inventory; using WingsAPI.Game.Extensions.ItemExtension.Item; using WingsAPI.Game.Extensions.Quicklist; using WingsEmu.DTOs.Skills; using WingsEmu.Game._enum; using WingsEmu.Game._i18n; using WingsEmu.Game.Battle; using WingsEmu.Game.Buffs; using WingsEmu.Game.Characters; using WingsEmu.Game.Characters.Events; using WingsEmu.Game.Configurations; using WingsEmu.Game.Entities; using WingsEmu.Game.Extensions; using WingsEmu.Game.Helpers.Damages; using WingsEmu.Game.Items; using WingsEmu.Game.Managers; using WingsEmu.Game.Managers.StaticData; using WingsEmu.Game.Maps; using WingsEmu.Game.Networking; using WingsEmu.Game.Skills; using WingsEmu.Packets.ClientPackets; using WingsEmu.Packets.Enums; using WingsEmu.Packets.Enums.Battle; using WingsEmu.Packets.Enums.Chat; namespace WingsEmu.Plugins.PacketHandling.Game.Battle; public class UseSkillPacketHandler : GenericGamePacketHandlerBase { private readonly ICardsManager _cardsManager; private readonly IDelayManager _delayManager; private readonly IGameLanguageService _gameLanguage; private readonly IItemsManager _items; private readonly RainbowBattleConfiguration _rainbowBattleConfiguration; private readonly ISkillsManager _skillsManager; public UseSkillPacketHandler(IGameLanguageService gameLanguage, IDelayManager delayManager, IItemsManager items, ICardsManager cardsManager, ISkillsManager skillsManager, RainbowBattleConfiguration rainbowBattleConfiguration) { _gameLanguage = gameLanguage; _delayManager = delayManager; _items = items; _cardsManager = cardsManager; _skillsManager = skillsManager; _rainbowBattleConfiguration = rainbowBattleConfiguration; } protected override async Task HandlePacketAsync(IClientSession session, UseSkillPacket packet) { session.SendDebugMessage("[U_S] Start u_s"); IPlayerEntity character = session.PlayerEntity; if (!BasicChecks(session, packet)) { return; } List skills = session.PlayerEntity.Skills; var characterSkill = skills.FirstOrDefault(s => s.Skill?.CastId == packet.CastId && (s.Skill?.UpgradeSkill == 0 || s.Skill?.SkillType == SkillType.NormalPlayerSkill) && s.Skill.TargetType != TargetType.NonTarget) as CharacterSkill; SkillDTO skill = characterSkill?.Skill; if (skill == null) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Skill does not exist"); return; } if (skill.ItemVNum != 0 && !session.PlayerEntity.HasItem(skill.ItemVNum)) { session.SendCancelPacket(CancelType.NotInCombatMode); IGameItem gameItem = _items.GetItem(skill.ItemVNum); if (gameItem == null) { return; } string itemName = gameItem.GetItemName(_gameLanguage, session.UserLanguage); session.SendInformationChatMessage(_gameLanguage.GetLanguageFormat(GameDialogKey.INVENTORY_SHOUTMESSAGE_NOT_ENOUGH_ITEMS, session.UserLanguage, 1, itemName)); return; } SkillInfo skillInfo = session.PlayerEntity.GetUpgradedSkill(skill, _cardsManager, _skillsManager) ?? skill.GetInfo(); if (skillInfo.AoERange != 0 && skill.CastId != 0 && skill.CastId != 1 && session.PlayerEntity.HasBuff(BuffVnums.EXPLOSIVE_ENCHACMENT)) { (int firstData, int secondData) buff = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.FireCannoneerRangeBuff, (byte)AdditionalTypes.FireCannoneerRangeBuff.AOEIncreased, session.PlayerEntity.Level); skillInfo.AoERange += (byte)buff.firstData; skillInfo.HitEffect = session.PlayerEntity.GetCannoneerHitEffect(skill.CastId); } skillInfo.Range += (byte)session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.FearSkill, (byte)AdditionalTypes.FearSkill.AttackRangedIncreased, session.PlayerEntity.Level) .Item1; bool canBeUsed = character.CharacterCanCastOrCancel(characterSkill, _gameLanguage, skillInfo, false); bool comboSkill = skill.CastId > 10 && character.UseSp && skill.SpecialCost == 999; if (!canBeUsed) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] !canBeUsed"); return; } if (!session.PlayerEntity.CanPerformAttack(skillInfo)) { session.SendCancelPacket(CancelType.NotInCombatMode); return; } if (comboSkill) { ComboSkillState comboSkillState = session.PlayerEntity.GetComboState(); if (comboSkillState == null) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] comboSkills == null"); return; } bool canCastComboSkill = comboSkillState.LastSkillByCastId == skillInfo.CastId; if (!canCastComboSkill) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] comboSkill && comboSkill == null"); return; } if (session.PlayerEntity.AngelElement.HasValue) { if (!character.HasBuff(BuffVnums.MAGIC_SPELL)) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Character without MAGIC_SPELL"); return; } ElementType? elementType = character.GetBuffElementType((short)skill.Id); if (!elementType.HasValue) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] ElementType == null"); return; } if (session.PlayerEntity.AngelElement.Value != elementType.Value) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Element != skill.Element"); return; } } } IBattleEntity target = character.MapInstance.GetBattleEntity(packet.VisualType, packet.MapMonsterId); if (await TargetChecks(session, target, skillInfo)) { return; } if (session.PlayerEntity.RainbowBattleComponent.IsFrozen) { session.SendCancelPacket(CancelType.NotInCombatMode); return; } if (skillInfo.TargetType == TargetType.Self || skillInfo.TargetType == TargetType.SelfOrTarget && target.Id == character.Id) { session.SendDebugMessage("[U_S] Target = character"); target = character; } if (skillInfo.TargetAffectedEntities == TargetAffectedEntities.BuffForAllies && character.IsEnemyWith(target)) { target = character; } int cellSizeBonus = target switch { IPlayerEntity => 7, _ => 3 }; if (target is INpcMonsterEntity npcMonsterEntity) { cellSizeBonus += npcMonsterEntity.CellSize; } if (!character.Position.IsInRange(target.Position, skillInfo.Range + cellSizeBonus) && skillInfo.AttackType != AttackType.Dash) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage($"[U_S] Out of range {character.Position.GetDistance(target.Position)} - {skill.Range}"); return; } if (target is IMonsterEntity mob) { if (!session.PlayerEntity.CanMonsterBeAttacked(mob) && !mob.IsMateTrainer) { session.SendCancelPacket(CancelType.NotInCombatMode); return; } if (mob.MonsterRaceType == MonsterRaceType.Fixed) { session.SendCancelPacket(CancelType.NotInCombatMode); return; } } // it should check before taking Mp/Hp if (character.IsAllyWith(target) && skillInfo.TargetType != TargetType.Self && skillInfo.TargetType != TargetType.SelfOrTarget && target.Id != character.Id && skillInfo.TargetAffectedEntities != TargetAffectedEntities.BuffForAllies) { character.CancelCastingSkill(); character.Session.SendDebugMessage("[U_S] !caster.IsEnemyWith(target) && caster.IsPlayer() && skill.TargetType != TargetType.Self"); return; } if (!character.IsEnemyWith(target) && skillInfo.TargetType != TargetType.Self && skillInfo.TargetType != TargetType.SelfOrTarget) { character.CancelCastingSkill(); return; } Position positionAfterDash = default; if (packet.MapX.HasValue && packet.MapY.HasValue) { if (skillInfo.AttackType != AttackType.Dash) { session.SendDebugMessage("[U_S] Skill.AttackType != Dash"); session.SendCancelPacket(CancelType.NotInCombatMode); return; } if (character.MapInstance.IsBlockedZone((int)packet.MapX, (int)packet.MapY)) { session.SendCancelPacket(CancelType.NotInCombatMode); return; } var newPosition = new Position((short)packet.MapX, (short)packet.MapY); if (!character.Position.IsInRange(newPosition, skillInfo.Range + 2)) { session.SendDebugMessage("[U_S] newPosition !IsInRange"); session.SendCancelPacket(CancelType.NotInCombatMode); return; } positionAfterDash = newPosition; } await FinalChecks(session, skill, character, skillInfo, target, comboSkill); session.SendDebugMessage($"Hit {skillInfo.HitType} / Target {skillInfo.TargetType} / Attack Type {skillInfo.AttackType} / Affected entities {skillInfo.TargetAffectedEntities}"); if (target is IPlayerEntity characterTarget) { session.SendDebugMessage($"Sender: {session.PlayerEntity.Name} -> Target: {characterTarget.Name} "); } character.SkillComponent.CanBeInterrupted = false; character.SkillComponent.IsSkillInterrupted = false; character.SkillComponent.CanBeInterrupted = character.CanBeInterrupted(skillInfo); character.WeaponLoaded(characterSkill, _gameLanguage, true); DateTime castTime = character.GenerateSkillCastTime(skillInfo); session.SendDebugMessage("[U_S] IsCasting = true"); await character.EmitEventAsync(new BattleExecuteSkillEvent(character, target, skillInfo, castTime, positionAfterDash)); } private async Task TargetChecks(IClientSession session, IBattleEntity target, SkillInfo skillInfo) { if (target == null) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] No target"); return true; } if (target.MapInstance.IsPvp && session.CurrentMapInstance.PvpZone(target.PositionX, target.PositionY)) { session.SendDebugMessage("[U_S] Target no-PvP zone / !map.IsPvp"); session.SendCancelPacket(CancelType.NotInCombatMode); return true; } switch (session.CurrentMapInstance.MapInstanceType) { case MapInstanceType.RainbowBattle: if (target is not IPlayerEntity rainbowFrozen) { break; } if (session.PlayerEntity.IsEnemyWith(rainbowFrozen)) { break; } if (!rainbowFrozen.RainbowBattleComponent.IsFrozen) { break; } if (rainbowFrozen.RainbowBattleComponent.Team != session.PlayerEntity.RainbowBattleComponent.Team) { break; } if (target.Position.GetDistance(session.PlayerEntity.Position) > 5) { break; } session.SendCancelPacket(CancelType.NotInCombatMode); DateTime now = DateTime.UtcNow; if (session.PlayerEntity.LastUnfreezedPlayer > now) { return true; } session.PlayerEntity.LastUnfreezedPlayer = now.AddSeconds(5); DateTime wait = await _delayManager.RegisterAction(session.PlayerEntity, DelayedActionType.RainbowBattleUnfreeze); session.SendDelay((int)(wait - DateTime.UtcNow).TotalMilliseconds, GuriType.Unfreezing, $"guri 505 {rainbowFrozen.Id}"); return true; } if (target is INpcEntity && skillInfo.Vnum == (short)SkillsVnums.SACRIFICE) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] MapNpc && Sacrifice"); return true; } if (!target.IsAlive()) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Target is dead"); return true; } if (target is not IMonsterEntity mob || mob.SummonerId == 0) { return false; } if (mob.SummonerType is not VisualType.Player) { return false; } if (session.PlayerEntity.CanMonsterBeAttacked(mob) && !mob.IsMateTrainer) { return false; } session.SendDebugMessage("[U_S] mob.SummonerId != 0"); session.SendCancelPacket(CancelType.NotInCombatMode); return true; } private async Task FinalChecks(IClientSession session, SkillDTO skill, IPlayerEntity character, SkillInfo skillInfo, IBattleEntity target, bool comboSkill) { if (!session.PlayerEntity.CheatComponent.HasGodMode) { skillInfo.ManaCost = session.PlayerEntity.BCardComponent.HasBCard(BCardType.TimeCircleSkills, (byte)AdditionalTypes.TimeCircleSkills.DisableMPConsumption) ? 0 : skillInfo.ManaCost; if (session.PlayerEntity.AdditionalMp > 0) { int removedAdditionalMp; if (session.PlayerEntity.AdditionalMp > skillInfo.ManaCost) { removedAdditionalMp = skillInfo.ManaCost; } else { removedAdditionalMp = session.PlayerEntity.AdditionalMp; int overflow = Math.Abs(session.PlayerEntity.AdditionalMp - skillInfo.ManaCost); session.PlayerEntity.Mp -= overflow; } await session.EmitEventAsync(new RemoveAdditionalHpMpEvent { Mp = removedAdditionalMp }); } else { session.PlayerEntity.RemoveEntityMp((short)skillInfo.ManaCost, skill); } (int firstDataNegative, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, (byte)AdditionalTypes.HealingBurningAndCasting.HPDecreasedByConsumingMP, session.PlayerEntity.Level); (int firstDataPositive, int _) = session.PlayerEntity.BCardComponent.GetAllBCardsInformation(BCardType.HealingBurningAndCasting, (byte)AdditionalTypes.HealingBurningAndCasting.HPIncreasedByConsumingMP, session.PlayerEntity.Level); int hpRemoved = (int)(firstDataPositive / 100.0 * skillInfo.ManaCost - firstDataNegative / 100.0 * skillInfo.ManaCost); if (hpRemoved > 0) { await session.PlayerEntity.EmitEventAsync(new BattleEntityHealEvent { Entity = session.PlayerEntity, HpHeal = hpRemoved }); } else { if (session.PlayerEntity.Hp - hpRemoved <= 0) { session.PlayerEntity.BroadcastDamage(session.PlayerEntity.Hp - 1); session.PlayerEntity.Hp = 1; } else { session.PlayerEntity.BroadcastDamage(hpRemoved); session.PlayerEntity.Hp -= hpRemoved; } } session.RefreshStat(); session.SendDebugMessage($"[U_S] MpCost: {skillInfo.ManaCost}"); } if (skill.Id == (short)SkillsVnums.SPY_OUT) { session.SendEffectObject(target, true, EffectType.Sp6ArcherTargetFalcon); } if (skill.ItemVNum != 0) { await session.RemoveItemFromInventory(skill.ItemVNum); } if (session.PlayerEntity.RainbowBattleComponent.IsInRainbowBattle) { session.PlayerEntity.RainbowBattleComponent.ActivityPoints += _rainbowBattleConfiguration.UsingSkillActivityPoints; } character.LastSkillUse = DateTime.UtcNow; character.ClearFoodBuffer(); ComboSkillState newState = null; if (!comboSkill && skill.CastId != 0) { newState = new ComboSkillState { State = 0 }; } if (skill.CastId < 11 && skill.CastId != 0 && newState != null) { newState.OriginalSkillCastId = (byte)skill.CastId; } session.RefreshStat(); if (newState != null && !session.PlayerEntity.AngelElement.HasValue) { session.PlayerEntity.SaveComboSkill(newState); } if (skill.CastId == 0) { return; } ComboSkillState state = session.PlayerEntity.GetComboState(); bool sendBuffIconWindow = session.PlayerEntity.AngelElement.HasValue && character.Specialist is { SpLevel: > 19 } && character.HasBuff(BuffVnums.MAGIC_SPELL); if (state != null) { if (state.State >= 10) { session.PlayerEntity.CleanComboState(); } else { if (comboSkill) { session.PlayerEntity.IncreaseComboState((byte)skill.CastId); } else if (newState == null) { session.PlayerEntity.CleanComboState(); } } if (sendBuffIconWindow) { character.Session.SendMSlotPacket(state.OriginalSkillCastId); } } if (sendBuffIconWindow) { session.RefreshQuicklist(); return; } session.SendMsCPacket(0); session.RefreshQuicklist(); } private bool BasicChecks(IClientSession session, UseSkillPacket packet) { IPlayerEntity character = session.PlayerEntity; if (!character.CanFight() || packet == null) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] !canFight, packet null"); return false; } if (!session.PlayerEntity.CanPerformAttack()) { session.SendDebugMessage("[U_S] !CanPerformAttack"); session.SendCancelPacket(CancelType.NotInCombatMode); return false; } if (session.CurrentMapInstance.IsPvp && session.CurrentMapInstance.PvpZone(session.PlayerEntity.PositionX, session.PlayerEntity.PositionY)) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Character no-PvP zone / !map.IsPvp"); return false; } if (session.IsMuted()) { session.SendMuteMessage(); session.SendCancelPacket(CancelType.NotInCombatMode); return false; } if (session.PlayerEntity.IsOnVehicle || session.PlayerEntity.CheatComponent.IsInvisible) { session.SendDebugMessage("[U_S] IsVehicled, InvisibleGm"); session.SendCancelPacket(CancelType.NotInCombatMode); return false; } if ((DateTime.UtcNow - session.PlayerEntity.LastTransform).TotalSeconds < 3) { session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Under transformation cooldown"); session.SendMsg(_gameLanguage.GetLanguage(GameDialogKey.SPECIALIST_SHOUTMESSAGE_CANT_ATTACK_YET, session.UserLanguage), MsgMessageType.Middle); return false; } if (!character.IsCastingSkill) { return true; } session.SendCancelPacket(CancelType.NotInCombatMode); session.SendDebugMessage("[U_S] Already using a skill"); return false; } }