using System; using System.Collections.Generic; using ModernUO.Serialization; using Server.Mobiles; using Server.Network; using Server.Spells; namespace Server.Items { [SerializationGenerator(0, false)] public abstract partial class BaseRanged : BaseMeleeWeapon { [SerializableField(0)] [InvalidateProperties] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _balanced; [SerializableField(1)] [InvalidateProperties] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _velocity; private TimerExecutionToken _recoveryTimerToken; public BaseRanged(int itemID) : base(itemID) { } public abstract int EffectID { get; } public abstract Type AmmoType { get; } public abstract Item Ammo { get; } public override int DefHitSound => 0x234; public override int DefMissSound => 0x238; public override SkillName DefSkill => SkillName.Archery; public override WeaponType DefType => WeaponType.Ranged; public override WeaponAnimation DefAnimation => WeaponAnimation.ShootXBow; public override SkillName AccuracySkill => SkillName.Archery; public override TimeSpan OnSwing(Mobile attacker, Mobile defender, double damageBonus = 1.0) { // WeaponAbility a = WeaponAbility.GetCurrentAbility( attacker ); // Make sure we've been standing still for .25/.5/1 second depending on Era if (Core.TickCount - attacker.LastMoveTime >= (Core.SE ? 250 : Core.AOS ? 500 : 1000) || Core.AOS && WeaponAbility.GetCurrentAbility(attacker) is MovingShot) { var canSwing = true; if (Core.AOS) { canSwing = !attacker.Paralyzed && !attacker.Frozen; if (canSwing) { // On OSI you can swing + hold a cast at the same time canSwing = attacker.Spell is not Spell { IsCasting: true, BlocksMovement: true }; } } if ((attacker as PlayerMobile)?.DuelContext?.CheckItemEquip(attacker, this) == false) { canSwing = false; } if (canSwing && attacker.HarmfulCheck(defender)) { attacker.DisruptiveAction(); attacker.NetState.SendSwing(attacker.Serial, defender.Serial); if (OnFired(attacker, defender)) { if (CheckHit(attacker, defender)) { OnHit(attacker, defender); } else { OnMiss(attacker, defender); } } else if (WeaponAbility.GetCurrentAbility(attacker) is LightningArrow lightningArrow) { // Lightning Arrow doesn't require ammunition attacker.MovingEffect(defender, EffectID, 18, 1, false, false); lightningArrow.OnHit(attacker, defender, 0, new WorldLocation(defender)); } } attacker.RevealingAction(); return GetDelay(attacker); } attacker.RevealingAction(); return TimeSpan.FromSeconds(0.25); } public override void OnHit(Mobile attacker, Mobile defender, double damageBonus = 1) { var ammo = Ammo; if (ammo != null && attacker.Player && !defender.Player && (defender.Body.IsAnimal || defender.Body.IsMonster) && Utility.RandomDouble() < 0.4) { defender.AddToBackpack(ammo); } if (Core.ML && _velocity > 0) { var bonus = (int)attacker.GetDistanceToSqrt(defender); if (bonus > 0 && _velocity > Utility.Random(100)) { AOS.Damage(defender, attacker, bonus * 3, 100, 0, 0, 0, 0); if (attacker.Player) { attacker.SendLocalizedMessage(1072794); // Your arrow hits its mark with velocity! } if (defender.Player) { defender.SendLocalizedMessage(1072795); // You have been hit by an arrow with velocity! } } } base.OnHit(attacker, defender, damageBonus); } public override void OnMiss(Mobile attacker, Mobile defender) { if (attacker.Player && Utility.RandomDouble() < 0.4) { if (Core.SE) { if (attacker is PlayerMobile pm) { var ammo = AmmoType; if (ammo != null) { pm.RecoverableAmmo ??= new Dictionary(); pm.RecoverableAmmo.TryGetValue(ammo, out var result); pm.RecoverableAmmo[ammo] = result + 1; } if (!pm.Warmode) { if (!_recoveryTimerToken.Running) { Timer.StartTimer(TimeSpan.FromSeconds(10), () => { _recoveryTimerToken.Cancel(); pm.RecoverAmmo(); }, out _recoveryTimerToken ); } } } } else { Ammo?.MoveToWorld( new Point3D( defender.X + Utility.RandomMinMax(-1, 1), defender.Y + Utility.RandomMinMax(-1, 1), defender.Z ), defender.Map ); } } base.OnMiss(attacker, defender); } public virtual bool OnFired(Mobile attacker, Mobile defender) { if (attacker.Player) { var quiver = attacker.FindItemOnLayer(Layer.Cloak); var pack = attacker.Backpack; if (quiver == null || Utility.Random(100) >= quiver.LowerAmmoCost) { // consume ammo if (quiver?.ConsumeTotal(AmmoType) == true) { quiver.InvalidateWeight(); } else if (pack?.ConsumeTotal(AmmoType) != true) { return false; } } else if (quiver.FindItemByType(AmmoType) == null && pack?.FindItemByType(AmmoType) == null) { // lower ammo cost should not work when we have no ammo at all return false; } } attacker.MovingEffect(defender, EffectID, 18, 1, false, false); return true; } } }