diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 96a790360..0fe992f60 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -23,7 +23,7 @@ public static class Utility private static Dictionary _ipAddressTable; - private static readonly SkillName[] m_AllSkills = + private static SkillName[] _allSkills = { SkillName.Alchemy, SkillName.Anatomy, @@ -79,7 +79,10 @@ public static class Utility SkillName.Chivalry, SkillName.Bushido, SkillName.Ninjitsu, - SkillName.Spellweaving + SkillName.Spellweaving, + // SkillName.Mysticism, + // SkillName.Imbuing, + SkillName.Throwing }; private static readonly SkillName[] m_CombatSkills = @@ -622,11 +625,7 @@ public static class Utility array.Length > 0 ? array.GetValue(Math.Clamp(index, 0, array.Length - 1)) : emptyValue; public static SkillName RandomSkill() => - m_AllSkills[Random( - m_AllSkills.Length - (Core.ML ? 0 : - Core.SE ? 1 : - Core.AOS ? 3 : 6) - )]; + _allSkills[Random(_allSkills.Length - (Core.ML ? 0 : Core.SE ? 1 : Core.AOS ? 3 : 6))]; public static SkillName RandomCombatSkill() => m_CombatSkills.RandomElement(); diff --git a/Projects/UOContent/Items/Addons/ArcheryButteAddon.cs b/Projects/UOContent/Items/Addons/ArcheryButteAddon.cs index fc9f7eceb..5834f8bb7 100644 --- a/Projects/UOContent/Items/Addons/ArcheryButteAddon.cs +++ b/Projects/UOContent/Items/Addons/ArcheryButteAddon.cs @@ -48,11 +48,12 @@ namespace Server.Items public override void OnDoubleClick(Mobile from) { - if ((_arrows > 0 || _bolts > 0) && from.InRange(GetWorldLocation(), 1)) + var inRange = from.InRange(GetWorldLocation(), 1); + if ((_arrows > 0 || _bolts > 0) && inRange) { Gather(from); } - else + else if (from.Weapon is not BaseThrown || inRange) { Fire(from); } diff --git a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs index 069b5b3a0..ddcb9b389 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs @@ -2,114 +2,108 @@ using System; using Server.Mobiles; using Server.Spells.Ninjitsu; -namespace Server.Items +namespace Server.Items; + +/// +/// Perfect for the foot-soldier, the Dismount special attack can unseat a mounted opponent. +/// The fighter using this ability must be on his own two feet and not in the saddle of a steed +/// (with one exception: players may use a lance to dismount other players while mounted). +/// If it works, the target will be knocked off his own mount and will take some extra damage from the fall! +/// +public class Dismount : WeaponAbility { - /// - /// Perfect for the foot-soldier, the Dismount special attack can unseat a mounted opponent. - /// The fighter using this ability must be on his own two feet and not in the saddle of a steed - /// (with one exception: players may use a lance to dismount other players while mounted). - /// If it works, the target will be knocked off his own mount and will take some extra damage from the fall! - /// - public class Dismount : WeaponAbility + public static readonly TimeSpan RemountDelay = TimeSpan.FromSeconds(10.0); + + public override int BaseMana => 20; + + public override void OnHit(Mobile attacker, Mobile defender, int damage) { - public static readonly TimeSpan RemountDelay = TimeSpan.FromSeconds(10.0); - - public override int BaseMana => 20; - - public override bool Validate(Mobile from) + if (!Validate(attacker) || !CheckMana(attacker, true)) { - if (!base.Validate(from)) - { - return false; - } - - if (from.Mounted && from.Weapon is not Lance) - { - from.SendLocalizedMessage(1061283); // You cannot perform that attack while mounted! - return false; - } - - return true; + return; } - public override void OnHit(Mobile attacker, Mobile defender, int damage) + if (defender is ChaosDragoon or ChaosDragoonElite) { - if (!Validate(attacker) || !CheckMana(attacker, true)) + return; + } + + if (attacker.Mounted || attacker.Flying) + { + if (attacker.Weapon is not Lance || !defender.Mounted && !defender.Flying && defender.Weapon is not Lance) { - return; + attacker.SendLocalizedMessage(1061283); // You cannot perform that attack while mounted! + } + } + + ClearCurrentAbility(attacker); + + var mount = defender.Mount; + + if (mount == null && !AnimalForm.UnderTransformation(defender)) + { + attacker.SendLocalizedMessage(1060848); // This attack only works on mounted targets + return; + } + + if (Core.ML && attacker is LesserHiryu && Utility.RandomDouble() <= 0.8) + { + return; // Lesser Hiryu have an 80% chance of missing this attack + } + + DoDismount(attacker, defender, TimeSpan.FromSeconds(10)); + + if (!attacker.Mounted) + { + AOS.Damage(defender, attacker, Utility.RandomMinMax(15, 25), 100, 0, 0, 0, 0); + } + } + + public static void DoDismount(Mobile attacker, Mobile defender, TimeSpan delay, BlockMountType type = BlockMountType.Dazed) + { + attacker.SendLocalizedMessage(1060082); // The force of your attack has dislodged them from their mount! + + if (attacker.Mounted) + { + defender.SendLocalizedMessage(1062315); // You fall off your mount! + } + else + { + defender.SendLocalizedMessage(1060083); // You fall off of your mount and take damage! + } + + defender.PlaySound(0x140); + defender.FixedParticles(0x3728, 10, 15, 9955, EffectLayer.Waist); + + if (defender is PlayerMobile mobile) + { + if (AnimalForm.UnderTransformation(mobile)) + { + mobile.SendLocalizedMessage(1114066, attacker.Name); // ~1_NAME~ knocked you out of animal form! + } + else if (defender.Flying) + { + defender.SendLocalizedMessage(1113590, attacker.Name); // You have been grounded by ~1_NAME~! + } + else if (mobile.Mounted) + { + mobile.SendLocalizedMessage(1040023); // You have been knocked off of your mount! } - if (defender is ChaosDragoon or ChaosDragoonElite) - { - return; - } + mobile.SetMountBlock(type, delay, true); + } + else + { + defender.Mount.Rider = null; + } - // TODO: Should there be a message here? - if (attacker.Mounted && (attacker.Weapon is not Lance || defender.Weapon is not Lance)) - { - return; - } - - ClearCurrentAbility(attacker); - - var mount = defender.Mount; - - if (mount == null && !AnimalForm.UnderTransformation(defender)) - { - attacker.SendLocalizedMessage(1060848); // This attack only works on mounted targets - return; - } - - if (Core.ML && attacker is LesserHiryu && Utility.RandomDouble() <= 0.8) - { - return; // Lesser Hiryu have an 80% chance of missing this attack - } - - attacker.SendLocalizedMessage(1060082); // The force of your attack has dislodged them from their mount! - - if (attacker.Mounted) - { - defender.SendLocalizedMessage(1062315); // You fall off your mount! - } - else - { - defender.SendLocalizedMessage(1060083); // You fall off of your mount and take damage! - } - - defender.PlaySound(0x140); - defender.FixedParticles(0x3728, 10, 15, 9955, EffectLayer.Waist); - - if (defender is PlayerMobile mobile) - { - if (AnimalForm.UnderTransformation(mobile)) - { - mobile.SendLocalizedMessage(1114066, attacker.Name); // ~1_NAME~ knocked you out of animal form! - } - else if (mobile.Mounted) - { - mobile.SendLocalizedMessage(1040023); // You have been knocked off of your mount! - } - - mobile.SetMountBlock(BlockMountType.Dazed, TimeSpan.FromSeconds(10), true); - } - else - { - defender.Mount.Rider = null; - } - - if (attacker is PlayerMobile playerMobile) - { - playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true); - } - else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm }) - { - pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false); - } - - if (!attacker.Mounted) - { - AOS.Damage(defender, attacker, Utility.RandomMinMax(15, 25), 100, 0, 0, 0, 0); - } + if (attacker is PlayerMobile playerMobile) + { + playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true); + } + else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm }) + { + pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false); } } } diff --git a/Projects/UOContent/Items/Weapons/Abilities/Disrobe.cs b/Projects/UOContent/Items/Weapons/Abilities/Disrobe.cs deleted file mode 100644 index fc4286c78..000000000 --- a/Projects/UOContent/Items/Weapons/Abilities/Disrobe.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace Server.Items -{ - /// - /// This attack allows you to disrobe your foe. - /// - public class Disrobe : WeaponAbility - { - public static readonly TimeSpan BlockEquipDuration = TimeSpan.FromSeconds(5.0); - - public override int BaseMana => 20; // Not Sure what amount of mana a creature uses. - - public override void OnHit(Mobile attacker, Mobile defender, int damage) - { - if (!Validate(attacker) || !CheckMana(attacker, true)) - { - return; - } - - ClearCurrentAbility(attacker); - var toDisrobe = defender.FindItemOnLayer(Layer.InnerTorso); - - if (toDisrobe?.Movable == false) - { - toDisrobe = defender.FindItemOnLayer(Layer.OuterTorso); - } - - var pack = defender.Backpack; - - if (pack == null || toDisrobe?.Movable == false) - { - attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent. - } - else - { - // attacker.SendLocalizedMessage( 1060092 ); // You disarm their weapon! - defender.SendLocalizedMessage(1062002); // You can no longer wear your ~1_ARMOR~ - - defender.PlaySound(0x3B9); - // defender.FixedParticles( 0x37BE, 232, 25, 9948, EffectLayer.InnerTorso ); - - pack.DropItem(toDisrobe); - - BaseWeapon.BlockEquip(defender, BlockEquipDuration); - } - } - } -} diff --git a/Projects/UOContent/Items/Weapons/Abilities/InfusedThrow.cs b/Projects/UOContent/Items/Weapons/Abilities/InfusedThrow.cs new file mode 100644 index 000000000..785aa1b32 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/InfusedThrow.cs @@ -0,0 +1,45 @@ +using Server.Mobiles; +using System; + +namespace Server.Items; + +// The projectile will be infused with energy by the attacker causing it to do more damage and stun or dismount the target. +// The player infuses their throwing projectile with mystical power. +// The infused projectile will dismount the target if possible; otherwise it will temporarily stun the target. +// The target will be hit with chaos damage regardless of whether they were dismounted or paralyzed. +public class InfusedThrow : WeaponAbility +{ + public override int BaseMana => 25; + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + if (!CheckMana(attacker, true)) + { + return; + } + + ClearCurrentAbility(attacker); + + attacker.SendLocalizedMessage(1149563); //The infused projectile strikes a target! + defender.SendLocalizedMessage(1149564); //You are struck by the infused projectile and take damage! + + AOS.Damage(defender, attacker, 15, false, 0, 0, 0, 0, 0, 100); + + IMount mount = defender.Mount; + + if ((defender.Mounted || defender.Flying || Spells.Ninjitsu.AnimalForm.UnderTransformation(defender)) && + !attacker.Mounted && !attacker.Flying && defender is not ChaosDragoon && defender is not ChaosDragoonElite) + { + defender.PlaySound(0x140); + defender.FixedParticles(0x3728, 10, 15, 9955, EffectLayer.Waist); + + Items.Dismount.DoDismount(attacker, defender, TimeSpan.FromSeconds(10)); + } + else + { + defender.FixedEffect(0x376A, 9, 32); + defender.PlaySound(0x204); + defender.Paralyze(TimeSpan.FromSeconds(defender.Player ? 3.0 : 6.0)); + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/MysticArc.cs b/Projects/UOContent/Items/Weapons/Abilities/MysticArc.cs new file mode 100644 index 000000000..0510820ba --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Abilities/MysticArc.cs @@ -0,0 +1,91 @@ +using System; +using Server.Collections; + +namespace Server.Items; + +// The thrown projectile will arc to a second target after hitting the primary target. +// Chaos energy will burst from the projectile at each target. This will only hit targets that are in combat with the user. +public class MysticArc : WeaponAbility +{ + private readonly int Damage = 15; + private Mobile _target; + private Mobile _mobile; + + public override int BaseMana => 20; + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + if (!CheckMana(attacker, true) && defender != null) + { + return; + } + + BaseThrown weapon = attacker.Weapon as BaseThrown; + + if (weapon == null) + { + return; + } + + using var queue = PooledRefQueue.Create(); + IPooledEnumerable eable = attacker.GetMobilesInRange(weapon.MaxRange); + + foreach (Mobile m in eable) + { + if (m == defender) + { + continue; + } + + if (m.Combatant != attacker) + { + continue; + } + + queue.Enqueue(m); + } + + eable.Free(); + + if (queue.Count > 0) + { + _target = queue.PeekRandom(); + } + + AOS.Damage(defender, attacker, Damage, 0, 0, 0, 0, 0, 100); + + if (_target != null) + { + defender?.MovingEffect(_target, weapon.ItemID, 18, 1, false, false); + + Timer.DelayCall(TimeSpan.FromSeconds(0.3), ThrowAgain); + _mobile = attacker; + } + + ClearCurrentAbility(attacker); + } + + public void ThrowAgain() + { + if (_target == null || _mobile == null) + { + return; + } + + if (_mobile.Weapon is not BaseThrown weapon) + { + return; + } + + if (GetCurrentAbility(_mobile) is MysticArc) + { + ClearCurrentAbility(_mobile); + } + + if (weapon.CheckHit(_mobile, _target)) + { + weapon.OnHit(_mobile, _target, 0.0); + AOS.Damage(_target, _mobile, Damage, 0, 0, 0, 0, 100); + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs b/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs index 9ad7e996f..800e83955 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/WeaponAbility.cs @@ -44,7 +44,8 @@ namespace Server.Items new PsychicAttack(), new SerpentArrow(), new ForceOfNature(), - new Disrobe() + new InfusedThrow(), + new MysticArc(), }; public static readonly WeaponAbility ArmorIgnore = Abilities[1]; @@ -79,7 +80,8 @@ namespace Server.Items public static readonly WeaponAbility SerpentArrow = Abilities[28]; public static readonly WeaponAbility ForceOfNature = Abilities[29]; - public static readonly WeaponAbility Disrobe = Abilities[30]; + public static readonly WeaponAbility InfusedThrow = Abilities[30]; + public static readonly WeaponAbility MysticArc = Abilities[31]; private static readonly Dictionary m_PlayersTable = new(); diff --git a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs new file mode 100644 index 000000000..1c99668ad --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs @@ -0,0 +1,105 @@ +using System; +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public abstract partial class BaseThrown : BaseRanged +{ + public BaseThrown(int itemID) : base(itemID) + { + } + + public abstract int MinThrowRange { get; } + + public virtual int MaxThrowRange => MinThrowRange + 3; + + public override int DefMaxRange + { + get + { + int baseRange = MaxThrowRange; + + return Parent is Mobile attacker + ? baseRange - 3 + (attacker.Str - AosStrengthReq) / ((140 - AosStrengthReq) / 3) + : baseRange; + } + } + + public override int EffectID => ItemID; + + public override Type AmmoType => null; + + public override Item Ammo => null; + + public override int DefHitSound => 0x5D3; + public override int DefMissSound => 0x5D4; + + public override SkillName DefSkill => SkillName.Throwing; + + public override WeaponAnimation DefAnimation => WeaponAnimation.Throwing; + + public override bool OnFired(Mobile attacker, Mobile defender) + { + if (!attacker.InRange(defender, 1)) + { + attacker.MovingEffect(defender, EffectID, 18, 1, false, false, Hue, 0); + } + + return true; + } + + public override void OnHit(Mobile attacker, Mobile defender, double damageBonus = 1) + { + if (WeaponAbility.GetCurrentAbility(attacker) is not MysticArc) + { + var location = new WorldLocation(defender.Location, attacker.Map); + Timer.StartTimer(TimeSpan.FromSeconds(0.3), () => Return(attacker, defender, location)); + } + + base.OnHit(attacker, defender, damageBonus); + } + + public override void OnMiss(Mobile attacker, Mobile defender) + { + if (WeaponAbility.GetCurrentAbility(attacker) is not MysticArc) + { + var location = new WorldLocation(defender.Location, attacker.Map); + Timer.StartTimer(TimeSpan.FromSeconds(0.3), () => Return(attacker, defender, location)); + } + + base.OnMiss(attacker, defender); + } + + public virtual void Return(Mobile thrower, Mobile target, WorldLocation worldLocation) + { + if (thrower == null) + { + return; + } + + if (target != null) + { + target.MovingEffect(thrower, EffectID, 18, 1, false, false, Hue, 0); + } + else + { + Effects.SendMovingParticles( + new Entity(Serial.Zero, worldLocation.Location, worldLocation.Map), + thrower, + ItemID, + 18, + 0, + false, + false, + Hue, + 0, + 9502, + 1, + 0, + (EffectLayer)255, + 0x100 + ); + } + } +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs b/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs new file mode 100644 index 000000000..67650361b --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs @@ -0,0 +1,25 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class Boomerang : BaseThrown +{ + [Constructible] + public Boomerang() : base(0x8FF) + { + Weight = 4.0; + Layer = Layer.OneHanded; + } + + public override int MinThrowRange => 4; + + public override WeaponAbility PrimaryAbility => WeaponAbility.MysticArc; + public override WeaponAbility SecondaryAbility => WeaponAbility.ConcussionBlow; + public override int AosStrengthReq => 25; + public override int AosMinDamage => 11; + public override int AosMaxDamage => 15; + public override float MlSpeed => 2.75f; + public override int InitMinHits => 31; + public override int InitMaxHits => 60; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs b/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs new file mode 100644 index 000000000..889d1e1fb --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs @@ -0,0 +1,27 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class Cyclone : BaseThrown +{ + [Constructible] + public Cyclone() : base(0x901) + { + Weight = 6.0; + Layer = Layer.OneHanded; + } + + public override int MinThrowRange => 6; + + public override WeaponAbility PrimaryAbility => WeaponAbility.MovingShot; + public override WeaponAbility SecondaryAbility => WeaponAbility.InfusedThrow; + public override int AosStrengthReq => 40; + public override int AosMinDamage => 13; + public override int AosMaxDamage => 17; + public override float MlSpeed => 3.25f; + public override int InitMinHits => 31; + public override int InitMaxHits => 60; + + +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs b/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs new file mode 100644 index 000000000..724053a39 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs @@ -0,0 +1,26 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class SoulGlaive : BaseThrown +{ + [Constructible] + public SoulGlaive() : base(0x090A) + { + Weight = 8.0; + Layer = Layer.OneHanded; + } + + public override int MinThrowRange => 8; + + public override WeaponAbility PrimaryAbility => WeaponAbility.ArmorIgnore; + public override WeaponAbility SecondaryAbility => WeaponAbility.MortalStrike; + public override int AosStrengthReq => 60; + public override int AosMinDamage => 16; + public override int AosMaxDamage => 20; + public override float MlSpeed => 4.00f; + + public override int InitMinHits => 31; + public override int InitMaxHits => 65; +} diff --git a/Projects/UOContent/Items/Weapons/WeaponEnums.cs b/Projects/UOContent/Items/Weapons/WeaponEnums.cs index b09563f05..ab29ac15f 100644 --- a/Projects/UOContent/Items/Weapons/WeaponEnums.cs +++ b/Projects/UOContent/Items/Weapons/WeaponEnums.cs @@ -59,6 +59,7 @@ namespace Server.Items Pierce2H = 14, ShootBow = 18, ShootXBow = 19, - Wrestle = 31 + Wrestle = 31, + Throwing = 32 } } diff --git a/Projects/UOContent/Migrations/Server.Items.BaseThrown.v0.json b/Projects/UOContent/Migrations/Server.Items.BaseThrown.v0.json new file mode 100644 index 000000000..e705c23b9 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.BaseThrown.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.BaseThrown" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.Boomerang.v0.json b/Projects/UOContent/Migrations/Server.Items.Boomerang.v0.json new file mode 100644 index 000000000..564180798 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.Boomerang.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.Boomerang" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.Cyclone.v0.json b/Projects/UOContent/Migrations/Server.Items.Cyclone.v0.json new file mode 100644 index 000000000..00615eced --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.Cyclone.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.Cyclone" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.SoulGlaive.v0.json b/Projects/UOContent/Migrations/Server.Items.SoulGlaive.v0.json new file mode 100644 index 000000000..325bcded6 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.SoulGlaive.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.SoulGlaive" +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/CharacterCreation.cs b/Projects/UOContent/Misc/CharacterCreation.cs index de115562c..c8c3e2875 100644 --- a/Projects/UOContent/Misc/CharacterCreation.cs +++ b/Projects/UOContent/Misc/CharacterCreation.cs @@ -1264,7 +1264,7 @@ public static class CharacterCreation { if (gargoyle) { - // EquipItem(m, new Boomerang()); + EquipItem(m, new Boomerang()); } break;