From a552cf4138ccd80956ca1ee405654c154d620f2d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 21 May 2024 19:44:42 -0700 Subject: [PATCH] fix: Fixes various memory leaks and spells (#1786) ### Summary - Fixes various memory leaks related to spells - Fixes Spell Plague - Added ability to determine if `sdi` should take effect for Spell Damage. - Fixes animal form timer ticking non-stop while logged out. --- .../UOContent/Accounting/AccountHandler.cs | 15 + Projects/UOContent/Misc/AOS.cs | 2 +- Projects/UOContent/Mobiles/BaseCreature.cs | 6 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 11 +- .../Network/Packets/IncomingAccountPackets.cs | 6 +- Projects/UOContent/Spells/Base/SpecialMove.cs | 3 +- Projects/UOContent/Spells/Base/Spell.cs | 28 +- Projects/UOContent/Spells/Base/SpellHelper.cs | 1 + .../UOContent/Spells/Fifth/MagicReflect.cs | 30 +- .../UOContent/Spells/First/ReactiveArmor.cs | 28 +- .../UOContent/Spells/Fourth/ArchProtection.cs | 18 +- Projects/UOContent/Spells/Fourth/ManaDrain.cs | 2 +- .../Spells/Mysticism/AnimatedWeaponSpell.cs | 113 +- .../Spells/Mysticism/BombardSpell.cs | 47 +- .../Spells/Mysticism/CleansingWindsSpell.cs | 355 +++--- .../Spells/Mysticism/EagleStrikeSpell.cs | 121 +- .../Spells/Mysticism/HailStormSpell.cs | 203 ++-- .../UOContent/Spells/Mysticism/MysticSpell.cs | 225 ++-- .../Spells/Mysticism/NetherCycloneSpell.cs | 219 ++-- .../Spells/Mysticism/SpellPlagueSpell.cs | 330 +++--- .../Spells/Necromancy/AnimateDeadSpell.cs | 754 ++++++------ .../Spells/Necromancy/BloodOathSpell.cs | 263 +++-- .../UOContent/Spells/Necromancy/CorpseSkin.cs | 215 ++-- .../Spells/Necromancy/CurseWeapon.cs | 125 +- .../UOContent/Spells/Necromancy/EvilOmen.cs | 161 ++- .../UOContent/Spells/Necromancy/Exorcism.cs | 391 ++++--- .../Spells/Necromancy/HorrificBeast.cs | 71 +- .../UOContent/Spells/Necromancy/LichForm.cs | 79 +- .../UOContent/Spells/Necromancy/MindRot.cs | 241 ++-- .../Spells/Necromancy/NecromancerSpell.cs | 83 +- .../UOContent/Spells/Necromancy/PainSpike.cs | 175 ++- .../Spells/Necromancy/PoisonStrike.cs | 255 ++-- .../UOContent/Spells/Necromancy/Strangle.cs | 395 ++++--- .../Spells/Necromancy/SummonFamiliar.cs | 341 +++--- .../Spells/Necromancy/TransformationSpell.cs | 77 +- .../Spells/Necromancy/VampiricEmbrace.cs | 117 +- .../Spells/Necromancy/VengefulSpirit.cs | 131 ++- .../UOContent/Spells/Necromancy/Wither.cs | 203 ++-- .../UOContent/Spells/Necromancy/WraithForm.cs | 115 +- .../UOContent/Spells/Ninjitsu/AnimalForm.cs | 1027 ++++++++--------- .../UOContent/Spells/Ninjitsu/Backstab.cs | 127 +- .../UOContent/Spells/Ninjitsu/DeathStrike.cs | 301 +++-- .../UOContent/Spells/Ninjitsu/FocusAttack.cs | 103 +- .../UOContent/Spells/Ninjitsu/KiAttack.cs | 187 ++- .../UOContent/Spells/Ninjitsu/MirrorImage.cs | 22 +- .../UOContent/Spells/Ninjitsu/NinjaMove.cs | 19 +- .../UOContent/Spells/Ninjitsu/NinjaSpell.cs | 185 ++- .../UOContent/Spells/Ninjitsu/ShadowJump.cs | 213 ++-- .../Spells/Ninjitsu/SurpriseAttack.cs | 205 ++-- .../UOContent/Spells/Second/Protection.cs | 4 +- Projects/UOContent/Spells/UnsummonTimer.cs | 4 +- 51 files changed, 4164 insertions(+), 4188 deletions(-) diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 3a0a058c3..1dc33cf6b 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -9,6 +9,12 @@ using Server.Engines.Virtues; using Server.Logging; using Server.Network; using Server.Regions; +using Server.Spells.Fifth; +using Server.Spells.First; +using Server.Spells.Mysticism; +using Server.Spells.Necromancy; +using Server.Spells.Ninjitsu; +using Server.Spells.Second; namespace Server.Misc; @@ -235,6 +241,15 @@ public static class AccountHandler PlayerMurderSystem.OnPlayerDeleted(m); ChampionTitleSystem.OnPlayerDeleted(m); + // Spells + MagicReflectSpell.EndReflect(m); + ReactiveArmorSpell.EndArmor(m); + ProtectionSpell.EndProtection(m); + StoneFormSpell.RemoveEffects(m); + AnimateDeadSpell.RemoveEffects(m); + SummonFamiliarSpell.RemoveEffects(m); + AnimalForm.RemoveLastAnimalForm(m); + state.SendCharacterListUpdate(acct); return; } diff --git a/Projects/UOContent/Misc/AOS.cs b/Projects/UOContent/Misc/AOS.cs index e85688839..3a078d24e 100644 --- a/Projects/UOContent/Misc/AOS.cs +++ b/Projects/UOContent/Misc/AOS.cs @@ -1216,7 +1216,7 @@ namespace Server public override string ToString() => "..."; - public void CheckCancelMorph(Mobile m) + public static void CheckCancelMorph(Mobile m) { if (m == null) { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 3213f896d..643a7d34f 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2395,13 +2395,17 @@ namespace Server.Mobiles AnimateDeadSpell.Unregister(m_SummonMaster, this); } + if (Summoned && SummonMaster != null) + { + SummonFamiliarSpell.Unregister(SummonMaster, this); + } + if (MLQuestSystem.Enabled) { MLQuestSystem.HandleDeletion(this); } UnsummonTimer.StopTimer(this); - StaminaSystem.RemoveEntry(this as IHasSteps); base.OnAfterDelete(); diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 437f73402..5a59e55af 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -1216,7 +1216,7 @@ namespace Server.Mobiles } } - public static void OnLogin(Mobile from) + public static void OnLogin(PlayerMobile from) { if (AccountHandler.LockdownLevel > AccessLevel.Player) { @@ -1253,11 +1253,9 @@ namespace Server.Mobiles return; } - if (from is PlayerMobile mobile) - { - VirtueSystem.CheckAtrophies(mobile); - mobile.ClaimAutoStabledPets(); - } + VirtueSystem.CheckAtrophies(from); + from.ClaimAutoStabledPets(); + AnimalForm.GetContext(from)?.Timer.Start(); } private class ServerLockdownNoticeGump : StaticNoticeGump @@ -2561,6 +2559,7 @@ namespace Server.Mobiles PolymorphSpell.StopTimer(this); IncognitoSpell.StopTimer(this); DisguisePersistence.RemoveTimer(this); + AnimalForm.RemoveContext(this, true); EndAction(); EndAction(); diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index 630ed9c4b..a6a87ae80 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -341,7 +341,11 @@ public static class IncomingAccountPackets PlayerMurderSystem.OnLogin(m); AssistantHandler.OnLogin(m); VisibilityList.OnLogin(m); - PlayerMobile.OnLogin(m); + + if (m is PlayerMobile pm) + { + PlayerMobile.OnLogin(pm); + } Account.OnLogin(m); GiftGiving.OnLogin(m); PreventInaccess.OnLogin(m); diff --git a/Projects/UOContent/Spells/Base/SpecialMove.cs b/Projects/UOContent/Spells/Base/SpecialMove.cs index b4ff72370..c7dc2092e 100644 --- a/Projects/UOContent/Spells/Base/SpecialMove.cs +++ b/Projects/UOContent/Spells/Base/SpecialMove.cs @@ -290,8 +290,7 @@ namespace Server.Spells } } - private static SpecialMoveContext GetContext(Mobile m) => - _playersTable.TryGetValue(m, out var context) ? context : null; + private static SpecialMoveContext GetContext(Mobile m) => _playersTable.GetValueOrDefault(m); private class SpecialMoveTimer : Timer { diff --git a/Projects/UOContent/Spells/Base/Spell.cs b/Projects/UOContent/Spells/Base/Spell.cs index 8b03d34c0..ec04e9158 100644 --- a/Projects/UOContent/Spells/Base/Spell.cs +++ b/Projects/UOContent/Spells/Base/Spell.cs @@ -194,7 +194,10 @@ namespace Server.Spells (m as BaseCreature)?.OnHarmfulSpell(Caster); } - public virtual int GetNewAosDamage(int bonus, int dice, int sides, Mobile singleTarget) + public int GetNewAosDamage(int bonus, int dice, int sides, Mobile singleTarget) => + GetNewAosDamage(bonus, dice, sides, true, singleTarget); + + public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool sdi = true, Mobile singleTarget = null) { if (singleTarget != null) { @@ -203,17 +206,15 @@ namespace Server.Spells dice, sides, Caster.Player && singleTarget.Player, + sdi, GetDamageScalar(singleTarget) ); } - return GetNewAosDamage(bonus, dice, sides, false); + return GetNewAosDamage(bonus, dice, sides, sdi, false); } - public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer) => - GetNewAosDamage(bonus, dice, sides, playerVsPlayer, 1.0); - - public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer, double scalar) + public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer, bool sdi, double scalar = 1.0) { var damage = Utility.Dice(dice, sides, bonus) * 100; @@ -224,14 +225,17 @@ namespace Server.Spells var intBonus = Caster.Int / 10; damageBonus += intBonus; - var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage); - // PvP spell damage increase cap of 15% from an item's magic property - if (playerVsPlayer && sdiBonus > 15) + if (sdi) { - sdiBonus = 15; - } + var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage); + // PvP spell damage increase cap of 15% from an item's magic property + if (playerVsPlayer && sdiBonus > 15) + { + sdiBonus = 15; + } - damageBonus += sdiBonus; + damageBonus += sdiBonus; + } var context = TransformationSpellHelper.GetContext(Caster); diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index 09a6e452e..76c9acd4b 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -1022,6 +1022,7 @@ namespace Server.Spells StaminaSystem.DFA = dfa; var damageGiven = AOS.Damage(target, from, dmg, phys, fire, cold, pois, nrgy, chaos); + Mysticism.SpellPlagueSpell.OnMobileDamaged(target); StaminaSystem.DFA = DFAlgorithm.Standard; diff --git a/Projects/UOContent/Spells/Fifth/MagicReflect.cs b/Projects/UOContent/Spells/Fifth/MagicReflect.cs index 970da9101..17ae14a83 100644 --- a/Projects/UOContent/Spells/Fifth/MagicReflect.cs +++ b/Projects/UOContent/Spells/Fifth/MagicReflect.cs @@ -58,47 +58,45 @@ namespace Server.Spells.Fifth if (CheckSequence()) { - var targ = Caster; - - if (_table.Remove(targ, out var mods)) + if (_table.Remove(Caster, out var mods)) { - targ.PlaySound(0x1ED); - targ.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); + Caster.PlaySound(0x1ED); + Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); for (var i = 0; i < mods.Length; ++i) { - targ.RemoveResistanceMod(mods[i]); + Caster.RemoveResistanceMod(mods[i]); } - BuffInfo.RemoveBuff(targ, BuffIcon.MagicReflection); + BuffInfo.RemoveBuff(Caster, BuffIcon.MagicReflection); } else { - targ.PlaySound(0x1E9); - targ.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); + Caster.PlaySound(0x1E9); + Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); - var physiMod = -25 + (int)(targ.Skills.Inscribe.Value / 20); + var physiMod = -25 + (int)(Caster.Skills.Inscribe.Value / 20); const int otherMod = 10; - mods = new[] - { + mods = + [ new ResistanceMod(ResistanceType.Physical, "PhysicalResistMagicResist", physiMod), new ResistanceMod(ResistanceType.Fire, "FireResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Cold, "ColdResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Poison, "PoisonResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Energy, "EnergyResistMagicResist", otherMod) - }; + ]; - _table[targ] = mods; + _table[Caster] = mods; for (var i = 0; i < mods.Length; ++i) { - targ.AddResistanceMod(mods[i]); + Caster.AddResistanceMod(mods[i]); } var buffFormat = $"{physiMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}"; - BuffInfo.AddBuff(targ, new BuffInfo(BuffIcon.MagicReflection, 1075817, buffFormat, true)); + BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.MagicReflection, 1075817, buffFormat, true)); } } diff --git a/Projects/UOContent/Spells/First/ReactiveArmor.cs b/Projects/UOContent/Spells/First/ReactiveArmor.cs index eee49802a..6185034a4 100644 --- a/Projects/UOContent/Spells/First/ReactiveArmor.cs +++ b/Projects/UOContent/Spells/First/ReactiveArmor.cs @@ -60,46 +60,44 @@ namespace Server.Spells.First if (CheckSequence()) { - var targ = Caster; - - if (_table.Remove(targ, out var mods)) + if (_table.Remove(Caster, out var mods)) { - targ.PlaySound(0x1ED); - targ.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); + Caster.PlaySound(0x1ED); + Caster.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); for (var i = 0; i < mods.Length; ++i) { - targ.RemoveResistanceMod(mods[i]); + Caster.RemoveResistanceMod(mods[i]); } BuffInfo.RemoveBuff(Caster, BuffIcon.ReactiveArmor); } else { - targ.PlaySound(0x1E9); - targ.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); + Caster.PlaySound(0x1E9); + Caster.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); - mods = new[] - { + mods = + [ new ResistanceMod( ResistanceType.Physical, "PhysicalResistReactiveArmorSpell", - 15 + (int)(targ.Skills.Inscribe.Value / 20) + 15 + (int)(Caster.Skills.Inscribe.Value / 20) ), new ResistanceMod(ResistanceType.Fire, "FireResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Cold, "ColdResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Poison, "PoisonResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Energy, "EnergyResistReactiveArmorSpell", -5) - }; + ]; - _table[targ] = mods; + _table[Caster] = mods; for (var i = 0; i < mods.Length; ++i) { - targ.AddResistanceMod(mods[i]); + Caster.AddResistanceMod(mods[i]); } - var physresist = 15 + (int)(targ.Skills.Inscribe.Value / 20); + var physresist = 15 + (int)(Caster.Skills.Inscribe.Value / 20); var args = $"{physresist}\t{5}\t{5}\t{5}\t{5}"; BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.ReactiveArmor, 1075812, 1075813, args)); diff --git a/Projects/UOContent/Spells/Fourth/ArchProtection.cs b/Projects/UOContent/Spells/Fourth/ArchProtection.cs index bcb7c293b..38247f2dc 100644 --- a/Projects/UOContent/Spells/Fourth/ArchProtection.cs +++ b/Projects/UOContent/Spells/Fourth/ArchProtection.cs @@ -117,24 +117,16 @@ namespace Server.Spells.Fourth private class InternalTimer : Timer { - private readonly Mobile m_Owner; + private readonly Mobile _owner; - public InternalTimer(Mobile target, Mobile caster) : base(TimeSpan.FromSeconds(0)) - { - var time = caster.Skills.Magery.Value * 1.2; - if (time > 144) - { - time = 144; - } + public InternalTimer(Mobile target, Mobile caster) : base(GetDelay(caster)) => _owner = target; - Delay = TimeSpan.FromSeconds(time); - - m_Owner = target; - } + private static TimeSpan GetDelay(Mobile caster) => + TimeSpan.FromSeconds(Math.Min(144, caster.Skills.Magery.Value * 1.2)); protected override void OnTick() { - RemoveEntry(m_Owner); + RemoveEntry(_owner); } } } diff --git a/Projects/UOContent/Spells/Fourth/ManaDrain.cs b/Projects/UOContent/Spells/Fourth/ManaDrain.cs index ad5d82f37..05c48c197 100644 --- a/Projects/UOContent/Spells/Fourth/ManaDrain.cs +++ b/Projects/UOContent/Spells/Fourth/ManaDrain.cs @@ -82,7 +82,7 @@ namespace Server.Spells.Fourth Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); } - private void AosDelay_Callback(Mobile m, int mana) + private static void AosDelay_Callback(Mobile m, int mana) { if (m.Alive && !m.IsDeadBondedPet) { diff --git a/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs b/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs index b54b9479b..5d39f1bfc 100644 --- a/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/AnimatedWeaponSpell.cs @@ -1,63 +1,62 @@ using System; using Server.Mobiles; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class AnimatedWeaponSpell : MysticSpell, ISpellTargetingPoint3D { - public class AnimatedWeaponSpell : MysticSpell, ISpellTargetingPoint3D + private static readonly SpellInfo _info = new( + "Animated Weapon", + "In Jux Por Ylem", + -1, + 9002, + Reagent.Bone, + Reagent.BlackPearl, + Reagent.MandrakeRoot, + Reagent.Nightshade + ); + + public AnimatedWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Animated Weapon", - "In Jux Por Ylem", - -1, - 9002, - Reagent.Bone, - Reagent.BlackPearl, - Reagent.MandrakeRoot, - Reagent.Nightshade - ); - - public AnimatedWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override SpellCircle Circle => SpellCircle.Fourth; - - public void Target(IPoint3D p) - { - if (Caster.Followers + 4 > Caster.FollowersMax) - { - Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature. - return; - } - - var map = Caster.Map; - - SpellHelper.GetSurfaceTop(ref p); - - if (map == null || Caster.Player && !map.CanSpawnMobile(p.X, p.Y, p.Z)) - { - Caster.SendLocalizedMessage(501942); // That location is blocked. - } - else if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) - { - var level = (int)((GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 2.0); - - var duration = TimeSpan.FromSeconds(10 + level); - - var summon = new AnimatedWeapon(Caster, level); - BaseCreature.Summon(summon, false, Caster, new Point3D(p), 0x212, duration); - - summon.PlaySound(0x64A); - - Effects.SendTargetParticles(summon, 0x3728, 10, 10, 0x13AA, (EffectLayer)255); - } - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetPoint3D(this); - } } -} + + public override SpellCircle Circle => SpellCircle.Fourth; + + public void Target(IPoint3D p) + { + if (Caster.Followers + 4 > Caster.FollowersMax) + { + Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature. + return; + } + + var map = Caster.Map; + + SpellHelper.GetSurfaceTop(ref p); + + if (map == null || Caster.Player && !map.CanSpawnMobile(p.X, p.Y, p.Z)) + { + Caster.SendLocalizedMessage(501942); // That location is blocked. + } + else if (SpellHelper.CheckTown(p, Caster) && CheckSequence()) + { + var level = (int)((GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 2.0); + + var duration = TimeSpan.FromSeconds(10 + level); + + var summon = new AnimatedWeapon(Caster, level); + BaseCreature.Summon(summon, false, Caster, new Point3D(p), 0x212, duration); + + summon.PlaySound(0x64A); + + Effects.SendTargetParticles(summon, 0x3728, 10, 10, 0x13AA, (EffectLayer)255); + } + + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetPoint3D(this); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/BombardSpell.cs b/Projects/UOContent/Spells/Mysticism/BombardSpell.cs index 54e44f30f..89b8d45fc 100644 --- a/Projects/UOContent/Spells/Mysticism/BombardSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/BombardSpell.cs @@ -1,36 +1,36 @@ using Server.Targeting; using System; -namespace Server.Spells.Mysticism -{ - public class BombardSpell : MysticSpell, ISpellTargetingMobile - { - private static readonly SpellInfo _info = new( - "Bombard", "Corp Por Ylem", - 230, - 9022, - Reagent.Bloodmoss, - Reagent.Garlic, - Reagent.SulfurousAsh, - Reagent.DragonsBlood - ); +namespace Server.Spells.Mysticism; - public BombardSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) - { +public class BombardSpell : MysticSpell, ISpellTargetingMobile +{ + private static readonly SpellInfo _info = new( + "Bombard", "Corp Por Ylem", + 230, + 9022, + Reagent.Bloodmoss, + Reagent.Garlic, + Reagent.SulfurousAsh, + Reagent.DragonsBlood + ); + + public BombardSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) + { } - public override SpellCircle Circle => SpellCircle.Sixth; - public override bool DelayedDamage => true; + public override SpellCircle Circle => SpellCircle.Sixth; + public override bool DelayedDamage => true; - public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; + public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; - public override void OnCast() - { + public override void OnCast() + { Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful); } - public void Target(Mobile m) - { + public void Target(Mobile m) + { if (CheckHSequence(m)) { SpellHelper.Turn(Caster, m); @@ -85,5 +85,4 @@ namespace Server.Spells.Mysticism FinishSequence(); } - } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs b/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs index 0d2074b8c..b888bc6fb 100644 --- a/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs @@ -5,212 +5,211 @@ using Server.Spells.Necromancy; using Server.Targeting; using Server.Collections; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class CleansingWindsSpell : MysticSpell, ISpellTargetingMobile { - public class CleansingWindsSpell : MysticSpell, ISpellTargetingMobile + public override SpellCircle Circle => SpellCircle.Sixth; + + private static readonly SpellInfo _info = new( + "Cleansing Winds", "In Vas Mani Hur", + 230, + 9022, + Reagent.Garlic, + Reagent.Ginseng, + Reagent.MandrakeRoot, + Reagent.DragonsBlood + ); + + public CleansingWindsSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) { - public override SpellCircle Circle => SpellCircle.Sixth; + } - private static readonly SpellInfo _info = new( - "Cleansing Winds", "In Vas Mani Hur", - 230, - 9022, - Reagent.Garlic, - Reagent.Ginseng, - Reagent.MandrakeRoot, - Reagent.DragonsBlood - ); + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Beneficial); + } - public CleansingWindsSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) + public void Target(Mobile m) + { + if (CheckBSequence(m)) { - } + /** + * Soothing winds attempt to neutralize poisons, lift curses, + * and heal a valid target and up to 3 party members. + */ - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Beneficial); - } + Caster.PlaySound(0x64C); - public void Target(Mobile m) - { - if (CheckBSequence(m)) + var primarySkill = GetBaseSkill(Caster); + var secondarySkill = GetDamageSkill(Caster); + var cureChance = 10000 + (int)((primarySkill + secondarySkill) / 2 * 75); + + using var pool = PooledRefQueue.Create(); + pool.Enqueue(m); + + var casterParty = Party.Get(Caster); + if (casterParty != null) { - /** - * Soothing winds attempt to neutralize poisons, lift curses, - * and heal a valid target and up to 3 party members. - */ - - Caster.PlaySound(0x64C); - - var primarySkill = GetBaseSkill(Caster); - var secondarySkill = GetDamageSkill(Caster); - var cureChance = 10000 + (int)((primarySkill + secondarySkill) / 2 * 75); - - using var pool = PooledRefQueue.Create(); - pool.Enqueue(m); - - var casterParty = Party.Get(Caster); - if (casterParty != null) + foreach (Mobile mob in Caster.Map.GetMobilesInRange(m.Location, 2)) { - foreach (Mobile mob in Caster.Map.GetMobilesInRange(m.Location, 2)) + if (mob != m && casterParty.Contains(mob) && Caster.CanBeBeneficial(mob, false)) { - if (mob != m && casterParty.Contains(mob) && Caster.CanBeBeneficial(mob, false)) + pool.Enqueue(mob); + if (pool.Count == 4) { - pool.Enqueue(mob); - if (pool.Count == 4) - { - break; - } + break; } } } + } - var toHeal = ((int)((primarySkill + secondarySkill) / 4.0) + Utility.RandomMinMax(-3, 3)) / pool.Count; + var toHeal = ((int)((primarySkill + secondarySkill) / 4.0) + Utility.RandomMinMax(-3, 3)) / pool.Count; - while (pool.Count > 0) + while (pool.Count > 0) + { + var target = pool.Dequeue(); + + Caster.DoBeneficial(target); + + target.FixedParticles(0x3709, 1, 30, 9963, 13, 3, EffectLayer.Head); + + var from = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z - 10), target.Map); + var to = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z + 50), target.Map); + Effects.SendMovingParticles( + from, + to, + 0x2255, + 1, + 0, + false, + false, + 13, + 3, + 9501, + 1, + 0, + EffectLayer.Head, + 0x100 + ); + + var toHealMod = toHeal; + + if (target.Poisoned) { - var target = pool.Dequeue(); + var poisonLevel = target.Poison.Level + 1; + var chanceToCure = cureChance - poisonLevel * 1750; - Caster.DoBeneficial(target); - - target.FixedParticles(0x3709, 1, 30, 9963, 13, 3, EffectLayer.Head); - - var from = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z - 10), target.Map); - var to = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z + 50), target.Map); - Effects.SendMovingParticles( - from, - to, - 0x2255, - 1, - 0, - false, - false, - 13, - 3, - 9501, - 1, - 0, - EffectLayer.Head, - 0x100 - ); - - var toHealMod = toHeal; - - if (target.Poisoned) + if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster)) { - var poisonLevel = target.Poison.Level + 1; - var chanceToCure = cureChance - poisonLevel * 1750; - - if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster)) - { - toHealMod -= (int)(toHeal * poisonLevel * 0.15); - } - else - { - toHealMod = 0; - } + toHealMod -= (int)(toHeal * poisonLevel * 0.15); } - - if (MortalStrike.IsWounded(target)) + else { toHealMod = 0; } + } - var curseLevel = RemoveCurses(target); + if (MortalStrike.IsWounded(target)) + { + toHealMod = 0; + } - if (toHealMod > 0 && curseLevel > 0) - { - toHealMod -= curseLevel * 3; - toHealMod -= (int)(toHealMod * (curseLevel / 100.0)); - } + var curseLevel = RemoveCurses(target); - if (toHealMod > 0) - { - SpellHelper.Heal(toHealMod, target, Caster); - } + if (toHealMod > 0 && curseLevel > 0) + { + toHealMod -= curseLevel * 3; + toHealMod -= (int)(toHealMod * (curseLevel / 100.0)); + } + + if (toHealMod > 0) + { + SpellHelper.Heal(toHealMod, target, Caster); } } - - FinishSequence(); } - public static int RemoveCurses(Mobile m) - { - var curseLevel = 0; - - // if (SleepSpell.EndSleep(m)) - // { - // curseLevel += 2; - // } - - if (EvilOmenSpell.EndEffect(m)) - { - curseLevel += 1; - } - - if (StrangleSpell.RemoveCurse(m)) - { - curseLevel += 2; - } - - if (CorpseSkinSpell.RemoveCurse(m)) - { - curseLevel += 3; - } - - if (CurseSpell.RemoveEffect(m)) - { - curseLevel += 4; - } - - if (BloodOathSpell.RemoveCurse(m)) - { - curseLevel += 3; - } - - if (MindRotSpell.ClearMindRotScalar(m)) - { - curseLevel += 2; - } - - if (SpellPlagueSpell.RemoveEffect(m)) - { - curseLevel += 4; - } - - var mod = m.GetStatMod("[Magic] Str Curse"); - if (mod?.Offset < 0) - { - m.RemoveStatMod("[Magic] Str Curse"); - } - - mod = m.GetStatMod("[Magic] Dex Curse"); - if (mod?.Offset < 0) - { - m.RemoveStatMod("[Magic] Dex Curse"); - } - - mod = m.GetStatMod("[Magic] Int Curse"); - if (mod?.Offset < 0) - { - m.RemoveStatMod("[Magic] Int Curse"); - } - - if (MortalStrike.EndWound(m)) - { - curseLevel += 2; - } - - BuffInfo.RemoveBuff(m, BuffIcon.Clumsy); - BuffInfo.RemoveBuff(m, BuffIcon.FeebleMind); - BuffInfo.RemoveBuff(m, BuffIcon.Weaken); - BuffInfo.RemoveBuff(m, BuffIcon.Curse); - BuffInfo.RemoveBuff(m, BuffIcon.MassCurse); - BuffInfo.RemoveBuff(m, BuffIcon.MortalStrike); - BuffInfo.RemoveBuff(m, BuffIcon.CorpseSkin); - BuffInfo.RemoveBuff(m, BuffIcon.Strangle); - BuffInfo.RemoveBuff(m, BuffIcon.EvilOmen); - - return curseLevel; - } + FinishSequence(); } -} + + public static int RemoveCurses(Mobile m) + { + var curseLevel = 0; + + // if (SleepSpell.EndSleep(m)) + // { + // curseLevel += 2; + // } + + if (EvilOmenSpell.EndEffect(m)) + { + curseLevel += 1; + } + + if (StrangleSpell.RemoveCurse(m)) + { + curseLevel += 2; + } + + if (CorpseSkinSpell.RemoveCurse(m)) + { + curseLevel += 3; + } + + if (CurseSpell.RemoveEffect(m)) + { + curseLevel += 4; + } + + if (BloodOathSpell.RemoveCurse(m)) + { + curseLevel += 3; + } + + if (MindRotSpell.ClearMindRotScalar(m)) + { + curseLevel += 2; + } + + if (SpellPlagueSpell.RemoveEffect(m)) + { + curseLevel += 4; + } + + var mod = m.GetStatMod("[Magic] Str Curse"); + if (mod?.Offset < 0) + { + m.RemoveStatMod("[Magic] Str Curse"); + } + + mod = m.GetStatMod("[Magic] Dex Curse"); + if (mod?.Offset < 0) + { + m.RemoveStatMod("[Magic] Dex Curse"); + } + + mod = m.GetStatMod("[Magic] Int Curse"); + if (mod?.Offset < 0) + { + m.RemoveStatMod("[Magic] Int Curse"); + } + + if (MortalStrike.EndWound(m)) + { + curseLevel += 2; + } + + BuffInfo.RemoveBuff(m, BuffIcon.Clumsy); + BuffInfo.RemoveBuff(m, BuffIcon.FeebleMind); + BuffInfo.RemoveBuff(m, BuffIcon.Weaken); + BuffInfo.RemoveBuff(m, BuffIcon.Curse); + BuffInfo.RemoveBuff(m, BuffIcon.MassCurse); + BuffInfo.RemoveBuff(m, BuffIcon.MortalStrike); + BuffInfo.RemoveBuff(m, BuffIcon.CorpseSkin); + BuffInfo.RemoveBuff(m, BuffIcon.Strangle); + BuffInfo.RemoveBuff(m, BuffIcon.EvilOmen); + + return curseLevel; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs b/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs index c8cfd430d..054e784f1 100644 --- a/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/EagleStrikeSpell.cs @@ -1,77 +1,76 @@ using System; using Server.Targeting; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class EagleStrikeSpell : MysticSpell, ISpellTargetingMobile { - public class EagleStrikeSpell : MysticSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Eagle Strike", + "Kal Por Xen", + -1, + 9002, + Reagent.Bloodmoss, + Reagent.Bone, + Reagent.SpidersSilk, + Reagent.MandrakeRoot + ); + + public EagleStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Eagle Strike", - "Kal Por Xen", - -1, - 9002, - Reagent.Bloodmoss, - Reagent.Bone, - Reagent.SpidersSilk, - Reagent.MandrakeRoot - ); + } - public EagleStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override SpellCircle Circle => SpellCircle.Third; + + public void Target(Mobile m) + { + if (CheckHSequence(m)) { - } + SpellHelper.Turn(Caster, m); - public override SpellCircle Circle => SpellCircle.Third; - - public void Target(Mobile m) - { - if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); - - if (Core.SA && HasDelayedDamageContext(m)) - { - DoHurtFizzle(); - return; - } - - var source = Caster; - - if (SpellHelper.CheckReflect(2, ref source, ref m)) - { - Timer.StartTimer(TimeSpan.FromSeconds(0.5), () => - { - /* Conjures a magical eagle that assaults the Target with its talons, dealing energy damage. */ - source.MovingEffect(m, 0x407A, 8, 1, false, true, 0, 0); - source.PlaySound(0x2EE); - }); - } - - Caster.MovingParticles(m, 0x407A, 7, 0, false, true, 0, 0, 0xBBE, 0xFA6, 0xFFFF, 0); - Caster.PlaySound(0x2EE); - - Timer.StartTimer(TimeSpan.FromSeconds(1.0), () => Damage(m)); - } - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful); - } - - private void Damage(Mobile to) - { - if (to == null) + if (Core.SA && HasDelayedDamageContext(m)) { + DoHurtFizzle(); return; } - double damage = GetNewAosDamage(19, 1, 5, to); + var source = Caster; - SpellHelper.Damage(this, to, damage, 0, 0, 0, 0, 100); + if (SpellHelper.CheckReflect(2, ref source, ref m)) + { + Timer.StartTimer(TimeSpan.FromSeconds(0.5), () => + { + /* Conjures a magical eagle that assaults the Target with its talons, dealing energy damage. */ + source.MovingEffect(m, 0x407A, 8, 1, false, true, 0, 0); + source.PlaySound(0x2EE); + }); + } - to.PlaySound(0x64D); + Caster.MovingParticles(m, 0x407A, 7, 0, false, true, 0, 0, 0xBBE, 0xFA6, 0xFFFF, 0); + Caster.PlaySound(0x2EE); + + Timer.StartTimer(TimeSpan.FromSeconds(1.0), () => Damage(m)); } + + FinishSequence(); } -} + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful); + } + + private void Damage(Mobile to) + { + if (to == null) + { + return; + } + + double damage = GetNewAosDamage(19, 1, 5, to); + + SpellHelper.Damage(this, to, damage, 0, 0, 0, 0, 100); + + to.PlaySound(0x64D); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs b/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs index e5d061ac7..bd89e9f44 100644 --- a/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/HailStormSpell.cs @@ -1,126 +1,125 @@ using Server.Collections; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class HailStormSpell : MysticSpell, ISpellTargetingPoint3D { - public class HailStormSpell : MysticSpell, ISpellTargetingPoint3D + private static readonly SpellInfo _info = new( + "Hail Storm", + "Kal Des Ylem", + -1, + 9002, + Reagent.DragonsBlood, + Reagent.Bloodmoss, + Reagent.BlackPearl, + Reagent.MandrakeRoot + ); + + public HailStormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Hail Storm", - "Kal Des Ylem", - -1, - 9002, - Reagent.DragonsBlood, - Reagent.Bloodmoss, - Reagent.BlackPearl, - Reagent.MandrakeRoot - ); + } - public HailStormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override SpellCircle Circle => SpellCircle.Seventh; + public override int GetMana() => 50; + + public void Target(IPoint3D p) + { + var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p); + + if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) { - } + /* Summons a storm of hailstones that strikes all Targets within a radius around the Target's Location, + * dealing cold damage. + */ - public override SpellCircle Circle => SpellCircle.Seventh; - public override int GetMana() => 50; + SpellHelper.Turn(Caster, p); - public void Target(IPoint3D p) - { - var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p); + var map = Caster.Map; - if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) + if (map != null) { - /* Summons a storm of hailstones that strikes all Targets within a radius around the Target's Location, - * dealing cold damage. - */ + using var pool = PooledRefQueue.Create(); + var pvp = false; - SpellHelper.Turn(Caster, p); + PlayEffect(loc, Caster.Map); - var map = Caster.Map; - - if (map != null) + foreach (var m in map.GetMobilesInRange(loc, 2)) { - using var pool = PooledRefQueue.Create(); - var pvp = false; - - PlayEffect(loc, Caster.Map); - - foreach (var m in map.GetMobilesInRange(loc, 2)) + if (m == Caster || !SpellHelper.ValidIndirectTarget(Caster, m) || !Caster.CanBeHarmful(m, false) + || !Caster.CanSee(m) || !Caster.InLOS(m)) { - if (m == Caster || !SpellHelper.ValidIndirectTarget(Caster, m) || !Caster.CanBeHarmful(m, false) - || !Caster.CanSee(m) || !Caster.InLOS(m)) - { - continue; - } - - pool.Enqueue(m); - - if (m.Player) - { - pvp = true; - } + continue; } - double damage = GetNewAosDamage(51, 1, 5, pvp); + pool.Enqueue(m); - while (pool.Count > 0) + if (m.Player) { - var m = pool.Dequeue(); - Caster.DoHarmful(m); - SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0); + pvp = true; } } + + double damage = GetNewAosDamage(51, 1, 5, pvp); + + while (pool.Count > 0) + { + var m = pool.Dequeue(); + Caster.DoHarmful(m); + SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0); + } } - - FinishSequence(); } - public override void OnCast() - { - Caster.Target = new SpellTargetPoint3D(this); - } - - private static void PlayEffect(Point3D p, Map map) - { - Effects.PlaySound(p, map, 0x64F); - - PlaySingleEffect(p, map, -1, 1, -1, 1); - PlaySingleEffect(p, map, -2, 0, -3, -1); - PlaySingleEffect(p, map, -3, -1, -1, 1); - PlaySingleEffect(p, map, 1, 3, -1, 1); - PlaySingleEffect(p, map, -1, 1, 1, 3); - } - - private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d) - { - var x = p.X; - var y = p.Y; - var z = p.Z + 18; - - SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z)); - - SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z)); - } - - private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest) - { - Effects.SendMovingEffect( - p, - map, - 0x36D4, - orig, - dest, - 0, - 0, - false, - false, - 0x63, - 0x4 - ); - } + FinishSequence(); } -} + + public override void OnCast() + { + Caster.Target = new SpellTargetPoint3D(this); + } + + private static void PlayEffect(Point3D p, Map map) + { + Effects.PlaySound(p, map, 0x64F); + + PlaySingleEffect(p, map, -1, 1, -1, 1); + PlaySingleEffect(p, map, -2, 0, -3, -1); + PlaySingleEffect(p, map, -3, -1, -1, 1); + PlaySingleEffect(p, map, 1, 3, -1, 1); + PlaySingleEffect(p, map, -1, 1, 1, 3); + } + + private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d) + { + var x = p.X; + var y = p.Y; + var z = p.Z + 18; + + SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z)); + + SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z)); + } + + private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest) + { + Effects.SendMovingEffect( + p, + map, + 0x36D4, + orig, + dest, + 0, + 0, + false, + false, + 0x63, + 0x4 + ); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/MysticSpell.cs b/Projects/UOContent/Spells/Mysticism/MysticSpell.cs index 75317ea2f..68a8f54d4 100644 --- a/Projects/UOContent/Spells/Mysticism/MysticSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/MysticSpell.cs @@ -1,140 +1,139 @@ using System; using Server.Items; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public abstract class MysticSpell : Spell { - public abstract class MysticSpell : Spell + private static int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; + private static double[] _requiredSkill = { 0.0, 8.0, 20.0, 33.0, 45.0, 58.0, 70.0, 83.0 }; + + public MysticSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { - private static int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; - private static double[] _requiredSkill = { 0.0, 8.0, 20.0, 33.0, 45.0, 58.0, 70.0, 83.0 }; + } - public MysticSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) + public abstract SpellCircle Circle { get; } + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.5 + 0.25 * (int)Circle); + + public virtual double RequiredSkill + { + get { + var circle = (int)Circle; + + if (Scroll != null) + { + circle -= 2; + } + + return _requiredSkill[circle]; + } + } + + public override SkillName CastSkill => SkillName.Mysticism; + + /* + * As per OSI Publish 64: + * Imbuing is not the only skill associated with Mysticism now. + * Players can use EITHER their Focus skill or Imbuing skill. + * Evaluate Intelligence no longer has any effect on a Mystic’s spell power. + */ + public override double GetDamageSkill(Mobile m) => Math.Max(m.Skills.Imbuing.Value, m.Skills.Focus.Value); + + public override int GetDamageFixed(Mobile m) => Math.Max(m.Skills.Imbuing.Fixed, m.Skills.Focus.Fixed); + + public override void GetCastSkills(out double min, out double max) + { + var requiredSkill = RequiredSkill; + + // As per Mysticism page at the UO Herald Playguide + // This means that we have 25% success chance at min Required Skill + min = requiredSkill - 12.5; + max = requiredSkill + 37.5; + } + + public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle]; + + public override bool CheckCast() + { + if (!base.CheckCast()) + { + return false; } - public abstract SpellCircle Circle { get; } + var mana = ScaleMana(GetMana()); - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.5 + 0.25 * (int)Circle); - - public virtual double RequiredSkill + if (Caster.Mana < mana) { - get - { - var circle = (int)Circle; - - if (Scroll != null) - { - circle -= 2; - } - - return _requiredSkill[circle]; - } + // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. + Caster.SendLocalizedMessage(1060174, mana.ToString()); + return false; } - public override SkillName CastSkill => SkillName.Mysticism; + var requiredSkill = RequiredSkill; - /* - * As per OSI Publish 64: - * Imbuing is not the only skill associated with Mysticism now. - * Players can use EITHER their Focus skill or Imbuing skill. - * Evaluate Intelligence no longer has any effect on a Mystic’s spell power. - */ - public override double GetDamageSkill(Mobile m) => Math.Max(m.Skills.Imbuing.Value, m.Skills.Focus.Value); - - public override int GetDamageFixed(Mobile m) => Math.Max(m.Skills.Imbuing.Fixed, m.Skills.Focus.Fixed); - - public override void GetCastSkills(out double min, out double max) + if (Caster.Skills[CastSkill].Value < requiredSkill) { - var requiredSkill = RequiredSkill; - - // As per Mysticism page at the UO Herald Playguide - // This means that we have 25% success chance at min Required Skill - min = requiredSkill - 12.5; - max = requiredSkill + 37.5; + // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. + Caster.SendLocalizedMessage(1063013, $"{requiredSkill:F1}\t{CastSkill}\t "); + return false; } - public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle]; + return true; + } - public override bool CheckCast() + public override void OnBeginCast() + { + base.OnBeginCast(); + + SendCastEffect(); + } + + public virtual void SendCastEffect() + { + Caster.FixedEffect(0x37C4, 10, (int)(GetCastDelay().TotalSeconds * 28), 0x66C, 3); + } + + public static double GetBaseSkill(Mobile m) => m.Skills.Mysticism.Value; + + public virtual bool CheckResisted(Mobile target) + { + var n = GetResistPercent(target); + + n /= 100.0; + + if (n <= 0.0) { - if (!base.CheckCast()) - { - return false; - } - - var mana = ScaleMana(GetMana()); - - if (Caster.Mana < mana) - { - // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. - Caster.SendLocalizedMessage(1060174, mana.ToString()); - return false; - } - - var requiredSkill = RequiredSkill; - - if (Caster.Skills[CastSkill].Value < requiredSkill) - { - // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. - Caster.SendLocalizedMessage(1063013, $"{requiredSkill:F1}\t{CastSkill}\t "); - return false; - } + return false; + } + if (n >= 1.0) + { return true; } - public override void OnBeginCast() - { - base.OnBeginCast(); + var maxSkill = (1 + (int)Circle) * 10; + maxSkill += (1 + (int)Circle / 6) * 25; - SendCastEffect(); + if (target.Skills.MagicResist.Value < maxSkill) + { + target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap); } - public virtual void SendCastEffect() - { - Caster.FixedEffect(0x37C4, 10, (int)(GetCastDelay().TotalSeconds * 28), 0x66C, 3); - } - - public static double GetBaseSkill(Mobile m) => m.Skills.Mysticism.Value; - - public virtual bool CheckResisted(Mobile target) - { - var n = GetResistPercent(target); - - n /= 100.0; - - if (n <= 0.0) - { - return false; - } - - if (n >= 1.0) - { - return true; - } - - var maxSkill = (1 + (int)Circle) * 10; - maxSkill += (1 + (int)Circle / 6) * 25; - - if (target.Skills.MagicResist.Value < maxSkill) - { - target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap); - } - - return n >= Utility.RandomDouble(); - } - - public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle) - { - var magicResist = target.Skills.MagicResist.Value; - var firstPercent = magicResist / 5.0; - var secondPercent = magicResist - - ((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0); - - // Seems should be about half of what stratics says. - return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0; - } - - public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle); + return n >= Utility.RandomDouble(); } -} + + public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle) + { + var magicResist = target.Skills.MagicResist.Value; + var firstPercent = magicResist / 5.0; + var secondPercent = magicResist - + ((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0); + + // Seems should be about half of what stratics says. + return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0; + } + + public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle); +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs b/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs index 96802faef..1a25e6e9b 100644 --- a/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/NetherCycloneSpell.cs @@ -1,140 +1,139 @@ using Server.Collections; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class NetherCycloneSpell : MysticSpell, ISpellTargetingPoint3D { - public class NetherCycloneSpell : MysticSpell, ISpellTargetingPoint3D + private static readonly SpellInfo _info = new( + "Nether Cyclone", + "Grav Hur", + -1, + 9002, + Reagent.MandrakeRoot, + Reagent.Nightshade, + Reagent.SulfurousAsh, + Reagent.Bloodmoss + ); + + public NetherCycloneSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Nether Cyclone", - "Grav Hur", - -1, - 9002, - Reagent.MandrakeRoot, - Reagent.Nightshade, - Reagent.SulfurousAsh, - Reagent.Bloodmoss - ); + } - public NetherCycloneSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override SpellCircle Circle => SpellCircle.Eighth; + + public void Target(IPoint3D p) + { + var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p); + + if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) { - } + /* Summons a gale of lethal winds that strikes all Targets within a radius around + * the Target's Location, dealing chaos damage. In addition to inflicting damage, + * each Target of the Nether Cyclone temporarily loses a percentage of mana and + * stamina. The effectiveness of the Nether Cyclone is determined by a comparison + * between the Caster's Mysticism and either Focus or Imbuing (whichever is greater) + * skills and the Resisting Spells skill of the Target. + */ - public override SpellCircle Circle => SpellCircle.Eighth; + SpellHelper.Turn(Caster, p); - public void Target(IPoint3D p) - { - var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p); + var map = Caster.Map; - if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) + if (map != null) { - /* Summons a gale of lethal winds that strikes all Targets within a radius around - * the Target's Location, dealing chaos damage. In addition to inflicting damage, - * each Target of the Nether Cyclone temporarily loses a percentage of mana and - * stamina. The effectiveness of the Nether Cyclone is determined by a comparison - * between the Caster's Mysticism and either Focus or Imbuing (whichever is greater) - * skills and the Resisting Spells skill of the Target. - */ + using var pool = PooledRefQueue.Create(); + var pvp = false; - SpellHelper.Turn(Caster, p); + PlayEffect(loc, Caster.Map); - var map = Caster.Map; - - if (map != null) + foreach (var m in map.GetMobilesInRange(loc, 2)) { - using var pool = PooledRefQueue.Create(); - var pvp = false; - - PlayEffect(loc, Caster.Map); - - foreach (var m in map.GetMobilesInRange(loc, 2)) + if (m == Caster) { - if (m == Caster) + continue; + } + + if (SpellHelper.ValidIndirectTarget(Caster, m) && Caster.CanBeHarmful(m, false) && Caster.CanSee(m)) + { + if (!Caster.InLOS(m)) { continue; } - if (SpellHelper.ValidIndirectTarget(Caster, m) && Caster.CanBeHarmful(m, false) && Caster.CanSee(m)) + pool.Enqueue(m); + + if (m.Player) { - if (!Caster.InLOS(m)) - { - continue; - } - - pool.Enqueue(m); - - if (m.Player) - { - pvp = true; - } + pvp = true; } } + } - var damage = GetNewAosDamage(51, 1, 5, pvp); - var reduction = (GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 1200.0; + var damage = GetNewAosDamage(51, 1, 5, pvp); + var reduction = (GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 1200.0; - while (pool.Count > 0) - { - var m = pool.Dequeue(); - Caster.DoHarmful(m); - SpellHelper.Damage(this, m, damage, 0, 0, 0, 0, 0, 100); + while (pool.Count > 0) + { + var m = pool.Dequeue(); + Caster.DoHarmful(m); + SpellHelper.Damage(this, m, damage, 0, 0, 0, 0, 0, 100); - var resistedReduction = reduction - m.Skills.MagicResist.Value / 800.0; + var resistedReduction = reduction - m.Skills.MagicResist.Value / 800.0; - m.Stam -= (int)(m.StamMax * resistedReduction); - m.Mana -= (int)(m.ManaMax * resistedReduction); - } + m.Stam -= (int)(m.StamMax * resistedReduction); + m.Mana -= (int)(m.ManaMax * resistedReduction); } } - - FinishSequence(); } - public override void OnCast() - { - Caster.Target = new SpellTargetPoint3D(this); - } - - private static void PlayEffect(Point3D p, Map map) - { - Effects.PlaySound(p, map, 0x64F); - - PlaySingleEffect(p, map, -1, 1, -1, 1); - PlaySingleEffect(p, map, -2, 0, -3, -1); - PlaySingleEffect(p, map, -3, -1, -1, 1); - PlaySingleEffect(p, map, 1, 3, -1, 1); - PlaySingleEffect(p, map, -1, 1, 1, 3); - } - - private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d) - { - int x = p.X, y = p.Y, z = p.Z + 18; - - SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z)); - - SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z)); - SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z)); - } - - private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest) - { - Effects.SendMovingEffect( - p, - map, - 0x375A, - orig, - dest, - 0, - 0, - false, - false, - 0x49A, - 0x4 - ); - } + FinishSequence(); } -} + + public override void OnCast() + { + Caster.Target = new SpellTargetPoint3D(this); + } + + private static void PlayEffect(Point3D p, Map map) + { + Effects.PlaySound(p, map, 0x64F); + + PlaySingleEffect(p, map, -1, 1, -1, 1); + PlaySingleEffect(p, map, -2, 0, -3, -1); + PlaySingleEffect(p, map, -3, -1, -1, 1); + PlaySingleEffect(p, map, 1, 3, -1, 1); + PlaySingleEffect(p, map, -1, 1, 1, 3); + } + + private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d) + { + int x = p.X, y = p.Y, z = p.Z + 18; + + SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z)); + + SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z)); + SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z)); + } + + private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest) + { + Effects.SendMovingEffect( + p, + map, + 0x375A, + orig, + dest, + 0, + 0, + false, + false, + 0x49A, + 0x4 + ); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs b/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs index 07fd59598..1b8126d9b 100644 --- a/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/SpellPlagueSpell.cs @@ -2,219 +2,199 @@ using System; using System.Collections.Generic; using Server.Targeting; -namespace Server.Spells.Mysticism +namespace Server.Spells.Mysticism; + +public class SpellPlagueSpell : MysticSpell, ISpellTargetingMobile { - public class SpellPlagueSpell : MysticSpell + private static readonly SpellInfo _info = new( + "Spell Plague", + "Vas Rel Jux Ort", + -1, + 9002, + Reagent.DaemonBone, + Reagent.DragonsBlood, + Reagent.Nightshade, + Reagent.SulfurousAsh + ); + + private static readonly Dictionary _table = new(); + + public SpellPlagueSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Spell Plague", - "Vas Rel Jux Ort", - -1, - 9002, - Reagent.DaemonBone, - Reagent.DragonsBlood, - Reagent.Nightshade, - Reagent.SulfurousAsh - ); + } - private static readonly Dictionary _table = new(); + public override SpellCircle Circle => SpellCircle.Seventh; - public SpellPlagueSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public static void Initialize() + { + EventSink.PlayerDeath += OnPlayerDeath; + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful); + } + + public void Target(Mobile targeted) + { + if (CheckHSequence(targeted)) { - } + SpellHelper.Turn(Caster, targeted); - public override SpellCircle Circle => SpellCircle.Seventh; + SpellHelper.CheckReflect(6, Caster, ref targeted); - public static void Initialize() - { - EventSink.PlayerDeath += OnPlayerDeath; - } + /* The target is hit with an explosion of chaos damage and then inflicted + * with the spell plague curse. Each time the target is damaged while under + * the effect of the spell plague, they may suffer an explosion of chaos + * damage. The initial chance to trigger the explosion starts at 90% and + * reduces by 30% every time an explosion occurs. Once the target is + * afflicted by 3 explosions or 8 seconds have passed, that spell plague + * is removed from the target. Spell Plague will stack with other spell + * plagues so that they are applied one after the other. + */ - public override void OnCast() - { - Caster.Target = new InternalTarget(this); - } + VisualEffect(targeted); - public void Target(Mobile targeted) - { - if (CheckHSequence(targeted)) + // Before Time of Legends, SDI was not applied to initial damage. + var damage = GetNewAosDamage(33, 1, 5, Core.TOL, targeted); + SpellHelper.Damage(this, targeted, damage, 0, 0, 0, 0, 0); + + var timer = new SpellPlagueTimer(this, targeted); + + if (_table.TryGetValue(targeted, out var oldtimer)) { - SpellHelper.Turn(Caster, targeted); - - SpellHelper.CheckReflect(6, Caster, ref targeted); - - /* The target is hit with an explosion of chaos damage and then inflicted - * with the spell plague curse. Each time the target is damaged while under - * the effect of the spell plague, they may suffer an explosion of chaos - * damage. The initial chance to trigger the explosion starts at 90% and - * reduces by 30% every time an explosion occurs. Once the target is - * afflicted by 3 explosions or 8 seconds have passed, that spell plague - * is removed from the target. Spell Plague will stack with other spell - * plagues so that they are applied one after the other. - */ - - VisualEffect(targeted); - - var damage = GetNewAosDamage(33, 1, 5, targeted); - SpellHelper.Damage(this, targeted, damage, 0, 0, 0, 0, 0); - - var timer = new SpellPlagueTimer(this, targeted); - - if (_table.TryGetValue(targeted, out var oldtimer)) - { - oldtimer.SetNext(timer); - } - else - { - _table[targeted] = timer; - timer.StartPlague(); - } + oldtimer.SetNext(timer); } - - FinishSequence(); - } - - public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); - - public static bool RemoveEffect(Mobile m) - { - if (_table.Remove(m, out var context)) + else { - context.Stop(); - BuffInfo.RemoveBuff(m, BuffIcon.SpellPlague); - return true; - } - - return false; - } - - public static void CheckPlague(Mobile m) - { - if (_table.TryGetValue(m, out var context)) - { - context.OnDamage(); + _table[targeted] = timer; + timer.StartPlague(); } } - private static void OnPlayerDeath(Mobile m) + FinishSequence(); + } + + public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); + + public static bool RemoveEffect(Mobile m) + { + if (_table.Remove(m, out var timer)) { - RemoveEffect(m); + timer.Stop(); + BuffInfo.RemoveBuff(m, BuffIcon.SpellPlague); + return true; } - protected void VisualEffect(Mobile to) - { - to.PlaySound(0x658); + return false; + } - to.FixedParticles(0x3728, 1, 13, 0x26B8, 0x47E, 7, EffectLayer.Head, 0); - to.FixedParticles(0x3779, 1, 15, 0x251E, 0x43, 7, EffectLayer.Head, 0); + public static void OnMobileDamaged(Mobile m) + { + if (m != null && _table.TryGetValue(m, out var context)) + { + context.OnDamage(); + } + } + + private static void OnPlayerDeath(Mobile m) => RemoveEffect(m); + + private static void VisualEffect(Mobile to) + { + to.PlaySound(0x658); + + to.FixedParticles(0x3728, 1, 13, 0x26B8, 0x47E, 7, EffectLayer.Head, 0); + to.FixedParticles(0x3779, 1, 15, 0x251E, 0x43, 7, EffectLayer.Head, 0); + } + + private class SpellPlagueTimer : Timer + { + private readonly SpellPlagueSpell _owner; + private readonly Mobile _target; + private int _explosions; + private DateTime _nextExplosion; + private SpellPlagueTimer _next; + + public SpellPlagueTimer(SpellPlagueSpell owner, Mobile target) : base(TimeSpan.FromSeconds(8.0)) + { + _owner = owner; + _target = target; } - private class SpellPlagueTimer : Timer + public void SetNext(SpellPlagueTimer timer) { - private readonly SpellPlagueSpell m_Owner; - private readonly Mobile m_Target; - private int m_Explosions; - private DateTime m_LastExploded; - private SpellPlagueTimer m_Next; - - public SpellPlagueTimer(SpellPlagueSpell owner, Mobile target) : base(TimeSpan.FromSeconds(8.0)) + if (_next == null) { - m_Owner = owner; - m_Target = target; + _next = timer; } - - public void SetNext(SpellPlagueTimer timer) + else { - if (m_Next == null) - { - m_Next = timer; - } - else - { - m_Next.SetNext(timer); - } - } - - public void StartPlague() - { - BuffInfo.AddBuff( - m_Target, - new BuffInfo(BuffIcon.SpellPlague, 1031690, 1080167, TimeSpan.FromSeconds(8.5), m_Target) - ); - - Start(); - } - - public void OnDamage() - { - if (DateTime.Now <= m_LastExploded + TimeSpan.FromSeconds(2.0)) - { - return; - } - - var exploChance = 90 - m_Explosions * 30; - - var resist = m_Target.Skills.MagicResist.Value; - - if (resist >= 70) - { - exploChance -= (int)((resist - 70.0) * 3.0 / 10.0); - } - - if (exploChance > Utility.Random(100)) - { - m_Owner.VisualEffect(m_Target); - - var damage = m_Owner.GetNewAosDamage(15 + m_Explosions * 3, 1, 5, m_Target); - - m_Explosions++; - m_LastExploded = DateTime.Now; - - SpellHelper.Damage(m_Owner, m_Target, damage, 0, 0, 0, 0, 0, 100); - - if (m_Explosions >= 3) - { - EndPlague(); - } - } - } - - public void EndPlague() - { - if (m_Next != null) - { - _table[m_Target] = m_Next; - m_Next.StartPlague(); - } - else - { - _table.Remove(m_Target); - BuffInfo.RemoveBuff(m_Target, BuffIcon.SpellPlague); - } - - Stop(); + _next.SetNext(timer); } } - private class InternalTarget : Target + public void StartPlague() { - private readonly SpellPlagueSpell _owner; + BuffInfo.AddBuff( + _target, + new BuffInfo(BuffIcon.SpellPlague, 1031690, 1080167, TimeSpan.FromSeconds(8.5), _target) + ); - public InternalTarget(SpellPlagueSpell owner) : base(12, false, TargetFlags.Harmful) => - _owner = owner; + _nextExplosion = Core.Now + TimeSpan.FromSeconds(1); + Start(); + } - protected override void OnTarget(Mobile from, object o) + public void OnDamage() + { + if (Core.Now < _nextExplosion) { - if (o is Mobile mobile) + return; + } + + var exploChance = 90 - _explosions * 30; + + var resist = _target.Skills.MagicResist.Fixed - 700; + + if (resist > 0) + { + exploChance -= resist / 100 * 3; + } + + if (exploChance > Utility.Random(100)) + { + VisualEffect(_target); + + var damage = _owner.GetNewAosDamage(15 + _explosions * 3, 1, 5, false, _target); + + _explosions++; + _nextExplosion = Core.Now + TimeSpan.FromSeconds(1); + + SpellHelper.Damage(_owner, _target, damage, 0, 0, 0, 0, 0, 100); + + if (_explosions >= 3) { - _owner.Target(mobile); + DoNextPlague(); } } + } - protected override void OnTargetFinish(Mobile from) + protected override void OnTick() => DoNextPlague(); + + private void DoNextPlague() + { + if (_next != null) { - _owner.FinishSequence(); + _table[_target] = _next; + _next.StartPlague(); } + else + { + _table.Remove(_target); + BuffInfo.RemoveBuff(_target, BuffIcon.SpellPlague); + } + + _next = null; + Stop(); } } } diff --git a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs index 9aedd7b25..a8178593d 100644 --- a/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/AnimateDeadSpell.cs @@ -5,419 +5,421 @@ using Server.Engines.Quests.Necro; using Server.Items; using Server.Mobiles; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class AnimateDeadSpell : NecromancerSpell, ISpellTargetingItem { - public class AnimateDeadSpell : NecromancerSpell, ISpellTargetingItem + private static readonly SpellInfo _info = new( + "Animate Dead", + "Uus Corp", + 203, + 9031, + Reagent.GraveDust, + Reagent.DaemonBlood + ); + + private static readonly CreatureGroup[] _groups = + [ + // Undead group--empty + new CreatureGroup(SlayerGroup.GetEntryByName(SlayerName.Silver).Types, []), + // Insects + new CreatureGroup( + [ + typeof(DreadSpider), typeof(FrostSpider), typeof(GiantSpider), typeof(GiantBlackWidow), + typeof(BlackSolenInfiltratorQueen), typeof(BlackSolenInfiltratorWarrior), + typeof(BlackSolenQueen), typeof(BlackSolenWarrior), typeof(BlackSolenWorker), + typeof(RedSolenInfiltratorQueen), typeof(RedSolenInfiltratorWarrior), + typeof(RedSolenQueen), typeof(RedSolenWarrior), typeof(RedSolenWorker), + typeof(TerathanAvenger), typeof(TerathanDrone), typeof(TerathanMatriarch), + typeof(TerathanWarrior) + // TODO: Giant beetle? Ant lion? Ophidians? + ], + [ + new SummonEntry(0, typeof(MoundOfMaggots)) + ] + ), + // Mounts + new CreatureGroup( + [ + typeof(Horse), typeof(Nightmare), typeof(FireSteed), + typeof(Kirin), typeof(Unicorn) + ], + [ + new SummonEntry(10000, typeof(HellSteed)), + new SummonEntry(0, typeof(SkeletalMount)) + ] + ), + // Elementals + new CreatureGroup( + [ + typeof(BloodElemental), typeof(EarthElemental), typeof(SummonedEarthElemental), + typeof(AgapiteElemental), typeof(BronzeElemental), typeof(CopperElemental), + typeof(DullCopperElemental), typeof(GoldenElemental), typeof(ShadowIronElemental), + typeof(ValoriteElemental), typeof(VeriteElemental), typeof(PoisonElemental), + typeof(FireElemental), typeof(SummonedFireElemental), typeof(SnowElemental), + typeof(AirElemental), typeof(SummonedAirElemental), typeof(WaterElemental), + typeof(SummonedAirElemental), typeof(AcidElemental) + ], + [ + new SummonEntry(5000, typeof(WailingBanshee)), + new SummonEntry(0, typeof(Wraith)) + ] + ), + // Dragons + new CreatureGroup( + [ + typeof(AncientWyrm), typeof(Dragon), typeof(GreaterDragon), typeof(SerpentineDragon), + typeof(ShadowWyrm), typeof(SkeletalDragon), typeof(WhiteWyrm), + typeof(Drake), typeof(Wyvern), typeof(LesserHiryu), typeof(Hiryu) + ], + [ + new SummonEntry(18000, typeof(SkeletalDragon)), + new SummonEntry(10000, typeof(FleshGolem)), + new SummonEntry(5000, typeof(Lich)), + new SummonEntry(3000, typeof(SkeletalKnight), typeof(BoneKnight)), + new SummonEntry(2000, typeof(Mummy)), + new SummonEntry(1000, typeof(SkeletalMage), typeof(BoneMagi)), + new SummonEntry(0, typeof(PatchworkSkeleton)) + ] + ), + // Default group + new CreatureGroup( + [], + [ + new SummonEntry(18000, typeof(LichLord)), + new SummonEntry(10000, typeof(FleshGolem)), + new SummonEntry(5000, typeof(Lich)), + new SummonEntry(3000, typeof(SkeletalKnight), typeof(BoneKnight)), + new SummonEntry(2000, typeof(Mummy)), + new SummonEntry(1000, typeof(SkeletalMage), typeof(BoneMagi)), + new SummonEntry(0, typeof(PatchworkSkeleton)) + ] + ) + ]; + + private static readonly Dictionary> _table = new(); + + public AnimateDeadSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Animate Dead", - "Uus Corp", - 203, - 9031, - Reagent.GraveDust, - Reagent.DaemonBlood + } + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); + + public override double RequiredSkill => 40.0; + public override int RequiredMana => 23; + + public void Target(Item item) + { + var comp = item as MaabusCoffinComponent; + + if (comp?.Addon is MaabusCoffin addon) + { + var pm = Caster as PlayerMobile; + + var qs = pm?.Quest; + + if (qs is DarkTidesQuest) + { + QuestObjective objective = qs.FindObjective(); + + if (objective?.Completed == false) + { + addon.Awake(Caster); + objective.Complete(); + } + } + + return; + } + + if (item is not Corpse c) + { + Caster.SendLocalizedMessage(1061084); // You cannot animate that. + } + else + { + Type type = c.Owner?.GetType(); + + if (c.ItemID != 0x2006 || c.Animated || type == typeof(PlayerMobile) || type == null || + c.Owner?.Fame < 100 || + c.Owner is BaseCreature creature && (creature.Summoned || creature.IsBonded)) + { + Caster.SendLocalizedMessage(1061085); // There's not enough life force there to animate. + } + else + { + var group = FindGroup(type); + + if (group != null) + { + if (group._entries.Length == 0 || type == typeof(DemonKnight)) + { + Caster.SendLocalizedMessage(1061086); // You cannot animate undead remains. + } + else if (CheckSequence()) + { + var p = c.GetWorldLocation(); + var map = c.Map; + + if (map != null) + { + Effects.PlaySound(p, map, 0x1FB); + Effects.SendLocationParticles( + EffectItem.Create(p, map, EffectItem.DefaultDuration), + 0x3789, + 1, + 40, + 0x3F, + 3, + 9907, + 0 + ); + + Timer.StartTimer( + TimeSpan.FromSeconds(2.0), + () => SummonDelay_Callback(Caster, c, p, map, group) + ); + } + } + } + } + } + + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetItem(this, range: Core.ML ? 10 : 12); + Caster.SendLocalizedMessage(1061083); // Animate what corpse? + } + + public static void RemoveEffects(Mobile caster) + { + if (_table.Remove(caster, out var list)) + { + foreach (var m in list) + { + m.Delete(); + } + + list.Clear(); + } + } + + private static CreatureGroup FindGroup(Type type) + { + for (var i = 0; i < _groups.Length; ++i) + { + var group = _groups[i]; + var types = group._types; + + var contains = types.Length == 0; + + for (var j = 0; !contains && j < types.Length; ++j) + { + contains = types[j].IsAssignableFrom(type); + } + + if (contains) + { + return group; + } + } + + return null; + } + + public static void Unregister(Mobile master, Mobile summoned) + { + if (master == null || !_table.TryGetValue(master, out var list)) + { + return; + } + + if (list.Remove(summoned) && list.Count == 0) + { + _table.Remove(master); + } + } + + public static void Register(Mobile master, Mobile summoned) + { + if (master == null) + { + return; + } + + if (!_table.TryGetValue(master, out var list)) + { + _table[master] = list = []; + } + + for (var i = list.Count - 1; i >= 0; --i) + { + if (i >= list.Count) + { + continue; + } + + var mob = list[i]; + + if (mob.Deleted) + { + list.RemoveAt(i--); + } + } + + list.Add(summoned); + + if (list.Count > 3) + { + var toKill = list[0]; + Unregister(master, toKill); + toKill.Kill(); + } + + Timer.DelayCall( + TimeSpan.FromMilliseconds(1650), + TimeSpan.FromMilliseconds(1650), + Summoned_Damage, + summoned ); + } - private static readonly CreatureGroup[] m_Groups = + private static void Summoned_Damage(Mobile mob) + { + if (mob.Hits > 0) { - // Undead group--empty - new(SlayerGroup.GetEntryByName(SlayerName.Silver).Types, Array.Empty()), - // Insects - new( - new[] - { - typeof(DreadSpider), typeof(FrostSpider), typeof(GiantSpider), typeof(GiantBlackWidow), - typeof(BlackSolenInfiltratorQueen), typeof(BlackSolenInfiltratorWarrior), - typeof(BlackSolenQueen), typeof(BlackSolenWarrior), typeof(BlackSolenWorker), - typeof(RedSolenInfiltratorQueen), typeof(RedSolenInfiltratorWarrior), - typeof(RedSolenQueen), typeof(RedSolenWarrior), typeof(RedSolenWorker), - typeof(TerathanAvenger), typeof(TerathanDrone), typeof(TerathanMatriarch), - typeof(TerathanWarrior) - // TODO: Giant beetle? Ant lion? Ophidians? - }, - new[] - { - new SummonEntry(0, typeof(MoundOfMaggots)) - } - ), - // Mounts - new( - new[] - { - typeof(Horse), typeof(Nightmare), typeof(FireSteed), - typeof(Kirin), typeof(Unicorn) - }, - new[] - { - new SummonEntry(10000, typeof(HellSteed)), - new SummonEntry(0, typeof(SkeletalMount)) - } - ), - // Elementals - new( - new[] - { - typeof(BloodElemental), typeof(EarthElemental), typeof(SummonedEarthElemental), - typeof(AgapiteElemental), typeof(BronzeElemental), typeof(CopperElemental), - typeof(DullCopperElemental), typeof(GoldenElemental), typeof(ShadowIronElemental), - typeof(ValoriteElemental), typeof(VeriteElemental), typeof(PoisonElemental), - typeof(FireElemental), typeof(SummonedFireElemental), typeof(SnowElemental), - typeof(AirElemental), typeof(SummonedAirElemental), typeof(WaterElemental), - typeof(SummonedAirElemental), typeof(AcidElemental) - }, - new[] - { - new SummonEntry(5000, typeof(WailingBanshee)), - new SummonEntry(0, typeof(Wraith)) - } - ), - // Dragons - new( - new[] - { - typeof(AncientWyrm), typeof(Dragon), typeof(GreaterDragon), typeof(SerpentineDragon), - typeof(ShadowWyrm), typeof(SkeletalDragon), typeof(WhiteWyrm), - typeof(Drake), typeof(Wyvern), typeof(LesserHiryu), typeof(Hiryu) - }, - new[] - { - new SummonEntry(18000, typeof(SkeletalDragon)), - new SummonEntry(10000, typeof(FleshGolem)), - new SummonEntry(5000, typeof(Lich)), - new SummonEntry(3000, typeof(SkeletalKnight), typeof(BoneKnight)), - new SummonEntry(2000, typeof(Mummy)), - new SummonEntry(1000, typeof(SkeletalMage), typeof(BoneMagi)), - new SummonEntry(0, typeof(PatchworkSkeleton)) - } - ), - // Default group - new( - Array.Empty(), - new[] - { - new SummonEntry(18000, typeof(LichLord)), - new SummonEntry(10000, typeof(FleshGolem)), - new SummonEntry(5000, typeof(Lich)), - new SummonEntry(3000, typeof(SkeletalKnight), typeof(BoneKnight)), - new SummonEntry(2000, typeof(Mummy)), - new SummonEntry(1000, typeof(SkeletalMage), typeof(BoneMagi)), - new SummonEntry(0, typeof(PatchworkSkeleton)) - } - ) - }; - - private static readonly Dictionary> _table = new(); - - public AnimateDeadSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + --mob.Hits; + } + else { + mob.Kill(); + } + } + + private static void SummonDelay_Callback(Mobile caster, Corpse corpse, Point3D loc, Map map, CreatureGroup group) + { + if (corpse.Animated || corpse.Deleted || caster.Deleted) + { + return; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); + var owner = corpse.Owner; - public override double RequiredSkill => 40.0; - public override int RequiredMana => 23; - - public void Target(Item item) + if (owner == null) { - var comp = item as MaabusCoffinComponent; - - if (comp?.Addon is MaabusCoffin addon) - { - var pm = Caster as PlayerMobile; - - var qs = pm?.Quest; - - if (qs is DarkTidesQuest) - { - QuestObjective objective = qs.FindObjective(); - - if (objective?.Completed == false) - { - addon.Awake(Caster); - objective.Complete(); - } - } - - return; - } - - if (item is not Corpse c) - { - Caster.SendLocalizedMessage(1061084); // You cannot animate that. - } - else - { - Type type = c.Owner?.GetType(); - - if (c.ItemID != 0x2006 || c.Animated || type == typeof(PlayerMobile) || type == null || - c.Owner?.Fame < 100 || - c.Owner is BaseCreature creature && (creature.Summoned || creature.IsBonded)) - { - Caster.SendLocalizedMessage(1061085); // There's not enough life force there to animate. - } - else - { - var group = FindGroup(type); - - if (group != null) - { - if (group.m_Entries.Length == 0 || type == typeof(DemonKnight)) - { - Caster.SendLocalizedMessage(1061086); // You cannot animate undead remains. - } - else if (CheckSequence()) - { - var p = c.GetWorldLocation(); - var map = c.Map; - - if (map != null) - { - Effects.PlaySound(p, map, 0x1FB); - Effects.SendLocationParticles( - EffectItem.Create(p, map, EffectItem.DefaultDuration), - 0x3789, - 1, - 40, - 0x3F, - 3, - 9907, - 0 - ); - - Timer.StartTimer( - TimeSpan.FromSeconds(2.0), - () => SummonDelay_Callback(Caster, c, p, map, group) - ); - } - } - } - } - } - - FinishSequence(); + return; } - public override void OnCast() + var necromancy = caster.Skills.Necromancy.Value; + var spiritSpeak = caster.Skills.SpiritSpeak.Value; + + var casterAbility = (int)(necromancy * 30) + (int)(spiritSpeak * 70); + casterAbility = Math.Clamp(casterAbility / 10 * 18, 0, owner.Fame); + + Type toSummon = null; + var entries = group._entries; + + for (var i = 0; toSummon == null && i < entries.Length; ++i) { - Caster.Target = new SpellTargetItem(this, range: Core.ML ? 10 : 12); - Caster.SendLocalizedMessage(1061083); // Animate what corpse? + var entry = entries[i]; + + if (casterAbility < entry._requirement) + { + continue; + } + + var animates = entry._toSummon; + + toSummon = animates.RandomElement(); } - private static CreatureGroup FindGroup(Type type) + if (toSummon == null) { - for (var i = 0; i < m_Groups.Length; ++i) - { - var group = m_Groups[i]; - var types = group.m_Types; - - var contains = types.Length == 0; - - for (var j = 0; !contains && j < types.Length; ++j) - { - contains = types[j].IsAssignableFrom(type); - } - - if (contains) - { - return group; - } - } - - return null; + return; } - public static void Unregister(Mobile master, Mobile summoned) - { - if (master == null || !_table.TryGetValue(master, out var list)) - { - return; - } + Mobile summoned = null; - if (list.Remove(summoned) && list.Count == 0) - { - _table.Remove(master); - } + try + { + summoned = toSummon.CreateInstance(); + } + catch + { + // ignored } - public static void Register(Mobile master, Mobile summoned) + if (summoned == null) { - if (master == null) - { - return; - } - - if (!_table.TryGetValue(master, out var list)) - { - _table[master] = list = new List(); - } - - for (var i = list.Count - 1; i >= 0; --i) - { - if (i >= list.Count) - { - continue; - } - - var mob = list[i]; - - if (mob.Deleted) - { - list.RemoveAt(i--); - } - } - - list.Add(summoned); - - if (list.Count > 3) - { - var toKill = list[0]; - Unregister(master, toKill); - toKill.Kill(); - } - - Timer.DelayCall( - TimeSpan.FromMilliseconds(1650), - TimeSpan.FromMilliseconds(1650), - Summoned_Damage, - summoned - ); + return; } - private static void Summoned_Damage(Mobile mob) + if (summoned is BaseCreature bc) { - if (mob.Hits > 0) - { - --mob.Hits; - } - else - { - mob.Kill(); - } + // to be sure + bc.Tamable = false; + + bc.ControlSlots = bc is BaseMount ? 1 : 0; + + Effects.PlaySound(loc, map, bc.GetAngerSound()); + + BaseCreature.Summon(bc, false, caster, loc, 0x28, TimeSpan.FromDays(1.0)); } - private static void SummonDelay_Callback(Mobile caster, Corpse corpse, Point3D loc, Map map, CreatureGroup group) + if (summoned is SkeletalDragon dragon) { - if (corpse.Animated) - { - return; - } - - var owner = corpse.Owner; - - if (owner == null) - { - return; - } - - var necromancy = caster.Skills.Necromancy.Value; - var spiritSpeak = caster.Skills.SpiritSpeak.Value; - - var casterAbility = (int)(necromancy * 30) + (int)(spiritSpeak * 70); - casterAbility = Math.Clamp(casterAbility / 10 * 18, 0, owner.Fame); - - Type toSummon = null; - var entries = group.m_Entries; - - for (var i = 0; toSummon == null && i < entries.Length; ++i) - { - var entry = entries[i]; - - if (casterAbility < entry.m_Requirement) - { - continue; - } - - var animates = entry.m_ToSummon; - - toSummon = animates.RandomElement(); - } - - if (toSummon == null) - { - return; - } - - Mobile summoned = null; - - try - { - summoned = toSummon.CreateInstance(); - } - catch - { - // ignored - } - - if (summoned == null) - { - return; - } - - if (summoned is BaseCreature bc) - { - // to be sure - bc.Tamable = false; - - bc.ControlSlots = bc is BaseMount ? 1 : 0; - - Effects.PlaySound(loc, map, bc.GetAngerSound()); - - BaseCreature.Summon(bc, false, caster, loc, 0x28, TimeSpan.FromDays(1.0)); - } - - if (summoned is SkeletalDragon dragon) - { - Scale(dragon, 50); // lose 50% hp and strength - } - - summoned.Fame = 0; - summoned.Karma = -1500; - - summoned.MoveToWorld(loc, map); - - corpse.Hue = 1109; - corpse.Animated = true; - - Register(caster, summoned); + Scale(dragon, 50); // lose 50% hp and strength } - public static void Scale(BaseCreature bc, int scalar) + summoned.Fame = 0; + summoned.Karma = -1500; + summoned.MoveToWorld(loc, map); + + corpse.Hue = 1109; + corpse.Animated = true; + + Register(caster, summoned); + } + + public static void Scale(BaseCreature bc, int scalar) + { + var toScale = bc.RawStr; + bc.RawStr = AOS.Scale(toScale, scalar); + + toScale = bc.HitsMaxSeed; + + if (toScale > 0) { - var toScale = bc.RawStr; - bc.RawStr = AOS.Scale(toScale, scalar); - - toScale = bc.HitsMaxSeed; - - if (toScale > 0) - { - bc.HitsMaxSeed = AOS.Scale(toScale, scalar); - } - - bc.Hits = bc.Hits; // refresh hits + bc.HitsMaxSeed = AOS.Scale(toScale, scalar); } - private class CreatureGroup - { - public readonly SummonEntry[] m_Entries; - public readonly Type[] m_Types; + bc.Hits = bc.Hits; // refresh hits + } - public CreatureGroup(Type[] types, SummonEntry[] entries) - { - m_Types = types; - m_Entries = entries; - } + private class CreatureGroup + { + public readonly SummonEntry[] _entries; + public readonly Type[] _types; + + public CreatureGroup(Type[] types, SummonEntry[] entries) + { + _types = types; + _entries = entries; } + } - private class SummonEntry + private class SummonEntry + { + public readonly int _requirement; + public readonly Type[] _toSummon; + + public SummonEntry(int requirement, params Type[] toSummon) { - public readonly int m_Requirement; - public readonly Type[] m_ToSummon; - - public SummonEntry(int requirement, params Type[] toSummon) - { - m_ToSummon = toSummon; - m_Requirement = requirement; - } + _toSummon = toSummon; + _requirement = requirement; } } } diff --git a/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs b/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs index 28f689e82..5129f7047 100644 --- a/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/BloodOathSpell.cs @@ -3,158 +3,157 @@ using System.Collections.Generic; using Server.Mobiles; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class BloodOathSpell : NecromancerSpell, ISpellTargetingMobile { - public class BloodOathSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Blood Oath", + "In Jux Mani Xen", + 203, + 9031, + Reagent.DaemonBlood + ); + + private static readonly Dictionary _oathTable = new(); + private static readonly Dictionary _table = new(); + + public BloodOathSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Blood Oath", - "In Jux Mani Xen", - 203, - 9031, - Reagent.DaemonBlood - ); + } - private static readonly Dictionary _oathTable = new(); - private static readonly Dictionary _table = new(); + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); - public BloodOathSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override double RequiredSkill => 20.0; + public override int RequiredMana => 13; + + public void Target(Mobile m) + { + if (m == null) { + Caster.SendLocalizedMessage(1060508); // You can't curse that. + } + // only PlayerMobile and BaseCreature implement blood oath checking + else if (Caster == m || m is not (PlayerMobile or BaseCreature)) + { + Caster.SendLocalizedMessage(1060508); // You can't curse that. + } + else if (_oathTable.ContainsKey(Caster)) + { + Caster.SendLocalizedMessage(1061607); // You are already bonded in a Blood Oath. + } + else if (_oathTable.ContainsKey(m)) + { + if (m.Player) + { + Caster.SendLocalizedMessage(1061608); // That player is already bonded in a Blood Oath. + } + else + { + Caster.SendLocalizedMessage(1061609); // That creature is already bonded in a Blood Oath. + } + } + else if (CheckHSequence(m)) + { + SpellHelper.Turn(Caster, m); + + /* Temporarily creates a dark pact between the caster and the target. + * Any damage dealt by the target to the caster is increased, but the target receives the same amount of damage. + * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 80 ) + 8 seconds. + * + * NOTE: The above algorithm must be fixed point, it should be: + * ((ss-rm)/8)+8 + */ + + RemoveCurse(m); + + _oathTable[Caster] = Caster; + _oathTable[m] = Caster; + + m.Spell?.OnCasterHurt(); + + Caster.PlaySound(0x175); + + Caster.FixedParticles(0x375A, 1, 17, 9919, 33, 7, EffectLayer.Waist); + Caster.FixedParticles(0x3728, 1, 13, 9502, 33, 7, (EffectLayer)255); + + m.FixedParticles(0x375A, 1, 17, 9919, 33, 7, EffectLayer.Waist); + m.FixedParticles(0x3728, 1, 13, 9502, 33, 7, (EffectLayer)255); + + var duration = TimeSpan.FromSeconds((GetDamageSkill(Caster) - GetResistSkill(m)) / 8 + 8); + m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain + + var timer = new ExpireTimer(Caster, m, duration); + timer.Start(); + + BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.BloodOathCaster, 1075659, duration, Caster, m.Name)); + BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.BloodOathCurse, 1075661, duration, m, Caster.Name)); + + _table[m] = timer; + HarmfulSpell(m); } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); + FinishSequence(); + } - public override double RequiredSkill => 20.0; - public override int RequiredMana => 13; + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } - public void Target(Mobile m) + public static bool RemoveCurse(Mobile target) + { + if (_table.Remove(target, out var timer)) { - if (m == null) + var caster = timer.Caster; + if (_oathTable.Remove(caster)) { - Caster.SendLocalizedMessage(1060508); // You can't curse that. - } - // only PlayerMobile and BaseCreature implement blood oath checking - else if (Caster == m || m is not (PlayerMobile or BaseCreature)) - { - Caster.SendLocalizedMessage(1060508); // You can't curse that. - } - else if (_oathTable.ContainsKey(Caster)) - { - Caster.SendLocalizedMessage(1061607); // You are already bonded in a Blood Oath. - } - else if (_oathTable.ContainsKey(m)) - { - if (m.Player) - { - Caster.SendLocalizedMessage(1061608); // That player is already bonded in a Blood Oath. - } - else - { - Caster.SendLocalizedMessage(1061609); // That creature is already bonded in a Blood Oath. - } - } - else if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); - - /* Temporarily creates a dark pact between the caster and the target. - * Any damage dealt by the target to the caster is increased, but the target receives the same amount of damage. - * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 80 ) + 8 seconds. - * - * NOTE: The above algorithm must be fixed point, it should be: - * ((ss-rm)/8)+8 - */ - - RemoveCurse(m); - - _oathTable[Caster] = Caster; - _oathTable[m] = Caster; - - m.Spell?.OnCasterHurt(); - - Caster.PlaySound(0x175); - - Caster.FixedParticles(0x375A, 1, 17, 9919, 33, 7, EffectLayer.Waist); - Caster.FixedParticles(0x3728, 1, 13, 9502, 33, 7, (EffectLayer)255); - - m.FixedParticles(0x375A, 1, 17, 9919, 33, 7, EffectLayer.Waist); - m.FixedParticles(0x3728, 1, 13, 9502, 33, 7, (EffectLayer)255); - - var duration = TimeSpan.FromSeconds((GetDamageSkill(Caster) - GetResistSkill(m)) / 8 + 8); - m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain - - var timer = new ExpireTimer(Caster, m, duration); - timer.Start(); - - BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.BloodOathCaster, 1075659, duration, Caster, m.Name)); - BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.BloodOathCurse, 1075661, duration, m, Caster.Name)); - - _table[m] = timer; - HarmfulSpell(m); + caster.SendLocalizedMessage(1061620); // Your Blood Oath has been broken. } - FinishSequence(); + if (_oathTable.Remove(target)) + { + target.SendLocalizedMessage(1061620); // Your Blood Oath has been broken. + } + + timer.Stop(); + + BuffInfo.RemoveBuff(caster, BuffIcon.BloodOathCaster); + BuffInfo.RemoveBuff(target, BuffIcon.BloodOathCurse); + + return true; } - public override void OnCast() + return false; + } + + public static Mobile GetBloodOath(Mobile m) => + m == null || _oathTable.TryGetValue(m, out var oath) && oath == m ? null : oath; + + private class ExpireTimer : Timer + { + private Mobile _target; + private DateTime _end; + + public Mobile Caster { get; } + + public ExpireTimer(Mobile caster, Mobile target, TimeSpan delay) : base( + TimeSpan.FromSeconds(1.0), + TimeSpan.FromSeconds(1.0) + ) { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + Caster = caster; + _target = target; + _end = Core.Now + delay; } - public static bool RemoveCurse(Mobile target) + protected override void OnTick() { - if (_table.Remove(target, out var timer)) + if (Caster.Deleted || _target.Deleted || !Caster.Alive || !_target.Alive || + Core.Now >= _end) { - var caster = timer.Caster; - if (_oathTable.Remove(caster)) - { - caster.SendLocalizedMessage(1061620); // Your Blood Oath has been broken. - } - - if (_oathTable.Remove(target)) - { - target.SendLocalizedMessage(1061620); // Your Blood Oath has been broken. - } - - timer.Stop(); - - BuffInfo.RemoveBuff(caster, BuffIcon.BloodOathCaster); - BuffInfo.RemoveBuff(target, BuffIcon.BloodOathCurse); - - return true; - } - - return false; - } - - public static Mobile GetBloodOath(Mobile m) => - m == null || _oathTable.TryGetValue(m, out var oath) && oath == m ? null : oath; - - private class ExpireTimer : Timer - { - private Mobile _target; - private DateTime _end; - - public Mobile Caster { get; } - - public ExpireTimer(Mobile caster, Mobile target, TimeSpan delay) : base( - TimeSpan.FromSeconds(1.0), - TimeSpan.FromSeconds(1.0) - ) - { - Caster = caster; - _target = target; - _end = Core.Now + delay; - } - - protected override void OnTick() - { - if (Caster.Deleted || _target.Deleted || !Caster.Alive || !_target.Alive || - Core.Now >= _end) - { - RemoveCurse(_target); - } + RemoveCurse(_target); } } } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/CorpseSkin.cs b/Projects/UOContent/Spells/Necromancy/CorpseSkin.cs index 19ed3ba8b..a495aeaef 100644 --- a/Projects/UOContent/Spells/Necromancy/CorpseSkin.cs +++ b/Projects/UOContent/Spells/Necromancy/CorpseSkin.cs @@ -2,144 +2,143 @@ using System; using System.Collections.Generic; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class CorpseSkinSpell : NecromancerSpell, ISpellTargetingMobile { - public class CorpseSkinSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Corpse Skin", + "In Agle Corp Ylem", + 203, + 9051, + Reagent.BatWing, + Reagent.GraveDust + ); + + private static readonly Dictionary _table = new(); + + public CorpseSkinSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Corpse Skin", - "In Agle Corp Ylem", - 203, - 9051, - Reagent.BatWing, - Reagent.GraveDust - ); + } - private static readonly Dictionary _table = new(); + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); - public CorpseSkinSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override double RequiredSkill => 20.0; + public override int RequiredMana => 11; + + public void Target(Mobile m) + { + if (m == null) { + return; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); - - public override double RequiredSkill => 20.0; - public override int RequiredMana => 11; - - public void Target(Mobile m) + if (CheckHSequence(m)) { - if (m == null) + SpellHelper.Turn(Caster, m); + + /* Transmogrifies the flesh of the target creature or player to resemble rotted corpse flesh, + * making them more vulnerable to Fire and Poison damage, + * but increasing their resistance to Physical and Cold damage. + * + * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 25 ) + 40 seconds. + * + * NOTE: Algorithm above is fixed point, should be: + * ((ss-mr)/2.5) + 40 + * + * NOTE: Resistance is not checked if targeting yourself + */ + + if (_table.TryGetValue(m, out var timer)) { - return; + timer.DoExpire(); + } + else + { + m.SendLocalizedMessage(1061689); // Your skin turns dry and corpselike. } - if (CheckHSequence(m)) + m.Spell?.OnCasterHurt(); + + m.FixedParticles(0x373A, 1, 15, 9913, 67, 7, EffectLayer.Head); + m.PlaySound(0x1BB); + + var ss = GetDamageSkill(Caster); + var mr = Caster == m ? 0.0 : GetResistSkill(m); + m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain + + var duration = TimeSpan.FromSeconds((ss - mr) / 2.5 + 40.0); + + ResistanceMod[] mods = { - SpellHelper.Turn(Caster, m); + new(ResistanceType.Fire, "FireResistCorpseSkinSpell", -15), + new(ResistanceType.Poison, "PoisonResistCorpseSkinSpell", -15), + new(ResistanceType.Cold, "ColdResistCorpseSkinSpell", +10), + new(ResistanceType.Physical, "PhysicalResistCorpseSkinSpell", +10) + }; - /* Transmogrifies the flesh of the target creature or player to resemble rotted corpse flesh, - * making them more vulnerable to Fire and Poison damage, - * but increasing their resistance to Physical and Cold damage. - * - * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 25 ) + 40 seconds. - * - * NOTE: Algorithm above is fixed point, should be: - * ((ss-mr)/2.5) + 40 - * - * NOTE: Resistance is not checked if targeting yourself - */ + timer = new ExpireTimer(m, mods, duration); + timer.Start(); - if (_table.TryGetValue(m, out var timer)) - { - timer.DoExpire(); - } - else - { - m.SendLocalizedMessage(1061689); // Your skin turns dry and corpselike. - } + BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.CorpseSkin, 1075663, duration, m)); - m.Spell?.OnCasterHurt(); + _table[m] = timer; - m.FixedParticles(0x373A, 1, 15, 9913, 67, 7, EffectLayer.Head); - m.PlaySound(0x1BB); - - var ss = GetDamageSkill(Caster); - var mr = Caster == m ? 0.0 : GetResistSkill(m); - m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain - - var duration = TimeSpan.FromSeconds((ss - mr) / 2.5 + 40.0); - - ResistanceMod[] mods = - { - new(ResistanceType.Fire, "FireResistCorpseSkinSpell", -15), - new(ResistanceType.Poison, "PoisonResistCorpseSkinSpell", -15), - new(ResistanceType.Cold, "ColdResistCorpseSkinSpell", +10), - new(ResistanceType.Physical, "PhysicalResistCorpseSkinSpell", +10) - }; - - timer = new ExpireTimer(m, mods, duration); - timer.Start(); - - BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.CorpseSkin, 1075663, duration, m)); - - _table[m] = timer; - - for (var i = 0; i < mods.Length; ++i) - { - m.AddResistanceMod(mods[i]); - } - - HarmfulSpell(m); + for (var i = 0; i < mods.Length; ++i) + { + m.AddResistanceMod(mods[i]); } - FinishSequence(); + HarmfulSpell(m); } - public override void OnCast() + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + public static bool RemoveCurse(Mobile m) + { + if (!_table.TryGetValue(m, out var t)) { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + return false; } - public static bool RemoveCurse(Mobile m) - { - if (!_table.TryGetValue(m, out var t)) - { - return false; - } + m.SendLocalizedMessage(1061688); // Your skin returns to normal. + t?.DoExpire(); + return true; + } - m.SendLocalizedMessage(1061688); // Your skin returns to normal. - t?.DoExpire(); - return true; + private class ExpireTimer : Timer + { + private Mobile _mobile; + private ResistanceMod[] _mods; + + public ExpireTimer(Mobile m, ResistanceMod[] mods, TimeSpan delay) : base(delay) + { + _mobile = m; + _mods = mods; } - private class ExpireTimer : Timer + public void DoExpire() { - private Mobile _mobile; - private ResistanceMod[] _mods; - - public ExpireTimer(Mobile m, ResistanceMod[] mods, TimeSpan delay) : base(delay) + for (var i = 0; i < _mods.Length; ++i) { - _mobile = m; - _mods = mods; + _mobile.RemoveResistanceMod(_mods[i]); } - public void DoExpire() - { - for (var i = 0; i < _mods.Length; ++i) - { - _mobile.RemoveResistanceMod(_mods[i]); - } + Stop(); + BuffInfo.RemoveBuff(_mobile, BuffIcon.CorpseSkin); + _table.Remove(_mobile); + } - Stop(); - BuffInfo.RemoveBuff(_mobile, BuffIcon.CorpseSkin); - _table.Remove(_mobile); - } - - protected override void OnTick() - { - _mobile.SendLocalizedMessage(1061688); // Your skin returns to normal. - DoExpire(); - } + protected override void OnTick() + { + _mobile.SendLocalizedMessage(1061688); // Your skin returns to normal. + DoExpire(); } } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/CurseWeapon.cs b/Projects/UOContent/Spells/Necromancy/CurseWeapon.cs index 4b3509836..c0cb7c705 100644 --- a/Projects/UOContent/Spells/Necromancy/CurseWeapon.cs +++ b/Projects/UOContent/Spells/Necromancy/CurseWeapon.cs @@ -2,78 +2,77 @@ using System; using System.Collections.Generic; using Server.Items; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class CurseWeaponSpell : NecromancerSpell { - public class CurseWeaponSpell : NecromancerSpell + private static readonly SpellInfo _info = new( + "Curse Weapon", + "An Sanct Gra Char", + 203, + 9031, + Reagent.PigIron + ); + + private static readonly Dictionary _table = new(); + + public CurseWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Curse Weapon", - "An Sanct Gra Char", - 203, - 9031, - Reagent.PigIron - ); + } - private static readonly Dictionary _table = new(); + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75); - public CurseWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override double RequiredSkill => 0.0; + public override int RequiredMana => 7; + + public override void OnCast() + { + if (Caster.Weapon is not BaseWeapon weapon || weapon is Fists) { + Caster.SendLocalizedMessage(501078); // You must be holding a weapon. + } + else if (CheckSequence()) + { + /* Temporarily imbues a weapon with a life draining effect. + * Half the damage that the weapon inflicts is added to the necromancer's health. + * The effects lasts for (Spirit Speak skill level / 34) + 1 seconds. + * + * NOTE: Above algorithm is fixed point, should be : + * (Spirit Speak skill level / 3.4) + 1 + * + * TODO: What happens if you curse a weapon then give it to someone else? Should they get the drain effect? + */ + + Caster.PlaySound(0x387); + Caster.FixedParticles(0x3779, 1, 15, 9905, 32, 2, EffectLayer.Head); + Caster.FixedParticles(0x37B9, 1, 14, 9502, 32, 5, (EffectLayer)255); + Timer.StartTimer(TimeSpan.FromSeconds(0.75), () => Caster.PlaySound(0xFA)); + + var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 3.4 + 1.0); + + _table.TryGetValue(weapon, out var timer); + timer?.Stop(); + + weapon.Cursed = true; + _table[weapon] = timer = new ExpireTimer(weapon, duration); + + timer.Start(); } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75); + FinishSequence(); + } - public override double RequiredSkill => 0.0; - public override int RequiredMana => 7; + private class ExpireTimer : Timer + { + private BaseWeapon _weapon; - public override void OnCast() + public ExpireTimer(BaseWeapon weapon, TimeSpan delay) : base(delay) => _weapon = weapon; + + protected override void OnTick() { - if (Caster.Weapon is not BaseWeapon weapon || weapon is Fists) - { - Caster.SendLocalizedMessage(501078); // You must be holding a weapon. - } - else if (CheckSequence()) - { - /* Temporarily imbues a weapon with a life draining effect. - * Half the damage that the weapon inflicts is added to the necromancer's health. - * The effects lasts for (Spirit Speak skill level / 34) + 1 seconds. - * - * NOTE: Above algorithm is fixed point, should be : - * (Spirit Speak skill level / 3.4) + 1 - * - * TODO: What happens if you curse a weapon then give it to someone else? Should they get the drain effect? - */ - - Caster.PlaySound(0x387); - Caster.FixedParticles(0x3779, 1, 15, 9905, 32, 2, EffectLayer.Head); - Caster.FixedParticles(0x37B9, 1, 14, 9502, 32, 5, (EffectLayer)255); - Timer.StartTimer(TimeSpan.FromSeconds(0.75), () => Caster.PlaySound(0xFA)); - - var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 3.4 + 1.0); - - _table.TryGetValue(weapon, out var timer); - timer?.Stop(); - - weapon.Cursed = true; - _table[weapon] = timer = new ExpireTimer(weapon, duration); - - timer.Start(); - } - - FinishSequence(); - } - - private class ExpireTimer : Timer - { - private BaseWeapon _weapon; - - public ExpireTimer(BaseWeapon weapon, TimeSpan delay) : base(delay) => _weapon = weapon; - - protected override void OnTick() - { - _weapon.Cursed = false; - Effects.PlaySound(_weapon.GetWorldLocation(), _weapon.Map, 0xFA); - _table.Remove(_weapon); - } + _weapon.Cursed = false; + Effects.PlaySound(_weapon.GetWorldLocation(), _weapon.Map, 0xFA); + _table.Remove(_weapon); } } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/EvilOmen.cs b/Projects/UOContent/Spells/Necromancy/EvilOmen.cs index 4b213fad8..506c08c16 100644 --- a/Projects/UOContent/Spells/Necromancy/EvilOmen.cs +++ b/Projects/UOContent/Spells/Necromancy/EvilOmen.cs @@ -3,87 +3,86 @@ using System.Collections.Generic; using Server.Mobiles; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class EvilOmenSpell : NecromancerSpell, ISpellTargetingMobile { - public class EvilOmenSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Evil Omen", + "Pas Tym An Sanct", + 203, + 9031, + Reagent.BatWing, + Reagent.NoxCrystal + ); + + private static readonly Dictionary _table = new(); + + public EvilOmenSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Evil Omen", - "Pas Tym An Sanct", - 203, - 9031, - Reagent.BatWing, - Reagent.NoxCrystal - ); - - private static readonly Dictionary _table = new(); - - public EvilOmenSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75); - - public override double RequiredSkill => 20.0; - public override int RequiredMana => 11; - - public void Target(Mobile m) - { - if (m is not (BaseCreature or PlayerMobile)) - { - Caster.SendLocalizedMessage(1060508); // You can't curse that. - } - else if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); - - /* Curses the target so that the next harmful event that affects them is magnified. - * Damage to the target's hit points is increased 25%, - * the poison level of the attack will be 1 higher - * and the Resist Magic skill of the target will be fixed on 50. - * - * The effect lasts for one harmful event only. - */ - - m.Spell?.OnCasterHurt(); - - m.PlaySound(0xFC); - m.FixedParticles(0x3728, 1, 13, 9912, 1150, 7, EffectLayer.Head); - m.FixedParticles(0x3779, 1, 15, 9502, 67, 7, EffectLayer.Head); - - if (!_table.ContainsKey(m)) - { - var mod = new DefaultSkillMod(SkillName.MagicResist, "EvilOmen", false, m.Skills.MagicResist.Value / 2); - m.AddSkillMod(mod); - _table[m] = mod; - } - - var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 12 + 1.0); - - Timer.StartTimer(duration, () => EndEffect(m)); - - HarmfulSpell(m); - - BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.EvilOmen, 1075647, 1075648, duration, m)); - } - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); - } - - public static bool EndEffect(Mobile m) - { - if (_table.Remove(m, out var mod)) - { - mod.Remove(); - return true; - } - - return false; - } } -} + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75); + + public override double RequiredSkill => 20.0; + public override int RequiredMana => 11; + + public void Target(Mobile m) + { + if (m is not (BaseCreature or PlayerMobile)) + { + Caster.SendLocalizedMessage(1060508); // You can't curse that. + } + else if (CheckHSequence(m)) + { + SpellHelper.Turn(Caster, m); + + /* Curses the target so that the next harmful event that affects them is magnified. + * Damage to the target's hit points is increased 25%, + * the poison level of the attack will be 1 higher + * and the Resist Magic skill of the target will be fixed on 50. + * + * The effect lasts for one harmful event only. + */ + + m.Spell?.OnCasterHurt(); + + m.PlaySound(0xFC); + m.FixedParticles(0x3728, 1, 13, 9912, 1150, 7, EffectLayer.Head); + m.FixedParticles(0x3779, 1, 15, 9502, 67, 7, EffectLayer.Head); + + if (!_table.ContainsKey(m)) + { + var mod = new DefaultSkillMod(SkillName.MagicResist, "EvilOmen", false, m.Skills.MagicResist.Value / 2); + m.AddSkillMod(mod); + _table[m] = mod; + } + + var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 12 + 1.0); + + Timer.StartTimer(duration, () => EndEffect(m)); + + HarmfulSpell(m); + + BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.EvilOmen, 1075647, 1075648, duration, m)); + } + + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + public static bool EndEffect(Mobile m) + { + if (_table.Remove(m, out var mod)) + { + mod.Remove(); + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/Exorcism.cs b/Projects/UOContent/Spells/Necromancy/Exorcism.cs index 351404647..298b76e18 100644 --- a/Projects/UOContent/Spells/Necromancy/Exorcism.cs +++ b/Projects/UOContent/Spells/Necromancy/Exorcism.cs @@ -7,206 +7,205 @@ using Server.Guilds; using Server.Items; using Server.Regions; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class ExorcismSpell : NecromancerSpell { - public class ExorcismSpell : NecromancerSpell + private static readonly SpellInfo _info = new( + "Exorcism", + "Ort Corp Grav", + 203, + 9031, + Reagent.NoxCrystal, + Reagent.GraveDust + ); + + private static readonly int Range = Core.ML ? 48 : 18; + + private static readonly Point3D[] m_BritanniaLocs = { - private static readonly SpellInfo _info = new( - "Exorcism", - "Ort Corp Grav", - 203, - 9031, - Reagent.NoxCrystal, - Reagent.GraveDust - ); + new(1470, 843, 0), + new(1857, 865, -1), + new(4220, 563, 36), + new(1732, 3528, 0), + new(1300, 644, 8), + new(3355, 302, 9), + new(1606, 2490, 5), + new(2500, 3931, 3), + new(4264, 3707, 0) + }; - private static readonly int Range = Core.ML ? 48 : 18; + private static readonly Point3D[] m_IllshLocs = + { + new(1222, 474, -17), + new(718, 1360, -60), + new(297, 1014, -19), + new(986, 1006, -36), + new(1180, 1288, -30), + new(1538, 1341, -3), + new(528, 223, -38) + }; - private static readonly Point3D[] m_BritanniaLocs = - { - new(1470, 843, 0), - new(1857, 865, -1), - new(4220, 563, 36), - new(1732, 3528, 0), - new(1300, 644, 8), - new(3355, 302, 9), - new(1606, 2490, 5), - new(2500, 3931, 3), - new(4264, 3707, 0) - }; + private static readonly Point3D[] m_MalasLocs = + { + new(976, 517, -30) + }; - private static readonly Point3D[] m_IllshLocs = - { - new(1222, 474, -17), - new(718, 1360, -60), - new(297, 1014, -19), - new(986, 1006, -36), - new(1180, 1288, -30), - new(1538, 1341, -3), - new(528, 223, -38) - }; + private static readonly Point3D[] m_TokunoLocs = + { + new(710, 1162, 25), + new(1034, 515, 18), + new(295, 712, 55) + }; - private static readonly Point3D[] m_MalasLocs = - { - new(976, 517, -30) - }; - - private static readonly Point3D[] m_TokunoLocs = - { - new(710, 1162, 25), - new(1034, 515, 18), - new(295, 712, 55) - }; - - public ExorcismSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 80.0; - public override int RequiredMana => 40; - - public override bool DelayedDamage => false; - - public override bool CheckCast() - { - if (Caster.Skills.SpiritSpeak.Value < 100.0) - { - Caster.SendLocalizedMessage(1072112); // You must have GM Spirit Speak to use this spell - return false; - } - - return base.CheckCast(); - } - - public override int ComputeKarmaAward() => 0; - - public override void OnCast() - { - var r = Caster.Region.GetRegion(); - if (r == null || !Caster.InRange(r.Spawn, Range)) - { - Caster.SendLocalizedMessage(1072111); // You are not in a valid exorcism region. - } - else if (CheckSequence()) - { - var map = Caster.Map; - - if (map != null) - { - // Cannot move a mobile while iterating mobiles in range, so use a queue - using var queue = PooledRefQueue.Create(); - foreach (var m in r.Spawn.GetMobilesInRange(Range)) - { - if (IsValidTarget(m)) - { - queue.Enqueue(m); - } - } - - while (queue.Count > 0) - { - var m = queue.Dequeue(); - - // Surprisingly, no sparkle type effects - m.Location = GetNearestShrine(m); - } - } - } - - FinishSequence(); - } - - private bool IsValidTarget(Mobile m) - { - if (!m.Player || m.Alive) - { - return false; - } - - var c = m.Corpse as Corpse; - var map = m.Map; - - if (c?.Deleted == false && map != null && c.Map == map) - { - if (SpellHelper.IsAnyT2A(map, c.Location) && SpellHelper.IsAnyT2A(map, m.Location)) - { - return false; // Same Map, both in T2A, ie, same 'sub server'. - } - - if (m.Region.IsPartOf() == Region.Find(c.Location, map).IsPartOf()) - { - return false; // Same Map, both in Dungeon region OR They're both NOT in a dungeon region. - } - - // Just an approximation cause RunUO doesn't divide up the world the same way OSI does ;p - } - - if (Party.Get(m)?.Contains(Caster) == true) - { - return false; - } - - if (m.Guild != null && Caster.Guild != null) - { - var mGuild = m.Guild as Guild; - var cGuild = Caster.Guild as Guild; - - if (mGuild?.IsAlly(cGuild) == true || mGuild == cGuild) - { - return false; - } - } - - var f = Faction.Find(m); - - return m.Map != Faction.Facet || f == null || f != Faction.Find(Caster); - } - - private static Point3D GetNearestShrine(Mobile m) - { - var map = m.Map; - - Point3D[] locList; - - if (map == Map.Felucca || map == Map.Trammel) - { - locList = m_BritanniaLocs; - } - else if (map == Map.Ilshenar) - { - locList = m_IllshLocs; - } - else if (map == Map.Tokuno) - { - locList = m_TokunoLocs; - } - else if (map == Map.Malas) - { - locList = m_MalasLocs; - } - else - { - locList = Array.Empty(); - } - - var closest = Point3D.Zero; - var minDist = double.MaxValue; - - for (var i = 0; i < locList.Length; i++) - { - var p = locList[i]; - - var dist = m.GetDistanceToSqrt(p); - if (minDist > dist) - { - closest = p; - minDist = dist; - } - } - - return closest; - } + public ExorcismSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + { } -} + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 80.0; + public override int RequiredMana => 40; + + public override bool DelayedDamage => false; + + public override bool CheckCast() + { + if (Caster.Skills.SpiritSpeak.Value < 100.0) + { + Caster.SendLocalizedMessage(1072112); // You must have GM Spirit Speak to use this spell + return false; + } + + return base.CheckCast(); + } + + public override int ComputeKarmaAward() => 0; + + public override void OnCast() + { + var r = Caster.Region.GetRegion(); + if (r == null || !Caster.InRange(r.Spawn, Range)) + { + Caster.SendLocalizedMessage(1072111); // You are not in a valid exorcism region. + } + else if (CheckSequence()) + { + var map = Caster.Map; + + if (map != null) + { + // Cannot move a mobile while iterating mobiles in range, so use a queue + using var queue = PooledRefQueue.Create(); + foreach (var m in r.Spawn.GetMobilesInRange(Range)) + { + if (IsValidTarget(m)) + { + queue.Enqueue(m); + } + } + + while (queue.Count > 0) + { + var m = queue.Dequeue(); + + // Surprisingly, no sparkle type effects + m.Location = GetNearestShrine(m); + } + } + } + + FinishSequence(); + } + + private bool IsValidTarget(Mobile m) + { + if (!m.Player || m.Alive) + { + return false; + } + + var c = m.Corpse as Corpse; + var map = m.Map; + + if (c?.Deleted == false && map != null && c.Map == map) + { + if (SpellHelper.IsAnyT2A(map, c.Location) && SpellHelper.IsAnyT2A(map, m.Location)) + { + return false; // Same Map, both in T2A, ie, same 'sub server'. + } + + if (m.Region.IsPartOf() == Region.Find(c.Location, map).IsPartOf()) + { + return false; // Same Map, both in Dungeon region OR They're both NOT in a dungeon region. + } + + // Just an approximation cause RunUO doesn't divide up the world the same way OSI does ;p + } + + if (Party.Get(m)?.Contains(Caster) == true) + { + return false; + } + + if (m.Guild != null && Caster.Guild != null) + { + var mGuild = m.Guild as Guild; + var cGuild = Caster.Guild as Guild; + + if (mGuild?.IsAlly(cGuild) == true || mGuild == cGuild) + { + return false; + } + } + + var f = Faction.Find(m); + + return m.Map != Faction.Facet || f == null || f != Faction.Find(Caster); + } + + private static Point3D GetNearestShrine(Mobile m) + { + var map = m.Map; + + Point3D[] locList; + + if (map == Map.Felucca || map == Map.Trammel) + { + locList = m_BritanniaLocs; + } + else if (map == Map.Ilshenar) + { + locList = m_IllshLocs; + } + else if (map == Map.Tokuno) + { + locList = m_TokunoLocs; + } + else if (map == Map.Malas) + { + locList = m_MalasLocs; + } + else + { + locList = Array.Empty(); + } + + var closest = Point3D.Zero; + var minDist = double.MaxValue; + + for (var i = 0; i < locList.Length; i++) + { + var p = locList[i]; + + var dist = m.GetDistanceToSqrt(p); + if (minDist > dist) + { + closest = p; + minDist = dist; + } + } + + return closest; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/HorrificBeast.cs b/Projects/UOContent/Spells/Necromancy/HorrificBeast.cs index cfc56e308..4d39360d2 100644 --- a/Projects/UOContent/Spells/Necromancy/HorrificBeast.cs +++ b/Projects/UOContent/Spells/Necromancy/HorrificBeast.cs @@ -1,41 +1,40 @@ using System; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class HorrificBeastSpell : TransformationSpell { - public class HorrificBeastSpell : TransformationSpell + private static readonly SpellInfo _info = new( + "Horrific Beast", + "Rel Xen Vas Bal", + 203, + 9031, + Reagent.BatWing, + Reagent.DaemonBlood + ); + + public HorrificBeastSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Horrific Beast", - "Rel Xen Vas Bal", - 203, - 9031, - Reagent.BatWing, - Reagent.DaemonBlood - ); - - public HorrificBeastSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 40.0; - public override int RequiredMana => 11; - - public override int Body => 746; - - public override void DoEffect(Mobile m) - { - m.PlaySound(0x165); - m.FixedParticles(0x3728, 1, 13, 9918, 92, 3, EffectLayer.Head); - - m.Delta(MobileDelta.WeaponDamage); - m.CheckStatTimers(); - } - - public override void RemoveEffect(Mobile m) - { - m.Delta(MobileDelta.WeaponDamage); - } } -} + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 40.0; + public override int RequiredMana => 11; + + public override int Body => 746; + + public override void DoEffect(Mobile m) + { + m.PlaySound(0x165); + m.FixedParticles(0x3728, 1, 13, 9918, 92, 3, EffectLayer.Head); + + m.Delta(MobileDelta.WeaponDamage); + m.CheckStatTimers(); + } + + public override void RemoveEffect(Mobile m) + { + m.Delta(MobileDelta.WeaponDamage); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/LichForm.cs b/Projects/UOContent/Spells/Necromancy/LichForm.cs index d53a6b2b2..899fa3ff8 100644 --- a/Projects/UOContent/Spells/Necromancy/LichForm.cs +++ b/Projects/UOContent/Spells/Necromancy/LichForm.cs @@ -1,45 +1,44 @@ using System; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class LichFormSpell : TransformationSpell { - public class LichFormSpell : TransformationSpell + private static readonly SpellInfo _info = new( + "Lich Form", + "Rel Xen Corp Ort", + 203, + 9031, + Reagent.GraveDust, + Reagent.DaemonBlood, + Reagent.NoxCrystal + ); + + public LichFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Lich Form", - "Rel Xen Corp Ort", - 203, - 9031, - Reagent.GraveDust, - Reagent.DaemonBlood, - Reagent.NoxCrystal - ); - - public LichFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 70.0; - public override int RequiredMana => 23; - - public override int Body => 749; - - public override int FireResistOffset => -10; - public override int ColdResistOffset => +10; - public override int PoisResistOffset => +10; - - public override double TickRate => 2.5; - - public override void DoEffect(Mobile m) - { - m.PlaySound(0x19C); - m.FixedParticles(0x3709, 1, 30, 9904, 1108, 6, EffectLayer.RightFoot); - } - - public override void OnTick(Mobile m) - { - --m.Hits; - } } -} + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 70.0; + public override int RequiredMana => 23; + + public override int Body => 749; + + public override int FireResistOffset => -10; + public override int ColdResistOffset => +10; + public override int PoisResistOffset => +10; + + public override double TickRate => 2.5; + + public override void DoEffect(Mobile m) + { + m.PlaySound(0x19C); + m.FixedParticles(0x3709, 1, 30, 9904, 1108, 6, EffectLayer.RightFoot); + } + + public override void OnTick(Mobile m) + { + --m.Hits; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/MindRot.cs b/Projects/UOContent/Spells/Necromancy/MindRot.cs index 33ec8d784..1aa3fafab 100644 --- a/Projects/UOContent/Spells/Necromancy/MindRot.cs +++ b/Projects/UOContent/Spells/Necromancy/MindRot.cs @@ -2,136 +2,135 @@ using System; using System.Collections.Generic; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class MindRotSpell : NecromancerSpell, ISpellTargetingMobile { - public class MindRotSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Mind Rot", + "Wis An Ben", + 203, + 9031, + Reagent.BatWing, + Reagent.PigIron, + Reagent.DaemonBlood + ); + + private static readonly Dictionary _table = new(); + + public MindRotSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Mind Rot", - "Wis An Ben", - 203, - 9031, - Reagent.BatWing, - Reagent.PigIron, - Reagent.DaemonBlood - ); - - private static readonly Dictionary _table = new(); - - public MindRotSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); - - public override double RequiredSkill => 30.0; - public override int RequiredMana => 17; - - public void Target(Mobile m) - { - if (m == null) - { - Caster.SendLocalizedMessage(1060508); // You can't curse that. - } - else if (HasMindRotScalar(m)) - { - Caster.SendLocalizedMessage(1005559); // This spell is already in effect. - } - else if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); - - /* Attempts to place a curse on the Target that increases the mana cost of any spells they cast, - * for a duration based off a comparison between the Caster's Spirit Speak skill and the Target's Resisting Spells skill. - * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 50 ) + 20 seconds. - */ - - m.Spell?.OnCasterHurt(); - - m.PlaySound(0x1FB); - m.PlaySound(0x258); - m.FixedParticles(0x373A, 1, 17, 9903, 15, 4, EffectLayer.Head); - - var duration = ((GetDamageSkill(Caster) - GetResistSkill(m)) / 5.0 + 20.0) * (m.Player ? 1.0 : 2.0); - m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain - - SetMindRotScalar(Caster, m, m.Player ? 1.25 : 2.00, TimeSpan.FromSeconds(duration)); - - HarmfulSpell(m); - } - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); - } - - public static bool ClearMindRotScalar(Mobile m) - { - if (_table.Remove(m, out var timer)) - { - timer.Stop(); - m.SendLocalizedMessage(1060872); // Your mind feels normal again. - BuffInfo.RemoveBuff(m, BuffIcon.Mindrot); - - return true; - } - - return false; - } - - public static bool HasMindRotScalar(Mobile m) => _table.ContainsKey(m); - - public static bool GetMindRotScalar(Mobile m, ref double scalar) - { - if (_table.TryGetValue(m, out var timer)) - { - scalar = timer._double; - return true; - } - - return false; - } - - public static void SetMindRotScalar(Mobile caster, Mobile target, double scalar, TimeSpan duration) - { - if (!_table.ContainsKey(target)) - { - var timer = new MRExpireTimer(target, scalar, duration); - timer.Start(); - _table[target] = timer; - - BuffInfo.AddBuff(target, new BuffInfo(BuffIcon.Mindrot, 1075665, duration, target)); - target.SendLocalizedMessage(1074384); - } - } } - public class MRExpireTimer : Timer - { - private DateTime _end; - private Mobile _target; - public double _double; + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5); - public MRExpireTimer(Mobile target, double scalar, TimeSpan delay) : base( - TimeSpan.FromSeconds(1.0), - TimeSpan.FromSeconds(1.0) - ) + public override double RequiredSkill => 30.0; + public override int RequiredMana => 17; + + public void Target(Mobile m) + { + if (m == null) { - _double = scalar; - _target = target; - _end = Core.Now + delay; + Caster.SendLocalizedMessage(1060508); // You can't curse that. + } + else if (HasMindRotScalar(m)) + { + Caster.SendLocalizedMessage(1005559); // This spell is already in effect. + } + else if (CheckHSequence(m)) + { + SpellHelper.Turn(Caster, m); + + /* Attempts to place a curse on the Target that increases the mana cost of any spells they cast, + * for a duration based off a comparison between the Caster's Spirit Speak skill and the Target's Resisting Spells skill. + * The effect lasts for ((Spirit Speak skill level - target's Resist Magic skill level) / 50 ) + 20 seconds. + */ + + m.Spell?.OnCasterHurt(); + + m.PlaySound(0x1FB); + m.PlaySound(0x258); + m.FixedParticles(0x373A, 1, 17, 9903, 15, 4, EffectLayer.Head); + + var duration = ((GetDamageSkill(Caster) - GetResistSkill(m)) / 5.0 + 20.0) * (m.Player ? 1.0 : 2.0); + m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain + + SetMindRotScalar(Caster, m, m.Player ? 1.25 : 2.00, TimeSpan.FromSeconds(duration)); + + HarmfulSpell(m); } - protected override void OnTick() + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + public static bool ClearMindRotScalar(Mobile m) + { + if (_table.Remove(m, out var timer)) { - if (_target.Deleted || !_target.Alive || Core.Now >= _end) - { - MindRotSpell.ClearMindRotScalar(_target); - Stop(); - } + timer.Stop(); + m.SendLocalizedMessage(1060872); // Your mind feels normal again. + BuffInfo.RemoveBuff(m, BuffIcon.Mindrot); + + return true; + } + + return false; + } + + public static bool HasMindRotScalar(Mobile m) => _table.ContainsKey(m); + + public static bool GetMindRotScalar(Mobile m, ref double scalar) + { + if (_table.TryGetValue(m, out var timer)) + { + scalar = timer._double; + return true; + } + + return false; + } + + public static void SetMindRotScalar(Mobile caster, Mobile target, double scalar, TimeSpan duration) + { + if (!_table.ContainsKey(target)) + { + var timer = new MRExpireTimer(target, scalar, duration); + timer.Start(); + _table[target] = timer; + + BuffInfo.AddBuff(target, new BuffInfo(BuffIcon.Mindrot, 1075665, duration, target)); + target.SendLocalizedMessage(1074384); } } } + +public class MRExpireTimer : Timer +{ + private DateTime _end; + private Mobile _target; + public double _double; + + public MRExpireTimer(Mobile target, double scalar, TimeSpan delay) : base( + TimeSpan.FromSeconds(1.0), + TimeSpan.FromSeconds(1.0) + ) + { + _double = scalar; + _target = target; + _end = Core.Now + delay; + } + + protected override void OnTick() + { + if (_target.Deleted || !_target.Alive || Core.Now >= _end) + { + MindRotSpell.ClearMindRotScalar(_target); + Stop(); + } + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/NecromancerSpell.cs b/Projects/UOContent/Spells/Necromancy/NecromancerSpell.cs index e1b6c33b5..4c070bf21 100644 --- a/Projects/UOContent/Spells/Necromancy/NecromancerSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/NecromancerSpell.cs @@ -1,47 +1,46 @@ using Server.Items; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public abstract class NecromancerSpell : Spell { - public abstract class NecromancerSpell : Spell + public NecromancerSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { - public NecromancerSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) - { - } - - public abstract double RequiredSkill { get; } - public abstract int RequiredMana { get; } - - public override SkillName CastSkill => SkillName.Necromancy; - public override SkillName DamageSkill => SkillName.SpiritSpeak; - - public override bool ClearHandsOnCast => false; - - // Necromancer spells are not affected by fast cast items, though they are by fast cast recovery - public override double CastDelayFastScalar => Core.SE ? base.CastDelayFastScalar : 0; - - public override int ComputeKarmaAward() - { - // TODO: Verify this formula being that Necro spells don't HAVE a circle. - // int karma = -(70 + (10 * (int)Circle)); - var karma = -(40 + (int)(10 * (CastDelayBase.TotalSeconds / CastDelaySecondsPerTick))); - - // Pub 36: "Added a new property called Increased Karma Loss which grants higher karma loss for casting necromancy spells." - if (Core.ML) - { - karma += AOS.Scale(karma, AosAttributes.GetValue(Caster, AosAttribute.IncreasedKarmaLoss)); - } - - return karma; - } - - public override void GetCastSkills(out double min, out double max) - { - min = RequiredSkill; - max = Scroll != null ? min : RequiredSkill + 40.0; - } - - public override bool ConsumeReagents() => base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, 1); - - public override int GetMana() => RequiredMana; } -} + + public abstract double RequiredSkill { get; } + public abstract int RequiredMana { get; } + + public override SkillName CastSkill => SkillName.Necromancy; + public override SkillName DamageSkill => SkillName.SpiritSpeak; + + public override bool ClearHandsOnCast => false; + + // Necromancer spells are not affected by fast cast items, though they are by fast cast recovery + public override double CastDelayFastScalar => Core.SE ? base.CastDelayFastScalar : 0; + + public override int ComputeKarmaAward() + { + // TODO: Verify this formula being that Necro spells don't HAVE a circle. + // int karma = -(70 + (10 * (int)Circle)); + var karma = -(40 + (int)(10 * (CastDelayBase.TotalSeconds / CastDelaySecondsPerTick))); + + // Pub 36: "Added a new property called Increased Karma Loss which grants higher karma loss for casting necromancy spells." + if (Core.ML) + { + karma += AOS.Scale(karma, AosAttributes.GetValue(Caster, AosAttribute.IncreasedKarmaLoss)); + } + + return karma; + } + + public override void GetCastSkills(out double min, out double max) + { + min = RequiredSkill; + max = Scroll != null ? min : RequiredSkill + 40.0; + } + + public override bool ConsumeReagents() => base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, 1); + + public override int GetMana() => RequiredMana; +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/PainSpike.cs b/Projects/UOContent/Spells/Necromancy/PainSpike.cs index c55cdcb83..a02cf03dd 100644 --- a/Projects/UOContent/Spells/Necromancy/PainSpike.cs +++ b/Projects/UOContent/Spells/Necromancy/PainSpike.cs @@ -3,116 +3,115 @@ using System.Collections.Generic; using Server.Misc; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class PainSpikeSpell : NecromancerSpell, ISpellTargetingMobile { - public class PainSpikeSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Pain Spike", + "In Sar", + 203, + 9031, + Reagent.GraveDust, + Reagent.PigIron + ); + + private static readonly Dictionary _table = new(); + + public PainSpikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Pain Spike", - "In Sar", - 203, - 9031, - Reagent.GraveDust, - Reagent.PigIron - ); + } - private static readonly Dictionary _table = new(); + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); - public PainSpikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override double RequiredSkill => 20.0; + public override int RequiredMana => 5; + + public override bool DelayedDamage => false; + + public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); + + public void Target(Mobile m) + { + if (m == null) { + return; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); - - public override double RequiredSkill => 20.0; - public override int RequiredMana => 5; - - public override bool DelayedDamage => false; - - public static bool UnderEffect(Mobile m) => _table.ContainsKey(m); - - public void Target(Mobile m) + if (CheckHSequence(m)) { - if (m == null) + SpellHelper.Turn(Caster, m); + + // SpellHelper.CheckReflect( (int)this.Circle, Caster, ref m ); //Irrelevent after AoS + + /* Temporarily causes intense physical pain to the target, dealing direct damage. + * After 10 seconds the spell wears off, and if the target is still alive, + * some of the Hit Points lost through Pain Spike are restored. + */ + + m.FixedParticles(0x37C4, 1, 8, 9916, 39, 3, EffectLayer.Head); + m.FixedParticles(0x37C4, 1, 8, 9502, 39, 4, EffectLayer.Head); + m.PlaySound(0x210); + + var damage = Math.Max((GetDamageSkill(Caster) - GetResistSkill(m)) / 10 + (m.Player ? 18 : 30), 1); + m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain + + var buffTime = TimeSpan.FromSeconds(10.0); + + if (!_table.TryGetValue(m, out var timer)) { - return; + _table[m] = timer = new InternalTimer(m, damage); + timer.Start(); + } + else + { + damage = Utility.RandomMinMax(3, 7); + timer.Delay += TimeSpan.FromSeconds(2.0); + buffTime = timer.Next - Core.Now; } - if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); + BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.PainSpike, 1075667, buffTime, m, Convert.ToString((int)damage))); - // SpellHelper.CheckReflect( (int)this.Circle, Caster, ref m ); //Irrelevent after AoS + // TODO: Find a better way to do this + StaminaSystem.DFA = DFAlgorithm.PainSpike; + m.Damage((int)damage, Caster, ignoreEvilOmen: true); + SpellHelper.DoLeech((int)damage, Caster, m); + StaminaSystem.DFA = DFAlgorithm.Standard; - /* Temporarily causes intense physical pain to the target, dealing direct damage. - * After 10 seconds the spell wears off, and if the target is still alive, - * some of the Hit Points lost through Pain Spike are restored. - */ - - m.FixedParticles(0x37C4, 1, 8, 9916, 39, 3, EffectLayer.Head); - m.FixedParticles(0x37C4, 1, 8, 9502, 39, 4, EffectLayer.Head); - m.PlaySound(0x210); - - var damage = Math.Max((GetDamageSkill(Caster) - GetResistSkill(m)) / 10 + (m.Player ? 18 : 30), 1); - m.CheckSkill(SkillName.MagicResist, 0.0, 120.0); // Skill check for gain - - var buffTime = TimeSpan.FromSeconds(10.0); - - if (!_table.TryGetValue(m, out var timer)) - { - _table[m] = timer = new InternalTimer(m, damage); - timer.Start(); - } - else - { - damage = Utility.RandomMinMax(3, 7); - timer.Delay += TimeSpan.FromSeconds(2.0); - buffTime = timer.Next - Core.Now; - } - - BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.PainSpike, 1075667, buffTime, m, Convert.ToString((int)damage))); - - // TODO: Find a better way to do this - StaminaSystem.DFA = DFAlgorithm.PainSpike; - m.Damage((int)damage, Caster, ignoreEvilOmen: true); - SpellHelper.DoLeech((int)damage, Caster, m); - StaminaSystem.DFA = DFAlgorithm.Standard; - - // SpellHelper.Damage( this, m, damage, 100, 0, 0, 0, 0, Misc.DFAlgorithm.PainSpike ); - HarmfulSpell(m); - } - - FinishSequence(); + // SpellHelper.Damage( this, m, damage, 100, 0, 0, 0, 0, Misc.DFAlgorithm.PainSpike ); + HarmfulSpell(m); } - public override void OnCast() + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + private class InternalTimer : Timer + { + private Mobile _mobile; + private int _toRestore; + + public InternalTimer(Mobile m, double toRestore) : base(TimeSpan.FromSeconds(10.0)) { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + + _mobile = m; + _toRestore = (int)toRestore; } - private class InternalTimer : Timer + protected override void OnTick() { - private Mobile _mobile; - private int _toRestore; + _table.Remove(_mobile); - public InternalTimer(Mobile m, double toRestore) : base(TimeSpan.FromSeconds(10.0)) + if (_mobile.Alive && !_mobile.IsDeadBondedPet) { - - _mobile = m; - _toRestore = (int)toRestore; + _mobile.Hits += _toRestore; } - protected override void OnTick() - { - _table.Remove(_mobile); - - if (_mobile.Alive && !_mobile.IsDeadBondedPet) - { - _mobile.Hits += _toRestore; - } - - BuffInfo.RemoveBuff(_mobile, BuffIcon.PainSpike); - } + BuffInfo.RemoveBuff(_mobile, BuffIcon.PainSpike); } } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/PoisonStrike.cs b/Projects/UOContent/Spells/Necromancy/PoisonStrike.cs index 5061f535c..c561c35ba 100644 --- a/Projects/UOContent/Spells/Necromancy/PoisonStrike.cs +++ b/Projects/UOContent/Spells/Necromancy/PoisonStrike.cs @@ -4,151 +4,150 @@ using Server.Items; using Server.Mobiles; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class PoisonStrikeSpell : NecromancerSpell, ISpellTargetingMobile { - public class PoisonStrikeSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Poison Strike", + "In Vas Nox", + 203, + 9031, + Reagent.NoxCrystal + ); + + public PoisonStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Poison Strike", - "In Vas Nox", - 203, - 9031, - Reagent.NoxCrystal - ); + } - public PoisonStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(Core.ML ? 1.75 : 1.5); + + public override double RequiredSkill => 50.0; + public override int RequiredMana => 17; + + public override bool DelayedDamage => false; + + public void Target(Mobile m) + { + if (CheckHSequence(m)) { - } + SpellHelper.Turn(Caster, m); - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(Core.ML ? 1.75 : 1.5); + /* Creates a blast of poisonous energy centered on the target. + * The main target is inflicted with a large amount of Poison damage, and all valid targets in a radius of 2 tiles around the main target are inflicted with a lesser effect. + * One tile from main target receives 50% damage, two tiles from target receives 33% damage. + */ - public override double RequiredSkill => 50.0; - public override int RequiredMana => 17; + // CheckResisted( m ); + // Check magic resist for skill, but do not use return value + // reports from OSI: Necro spells don't give Resist gain - public override bool DelayedDamage => false; + var map = m.Map; - public void Target(Mobile m) - { - if (CheckHSequence(m)) + if (map != null) { - SpellHelper.Turn(Caster, m); + Effects.SendLocationParticles( + EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), + 0x36B0, + 1, + 14, + 63, + 7, + 9915, + 0 + ); + Effects.PlaySound(m.Location, m.Map, 0x229); - /* Creates a blast of poisonous energy centered on the target. - * The main target is inflicted with a large amount of Poison damage, and all valid targets in a radius of 2 tiles around the main target are inflicted with a lesser effect. - * One tile from main target receives 50% damage, two tiles from target receives 33% damage. - */ + var damage = Utility.RandomMinMax(Core.ML ? 32 : 36, 40) * ((300 + GetDamageSkill(Caster) * 9) / 1000); - // CheckResisted( m ); - // Check magic resist for skill, but do not use return value - // reports from OSI: Necro spells don't give Resist gain + var sdiBonus = (double)AosAttributes.GetValue(Caster, AosAttribute.SpellDamage) / 100; + var pvmDamage = damage * (1 + sdiBonus); - var map = m.Map; - - if (map != null) + if (Core.ML && sdiBonus > 0.15) { - Effects.SendLocationParticles( - EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), - 0x36B0, - 1, - 14, - 63, - 7, - 9915, + sdiBonus = 0.15; + } + + var pvpDamage = damage * (1 + sdiBonus); + + using var pool = PooledRefQueue.Create(); + + if (Caster.CanBeHarmful(m, false)) + { + pool.Enqueue(m); + } + + var cbc = Caster as BaseCreature; + var isMonster = cbc?.Controlled == false && (cbc.IsAnimatedDead || !cbc.Summoned); + + foreach (Mobile targ in m.GetMobilesInRange(2)) + { + if (targ == Caster || m == targ || !SpellHelper.ValidIndirectTarget(Caster, targ) + || !Caster.CanBeHarmful(targ, false)) + { + continue; + } + + if (isMonster && targ.Player) + { + continue; + } + + // Animate dead casting poison strike shouldn't hit: familiars or player or pets + if (targ is BaseCreature bc) + { + if (bc.IsAnimatedDead) + { + continue; + } + + if (isMonster && (bc.Controlled || bc.Summoned || bc.Team == cbc.Team || bc.IsNecroFamiliar)) + { + continue; + } + } + + pool.Enqueue(targ); + } + + while (pool.Count > 0) + { + var targ = pool.Dequeue(); + int num; + + if (targ.InRange(m.Location, 0)) + { + num = 1; + } + else if (targ.InRange(m.Location, 1)) + { + num = 2; + } + else + { + num = 3; + } + + Caster.DoHarmful(targ); + SpellHelper.Damage( + this, + targ, + (m.Player && Caster.Player ? pvpDamage : pvmDamage) / num, + 0, + 0, + 0, + 100, 0 ); - Effects.PlaySound(m.Location, m.Map, 0x229); - - var damage = Utility.RandomMinMax(Core.ML ? 32 : 36, 40) * ((300 + GetDamageSkill(Caster) * 9) / 1000); - - var sdiBonus = (double)AosAttributes.GetValue(Caster, AosAttribute.SpellDamage) / 100; - var pvmDamage = damage * (1 + sdiBonus); - - if (Core.ML && sdiBonus > 0.15) - { - sdiBonus = 0.15; - } - - var pvpDamage = damage * (1 + sdiBonus); - - using var pool = PooledRefQueue.Create(); - - if (Caster.CanBeHarmful(m, false)) - { - pool.Enqueue(m); - } - - var cbc = Caster as BaseCreature; - var isMonster = cbc?.Controlled == false && (cbc.IsAnimatedDead || !cbc.Summoned); - - foreach (Mobile targ in m.GetMobilesInRange(2)) - { - if (targ == Caster || m == targ || !SpellHelper.ValidIndirectTarget(Caster, targ) - || !Caster.CanBeHarmful(targ, false)) - { - continue; - } - - if (isMonster && targ.Player) - { - continue; - } - - // Animate dead casting poison strike shouldn't hit: familiars or player or pets - if (targ is BaseCreature bc) - { - if (bc.IsAnimatedDead) - { - continue; - } - - if (isMonster && (bc.Controlled || bc.Summoned || bc.Team == cbc.Team || bc.IsNecroFamiliar)) - { - continue; - } - } - - pool.Enqueue(targ); - } - - while (pool.Count > 0) - { - var targ = pool.Dequeue(); - int num; - - if (targ.InRange(m.Location, 0)) - { - num = 1; - } - else if (targ.InRange(m.Location, 1)) - { - num = 2; - } - else - { - num = 3; - } - - Caster.DoHarmful(targ); - SpellHelper.Damage( - this, - targ, - (m.Player && Caster.Player ? pvpDamage : pvmDamage) / num, - 0, - 0, - 0, - 100, - 0 - ); - } } } - - FinishSequence(); } - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); - } + FinishSequence(); } -} + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/Strangle.cs b/Projects/UOContent/Spells/Necromancy/Strangle.cs index 289726e5e..1d90ee364 100644 --- a/Projects/UOContent/Spells/Necromancy/Strangle.cs +++ b/Projects/UOContent/Spells/Necromancy/Strangle.cs @@ -2,229 +2,228 @@ using System; using System.Collections.Generic; using Server.Targeting; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class StrangleSpell : NecromancerSpell, ISpellTargetingMobile { - public class StrangleSpell : NecromancerSpell, ISpellTargetingMobile + private static readonly SpellInfo _info = new( + "Strangle", + "In Bal Nox", + 209, + 9031, + Reagent.DaemonBlood, + Reagent.NoxCrystal + ); + + private static readonly Dictionary _table = new(); + + public StrangleSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Strangle", - "In Bal Nox", - 209, - 9031, - Reagent.DaemonBlood, - Reagent.NoxCrystal - ); + } - private static readonly Dictionary _table = new(); + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - public StrangleSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override double RequiredSkill => 65.0; + public override int RequiredMana => 29; + + public void Target(Mobile m) + { + if (m == null) { + return; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 65.0; - public override int RequiredMana => 29; - - public void Target(Mobile m) + if (CheckHSequence(m)) { - if (m == null) + SpellHelper.Turn(Caster, m); + + // SpellHelper.CheckReflect( (int)this.Circle, Caster, ref m ); + // Irrelevant after AoS + + /* Temporarily chokes off the air supply of the target with poisonous fumes. + * The target is inflicted with poison damage over time. + * The amount of damage dealt each "hit" is based off of the caster's Spirit Speak skill and the Target's current Stamina. + * The less Stamina the target has, the more damage is done by Strangle. + * Duration of the effect is Spirit Speak skill level / 10 rounds, with a minimum number of 4 rounds. + * The first round of damage is dealt after 5 seconds, and every next round after that comes 1 second sooner than the one before, until there is only 1 second between rounds. + * The base damage of the effect lies between (Spirit Speak skill level / 10) - 2 and (Spirit Speak skill level / 10) + 1. + * Base damage is multiplied by the following formula: (3 - (target's current Stamina / target's maximum Stamina) * 2). + * Example: + * For a target at full Stamina the damage multiplier is 1, + * for a target at 50% Stamina the damage multiplier is 2 and + * for a target at 20% Stamina the damage multiplier is 2.6 + */ + + m.Spell?.OnCasterHurt(); + + m.PlaySound(0x22F); + m.FixedParticles(0x36CB, 1, 9, 9911, 67, 5, EffectLayer.Head); + m.FixedParticles(0x374A, 1, 17, 9502, 1108, 4, (EffectLayer)255); + + // According to testing on OSI, it is refreshed. + if (_table.TryGetValue(m, out var timer)) + { + timer.Stop(); + } + + timer = new InternalTimer(m, Caster); + _table[m] = timer; + timer.Start(); + + HarmfulSpell(m); + } + + // Calculations for the buff bar + var spiritlevel = Math.Max(4, Caster.Skills.SpiritSpeak.Value / 10); + + const int minDamage = 4; + var maxDamage = ((int)spiritlevel + 1) * 3; + var args = $"{minDamage}\t{maxDamage}"; + + var count = (int)spiritlevel; + var maxCount = count; + var hitDelay = 5; + var length = hitDelay; + + while (count >= 1) + { + --count; + if (hitDelay > 1) + { + if (maxCount < 5) + { + --hitDelay; + } + else + { + var delay = (int)Math.Ceiling((1.0 + 5 * count) / maxCount); + + hitDelay = delay <= 5 ? delay : 5; + } + } + + length += hitDelay; + } + + var t_Duration = TimeSpan.FromSeconds(length); + BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.Strangle, 1075794, 1075795, t_Duration, m, args)); + + FinishSequence(); + } + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + public static bool RemoveCurse(Mobile m) + { + if (!_table.Remove(m, out var timer)) + { + return false; + } + + timer.Stop(); + m.SendLocalizedMessage(1061687); // You can breath normally again. + return true; + } + + private class InternalTimer : Timer + { + private Mobile _from; + private double _maxBaseDamage; + private int _maxCount; + private double _minBaseDamage; + private Mobile _target; + private int _count; + private int _hitDelay; + private DateTime _nextHit; + + public InternalTimer(Mobile target, Mobile from) : base(TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(0.1)) + { + + _target = target; + _from = from; + + var spiritLevel = from.Skills.SpiritSpeak.Value / 10; + + _minBaseDamage = spiritLevel - 2; + _maxBaseDamage = spiritLevel + 1; + + _hitDelay = 5; + _nextHit = Core.Now + TimeSpan.FromSeconds(_hitDelay); + + _maxCount = _count = Math.Max(4, (int)spiritLevel); + } + + protected override void OnTick() + { + if (!_target.Alive) + { + _table.Remove(_target); + Stop(); + } + + if (!_target.Alive || Core.Now < _nextHit) { return; } - if (CheckHSequence(m)) + --_count; + + if (_hitDelay > 1) { - SpellHelper.Turn(Caster, m); - - // SpellHelper.CheckReflect( (int)this.Circle, Caster, ref m ); - // Irrelevant after AoS - - /* Temporarily chokes off the air supply of the target with poisonous fumes. - * The target is inflicted with poison damage over time. - * The amount of damage dealt each "hit" is based off of the caster's Spirit Speak skill and the Target's current Stamina. - * The less Stamina the target has, the more damage is done by Strangle. - * Duration of the effect is Spirit Speak skill level / 10 rounds, with a minimum number of 4 rounds. - * The first round of damage is dealt after 5 seconds, and every next round after that comes 1 second sooner than the one before, until there is only 1 second between rounds. - * The base damage of the effect lies between (Spirit Speak skill level / 10) - 2 and (Spirit Speak skill level / 10) + 1. - * Base damage is multiplied by the following formula: (3 - (target's current Stamina / target's maximum Stamina) * 2). - * Example: - * For a target at full Stamina the damage multiplier is 1, - * for a target at 50% Stamina the damage multiplier is 2 and - * for a target at 20% Stamina the damage multiplier is 2.6 - */ - - m.Spell?.OnCasterHurt(); - - m.PlaySound(0x22F); - m.FixedParticles(0x36CB, 1, 9, 9911, 67, 5, EffectLayer.Head); - m.FixedParticles(0x374A, 1, 17, 9502, 1108, 4, (EffectLayer)255); - - // According to testing on OSI, it is refreshed. - if (_table.TryGetValue(m, out var timer)) + if (_maxCount < 5) { - timer.Stop(); - } - - timer = new InternalTimer(m, Caster); - _table[m] = timer; - timer.Start(); - - HarmfulSpell(m); - } - - // Calculations for the buff bar - var spiritlevel = Math.Max(4, Caster.Skills.SpiritSpeak.Value / 10); - - const int minDamage = 4; - var maxDamage = ((int)spiritlevel + 1) * 3; - var args = $"{minDamage}\t{maxDamage}"; - - var count = (int)spiritlevel; - var maxCount = count; - var hitDelay = 5; - var length = hitDelay; - - while (count >= 1) - { - --count; - if (hitDelay > 1) - { - if (maxCount < 5) - { - --hitDelay; - } - else - { - var delay = (int)Math.Ceiling((1.0 + 5 * count) / maxCount); - - hitDelay = delay <= 5 ? delay : 5; - } - } - - length += hitDelay; - } - - var t_Duration = TimeSpan.FromSeconds(length); - BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.Strangle, 1075794, 1075795, t_Duration, m, args)); - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); - } - - public static bool RemoveCurse(Mobile m) - { - if (!_table.Remove(m, out var timer)) - { - return false; - } - - timer.Stop(); - m.SendLocalizedMessage(1061687); // You can breath normally again. - return true; - } - - private class InternalTimer : Timer - { - private Mobile _from; - private double _maxBaseDamage; - private int _maxCount; - private double _minBaseDamage; - private Mobile _target; - private int _count; - private int _hitDelay; - private DateTime _nextHit; - - public InternalTimer(Mobile target, Mobile from) : base(TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(0.1)) - { - - _target = target; - _from = from; - - var spiritLevel = from.Skills.SpiritSpeak.Value / 10; - - _minBaseDamage = spiritLevel - 2; - _maxBaseDamage = spiritLevel + 1; - - _hitDelay = 5; - _nextHit = Core.Now + TimeSpan.FromSeconds(_hitDelay); - - _maxCount = _count = Math.Max(4, (int)spiritLevel); - } - - protected override void OnTick() - { - if (!_target.Alive) - { - _table.Remove(_target); - Stop(); - } - - if (!_target.Alive || Core.Now < _nextHit) - { - return; - } - - --_count; - - if (_hitDelay > 1) - { - if (_maxCount < 5) - { - --_hitDelay; - } - else - { - var delay = (int)Math.Ceiling((1.0 + 5 * _count) / _maxCount); - - if (delay <= 5) - { - _hitDelay = delay; - } - else - { - _hitDelay = 5; - } - } - } - - if (_count == 0) - { - _target.SendLocalizedMessage(1061687); // You can breath normally again. - _table.Remove(_target); - Stop(); + --_hitDelay; } else { - _nextHit = Core.Now + TimeSpan.FromSeconds(_hitDelay); + var delay = (int)Math.Ceiling((1.0 + 5 * _count) / _maxCount); - var damage = _minBaseDamage + Utility.RandomDouble() * (_maxBaseDamage - _minBaseDamage); - - damage *= 3 - (double)_target.Stam / _target.StamMax * 2; - - if (damage < 1) + if (delay <= 5) { - damage = 1; + _hitDelay = delay; } - - if (!_target.Player) + else { - damage *= 1.75; + _hitDelay = 5; } + } + } - AOS.Damage(_target, _from, (int)damage, 0, 0, 0, 100, 0); + if (_count == 0) + { + _target.SendLocalizedMessage(1061687); // You can breath normally again. + _table.Remove(_target); + Stop(); + } + else + { + _nextHit = Core.Now + TimeSpan.FromSeconds(_hitDelay); - // OSI: randomly revealed between first and third damage tick, guessing 60% chance - if (Utility.RandomDouble() < 0.40) - { - _target.RevealingAction(); - } + var damage = _minBaseDamage + Utility.RandomDouble() * (_maxBaseDamage - _minBaseDamage); + + damage *= 3 - (double)_target.Stam / _target.StamMax * 2; + + if (damage < 1) + { + damage = 1; + } + + if (!_target.Player) + { + damage *= 1.75; + } + + AOS.Damage(_target, _from, (int)damage, 0, 0, 0, 100, 0); + + // OSI: randomly revealed between first and third damage tick, guessing 60% chance + if (Utility.RandomDouble() < 0.40) + { + _target.RevealingAction(); } } } } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/SummonFamiliar.cs b/Projects/UOContent/Spells/Necromancy/SummonFamiliar.cs index a1744c691..2fba42cc2 100644 --- a/Projects/UOContent/Spells/Necromancy/SummonFamiliar.cs +++ b/Projects/UOContent/Spells/Necromancy/SummonFamiliar.cs @@ -4,203 +4,218 @@ using Server.Gumps; using Server.Mobiles; using Server.Network; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class SummonFamiliarSpell : NecromancerSpell { - public class SummonFamiliarSpell : NecromancerSpell + private static readonly SpellInfo _info = new( + "Summon Familiar", + "Kal Xen Bal", + 203, + 9031, + Reagent.BatWing, + Reagent.GraveDust, + Reagent.DaemonBlood + ); + + public SummonFamiliarSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Summon Familiar", - "Kal Xen Bal", - 203, - 9031, - Reagent.BatWing, - Reagent.GraveDust, - Reagent.DaemonBlood - ); + } - public SummonFamiliarSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 30.0; + public override int RequiredMana => 17; + + public static Dictionary Table { get; } = new(); + + public static SummonFamiliarEntry[] Entries { get; } = + { + new(typeof(HordeMinionFamiliar), 1060146, 30.0, 30.0), // Horde Minion + new(typeof(ShadowWispFamiliar), 1060142, 50.0, 50.0), // Shadow Wisp + new(typeof(DarkWolfFamiliar), 1060143, 60.0, 60.0), // Dark Wolf + new(typeof(DeathAdder), 1060145, 80.0, 80.0), // Death Adder + new(typeof(VampireBatFamiliar), 1060144, 100.0, 100.0) // Vampire Bat + }; + + public static void RemoveEffects(Mobile m) + { + if (Table.Remove(m, out var summon)) { + summon.Delete(); } + } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 30.0; - public override int RequiredMana => 17; - - public static Dictionary Table { get; } = new(); - - public static SummonFamiliarEntry[] Entries { get; } = + public static void Unregister(Mobile master, Mobile summoned) + { + if (master != null && Table.TryGetValue(master, out var summon) && summon == summoned) { - new(typeof(HordeMinionFamiliar), 1060146, 30.0, 30.0), // Horde Minion - new(typeof(ShadowWispFamiliar), 1060142, 50.0, 50.0), // Shadow Wisp - new(typeof(DarkWolfFamiliar), 1060143, 60.0, 60.0), // Dark Wolf - new(typeof(DeathAdder), 1060145, 80.0, 80.0), // Death Adder - new(typeof(VampireBatFamiliar), 1060144, 100.0, 100.0) // Vampire Bat - }; + Table.Remove(master); + } + } - public override bool CheckCast() + public override bool CheckCast() + { + if (Table.GetValueOrDefault(Caster)?.Deleted == false) { - if (!(Table.TryGetValue(Caster, out var check) && check?.Deleted == false)) - { - return base.CheckCast(); - } - Caster.SendLocalizedMessage(1061605); // You already have a familiar. return false; } - public override void OnCast() - { - if (CheckSequence()) - { - Caster.CloseGump(); - Caster.SendGump(new SummonFamiliarGump(Caster, Entries, this)); - } + return base.CheckCast(); + } - FinishSequence(); + public override void OnCast() + { + if (CheckSequence()) + { + Caster.CloseGump(); + Caster.SendGump(new SummonFamiliarGump(Caster, Entries, this)); + } + + FinishSequence(); + } +} + +public class SummonFamiliarEntry +{ + public SummonFamiliarEntry(Type type, object name, double reqNecromancy, double reqSpiritSpeak) + { + Type = type; + Name = name; + ReqNecromancy = reqNecromancy; + ReqSpiritSpeak = reqSpiritSpeak; + } + + public Type Type { get; } + + public object Name { get; } + + public double ReqNecromancy { get; } + + public double ReqSpiritSpeak { get; } +} + +public class SummonFamiliarGump : Gump +{ + private const int EnabledColor16 = 0x0F20; + private const int DisabledColor16 = 0x262A; + + private const int EnabledColor32 = 0x18CD00; + private const int DisabledColor32 = 0x4A8B52; + + private readonly SummonFamiliarEntry[] _entries; + private readonly Mobile _from; + + private readonly SummonFamiliarSpell _spell; + + public SummonFamiliarGump(Mobile from, SummonFamiliarEntry[] entries, SummonFamiliarSpell spell) : base(200, 100) + { + _from = from; + _entries = entries; + _spell = spell; + + AddPage(0); + + AddBackground(10, 10, 250, 178, 9270); + AddAlphaRegion(20, 20, 230, 158); + + AddImage(220, 20, 10464); + AddImage(220, 72, 10464); + AddImage(220, 124, 10464); + + AddItem(188, 16, 6883); + AddItem(198, 168, 6881); + AddItem(8, 15, 6882); + AddItem(2, 168, 6880); + + AddHtmlLocalized(30, 26, 200, 20, 1060147, EnabledColor16); // Chose thy familiar... + + var necro = from.Skills.Necromancy.Value; + var spirit = from.Skills.SpiritSpeak.Value; + + for (var i = 0; i < entries.Length; ++i) + { + var entry = entries[i]; + var name = entry.Name; + + var enabled = necro >= entry.ReqNecromancy && spirit >= entry.ReqSpiritSpeak; + + AddButton(27, 53 + i * 21, 9702, 9703, i + 1); + + if (name is int intName) + { + AddHtmlLocalized(50, 51 + i * 21, 150, 20, intName, enabled ? EnabledColor16 : DisabledColor16); + } + else if (name is string strName) + { + AddHtml( + 50, + 51 + i * 21, + 150, + 20, + strName.Color(enabled ? EnabledColor32 : DisabledColor32) + ); + } } } - public class SummonFamiliarEntry + public override void OnResponse(NetState sender, in RelayInfo info) { - public SummonFamiliarEntry(Type type, object name, double reqNecromancy, double reqSpiritSpeak) + var index = info.ButtonID - 1; + + if (index < 0 || index >= _entries.Length) { - Type = type; - Name = name; - ReqNecromancy = reqNecromancy; - ReqSpiritSpeak = reqSpiritSpeak; + _from.SendLocalizedMessage(1061825); // You decide not to summon a familiar. + return; } - public Type Type { get; } + var entry = _entries[index]; - public object Name { get; } + var necro = _from.Skills.Necromancy.Value; + var spirit = _from.Skills.SpiritSpeak.Value; - public double ReqNecromancy { get; } - - public double ReqSpiritSpeak { get; } - } - - public class SummonFamiliarGump : Gump - { - private const int EnabledColor16 = 0x0F20; - private const int DisabledColor16 = 0x262A; - - private const int EnabledColor32 = 0x18CD00; - private const int DisabledColor32 = 0x4A8B52; - - private readonly SummonFamiliarEntry[] m_Entries; - private readonly Mobile m_From; - - private readonly SummonFamiliarSpell m_Spell; - - public SummonFamiliarGump(Mobile from, SummonFamiliarEntry[] entries, SummonFamiliarSpell spell) : base(200, 100) + if ((_from as PlayerMobile)?.DuelContext?.AllowSpellCast(_from, _spell) == false) { - m_From = from; - m_Entries = entries; - m_Spell = spell; - - AddPage(0); - - AddBackground(10, 10, 250, 178, 9270); - AddAlphaRegion(20, 20, 230, 158); - - AddImage(220, 20, 10464); - AddImage(220, 72, 10464); - AddImage(220, 124, 10464); - - AddItem(188, 16, 6883); - AddItem(198, 168, 6881); - AddItem(8, 15, 6882); - AddItem(2, 168, 6880); - - AddHtmlLocalized(30, 26, 200, 20, 1060147, EnabledColor16); // Chose thy familiar... - - var necro = from.Skills.Necromancy.Value; - var spirit = from.Skills.SpiritSpeak.Value; - - for (var i = 0; i < entries.Length; ++i) - { - var name = entries[i].Name; - - var enabled = necro >= entries[i].ReqNecromancy && spirit >= entries[i].ReqSpiritSpeak; - - AddButton(27, 53 + i * 21, 9702, 9703, i + 1); - - if (name is int intName) - { - AddHtmlLocalized(50, 51 + i * 21, 150, 20, intName, enabled ? EnabledColor16 : DisabledColor16); - } - else if (name is string strName) - { - AddHtml( - 50, - 51 + i * 21, - 150, - 20, - strName.Color(enabled ? EnabledColor32 : DisabledColor32) - ); - } - } } - - public override void OnResponse(NetState sender, in RelayInfo info) + else if (SummonFamiliarSpell.Table.TryGetValue(_from, out var check) && check?.Deleted == false) { - var index = info.ButtonID - 1; + _from.SendLocalizedMessage(1061605); // You already have a familiar. + } + else if (necro < entry.ReqNecromancy || spirit < entry.ReqSpiritSpeak) + { + // That familiar requires ~1_NECROMANCY~ Necromancy and ~2_SPIRIT~ Spirit Speak. + _from.SendLocalizedMessage(1061606, $"{entry.ReqNecromancy:F1}\t{entry.ReqSpiritSpeak:F1}"); - if (index >= 0 && index < m_Entries.Length) + _from.CloseGump(); + _from.SendGump(new SummonFamiliarGump(_from, SummonFamiliarSpell.Entries, _spell)); + } + else if (entry.Type == null) + { + _from.SendMessage("That familiar has not yet been defined."); + + _from.CloseGump(); + _from.SendGump(new SummonFamiliarGump(_from, SummonFamiliarSpell.Entries, _spell)); + } + else + { + try { - var entry = m_Entries[index]; + var bc = entry.Type.CreateInstance(); - var necro = m_From.Skills.Necromancy.Value; - var spirit = m_From.Skills.SpiritSpeak.Value; + // TODO: Is this right? + bc.Skills.MagicResist.Base = _from.Skills.MagicResist.Base; - if ((m_From as PlayerMobile)?.DuelContext?.AllowSpellCast(m_From, m_Spell) == false) + if (BaseCreature.Summon(bc, _from, _from.Location, -1, TimeSpan.FromDays(1.0))) { - } - else if (SummonFamiliarSpell.Table.TryGetValue(m_From, out var check) && check?.Deleted == false) - { - m_From.SendLocalizedMessage(1061605); // You already have a familiar. - } - else if (necro < entry.ReqNecromancy || spirit < entry.ReqSpiritSpeak) - { - // That familiar requires ~1_NECROMANCY~ Necromancy and ~2_SPIRIT~ Spirit Speak. - m_From.SendLocalizedMessage(1061606, $"{entry.ReqNecromancy:F1}\t{entry.ReqSpiritSpeak:F1}"); - - m_From.CloseGump(); - m_From.SendGump(new SummonFamiliarGump(m_From, SummonFamiliarSpell.Entries, m_Spell)); - } - else if (entry.Type == null) - { - m_From.SendMessage("That familiar has not yet been defined."); - - m_From.CloseGump(); - m_From.SendGump(new SummonFamiliarGump(m_From, SummonFamiliarSpell.Entries, m_Spell)); - } - else - { - try - { - var bc = entry.Type.CreateInstance(); - - // TODO: Is this right? - bc.Skills.MagicResist.Base = m_From.Skills.MagicResist.Base; - - if (BaseCreature.Summon(bc, m_From, m_From.Location, -1, TimeSpan.FromDays(1.0))) - { - m_From.FixedParticles(0x3728, 1, 10, 9910, EffectLayer.Head); - bc.PlaySound(bc.GetIdleSound()); - SummonFamiliarSpell.Table[m_From] = bc; - } - } - catch - { - // ignored - } + _from.FixedParticles(0x3728, 1, 10, 9910, EffectLayer.Head); + bc.PlaySound(bc.GetIdleSound()); + SummonFamiliarSpell.Table[_from] = bc; } } - else + catch { - m_From.SendLocalizedMessage(1061825); // You decide not to summon a familiar. + // ignored } } } diff --git a/Projects/UOContent/Spells/Necromancy/TransformationSpell.cs b/Projects/UOContent/Spells/Necromancy/TransformationSpell.cs index 4647fbd6f..eade0ccf2 100644 --- a/Projects/UOContent/Spells/Necromancy/TransformationSpell.cs +++ b/Projects/UOContent/Spells/Necromancy/TransformationSpell.cs @@ -1,42 +1,41 @@ -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public abstract class TransformationSpell : NecromancerSpell, ITransformationSpell { - public abstract class TransformationSpell : NecromancerSpell, ITransformationSpell + public TransformationSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { - public TransformationSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) - { - } - - public override bool BlockedByHorrificBeast => false; - public abstract int Body { get; } - public virtual int Hue => 0; - - public virtual int PhysResistOffset => 0; - public virtual int FireResistOffset => 0; - public virtual int ColdResistOffset => 0; - public virtual int PoisResistOffset => 0; - public virtual int NrgyResistOffset => 0; - - public virtual double TickRate => 1.0; - - public virtual void OnTick(Mobile m) - { - } - - public virtual void DoEffect(Mobile m) - { - } - - public virtual void RemoveEffect(Mobile m) - { - } - - public override bool CheckCast() => TransformationSpellHelper.CheckCast(Caster, this) && base.CheckCast(); - - public override void OnCast() - { - TransformationSpellHelper.OnCast(Caster, this); - - FinishSequence(); - } } -} + + public override bool BlockedByHorrificBeast => false; + public abstract int Body { get; } + public virtual int Hue => 0; + + public virtual int PhysResistOffset => 0; + public virtual int FireResistOffset => 0; + public virtual int ColdResistOffset => 0; + public virtual int PoisResistOffset => 0; + public virtual int NrgyResistOffset => 0; + + public virtual double TickRate => 1.0; + + public virtual void OnTick(Mobile m) + { + } + + public virtual void DoEffect(Mobile m) + { + } + + public virtual void RemoveEffect(Mobile m) + { + } + + public override bool CheckCast() => TransformationSpellHelper.CheckCast(Caster, this) && base.CheckCast(); + + public override void OnCast() + { + TransformationSpellHelper.OnCast(Caster, this); + + FinishSequence(); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/VampiricEmbrace.cs b/Projects/UOContent/Spells/Necromancy/VampiricEmbrace.cs index 31baa8533..f4a50d826 100644 --- a/Projects/UOContent/Spells/Necromancy/VampiricEmbrace.cs +++ b/Projects/UOContent/Spells/Necromancy/VampiricEmbrace.cs @@ -1,70 +1,69 @@ using System; using Server.Items; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class VampiricEmbraceSpell : TransformationSpell { - public class VampiricEmbraceSpell : TransformationSpell + private static readonly SpellInfo _info = new( + "Vampiric Embrace", + "Rel Xen An Sanct", + 203, + 9031, + Reagent.BatWing, + Reagent.NoxCrystal, + Reagent.PigIron + ); + + public VampiricEmbraceSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Vampiric Embrace", - "Rel Xen An Sanct", - 203, - 9031, - Reagent.BatWing, - Reagent.NoxCrystal, - Reagent.PigIron - ); + } - public VampiricEmbraceSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 99.0; + public override int RequiredMana => 23; + + public override int Body => Caster.Female ? 745 : 744; + public override int Hue => 0x847E; + + public override int FireResistOffset => -25; + + public override void GetCastSkills(out double min, out double max) + { + if (Caster.Skills[CastSkill].Value >= RequiredSkill) { + min = 80.0; + max = 120.0; } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 99.0; - public override int RequiredMana => 23; - - public override int Body => Caster.Female ? 745 : 744; - public override int Hue => 0x847E; - - public override int FireResistOffset => -25; - - public override void GetCastSkills(out double min, out double max) + else { - if (Caster.Skills[CastSkill].Value >= RequiredSkill) - { - min = 80.0; - max = 120.0; - } - else - { - base.GetCastSkills(out min, out max); - } - } - - public override void DoEffect(Mobile m) - { - Effects.SendLocationParticles( - EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), - 0x373A, - 1, - 17, - 1108, - 7, - 9914, - 0 - ); - Effects.SendLocationParticles( - EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), - 0x376A, - 1, - 22, - 67, - 7, - 9502, - 0 - ); - Effects.PlaySound(m.Location, m.Map, 0x4B1); + base.GetCastSkills(out min, out max); } } -} + + public override void DoEffect(Mobile m) + { + Effects.SendLocationParticles( + EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), + 0x373A, + 1, + 17, + 1108, + 7, + 9914, + 0 + ); + Effects.SendLocationParticles( + EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), + 0x376A, + 1, + 22, + 67, + 7, + 9502, + 0 + ); + Effects.PlaySound(m.Location, m.Map, 0x4B1); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/VengefulSpirit.cs b/Projects/UOContent/Spells/Necromancy/VengefulSpirit.cs index ea6428845..93806ef43 100644 --- a/Projects/UOContent/Spells/Necromancy/VengefulSpirit.cs +++ b/Projects/UOContent/Spells/Necromancy/VengefulSpirit.cs @@ -2,55 +2,55 @@ using System; using Server.Mobiles; using Server.Targeting; -namespace Server.Spells.Necromancy -{ - public class VengefulSpiritSpell : NecromancerSpell, ISpellTargetingMobile - { - private static readonly SpellInfo _info = new( - "Vengeful Spirit", - "Kal Xen Bal Beh", - 203, - 9031, - Reagent.BatWing, - Reagent.GraveDust, - Reagent.PigIron - ); +namespace Server.Spells.Necromancy; - public VengefulSpiritSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) +public class VengefulSpiritSpell : NecromancerSpell, ISpellTargetingMobile +{ + private static readonly SpellInfo _info = new( + "Vengeful Spirit", + "Kal Xen Bal Beh", + 203, + 9031, + Reagent.BatWing, + Reagent.GraveDust, + Reagent.PigIron + ); + + public VengefulSpiritSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + { + } + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 80.0; + public override int RequiredMana => 41; + + public void Target(Mobile m) + { + if (m == null) { + return; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 80.0; - public override int RequiredMana => 41; - - public void Target(Mobile m) + if (Caster == m) { - if (m == null) - { - return; - } + Caster.SendLocalizedMessage(1061832); // You cannot exact vengeance on yourself. + } + else if (CheckHSequence(m)) + { + SpellHelper.Turn(Caster, m); - if (Caster == m) - { - Caster.SendLocalizedMessage(1061832); // You cannot exact vengeance on yourself. - } - else if (CheckHSequence(m)) - { - SpellHelper.Turn(Caster, m); + /* Summons a Revenant which haunts the target until either the target or the Revenant is dead. + * Revenants have the ability to track down their targets wherever they may travel. + * A Revenant's strength is determined by the Necromancy and Spirit Speak skills of the Caster. + * The effect lasts for ((Spirit Speak skill level * 80) / 120) + 10 seconds. + */ - /* Summons a Revenant which haunts the target until either the target or the Revenant is dead. - * Revenants have the ability to track down their targets wherever they may travel. - * A Revenant's strength is determined by the Necromancy and Spirit Speak skills of the Caster. - * The effect lasts for ((Spirit Speak skill level * 80) / 120) + 10 seconds. - */ + var duration = TimeSpan.FromSeconds(GetDamageSkill(Caster) * 80 / 120 + 10); - var duration = TimeSpan.FromSeconds(GetDamageSkill(Caster) * 80 / 120 + 10); + var rev = new Revenant(Caster, m, duration); - var rev = new Revenant(Caster, m, duration); - - if (BaseCreature.Summon( + if (BaseCreature.Summon( rev, false, Caster, @@ -58,33 +58,32 @@ namespace Server.Spells.Necromancy 0x81, TimeSpan.FromSeconds(duration.TotalSeconds + 2.0) )) - { - rev.FixedParticles(0x373A, 1, 15, 9909, EffectLayer.Waist); - } - } - - FinishSequence(); - } - - public override void OnCast() - { - Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); - } - - public override bool CheckCast() - { - if (!base.CheckCast()) { - return false; + rev.FixedParticles(0x373A, 1, 15, 9909, EffectLayer.Waist); } - - if (Caster.Followers + 3 > Caster.FollowersMax) - { - Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature. - return false; - } - - return true; } + + FinishSequence(); } -} + + public override void OnCast() + { + Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); + } + + public override bool CheckCast() + { + if (!base.CheckCast()) + { + return false; + } + + if (Caster.Followers + 3 > Caster.FollowersMax) + { + Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature. + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/Wither.cs b/Projects/UOContent/Spells/Necromancy/Wither.cs index cbed55f91..5ea26f940 100644 --- a/Projects/UOContent/Spells/Necromancy/Wither.cs +++ b/Projects/UOContent/Spells/Necromancy/Wither.cs @@ -3,132 +3,131 @@ using Server.Collections; using Server.Items; using Server.Mobiles; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class WitherSpell : NecromancerSpell { - public class WitherSpell : NecromancerSpell + private static readonly SpellInfo _info = new( + "Wither", + "Kal Vas An Flam", + 203, + 9031, + Reagent.NoxCrystal, + Reagent.GraveDust, + Reagent.PigIron + ); + + public WitherSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Wither", - "Kal Vas An Flam", - 203, - 9031, - Reagent.NoxCrystal, - Reagent.GraveDust, - Reagent.PigIron - ); + } - public WitherSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(Core.Expansion switch + { + >= Expansion.SA => 1.25, + >= Expansion.ML => 1.5, + _ => 1.0 + }); + + public override double RequiredSkill => 60.0; + + public override int RequiredMana => 23; + + public override bool DelayedDamage => false; + + public override void OnCast() + { + if (CheckSequence()) { - } + /* Creates a withering frost around the Caster, + * which deals Cold Damage to all valid targets in a radius of 5 tiles. + */ - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(Core.Expansion switch - { - >= Expansion.SA => 1.25, - >= Expansion.ML => 1.5, - _ => 1.0 - }); + var map = Caster.Map; - public override double RequiredSkill => 60.0; - - public override int RequiredMana => 23; - - public override bool DelayedDamage => false; - - public override void OnCast() - { - if (CheckSequence()) + if (map != null) { - /* Creates a withering frost around the Caster, - * which deals Cold Damage to all valid targets in a radius of 5 tiles. - */ + using var pool = PooledRefQueue.Create(); - var map = Caster.Map; + var cbc = Caster as BaseCreature; + var isMonster = cbc?.Controlled == false && (cbc.IsAnimatedDead || !cbc.Summoned); - if (map != null) + foreach (var targ in Caster.GetMobilesInRange(Core.ML ? 4 : 5)) { - using var pool = PooledRefQueue.Create(); - - var cbc = Caster as BaseCreature; - var isMonster = cbc?.Controlled == false && (cbc.IsAnimatedDead || !cbc.Summoned); - - foreach (var targ in Caster.GetMobilesInRange(Core.ML ? 4 : 5)) + if (targ == Caster + || !Caster.InLOS(targ) + || !isMonster && !SpellHelper.ValidIndirectTarget(Caster, targ) + || !Caster.CanBeHarmful(targ, false)) { - if (targ == Caster - || !Caster.InLOS(targ) - || !isMonster && !SpellHelper.ValidIndirectTarget(Caster, targ) - || !Caster.CanBeHarmful(targ, false)) + continue; + } + + if (isMonster && targ.Player) + { + continue; + } + + // Animate dead casting poison strike shouldn't hit: familiars or player or pets + if (targ is BaseCreature bc) + { + if (bc.IsAnimatedDead) { continue; } - if (isMonster && targ.Player) + if (isMonster && (bc.Controlled || bc.Summoned || bc.Team == cbc.Team || bc.IsNecroFamiliar)) { continue; } - - // Animate dead casting poison strike shouldn't hit: familiars or player or pets - if (targ is BaseCreature bc) - { - if (bc.IsAnimatedDead) - { - continue; - } - - if (isMonster && (bc.Controlled || bc.Summoned || bc.Team == cbc.Team || bc.IsNecroFamiliar)) - { - continue; - } - } - - pool.Enqueue(targ); } - Effects.PlaySound(Caster.Location, map, 0x1FB); - Effects.PlaySound(Caster.Location, map, 0x10B); - Effects.SendLocationParticles( - EffectItem.Create(Caster.Location, map, EffectItem.DefaultDuration), - 0x37CC, - 1, - 40, - 97, - 3, - 9917, - 0 - ); + pool.Enqueue(targ); + } - while (pool.Count > 0) + Effects.PlaySound(Caster.Location, map, 0x1FB); + Effects.PlaySound(Caster.Location, map, 0x10B); + Effects.SendLocationParticles( + EffectItem.Create(Caster.Location, map, EffectItem.DefaultDuration), + 0x37CC, + 1, + 40, + 97, + 3, + 9917, + 0 + ); + + while (pool.Count > 0) + { + var m = pool.Dequeue(); + + Caster.DoHarmful(m); + m.FixedParticles(0x374A, 1, 15, 9502, 97, 3, (EffectLayer)255); + + double damage = Utility.RandomMinMax(30, 35); + + damage *= 300 + m.Karma / 100.0 + GetDamageSkill(Caster) * 10; + damage /= 1000; + + var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage); + + // PvP spell damage increase cap of 15% from an item's magic property in Publish 33(SE) + if (Core.SE && m.Player && Caster.Player && sdiBonus > 15) { - var m = pool.Dequeue(); - - Caster.DoHarmful(m); - m.FixedParticles(0x374A, 1, 15, 9502, 97, 3, (EffectLayer)255); - - double damage = Utility.RandomMinMax(30, 35); - - damage *= 300 + m.Karma / 100.0 + GetDamageSkill(Caster) * 10; - damage /= 1000; - - var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage); - - // PvP spell damage increase cap of 15% from an item's magic property in Publish 33(SE) - if (Core.SE && m.Player && Caster.Player && sdiBonus > 15) - { - sdiBonus = 15; - } - - damage *= 100 + sdiBonus; - damage /= 100; - - // TODO: cap? - // if (damage > 40) - // damage = 40; - - SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0); + sdiBonus = 15; } + + damage *= 100 + sdiBonus; + damage /= 100; + + // TODO: cap? + // if (damage > 40) + // damage = 40; + + SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0); } } - - FinishSequence(); } + + FinishSequence(); } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Necromancy/WraithForm.cs b/Projects/UOContent/Spells/Necromancy/WraithForm.cs index dbdc0a6e5..c57eadeaa 100644 --- a/Projects/UOContent/Spells/Necromancy/WraithForm.cs +++ b/Projects/UOContent/Spells/Necromancy/WraithForm.cs @@ -1,67 +1,66 @@ using System; using Server.Mobiles; -namespace Server.Spells.Necromancy +namespace Server.Spells.Necromancy; + +public class WraithFormSpell : TransformationSpell { - public class WraithFormSpell : TransformationSpell + private static readonly SpellInfo _info = new( + "Wraith Form", + "Rel Xen Um", + 203, + 9031, + Reagent.NoxCrystal, + Reagent.PigIron + ); + + public WraithFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Wraith Form", - "Rel Xen Um", - 203, - 9031, - Reagent.NoxCrystal, - Reagent.PigIron - ); + } - public WraithFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); + + public override double RequiredSkill => 20.0; + public override int RequiredMana => 17; + + public override int Body => Caster.Female ? 747 : 748; + public override int Hue => Caster.Female ? 0 : 0x4001; + + public override int PhysResistOffset => +15; + public override int FireResistOffset => -5; + public override int ColdResistOffset => 0; + public override int PoisResistOffset => 0; + public override int NrgyResistOffset => -5; + + public static void DoWraithLeech(Mobile wraith, Mobile defender, int damageGiven) + { + var wraithLeech = 5 + (int)(15 * wraith.Skills.SpiritSpeak.Value / 100); // Wraith form gives 5-20% mana leech + var manaLeech = Math.Min(defender.Mana, AOS.Scale(damageGiven, wraithLeech)); + + if (manaLeech != 0) { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); - - public override double RequiredSkill => 20.0; - public override int RequiredMana => 17; - - public override int Body => Caster.Female ? 747 : 748; - public override int Hue => Caster.Female ? 0 : 0x4001; - - public override int PhysResistOffset => +15; - public override int FireResistOffset => -5; - public override int ColdResistOffset => 0; - public override int PoisResistOffset => 0; - public override int NrgyResistOffset => -5; - - public static void DoWraithLeech(Mobile wraith, Mobile defender, int damageGiven) - { - var wraithLeech = 5 + (int)(15 * wraith.Skills.SpiritSpeak.Value / 100); // Wraith form gives 5-20% mana leech - var manaLeech = Math.Min(defender.Mana, AOS.Scale(damageGiven, wraithLeech)); - - if (manaLeech != 0) - { - wraith.Mana += manaLeech; - defender.Mana -= manaLeech; - wraith.PlaySound(0x44D); - } - } - - public override void DoEffect(Mobile m) - { - if (m is PlayerMobile mobile) - { - mobile.IgnoreMobiles = true; - } - - m.PlaySound(0x17F); - m.FixedParticles(0x374A, 1, 15, 9902, 1108, 4, EffectLayer.Waist); - } - - public override void RemoveEffect(Mobile m) - { - if (m is PlayerMobile { AccessLevel: AccessLevel.Player } mobile) - { - mobile.IgnoreMobiles = false; - } + wraith.Mana += manaLeech; + defender.Mana -= manaLeech; + wraith.PlaySound(0x44D); } } -} + + public override void DoEffect(Mobile m) + { + if (m is PlayerMobile mobile) + { + mobile.IgnoreMobiles = true; + } + + m.PlaySound(0x17F); + m.FixedParticles(0x374A, 1, 15, 9902, 1108, 4, EffectLayer.Waist); + } + + public override void RemoveEffect(Mobile m) + { + if (m is PlayerMobile { AccessLevel: AccessLevel.Player } mobile) + { + mobile.IgnoreMobiles = false; + } + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs index 40806922a..f2cc23693 100644 --- a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs +++ b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs @@ -7,159 +7,146 @@ using Server.Network; using Server.Spells.Fifth; using Server.Spells.Seventh; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class AnimalForm : NinjaSpell { - public class AnimalForm : NinjaSpell + public enum MorphResult { - public enum MorphResult + Success, + Fail, + NoSkill + } + + private static readonly SpellInfo _info = new( + "Animal Form", + null, + -1, + 9002 + ); + + // TODO: Cleanup periodically if players have logged out for a while + private static readonly Dictionary _lastAnimalForms = new(); + private static readonly Dictionary _table = new(); + + private bool _wasMoving; + + public AnimalForm(Mobile caster, Item scroll) : base(caster, scroll, _info) + { + } + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); + + public override double RequiredSkill => 0.0; + public override int RequiredMana => Core.ML ? 10 : 0; + public override int CastRecoveryBase => Core.ML ? 10 : base.CastRecoveryBase; + + public override bool BlockedByAnimalForm => false; + + public static AnimalFormEntry[] Entries { get; } = + { + new(typeof(Kirin), 1029632, 9632, 0, 1070811, 100.0, 0x84, 0, 0), + new(typeof(Unicorn), 1018214, 9678, 0, 1070812, 100.0, 0x7A, 0, 0), + new(typeof(BakeKitsune), 1030083, 10083, 0, 1070810, 82.5, 0xF6, 0, 0), + new(typeof(GreyWolf), 1028482, 9681, 2309, 1070810, 82.5, 0x19, 0x8FD, 0x90E), + new(typeof(Llama), 1028438, 8438, 0, 1070809, 70.0, 0xDC, 0, 0), + new(typeof(ForestOstard), 1018273, 8503, 2212, 1070809, 70.0, 0xDB, 0x899, 0x8B0), + new(typeof(BullFrog), 1028496, 8496, 2003, 1070807, 50.0, 0x51, 0x7D1, 0x7D6, false, false), + new(typeof(GiantSerpent), 1018114, 9663, 2009, 1070808, 50.0, 0x15, 0x7D1, 0x7E2, false, false), + new(typeof(Dog), 1018280, 8476, 2309, 1070806, 40.0, 0xD9, 0x8FD, 0x90E, false, false), + new(typeof(Cat), 1018264, 8475, 2309, 1070806, 40.0, 0xC9, 0x8FD, 0x90E, false, false), + new(typeof(Rat), 1018294, 8483, 2309, 1070805, 20.0, 0xEE, 0x8FD, 0x90E, true, false), + new(typeof(Rabbit), 1028485, 8485, 2309, 1070805, 20.0, 0xCD, 0x8FD, 0x90E, true, false), + new(typeof(Squirrel), 1031671, 11671, 0, 0, 20.0, 0x116, 0, 0, false, false), + new(typeof(Ferret), 1031672, 11672, 0, 1075220, 40.0, 0x117, 0, 0, false, false, true), + new(typeof(CuSidhe), 1031670, 11670, 0, 1075221, 60.0, 0x115, 0, 0, false, false), + new(typeof(Reptalon), 1075202, 11669, 0, 1075222, 90.0, 0x114, 0, 0, false, false) + }; + + public static void OnLogin(Mobile m) + { + if (GetContext(m)?.SpeedBoost == true) { - Success, - Fail, - NoSkill + m.NetState.SendSpeedControl(SpeedControlSetting.Mount); + } + } + + public override bool CheckCast() + { + if (!Caster.CanBeginAction()) + { + Caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed. + return false; } - private static readonly SpellInfo _info = new( - "Animal Form", - null, - -1, - 9002 - ); - - private static readonly Dictionary _lastAnimalForms = new(); - private static readonly Dictionary _table = new(); - - private bool m_WasMoving; - - public AnimalForm(Mobile caster, Item scroll) : base(caster, scroll, _info) + if (TransformationSpellHelper.UnderTransformation(Caster)) { + Caster.SendLocalizedMessage(1063219); // You cannot mimic an animal while in that form. + return false; } - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); - - public override double RequiredSkill => 0.0; - public override int RequiredMana => Core.ML ? 10 : 0; - public override int CastRecoveryBase => Core.ML ? 10 : base.CastRecoveryBase; - - public override bool BlockedByAnimalForm => false; - - public static AnimalFormEntry[] Entries { get; } = + if (DisguisePersistence.IsDisguised(Caster)) { - new(typeof(Kirin), 1029632, 9632, 0, 1070811, 100.0, 0x84, 0, 0), - new(typeof(Unicorn), 1018214, 9678, 0, 1070812, 100.0, 0x7A, 0, 0), - new(typeof(BakeKitsune), 1030083, 10083, 0, 1070810, 82.5, 0xF6, 0, 0), - new(typeof(GreyWolf), 1028482, 9681, 2309, 1070810, 82.5, 0x19, 0x8FD, 0x90E), - new(typeof(Llama), 1028438, 8438, 0, 1070809, 70.0, 0xDC, 0, 0), - new(typeof(ForestOstard), 1018273, 8503, 2212, 1070809, 70.0, 0xDB, 0x899, 0x8B0), - new(typeof(BullFrog), 1028496, 8496, 2003, 1070807, 50.0, 0x51, 0x7D1, 0x7D6, false, false), - new(typeof(GiantSerpent), 1018114, 9663, 2009, 1070808, 50.0, 0x15, 0x7D1, 0x7E2, false, false), - new(typeof(Dog), 1018280, 8476, 2309, 1070806, 40.0, 0xD9, 0x8FD, 0x90E, false, false), - new(typeof(Cat), 1018264, 8475, 2309, 1070806, 40.0, 0xC9, 0x8FD, 0x90E, false, false), - new(typeof(Rat), 1018294, 8483, 2309, 1070805, 20.0, 0xEE, 0x8FD, 0x90E, true, false), - new(typeof(Rabbit), 1028485, 8485, 2309, 1070805, 20.0, 0xCD, 0x8FD, 0x90E, true, false), - new(typeof(Squirrel), 1031671, 11671, 0, 0, 20.0, 0x116, 0, 0, false, false), - new(typeof(Ferret), 1031672, 11672, 0, 1075220, 40.0, 0x117, 0, 0, false, false, true), - new(typeof(CuSidhe), 1031670, 11670, 0, 1075221, 60.0, 0x115, 0, 0, false, false), - new(typeof(Reptalon), 1075202, 11669, 0, 1075222, 90.0, 0x114, 0, 0, false, false) - }; - - public static void OnLogin(Mobile m) - { - if (GetContext(m)?.SpeedBoost == true) - { - m.NetState.SendSpeedControl(SpeedControlSetting.Mount); - } + Caster.SendLocalizedMessage(1061631); // You can't do that while disguised. + return false; } - public override bool CheckCast() + return base.CheckCast(); + } + + public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false; + + private bool CasterIsMoving() => + Core.TickCount - Caster.LastMoveTime <= Caster.ComputeMovementSpeed(Caster.Direction); + + public override void OnBeginCast() + { + base.OnBeginCast(); + + Caster.FixedEffect(0x37C4, 10, 14, 4, 3); + _wasMoving = CasterIsMoving(); + } + + public override bool CheckFizzle() => true; + + public override void OnCast() + { + if (!Caster.CanBeginAction()) { - if (!Caster.CanBeginAction()) - { - Caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed. - return false; - } - - if (TransformationSpellHelper.UnderTransformation(Caster)) - { - Caster.SendLocalizedMessage(1063219); // You cannot mimic an animal while in that form. - return false; - } - - if (DisguisePersistence.IsDisguised(Caster)) - { - Caster.SendLocalizedMessage(1061631); // You can't do that while disguised. - return false; - } - - return base.CheckCast(); + Caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed. } - - public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false; - - private bool CasterIsMoving() => - Core.TickCount - Caster.LastMoveTime <= Caster.ComputeMovementSpeed(Caster.Direction); - - public override void OnBeginCast() + else if (TransformationSpellHelper.UnderTransformation(Caster)) { - base.OnBeginCast(); - - Caster.FixedEffect(0x37C4, 10, 14, 4, 3); - m_WasMoving = CasterIsMoving(); + Caster.SendLocalizedMessage(1063219); // You cannot mimic an animal while in that form. } - - public override bool CheckFizzle() => true; - - public override void OnCast() + else if (!Caster.CanBeginAction() || Caster.IsBodyMod && GetContext(Caster) == null) { - if (!Caster.CanBeginAction()) - { - Caster.SendLocalizedMessage(1061628); // You can't do that while polymorphed. - } - else if (TransformationSpellHelper.UnderTransformation(Caster)) - { - Caster.SendLocalizedMessage(1063219); // You cannot mimic an animal while in that form. - } - else if (!Caster.CanBeginAction() || Caster.IsBodyMod && GetContext(Caster) == null) - { - DoFizzle(); - } - else if (CheckSequence()) - { - var context = GetContext(Caster); + DoFizzle(); + } + else if (CheckSequence()) + { + var context = GetContext(Caster); - var mana = ScaleMana(RequiredMana); - if (mana > Caster.Mana) + var mana = ScaleMana(RequiredMana); + if (mana > Caster.Mana) + { + // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. + Caster.SendLocalizedMessage(1060174, mana.ToString()); + } + else if (context != null) + { + RemoveContext(Caster, context, true); + Caster.Mana -= mana; + } + else + { + var lastAnimalForm = GetLastAnimalForm(Caster); + if (Caster is PlayerMobile && lastAnimalForm == -1 && !_wasMoving && !CasterIsMoving()) { - // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. - Caster.SendLocalizedMessage(1060174, mana.ToString()); + Caster.CloseGump(); + Caster.SendGump(new AnimalFormGump(Caster, Entries, this)); } - else if (context != null) - { - RemoveContext(Caster, context, true); - Caster.Mana -= mana; - } - else if (Caster is PlayerMobile) - { - var skipGump = m_WasMoving || CasterIsMoving(); - - if (GetLastAnimalForm(Caster) == -1 || !skipGump) - { - Caster.CloseGump(); - Caster.SendGump(new AnimalFormGump(Caster, Entries, this)); - } - else - { - if (Morph(Caster, GetLastAnimalForm(Caster)) == MorphResult.Fail) - { - DoFizzle(); - } - else - { - Caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); - Caster.Mana -= mana; - } - } - } - else if (Morph(Caster, GetLastAnimalForm(Caster)) == MorphResult.Fail) + else if (Morph(Caster, lastAnimalForm) == MorphResult.Fail) { DoFizzle(); } @@ -169,443 +156,449 @@ namespace Server.Spells.Ninjitsu Caster.Mana -= mana; } } - - FinishSequence(); } - public int GetLastAnimalForm(Mobile m) => _lastAnimalForms.TryGetValue(m, out var value) ? value : -1; + FinishSequence(); + } - public static MorphResult Morph(Mobile m, int entryID) + public static int GetLastAnimalForm(Mobile m) => _lastAnimalForms.GetValueOrDefault(m, -1); + + public static MorphResult Morph(Mobile m, int entryID) + { + if (entryID < 0 || entryID >= Entries.Length) { - if (entryID < 0 || entryID >= Entries.Length) + return MorphResult.Fail; + } + + var entry = Entries[entryID]; + + _lastAnimalForms[m] = entryID; // On OSI, it's the last /attempted/ one not the last succeeded one + + if (m.Skills.Ninjitsu.Value < entry.ReqSkill) + { + // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. + m.SendLocalizedMessage(1063013, $"{entry.ReqSkill:F1}\t{SkillName.Ninjitsu}\t "); + return MorphResult.NoSkill; + } + + /* + if (!m.CheckSkill( SkillName.Ninjitsu, entry.ReqSkill, entry.ReqSkill + 37.5 )) + return MorphResult.Fail; + * + * On OSI,it seems you can only gain starting at '0' using Animal form. + */ + + var ninjitsu = m.Skills.Ninjitsu.Value; + + if (ninjitsu < entry.ReqSkill + 37.5) + { + var chance = (ninjitsu - entry.ReqSkill) / 37.5; + + if (chance < Utility.RandomDouble()) { return MorphResult.Fail; } + } - var entry = Entries[entryID]; + m.CheckSkill(SkillName.Ninjitsu, 0.0, 37.5); - _lastAnimalForms[m] = entryID; // On OSI, it's the last /attempted/ one not the last succeeded one + if (!BaseFormTalisman.EntryEnabled(m, entry.Type)) + { + return MorphResult.Success; // Still consumes mana, just no effect + } - if (m.Skills.Ninjitsu.Value < entry.ReqSkill) - { - // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. - m.SendLocalizedMessage(1063013, $"{entry.ReqSkill:F1}\t{SkillName.Ninjitsu}\t "); - return MorphResult.NoSkill; - } + BaseMount.Dismount(m); - /* - if (!m.CheckSkill( SkillName.Ninjitsu, entry.ReqSkill, entry.ReqSkill + 37.5 )) - return MorphResult.Fail; - * - * On OSI,it seems you can only gain starting at '0' using Animal form. - */ + var bodyMod = entry.BodyMod; + var hueMod = entry.HueMod; - var ninjitsu = m.Skills.Ninjitsu.Value; + m.BodyMod = bodyMod; + m.HueMod = hueMod; - if (ninjitsu < entry.ReqSkill + 37.5) - { - var chance = (ninjitsu - entry.ReqSkill) / 37.5; + if (entry.SpeedBoost) + { + m.NetState.SendSpeedControl(SpeedControlSetting.Mount); + } - if (chance < Utility.RandomDouble()) - { - return MorphResult.Fail; - } - } + // TODO: Determine if transform spell skill mods to a generic location like stat mods + SkillMod mod = null; - m.CheckSkill(SkillName.Ninjitsu, 0.0, 37.5); + if (entry.StealthBonus) + { + mod = new DefaultSkillMod(SkillName.Stealth, "StealthAnimalForm", true, 20.0) { ObeyCap = true }; + m.AddSkillMod(mod); + } - if (!BaseFormTalisman.EntryEnabled(m, entry.Type)) - { - return MorphResult.Success; // Still consumes mana, just no effect - } + SkillMod stealingMod = null; - BaseMount.Dismount(m); + if (entry.StealingBonus) + { + stealingMod = new DefaultSkillMod(SkillName.Stealing, "StealingAnimalForm", true, 10.0) { ObeyCap = true }; + m.AddSkillMod(stealingMod); + } - var bodyMod = entry.BodyMod; - var hueMod = entry.HueMod; + Timer timer = new AnimalFormTimer(m, bodyMod, hueMod); + timer.Start(); - m.BodyMod = bodyMod; - m.HueMod = hueMod; + AddContext(m, new AnimalFormContext(timer, mod, entry.SpeedBoost, entry.Type, stealingMod)); + m.CheckStatTimers(); + return MorphResult.Success; + } - if (entry.SpeedBoost) - { - m.NetState.SendSpeedControl(SpeedControlSetting.Mount); - } + public static void AddContext(Mobile m, AnimalFormContext context) + { + _table[m] = context; - // TODO: Determine if transform spell skill mods to a generic location like stat mods - SkillMod mod = null; - - if (entry.StealthBonus) - { - mod = new DefaultSkillMod(SkillName.Stealth, "StealthAnimalForm", true, 20.0) { ObeyCap = true }; - m.AddSkillMod(mod); - } - - SkillMod stealingMod = null; - - if (entry.StealingBonus) - { - stealingMod = new DefaultSkillMod(SkillName.Stealing, "StealingAnimalForm", true, 10.0) { ObeyCap = true }; - m.AddSkillMod(stealingMod); - } - - Timer timer = new AnimalFormTimer(m, bodyMod, hueMod); - timer.Start(); - - AddContext(m, new AnimalFormContext(timer, mod, entry.SpeedBoost, entry.Type, stealingMod)); + if (context.Type == typeof(BakeKitsune) || context.Type == typeof(GreyWolf)) + { m.CheckStatTimers(); - return MorphResult.Success; - } - - public static void AddContext(Mobile m, AnimalFormContext context) - { - _table[m] = context; - - if (context.Type == typeof(BakeKitsune) || context.Type == typeof(GreyWolf)) - { - m.CheckStatTimers(); - } - } - - public static void RemoveContext(Mobile m, bool resetGraphics) - { - var context = GetContext(m); - - if (context != null) - { - RemoveContext(m, context, resetGraphics); - } - } - - public static void RemoveContext(Mobile m, AnimalFormContext context, bool resetGraphics) - { - _table.Remove(m); - - if (context.SpeedBoost) - { - m.NetState.SendSpeedControl(SpeedControlSetting.Disable); - } - - var mod = context.Mod; - - if (mod != null) - { - m.RemoveSkillMod(mod); - } - - mod = context.StealingMod; - - if (mod != null) - { - m.RemoveSkillMod(mod); - } - - if (resetGraphics) - { - m.HueMod = -1; - m.BodyMod = 0; - } - - m.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); - - context.Timer.Stop(); - } - - public static AnimalFormContext GetContext(Mobile m) => _table.TryGetValue(m, out var context) ? context : null; - - public static bool UnderTransformation(Mobile m) => _table.ContainsKey(m); - - public static bool UnderTransformation(Mobile m, Type type) => GetContext(m)?.Type == type; - - /* - private delegate void AnimalFormCallback( Mobile from ); - private delegate bool AnimalFormRequirementCallback( Mobile from ); - */ - - public class AnimalFormEntry - { - private readonly int m_HueModMax; - - private readonly int m_HueModMin; - /* - private AnimalFormCallback m_TransformCallback; - private AnimalFormCallback m_UntransformCallback; - private AnimalFormRequirementCallback m_RequirementCallback; - */ - - public AnimalFormEntry( - Type type, TextDefinition name, int itemID, int hue, int tooltip, double reqSkill, - int bodyMod, int hueModMin, int hueModMax, bool stealthBonus = false, bool speedBoost = true, - bool stealingBonus = false - ) - { - Type = type; - Name = name; - ItemID = itemID; - Hue = hue; - Tooltip = tooltip; - ReqSkill = reqSkill; - BodyMod = bodyMod; - m_HueModMin = hueModMin; - m_HueModMax = hueModMax; - StealthBonus = stealthBonus; - SpeedBoost = speedBoost; - StealingBonus = stealingBonus; - } - - public Type Type { get; } - - public TextDefinition Name { get; } - - public int ItemID { get; } - - public int Hue { get; } - - public int Tooltip { get; } - - public double ReqSkill { get; } - - public int BodyMod { get; } - - public int HueMod => Utility.RandomMinMax(m_HueModMin, m_HueModMax); - public bool StealthBonus { get; } - - public bool SpeedBoost { get; } - - public bool StealingBonus { get; } - } - - public class AnimalFormGump : DynamicGump - { - // TODO: Convert this for ML to the BaseImageTileButtonsGump - private readonly Mobile _caster; - private readonly AnimalForm _spell; - private readonly AnimalFormEntry[] _entries; - - public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell) : base(50, 50) - { - _caster = caster; - _spell = spell; - _entries = entries; - } - - protected override void BuildLayout(ref DynamicGumpBuilder builder) - { - builder.AddPage(); - - builder.AddBackground(0, 0, 520, 404, 0x13BE); - builder.AddImageTiled(10, 10, 500, 20, 0xA40); - builder.AddImageTiled(10, 40, 500, 324, 0xA40); - builder.AddImageTiled(10, 374, 500, 20, 0xA40); - builder.AddAlphaRegion(10, 10, 500, 384); - - builder.AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); //
Polymorph Selection Menu
- - builder.AddButton(10, 374, 0xFB1, 0xFB2, 0); - builder.AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL - - int ninjitsu = _caster.Skills[SkillName.Ninjitsu].Fixed; - int current = 0; - - for (int i = 0; i < _entries.Length; ++i) - { - bool enabled = ninjitsu >= _entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(_caster, _entries[i].Type); - - int page = current / 10 + 1; - int pos = current % 10; - - if (pos == 0) - { - if (page > 1) - { - builder.AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page); - builder.AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next - } - - builder.AddPage(page); - - if (page > 1) - { - builder.AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1); - builder.AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back - } - } - - if (!enabled) - { - continue; - } - - AnimalFormEntry entry = _entries[i]; - - int y = Math.DivRem(pos, 2, out var rem) * 64 + 44; - int x = rem == 0 ? 14 : 264; - Rectangle2D b = ItemBounds.Table[entry.ItemID]; - - builder.AddImageTiledButton(x, y, 0x918, 0x919, i + 1, GumpButtonType.Reply, 0, entry.ItemID, - entry.Hue, 40 - b.Width / 2 - b.X, 30 - b.Height / 2 - b.Y, entry.Tooltip); - - builder.AddHtmlLocalized(x + 84, y, 250, 60, entry.Name, 0x7FFF); - - current++; - } - } - - public override void OnResponse(NetState sender, in RelayInfo info) - { - var entryID = info.ButtonID - 1; - - if (entryID < 0 || entryID >= Entries.Length) - { - return; - } - - var mana = _spell.ScaleMana(_spell.RequiredMana); - var entry = Entries[entryID]; - - if (!BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type)) - { - return; - } - - if (mana > _caster.Mana) - { - // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. - _caster.SendLocalizedMessage(1060174, mana.ToString()); - } - else if (_caster is PlayerMobile mobile - && (mobile.MountBlockReason != BlockMountType.None - || mobile.DuelContext?.AllowSpellCast(_caster, _spell) == false)) - { - _caster.SendLocalizedMessage(1063108); // You cannot use this ability right now. - } - else if (Morph(_caster, entryID) == MorphResult.Fail) - { - _caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles. - _caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist); - _caster.PlaySound(0x5C); - } - else - { - _caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); - _caster.Mana -= mana; - } - } } } - public class AnimalFormContext + public static void RemoveContext(Mobile m, bool resetGraphics) { - public AnimalFormContext(Timer timer, SkillMod mod, bool speedBoost, Type type, SkillMod stealingMod) + var context = GetContext(m); + + if (context != null) { - Timer = timer; - Mod = mod; - SpeedBoost = speedBoost; - Type = type; - StealingMod = stealingMod; + RemoveContext(m, context, resetGraphics); + } + } + + public static void RemoveContext(Mobile m, AnimalFormContext context, bool resetGraphics) + { + _table.Remove(m); + + if (context.SpeedBoost) + { + m.NetState.SendSpeedControl(SpeedControlSetting.Disable); } - public Timer Timer { get; } + var mod = context.Mod; - public SkillMod Mod { get; } + if (mod != null) + { + m.RemoveSkillMod(mod); + } - public bool SpeedBoost { get; } + mod = context.StealingMod; + + if (mod != null) + { + m.RemoveSkillMod(mod); + } + + if (resetGraphics) + { + m.HueMod = -1; + m.BodyMod = 0; + } + + m.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); + + context.Timer.Stop(); + } + + public static AnimalFormContext GetContext(Mobile m) => _table.GetValueOrDefault(m); + + public static bool UnderTransformation(Mobile m) => _table.ContainsKey(m); + + public static bool UnderTransformation(Mobile m, Type type) => GetContext(m)?.Type == type; + + public static void RemoveLastAnimalForm(Mobile m) => _table.Remove(m); + + public class AnimalFormEntry + { + private readonly int _hueModMax; + private readonly int _hueModMin; + + public AnimalFormEntry( + Type type, TextDefinition name, int itemID, int hue, int tooltip, double reqSkill, + int bodyMod, int hueModMin, int hueModMax, bool stealthBonus = false, bool speedBoost = true, + bool stealingBonus = false + ) + { + Type = type; + Name = name; + ItemID = itemID; + Hue = hue; + Tooltip = tooltip; + ReqSkill = reqSkill; + BodyMod = bodyMod; + _hueModMin = hueModMin; + _hueModMax = hueModMax; + StealthBonus = stealthBonus; + SpeedBoost = speedBoost; + StealingBonus = stealingBonus; + } public Type Type { get; } - public SkillMod StealingMod { get; } + public TextDefinition Name { get; } + + public int ItemID { get; } + + public int Hue { get; } + + public int Tooltip { get; } + + public double ReqSkill { get; } + + public int BodyMod { get; } + + public int HueMod => Utility.RandomMinMax(_hueModMin, _hueModMax); + public bool StealthBonus { get; } + + public bool SpeedBoost { get; } + + public bool StealingBonus { get; } } - public class AnimalFormTimer : Timer + public class AnimalFormGump : DynamicGump { - private int _body; - private int _hue; - private Mobile _mobile; - private int _counter; - private Mobile _lastTarget; + // TODO: Convert this for ML to the BaseImageTileButtonsGump + private readonly Mobile _caster; + private readonly AnimalForm _spell; + private readonly AnimalFormEntry[] _entries; - public AnimalFormTimer(Mobile from, int body, int hue) - : base(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0)) + public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell) : base(50, 50) { - _mobile = from; - _body = body; - _hue = hue; - _counter = 0; + _caster = caster; + _spell = spell; + _entries = entries; } - protected override void OnTick() + protected override void BuildLayout(ref DynamicGumpBuilder builder) { - if (_mobile.Deleted || !_mobile.Alive || _mobile.Body != _body || _mobile.Hue != _hue) + builder.AddPage(); + + builder.AddBackground(0, 0, 520, 404, 0x13BE); + builder.AddImageTiled(10, 10, 500, 20, 0xA40); + builder.AddImageTiled(10, 40, 500, 324, 0xA40); + builder.AddImageTiled(10, 374, 500, 20, 0xA40); + builder.AddAlphaRegion(10, 10, 500, 384); + + builder.AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); //
Polymorph Selection Menu
+ + builder.AddButton(10, 374, 0xFB1, 0xFB2, 0); + builder.AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL + + int ninjitsu = _caster.Skills[SkillName.Ninjitsu].Fixed; + int current = 0; + + for (int i = 0; i < _entries.Length; ++i) + { + bool enabled = ninjitsu >= _entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(_caster, _entries[i].Type); + + int page = current / 10 + 1; + int pos = current % 10; + + if (pos == 0) + { + if (page > 1) + { + builder.AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page); + builder.AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next + } + + builder.AddPage(page); + + if (page > 1) + { + builder.AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1); + builder.AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back + } + } + + if (!enabled) + { + continue; + } + + AnimalFormEntry entry = _entries[i]; + + int y = Math.DivRem(pos, 2, out var rem) * 64 + 44; + int x = rem == 0 ? 14 : 264; + Rectangle2D b = ItemBounds.Table[entry.ItemID]; + + builder.AddImageTiledButton(x, y, 0x918, 0x919, i + 1, GumpButtonType.Reply, 0, entry.ItemID, + entry.Hue, 40 - b.Width / 2 - b.X, 30 - b.Height / 2 - b.Y, entry.Tooltip); + + builder.AddHtmlLocalized(x + 84, y, 250, 60, entry.Name, 0x7FFF); + + current++; + } + } + + public override void OnResponse(NetState sender, in RelayInfo info) + { + var entryID = info.ButtonID - 1; + + if (entryID < 0 || entryID >= Entries.Length) { - AnimalForm.RemoveContext(_mobile, true); - Stop(); return; } - if (_body == 0x115) // Cu Sidhe + var mana = _spell.ScaleMana(_spell.RequiredMana); + var entry = Entries[entryID]; + + if (!BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type)) { - if (_counter++ >= 8) - { - if (_mobile.Hits < _mobile.HitsMax && _mobile.Backpack != null) - { - var b = _mobile.Backpack.FindItemByType(); - - if (b != null) - { - _mobile.Hits += Utility.RandomMinMax(20, 50); - b.Consume(); - } - } - - _counter = 0; - } + return; } - else if (_body == 0x114) // Reptalon + + if (mana > _caster.Mana) { - if (_mobile.Combatant != null && _mobile.Combatant != _lastTarget) - { - _counter = 1; - _lastTarget = _mobile.Combatant; - } - - if (_mobile.Warmode && _lastTarget is { Alive: true, Deleted: false } && _counter-- <= 0) - { - if (_mobile.CanBeHarmful(_lastTarget) && _lastTarget.Map == _mobile.Map && - _lastTarget.InRange(_mobile.Location, BaseCreature.DefaultRangePerception) && - _mobile.InLOS(_lastTarget)) - { - _mobile.Direction = _mobile.GetDirectionTo(_lastTarget); - _mobile.Freeze(TimeSpan.FromSeconds(1)); - _mobile.PlaySound(0x16A); - - StartTimer(TimeSpan.FromSeconds(1.3), () => BreathEffect_Callback(_lastTarget)); - } - - _counter = Math.Min((int)_mobile.GetDistanceToSqrt(_lastTarget), 10); - } + // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. + _caster.SendLocalizedMessage(1060174, mana.ToString()); } - } - - public void BreathEffect_Callback(Mobile target) - { - if (_mobile.CanBeHarmful(target)) + else if (_caster is PlayerMobile mobile + && (mobile.MountBlockReason != BlockMountType.None + || mobile.DuelContext?.AllowSpellCast(_caster, _spell) == false)) { - _mobile.RevealingAction(); - _mobile.PlaySound(0x227); - Effects.SendMovingEffect(_mobile, target, 0x36D4, 5, 0); - - StartTimer(TimeSpan.FromSeconds(1), () => BreathDamage_Callback(target)); + _caster.SendLocalizedMessage(1063108); // You cannot use this ability right now. } - } - - public void BreathDamage_Callback(Mobile target) - { - if (_mobile.CanBeHarmful(target)) + else if (Morph(_caster, entryID) == MorphResult.Fail) { - _mobile.RevealingAction(); - _mobile.DoHarmful(target); - AOS.Damage(target, _mobile, 20, !target.Player, 0, 100, 0, 0, 0); + _caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles. + _caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist); + _caster.PlaySound(0x5C); + } + else + { + _caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist); + _caster.Mana -= mana; } } } } + +public class AnimalFormContext +{ + public AnimalFormContext(Timer timer, SkillMod mod, bool speedBoost, Type type, SkillMod stealingMod) + { + Timer = timer; + Mod = mod; + SpeedBoost = speedBoost; + Type = type; + StealingMod = stealingMod; + } + + public Timer Timer { get; } + + public SkillMod Mod { get; } + + public bool SpeedBoost { get; } + + public Type Type { get; } + + public SkillMod StealingMod { get; } +} + +public class AnimalFormTimer : Timer +{ + private readonly int _body; + private readonly int _hue; + private readonly Mobile _mobile; + private int _counter; + private Mobile _lastTarget; + + public AnimalFormTimer(Mobile from, int body, int hue) + : base(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0)) + { + _mobile = from; + _body = body; + _hue = hue; + _counter = 0; + } + + protected override void OnTick() + { + if (_mobile.Deleted || !_mobile.Alive || _mobile.Body != _body || _mobile.Hue != _hue) + { + AnimalForm.RemoveContext(_mobile, true); + Stop(); + return; + } + + if (_body == 0x115) // Cu Sidhe + { + // Once every 8 seconds + if (_counter++ < 8) + { + return; + } + + if (_mobile.Hits < _mobile.HitsMax) + { + var b = _mobile.Backpack?.FindItemByType(); + + if (b != null) + { + _mobile.Hits += Utility.RandomMinMax(20, 50); + b.Consume(); + } + } + else if (_mobile.Map == null || _mobile.Map == Map.Internal) // Logged out + { + Stop(); + } + + _counter = 0; + } + else if (_body == 0x114) // Reptalon + { + // Logged out + if (_mobile.Map == null || _mobile.Map == Map.Internal) + { + Stop(); + _counter = 0; + return; + } + + if (_mobile.Combatant != null && _mobile.Combatant != _lastTarget) + { + _counter = 1; + _lastTarget = _mobile.Combatant; + } + else if (_mobile.Warmode && _lastTarget is { Alive: true, Deleted: false } && _lastTarget.Map == _mobile.Map && + _counter-- <= 0) + { + if (_mobile.CanBeHarmful(_lastTarget) && _lastTarget.Map == _mobile.Map && + _lastTarget.InRange(_mobile.Location, BaseCreature.DefaultRangePerception) && + _mobile.InLOS(_lastTarget)) + { + _mobile.Direction = _mobile.GetDirectionTo(_lastTarget); + _mobile.Freeze(TimeSpan.FromSeconds(1)); + _mobile.PlaySound(0x16A); + + StartTimer(TimeSpan.FromSeconds(1.3), () => BreathEffect_Callback(_lastTarget)); + } + + _counter = Math.Min((int)_mobile.GetDistanceToSqrt(_lastTarget), 10); + } + } + } + + public void BreathEffect_Callback(Mobile target) + { + if (_mobile.CanBeHarmful(target)) + { + _mobile.RevealingAction(); + _mobile.PlaySound(0x227); + Effects.SendMovingEffect(_mobile, target, 0x36D4, 5, 0); + + StartTimer(TimeSpan.FromSeconds(1), () => BreathDamage_Callback(target)); + } + } + + public void BreathDamage_Callback(Mobile target) + { + if (_mobile.CanBeHarmful(target)) + { + _mobile.RevealingAction(); + _mobile.DoHarmful(target); + AOS.Damage(target, _mobile, 20, !target.Player, 0, 100, 0, 0, 0); + } + } +} diff --git a/Projects/UOContent/Spells/Ninjitsu/Backstab.cs b/Projects/UOContent/Spells/Ninjitsu/Backstab.cs index 11392721a..d57c39682 100644 --- a/Projects/UOContent/Spells/Ninjitsu/Backstab.cs +++ b/Projects/UOContent/Spells/Ninjitsu/Backstab.cs @@ -1,71 +1,70 @@ using System; using Server.SkillHandlers; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class Backstab : NinjaMove { - public class Backstab : NinjaMove + public override int BaseMana => 30; + public override double RequiredSkill => Core.ML ? 40.0 : 20.0; + + // You prepare to Backstab your opponent. + public override TextDefinition AbilityMessage { get; } = 1063089; + + public override bool ValidatesDuringHit => false; + + public override double GetDamageScalar(Mobile attacker, Mobile defender) { - public override int BaseMana => 30; - public override double RequiredSkill => Core.ML ? 40.0 : 20.0; + var ninjitsu = attacker.Skills.Ninjitsu.Value; - // You prepare to Backstab your opponent. - public override TextDefinition AbilityMessage { get; } = 1063089; - - public override bool ValidatesDuringHit => false; - - public override double GetDamageScalar(Mobile attacker, Mobile defender) - { - var ninjitsu = attacker.Skills.Ninjitsu.Value; - - return 1.0 + ninjitsu / 360 + Tracking.GetStalkingBonus(attacker, defender) / 100; - } - - public override bool Validate(Mobile from) - { - if (!from.Hidden || from.AllowedStealthSteps <= 0) - { - from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. - return false; - } - - return base.Validate(from); - } - - public override bool OnBeforeSwing(Mobile attacker, Mobile defender) - { - var valid = Validate(attacker) && CheckMana(attacker, true); - - if (valid) - { - attacker.BeginAction(); - Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction); - } - - return valid; - } - - public override void OnHit(Mobile attacker, Mobile defender, int damage) - { - // Validates before swing - - ClearCurrentMove(attacker); - - attacker.SendLocalizedMessage(1063090); // You quickly stab your opponent as you come out of hiding! - - defender.FixedParticles(0x37B9, 1, 5, 0x251D, 0x651, 0, EffectLayer.Waist); - - attacker.RevealingAction(); - - CheckGain(attacker); - } - - public override void OnMiss(Mobile attacker, Mobile defender) - { - ClearCurrentMove(attacker); - - attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise. - - attacker.RevealingAction(); - } + return 1.0 + ninjitsu / 360 + Tracking.GetStalkingBonus(attacker, defender) / 100; } -} + + public override bool Validate(Mobile from) + { + if (!from.Hidden || from.AllowedStealthSteps <= 0) + { + from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. + return false; + } + + return base.Validate(from); + } + + public override bool OnBeforeSwing(Mobile attacker, Mobile defender) + { + var valid = Validate(attacker) && CheckMana(attacker, true); + + if (valid) + { + attacker.BeginAction(); + Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction); + } + + return valid; + } + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + // Validates before swing + + ClearCurrentMove(attacker); + + attacker.SendLocalizedMessage(1063090); // You quickly stab your opponent as you come out of hiding! + + defender.FixedParticles(0x37B9, 1, 5, 0x251D, 0x651, 0, EffectLayer.Waist); + + attacker.RevealingAction(); + + CheckGain(attacker); + } + + public override void OnMiss(Mobile attacker, Mobile defender) + { + ClearCurrentMove(attacker); + + attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise. + + attacker.RevealingAction(); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/DeathStrike.cs b/Projects/UOContent/Spells/Ninjitsu/DeathStrike.cs index de2dd5a14..a7a01f680 100644 --- a/Projects/UOContent/Spells/Ninjitsu/DeathStrike.cs +++ b/Projects/UOContent/Spells/Ninjitsu/DeathStrike.cs @@ -4,175 +4,174 @@ using System.Runtime.CompilerServices; using Server.Items; using Server.SkillHandlers; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class DeathStrike : NinjaMove { - public class DeathStrike : NinjaMove + private static readonly Dictionary _table = new(); + + public override int BaseMana => 30; + public override double RequiredSkill => 85.0; + + // You prepare to hit your opponent with a Death Strike. + public override TextDefinition AbilityMessage { get; } = 1063091; + + public override double GetDamageScalar(Mobile attacker, Mobile defender) => 0.5; + + public override void OnHit(Mobile attacker, Mobile defender, int damage) { - private static readonly Dictionary _table = new(); - - public override int BaseMana => 30; - public override double RequiredSkill => 85.0; - - // You prepare to hit your opponent with a Death Strike. - public override TextDefinition AbilityMessage { get; } = 1063091; - - public override double GetDamageScalar(Mobile attacker, Mobile defender) => 0.5; - - public override void OnHit(Mobile attacker, Mobile defender, int damage) + if (!Validate(attacker) || !CheckMana(attacker, true)) { - if (!Validate(attacker) || !CheckMana(attacker, true)) + return; + } + + ClearCurrentMove(attacker); + + var ninjitsu = attacker.Skills.Ninjitsu.Value; + + // TODO: should be defined onHit method, what if the player hit and remove the weapon before process? ;) + var isRanged = attacker.Weapon is BaseRanged; + + var chance = ninjitsu switch + { + // This formula is an approximation from OSI data. TODO: find correct formula + < 100 => 30 + (ninjitsu - 85) * 2.2, + _ => 63 + (ninjitsu - 100) * 1.1 + }; + + if (chance / 100 < Utility.RandomDouble()) + { + attacker.SendLocalizedMessage(1070779); // You missed your opponent with a Death Strike. + return; + } + + var damageBonus = 0; + + if (_table.Remove(defender, out var timer)) + { + defender.SendLocalizedMessage(1063092); // Your opponent lands another Death Strike! + + if (timer.Steps > 0) { - return; + damageBonus = (int)(attacker.Skills.Ninjitsu.Value / 15); } - ClearCurrentMove(attacker); + timer.Stop(); + } + else + { + defender.SendLocalizedMessage(1063093); // You have been hit by a Death Strike! Move with caution! + } - var ninjitsu = attacker.Skills.Ninjitsu.Value; + attacker.SendLocalizedMessage(1063094); // You inflict a Death Strike upon your opponent! - // TODO: should be defined onHit method, what if the player hit and remove the weapon before process? ;) - var isRanged = attacker.Weapon is BaseRanged; + defender.FixedParticles(0x374A, 1, 17, 0x26BC, EffectLayer.Waist); + attacker.PlaySound(attacker.Female ? 0x50D : 0x50E); - var chance = ninjitsu switch + var t = new DeathStrikeTimer(defender, attacker, damageBonus, isRanged); + + _table[defender] = t; + + t.Start(); + + CheckGain(attacker); + } + + public static void AddStep(Mobile m) + { + if (_table.TryGetValue(m, out var timer) && ++timer.Steps >= 5) + { + timer.ProcessDeathStrike(); + } + } + + public static void RemoveEffect(Mobile m) + { + if (_table.Remove(m, out var timer)) + { + timer.Stop(); + } + } + + private class DeathStrikeTimer : Timer + { + private Mobile _attacker; + private int _damageBonus; + private bool _isRanged; + private Mobile _target; + public int Steps { get; set; } + + internal DeathStrikeTimer(Mobile target, Mobile attacker, int damageBonus, bool isRanged) + : base(TimeSpan.FromSeconds(5.0)) + { + _target = target; + _attacker = attacker; + _damageBonus = damageBonus; + _isRanged = isRanged; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnTick() + { + ProcessDeathStrike(); + } + + public void ProcessDeathStrike() + { + int damage; + + var ninjitsu = _attacker.Skills.Ninjitsu.Value; + var stalkingBonus = Tracking.GetStalkingBonus(_attacker, _target); + + if (Core.ML) { - // This formula is an approximation from OSI data. TODO: find correct formula - < 100 => 30 + (ninjitsu - 85) * 2.2, - _ => 63 + (ninjitsu - 100) * 1.1 - }; + var scalar = Math.Min(1, (_attacker.Skills.Hiding.Value + _attacker.Skills.Stealth.Value) / 220); - if (chance / 100 < Utility.RandomDouble()) - { - attacker.SendLocalizedMessage(1070779); // You missed your opponent with a Death Strike. - return; - } - - var damageBonus = 0; - - if (_table.Remove(defender, out var timer)) - { - defender.SendLocalizedMessage(1063092); // Your opponent lands another Death Strike! - - if (timer.Steps > 0) + // New formula doesn't apply DamageBonus anymore, caps must be, directly, 60/30. + if (Steps >= 5) { - damageBonus = (int)(attacker.Skills.Ninjitsu.Value / 15); - } - - timer.Stop(); - } - else - { - defender.SendLocalizedMessage(1063093); // You have been hit by a Death Strike! Move with caution! - } - - attacker.SendLocalizedMessage(1063094); // You inflict a Death Strike upon your opponent! - - defender.FixedParticles(0x374A, 1, 17, 0x26BC, EffectLayer.Waist); - attacker.PlaySound(attacker.Female ? 0x50D : 0x50E); - - var t = new DeathStrikeTimer(defender, attacker, damageBonus, isRanged); - - _table[defender] = t; - - t.Start(); - - CheckGain(attacker); - } - - public static void AddStep(Mobile m) - { - if (_table.TryGetValue(m, out var timer) && ++timer.Steps >= 5) - { - timer.ProcessDeathStrike(); - } - } - - public static void RemoveEffect(Mobile m) - { - if (_table.Remove(m, out var timer)) - { - timer.Stop(); - } - } - - private class DeathStrikeTimer : Timer - { - private Mobile _attacker; - private int _damageBonus; - private bool _isRanged; - private Mobile _target; - public int Steps { get; set; } - - internal DeathStrikeTimer(Mobile target, Mobile attacker, int damageBonus, bool isRanged) - : base(TimeSpan.FromSeconds(5.0)) - { - _target = target; - _attacker = attacker; - _damageBonus = damageBonus; - _isRanged = isRanged; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void OnTick() - { - ProcessDeathStrike(); - } - - public void ProcessDeathStrike() - { - int damage; - - var ninjitsu = _attacker.Skills.Ninjitsu.Value; - var stalkingBonus = Tracking.GetStalkingBonus(_attacker, _target); - - if (Core.ML) - { - var scalar = Math.Min(1, (_attacker.Skills.Hiding.Value + _attacker.Skills.Stealth.Value) / 220); - - // New formula doesn't apply DamageBonus anymore, caps must be, directly, 60/30. - if (Steps >= 5) - { - damage = (int)Math.Floor(Math.Min(60, ninjitsu / 3 * (0.3 + 0.7 * scalar) + stalkingBonus)); - } - else - { - damage = (int)Math.Floor(Math.Min(30, ninjitsu / 9 * (0.3 + 0.7 * scalar) + stalkingBonus)); - } - - if (_isRanged) - { - damage /= 2; - } - - _target.Damage(damage, _attacker); // Damage is direct. + damage = (int)Math.Floor(Math.Min(60, ninjitsu / 3 * (0.3 + 0.7 * scalar) + stalkingBonus)); } else { - var divisor = Steps >= 5 ? 30 : 80; - var baseDamage = ninjitsu / divisor * 10; - - var maxDamage = Steps >= 5 ? 62 : 22; - damage = Math.Clamp((int)(baseDamage + stalkingBonus), 0, maxDamage) + _damageBonus; - - // Damage is physical. - AOS.Damage( - _target, - _attacker, - damage, - true, - 100, - 0, - 0, - 0, - 0, - 0, - 0, - false, - false, - true - ); + damage = (int)Math.Floor(Math.Min(30, ninjitsu / 9 * (0.3 + 0.7 * scalar) + stalkingBonus)); } - RemoveEffect(_target); + if (_isRanged) + { + damage /= 2; + } + + _target.Damage(damage, _attacker); // Damage is direct. } + else + { + var divisor = Steps >= 5 ? 30 : 80; + var baseDamage = ninjitsu / divisor * 10; + + var maxDamage = Steps >= 5 ? 62 : 22; + damage = Math.Clamp((int)(baseDamage + stalkingBonus), 0, maxDamage) + _damageBonus; + + // Damage is physical. + AOS.Damage( + _target, + _attacker, + damage, + true, + 100, + 0, + 0, + 0, + 0, + 0, + 0, + false, + false, + true + ); + } + + RemoveEffect(_target); } } } diff --git a/Projects/UOContent/Spells/Ninjitsu/FocusAttack.cs b/Projects/UOContent/Spells/Ninjitsu/FocusAttack.cs index 2d78be5db..b6eb26e60 100644 --- a/Projects/UOContent/Spells/Ninjitsu/FocusAttack.cs +++ b/Projects/UOContent/Spells/Ninjitsu/FocusAttack.cs @@ -1,64 +1,63 @@ using Server.Items; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class FocusAttack : NinjaMove { - public class FocusAttack : NinjaMove + public override int BaseMana => Core.ML ? 10 : 20; + public override double RequiredSkill => Core.ML ? 30.0 : 60; + + // You prepare to focus all of your abilities into your next strike. + public override TextDefinition AbilityMessage { get; } = 1063095; + + public override bool Validate(Mobile from) { - public override int BaseMana => Core.ML ? 10 : 20; - public override double RequiredSkill => Core.ML ? 30.0 : 60; - - // You prepare to focus all of your abilities into your next strike. - public override TextDefinition AbilityMessage { get; } = 1063095; - - public override bool Validate(Mobile from) + var twoHanded = from.FindItemOnLayer(Layer.TwoHanded); + if (twoHanded is BaseShield) { - var twoHanded = from.FindItemOnLayer(Layer.TwoHanded); - if (twoHanded is BaseShield) - { - from.SendLocalizedMessage(1063096); // You cannot use this ability while holding a shield. - return false; - } - - var meleeWeapon = - twoHanded is BaseWeapon and not BaseRanged || - from.FindItemOnLayer(Layer.OneHanded) is BaseWeapon and not BaseRanged; - - if (meleeWeapon) - { - return base.Validate(from); - } - - from.SendLocalizedMessage(1063097); // You must be wielding a melee weapon without a shield to use this ability. + from.SendLocalizedMessage(1063096); // You cannot use this ability while holding a shield. return false; } - public override double GetDamageScalar(Mobile attacker, Mobile defender) - { - var ninjitsu = attacker.Skills.Ninjitsu.Value; + var meleeWeapon = + twoHanded is BaseWeapon and not BaseRanged || + from.FindItemOnLayer(Layer.OneHanded) is BaseWeapon and not BaseRanged; - return 1.0 + ninjitsu * ninjitsu / 43636; + if (meleeWeapon) + { + return base.Validate(from); } - public override double GetPropertyBonus(Mobile attacker) - { - var ninjitsu = attacker.Skills.Ninjitsu.Value; - - var bonus = ninjitsu * ninjitsu / 43636; - - return 1.0 + (bonus * 3 + 0.01); - } - - public override bool OnBeforeDamage(Mobile attacker, Mobile defender) => - Validate(attacker) && CheckMana(attacker, true); - - public override void OnHit(Mobile attacker, Mobile defender, int damage) - { - ClearCurrentMove(attacker); - - attacker.SendLocalizedMessage(1063098); // You focus all of your abilities and strike with deadly force! - attacker.PlaySound(0x510); - - CheckGain(attacker); - } + from.SendLocalizedMessage(1063097); // You must be wielding a melee weapon without a shield to use this ability. + return false; } -} + + public override double GetDamageScalar(Mobile attacker, Mobile defender) + { + var ninjitsu = attacker.Skills.Ninjitsu.Value; + + return 1.0 + ninjitsu * ninjitsu / 43636; + } + + public override double GetPropertyBonus(Mobile attacker) + { + var ninjitsu = attacker.Skills.Ninjitsu.Value; + + var bonus = ninjitsu * ninjitsu / 43636; + + return 1.0 + (bonus * 3 + 0.01); + } + + public override bool OnBeforeDamage(Mobile attacker, Mobile defender) => + Validate(attacker) && CheckMana(attacker, true); + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + ClearCurrentMove(attacker); + + attacker.SendLocalizedMessage(1063098); // You focus all of your abilities and strike with deadly force! + attacker.PlaySound(0x510); + + CheckGain(attacker); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/KiAttack.cs b/Projects/UOContent/Spells/Ninjitsu/KiAttack.cs index 172c77831..ae04c204c 100644 --- a/Projects/UOContent/Spells/Ninjitsu/KiAttack.cs +++ b/Projects/UOContent/Spells/Ninjitsu/KiAttack.cs @@ -2,125 +2,124 @@ using System; using System.Collections.Generic; using Server.Items; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class KiAttack : NinjaMove { - public class KiAttack : NinjaMove + private static readonly Dictionary _table = new(); + + public override int BaseMana => 25; + public override double RequiredSkill => 80.0; + + // Your Ki Attack must be complete within 2 seconds for the damage bonus! + public override TextDefinition AbilityMessage { get; } = 1063099; + + public override void OnUse(Mobile from) { - private static readonly Dictionary _table = new(); - - public override int BaseMana => 25; - public override double RequiredSkill => 80.0; - - // Your Ki Attack must be complete within 2 seconds for the damage bonus! - public override TextDefinition AbilityMessage { get; } = 1063099; - - public override void OnUse(Mobile from) + if (!Validate(from)) { - if (!Validate(from)) - { - return; - } - - var t = new KiAttackTimer(from); - _table[from] = t; - t.Start(); + return; } - public override bool Validate(Mobile from) + var t = new KiAttackTimer(from); + _table[from] = t; + t.Start(); + } + + public override bool Validate(Mobile from) + { + if (from.Hidden && from.AllowedStealthSteps > 0) { - if (from.Hidden && from.AllowedStealthSteps > 0) - { - from.SendLocalizedMessage(1063127); // You cannot use this ability while in stealth mode. - return false; - } - - if (Core.ML && from.Weapon is BaseRanged) - { - from.SendLocalizedMessage(1075858); // You can only use this with melee attacks. - return false; - } - - return base.Validate(from); + from.SendLocalizedMessage(1063127); // You cannot use this ability while in stealth mode. + return false; } - public override double GetDamageScalar(Mobile attacker, Mobile defender) + if (Core.ML && from.Weapon is BaseRanged) { - if (attacker.Hidden) - { - return 1.0; - } - - /* - * Pub40 changed pvp damage max to 55% - */ - return 1.0 + GetBonus(attacker) / (Core.ML && attacker.Player && defender.Player ? 40 : 10); + from.SendLocalizedMessage(1075858); // You can only use this with melee attacks. + return false; } - public override void OnHit(Mobile attacker, Mobile defender, int damage) + return base.Validate(from); + } + + public override double GetDamageScalar(Mobile attacker, Mobile defender) + { + if (attacker.Hidden) { - if (!Validate(attacker) || !CheckMana(attacker, true)) - { - return; - } - - ClearCurrentMove(attacker); - - if (GetBonus(attacker) == 0.0) - { - attacker.SendLocalizedMessage(1063101); // You were too close to your target to cause any additional damage. - } - else - { - attacker.FixedParticles(0x37BE, 1, 5, 0x26BD, 0x0, 0x1, EffectLayer.Waist); - attacker.PlaySound(0x510); - - // Your quick flight to your target causes extra damage as you strike! - attacker.SendLocalizedMessage(1063100); - defender.FixedParticles(0x37BE, 1, 5, 0x26BD, 0, 0x1, EffectLayer.Waist); - - CheckGain(attacker); - } + return 1.0; } - public override void OnClearMove(Mobile from) + /* + * Pub40 changed pvp damage max to 55% + */ + return 1.0 + GetBonus(attacker) / (Core.ML && attacker.Player && defender.Player ? 40 : 10); + } + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + if (!Validate(attacker) || !CheckMana(attacker, true)) { - if (_table.Remove(from, out var t)) - { - t.Stop(); - } + return; } - public static double GetBonus(Mobile from) + ClearCurrentMove(attacker); + + if (GetBonus(attacker) == 0.0) { - if (!_table.TryGetValue(from, out var t)) - { - return 0; - } + attacker.SendLocalizedMessage(1063101); // You were too close to your target to cause any additional damage. + } + else + { + attacker.FixedParticles(0x37BE, 1, 5, 0x26BD, 0x0, 0x1, EffectLayer.Waist); + attacker.PlaySound(0x510); - var xDelta = t._location.X - from.X; - var yDelta = t._location.Y - from.Y; + // Your quick flight to your target causes extra damage as you strike! + attacker.SendLocalizedMessage(1063100); + defender.FixedParticles(0x37BE, 1, 5, 0x26BD, 0, 0x1, EffectLayer.Waist); - return Math.Min(Math.Sqrt(xDelta * xDelta + yDelta * yDelta), 20.0); + CheckGain(attacker); + } + } + + public override void OnClearMove(Mobile from) + { + if (_table.Remove(from, out var t)) + { + t.Stop(); + } + } + + public static double GetBonus(Mobile from) + { + if (!_table.TryGetValue(from, out var t)) + { + return 0; } - private class KiAttackTimer : Timer + var xDelta = t._location.X - from.X; + var yDelta = t._location.Y - from.Y; + + return Math.Min(Math.Sqrt(xDelta * xDelta + yDelta * yDelta), 20.0); + } + + private class KiAttackTimer : Timer + { + public Mobile _mobile; + public Point3D _location; + + public KiAttackTimer(Mobile m) : base(TimeSpan.FromSeconds(2.0)) { - public Mobile _mobile; - public Point3D _location; + _mobile = m; + _location = m.Location; + } - public KiAttackTimer(Mobile m) : base(TimeSpan.FromSeconds(2.0)) - { - _mobile = m; - _location = m.Location; - } + protected override void OnTick() + { + ClearCurrentMove(_mobile); + _mobile.SendLocalizedMessage(1063102); // You failed to complete your Ki Attack in time. - protected override void OnTick() - { - ClearCurrentMove(_mobile); - _mobile.SendLocalizedMessage(1063102); // You failed to complete your Ki Attack in time. - - _table.Remove(_mobile); - } + _table.Remove(_mobile); } } } diff --git a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs index 6cde043a2..d5a088123 100644 --- a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs +++ b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs @@ -37,7 +37,7 @@ namespace Server.Spells.Ninjitsu { if (m != null) { - _cloneCount[m] = 1 + (_cloneCount.TryGetValue(m, out var count) ? count : 0); + _cloneCount[m] = 1 + _cloneCount.GetValueOrDefault(m, 0); } } @@ -144,8 +144,10 @@ namespace Server.Mobiles for (var i = 0; i < caster.Skills.Length; ++i) { - Skills[i].Base = caster.Skills[i].Base; - Skills[i].Cap = caster.Skills[i].Cap; + var skill = Skills[i]; + var casterSkill = caster.Skills[i]; + skill.Base = casterSkill.Base; + skill.Cap = casterSkill.Cap; } for (var i = 0; i < caster.Items.Count; i++) @@ -161,7 +163,12 @@ namespace Server.Mobiles ControlOrder = OrderType.Follow; ControlTarget = caster; - var duration = TimeSpan.FromSeconds(30.0 + caster.Skills.Ninjitsu.Value / 4.0); + AddClone(); + } + + private void AddClone() + { + var duration = TimeSpan.FromSeconds(30.0 + _caster.Skills.Ninjitsu.Value / 4.0); new UnsummonTimer(this, duration).Start(); SummonEnd = Core.Now + duration; @@ -178,7 +185,7 @@ namespace Server.Mobiles public override bool IsHumanInTown() => false; - private Item CloneItem(Item item) + private static Item CloneItem(Item item) { var newItem = new Item(item.ItemID); newItem.Hue = item.Hue; @@ -214,13 +221,10 @@ namespace Server.Mobiles [AfterDeserialization] private void AfterDeserialization() { - MirrorImage.AddClone(_caster); + AddClone(); } } -} -namespace Server.Mobiles -{ public class CloneAI : BaseAI { public CloneAI(Clone m) : base(m) => m.SetCurrentSpeedToActive(); diff --git a/Projects/UOContent/Spells/Ninjitsu/NinjaMove.cs b/Projects/UOContent/Spells/Ninjitsu/NinjaMove.cs index ef85608ec..795b80328 100644 --- a/Projects/UOContent/Spells/Ninjitsu/NinjaMove.cs +++ b/Projects/UOContent/Spells/Ninjitsu/NinjaMove.cs @@ -1,12 +1,11 @@ -namespace Server.Spells -{ - public class NinjaMove : SpecialMove - { - public override SkillName MoveSkill => SkillName.Ninjitsu; +namespace Server.Spells; - public override void CheckGain(Mobile m) - { - m.CheckSkill(MoveSkill, RequiredSkill - 12.5, RequiredSkill + 37.5); // Per five on friday 02/16/07 - } +public class NinjaMove : SpecialMove +{ + public override SkillName MoveSkill => SkillName.Ninjitsu; + + public override void CheckGain(Mobile m) + { + m.CheckSkill(MoveSkill, RequiredSkill - 12.5, RequiredSkill + 37.5); // Per five on friday 02/16/07 } -} +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/NinjaSpell.cs b/Projects/UOContent/Spells/Ninjitsu/NinjaSpell.cs index 79452f6f9..63482aba4 100644 --- a/Projects/UOContent/Spells/Ninjitsu/NinjaSpell.cs +++ b/Projects/UOContent/Spells/Ninjitsu/NinjaSpell.cs @@ -1,98 +1,97 @@ using Server.Mobiles; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public abstract class NinjaSpell : Spell { - public abstract class NinjaSpell : Spell + public NinjaSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { - public NinjaSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) - { - } - - public abstract double RequiredSkill { get; } - public abstract int RequiredMana { get; } - - public override SkillName CastSkill => SkillName.Ninjitsu; - public override SkillName DamageSkill => SkillName.Ninjitsu; - - public override bool RevealOnCast => false; - public override bool ClearHandsOnCast => false; - public override bool ShowHandMovement => false; - - public override bool BlocksMovement => false; - - // public override int CastDelayBase => 1; - - public override int CastRecoveryBase => 7; - - public static bool CheckExpansion(Mobile from) => - (from as PlayerMobile)?.NetState?.SupportsExpansion(Expansion.SE) == true; - - public override bool CheckCast() - { - var mana = ScaleMana(RequiredMana); - - if (!base.CheckCast()) - { - return false; - } - - if (!CheckExpansion(Caster)) - { - Caster.SendLocalizedMessage(1063456); // You must upgrade to Samurai Empire in order to use that ability. - return false; - } - - if (Caster.Skills[CastSkill].Value < RequiredSkill) - { - // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. - Caster.SendLocalizedMessage(1063013, $"{RequiredSkill:F1}\t{CastSkill}\t "); - return false; - } - - if (Caster.Mana < mana) - { - // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. - Caster.SendLocalizedMessage(1060174, mana.ToString()); - return false; - } - - return true; - } - - public override bool CheckFizzle() - { - var mana = ScaleMana(RequiredMana); - - if (Caster.Skills[CastSkill].Value < RequiredSkill) - { - // You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack! - Caster.SendLocalizedMessage(1063352, RequiredSkill.ToString("F1")); - return false; - } - - if (Caster.Mana < mana) - { - // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. - Caster.SendLocalizedMessage(1060174, mana.ToString()); - return false; - } - - if (!base.CheckFizzle()) - { - return false; - } - - Caster.Mana -= mana; - - return true; - } - - public override void GetCastSkills(out double min, out double max) - { - min = RequiredSkill - 12.5; // Per 5 on friday 2/16/07 - max = RequiredSkill + 37.5; - } - - public override int GetMana() => 0; } -} + + public abstract double RequiredSkill { get; } + public abstract int RequiredMana { get; } + + public override SkillName CastSkill => SkillName.Ninjitsu; + public override SkillName DamageSkill => SkillName.Ninjitsu; + + public override bool RevealOnCast => false; + public override bool ClearHandsOnCast => false; + public override bool ShowHandMovement => false; + + public override bool BlocksMovement => false; + + // public override int CastDelayBase => 1; + + public override int CastRecoveryBase => 7; + + public static bool CheckExpansion(Mobile from) => + (from as PlayerMobile)?.NetState?.SupportsExpansion(Expansion.SE) == true; + + public override bool CheckCast() + { + var mana = ScaleMana(RequiredMana); + + if (!base.CheckCast()) + { + return false; + } + + if (!CheckExpansion(Caster)) + { + Caster.SendLocalizedMessage(1063456); // You must upgrade to Samurai Empire in order to use that ability. + return false; + } + + if (Caster.Skills[CastSkill].Value < RequiredSkill) + { + // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability. + Caster.SendLocalizedMessage(1063013, $"{RequiredSkill:F1}\t{CastSkill}\t "); + return false; + } + + if (Caster.Mana < mana) + { + // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. + Caster.SendLocalizedMessage(1060174, mana.ToString()); + return false; + } + + return true; + } + + public override bool CheckFizzle() + { + var mana = ScaleMana(RequiredMana); + + if (Caster.Skills[CastSkill].Value < RequiredSkill) + { + // You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack! + Caster.SendLocalizedMessage(1063352, RequiredSkill.ToString("F1")); + return false; + } + + if (Caster.Mana < mana) + { + // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability. + Caster.SendLocalizedMessage(1060174, mana.ToString()); + return false; + } + + if (!base.CheckFizzle()) + { + return false; + } + + Caster.Mana -= mana; + + return true; + } + + public override void GetCastSkills(out double min, out double max) + { + min = RequiredSkill - 12.5; // Per 5 on friday 2/16/07 + max = RequiredSkill + 37.5; + } + + public override int GetMana() => 0; +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs b/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs index ccb6c50b0..e225a7102 100644 --- a/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs +++ b/Projects/UOContent/Spells/Ninjitsu/ShadowJump.cs @@ -7,113 +7,112 @@ using Server.Regions; using Server.SkillHandlers; using Server.Targeting; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class Shadowjump : NinjaSpell, ISpellTargetingPoint3D { - public class Shadowjump : NinjaSpell, ISpellTargetingPoint3D + private static readonly SpellInfo _info = new( + "Shadowjump", + null, + -1, + 9002 + ); + + public Shadowjump(Mobile caster, Item scroll) : base(caster, scroll, _info) { - private static readonly SpellInfo _info = new( - "Shadowjump", - null, - -1, - 9002 - ); - - public Shadowjump(Mobile caster, Item scroll) : base(caster, scroll, _info) - { - } - - public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); - - public override double RequiredSkill => 50.0; - public override int RequiredMana => 15; - - public override bool BlockedByAnimalForm => false; - - public void Target(IPoint3D p) - { - var orig = p; - var map = Caster.Map; - - SpellHelper.GetSurfaceTop(ref p); - - var from = Caster.Location; - var to = new Point3D(p); - - if ((Caster as PlayerMobile)?.IsStealthing != true) - { - Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. - } - else if (Sigil.ExistsOn(Caster)) - { - Caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil. - } - else if (StaminaSystem.IsOverloaded(Caster)) - { - Caster.SendLocalizedMessage(502359, "", 0x22); // Thou art too encumbered to move. - } - else if (!SpellHelper.CheckTravel(Caster, TravelCheckType.TeleportFrom, out var failureMessage)) - { - failureMessage.SendMessageTo(Caster); - } - else if (!SpellHelper.CheckTravel(Caster, map, to, TravelCheckType.TeleportTo, out failureMessage)) - { - failureMessage.SendMessageTo(Caster); - } - else if (map?.CanSpawnMobile(p.X, p.Y, p.Z) != true) - { - Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot. - } - else if (SpellHelper.CheckMulti(to, map, true, 5)) - { - Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot. - } - else if (Region.Find(to, map).IsPartOf()) - { - Caster.SendLocalizedMessage(502829); // Cannot teleport to that spot. - } - else if (CheckSequence()) - { - SpellHelper.Turn(Caster, orig); - - var m = Caster; - - m.Location = to; - m.ProcessDelta(); - - Effects.SendLocationParticles( - EffectItem.Create(from, m.Map, EffectItem.DefaultDuration), - 0x3728, - 10, - 10, - 2023 - ); - - m.PlaySound(0x512); - - Stealth.OnUse(m); // stealth check after the shadow jump - } - - FinishSequence(); - } - - public override bool CheckCast() - { - // IsStealthing should be moved to Server.Mobiles - if ((Caster as PlayerMobile)?.IsStealthing != true) - { - Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. - return false; - } - - return base.CheckCast(); - } - - public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false; - - public override void OnCast() - { - Caster.SendLocalizedMessage(1063088); // You prepare to perform a Shadowjump. - Caster.Target = new SpellTargetPoint3D(this, TargetFlags.None, 11); - } } -} + + public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0); + + public override double RequiredSkill => 50.0; + public override int RequiredMana => 15; + + public override bool BlockedByAnimalForm => false; + + public void Target(IPoint3D p) + { + var orig = p; + var map = Caster.Map; + + SpellHelper.GetSurfaceTop(ref p); + + var from = Caster.Location; + var to = new Point3D(p); + + if ((Caster as PlayerMobile)?.IsStealthing != true) + { + Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. + } + else if (Sigil.ExistsOn(Caster)) + { + Caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil. + } + else if (StaminaSystem.IsOverloaded(Caster)) + { + Caster.SendLocalizedMessage(502359, "", 0x22); // Thou art too encumbered to move. + } + else if (!SpellHelper.CheckTravel(Caster, TravelCheckType.TeleportFrom, out var failureMessage)) + { + failureMessage.SendMessageTo(Caster); + } + else if (!SpellHelper.CheckTravel(Caster, map, to, TravelCheckType.TeleportTo, out failureMessage)) + { + failureMessage.SendMessageTo(Caster); + } + else if (map?.CanSpawnMobile(p.X, p.Y, p.Z) != true) + { + Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot. + } + else if (SpellHelper.CheckMulti(to, map, true, 5)) + { + Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot. + } + else if (Region.Find(to, map).IsPartOf()) + { + Caster.SendLocalizedMessage(502829); // Cannot teleport to that spot. + } + else if (CheckSequence()) + { + SpellHelper.Turn(Caster, orig); + + var m = Caster; + + m.Location = to; + m.ProcessDelta(); + + Effects.SendLocationParticles( + EffectItem.Create(from, m.Map, EffectItem.DefaultDuration), + 0x3728, + 10, + 10, + 2023 + ); + + m.PlaySound(0x512); + + Stealth.OnUse(m); // stealth check after the shadow jump + } + + FinishSequence(); + } + + public override bool CheckCast() + { + // IsStealthing should be moved to Server.Mobiles + if ((Caster as PlayerMobile)?.IsStealthing != true) + { + Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. + return false; + } + + return base.CheckCast(); + } + + public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false; + + public override void OnCast() + { + Caster.SendLocalizedMessage(1063088); // You prepare to perform a Shadowjump. + Caster.Target = new SpellTargetPoint3D(this, TargetFlags.None, 11); + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Ninjitsu/SurpriseAttack.cs b/Projects/UOContent/Spells/Ninjitsu/SurpriseAttack.cs index 7e470944e..c35376b35 100644 --- a/Projects/UOContent/Spells/Ninjitsu/SurpriseAttack.cs +++ b/Projects/UOContent/Spells/Ninjitsu/SurpriseAttack.cs @@ -2,120 +2,119 @@ using System; using System.Collections.Generic; using Server.SkillHandlers; -namespace Server.Spells.Ninjitsu +namespace Server.Spells.Ninjitsu; + +public class SurpriseAttack : NinjaMove { - public class SurpriseAttack : NinjaMove + private static readonly Dictionary _table = new(); + + public override int BaseMana => 20; + public override double RequiredSkill => Core.ML ? 60.0 : 30.0; + + public override TextDefinition AbilityMessage { get; } = 1063128; // You prepare to surprise your prey. + + public override bool ValidatesDuringHit => false; + + public override bool Validate(Mobile from) { - private static readonly Dictionary _table = new(); - - public override int BaseMana => 20; - public override double RequiredSkill => Core.ML ? 60.0 : 30.0; - - public override TextDefinition AbilityMessage { get; } = 1063128; // You prepare to surprise your prey. - - public override bool ValidatesDuringHit => false; - - public override bool Validate(Mobile from) + if (!from.Hidden || from.AllowedStealthSteps <= 0) { - if (!from.Hidden || from.AllowedStealthSteps <= 0) - { - from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. - return false; - } - - return base.Validate(from); + from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability. + return false; } - public override bool OnBeforeSwing(Mobile attacker, Mobile defender) + return base.Validate(from); + } + + public override bool OnBeforeSwing(Mobile attacker, Mobile defender) + { + var valid = Validate(attacker) && CheckMana(attacker, true); + + if (valid) { - var valid = Validate(attacker) && CheckMana(attacker, true); - - if (valid) - { - attacker.BeginAction(); - Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction); - } - - return valid; + attacker.BeginAction(); + Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction); } - public override void OnHit(Mobile attacker, Mobile defender, int damage) + return valid; + } + + public override void OnHit(Mobile attacker, Mobile defender, int damage) + { + // Validates before swing + + ClearCurrentMove(attacker); + + attacker.SendLocalizedMessage(1063129); // You catch your opponent off guard with your Surprise Attack! + defender.SendLocalizedMessage(1063130); // Your defenses are lowered as your opponent surprises you! + + defender.FixedParticles(0x37B9, 1, 5, 0x26DA, 0, 3, EffectLayer.Head); + + attacker.RevealingAction(); + + StopTimer(defender); + + var ninjitsu = attacker.Skills.Ninjitsu.Fixed; + + var malus = ninjitsu / 60 + (int)Tracking.GetStalkingBonus(attacker, defender); + + var timer = new SurpriseAttackTimer(defender, malus); + timer.Start(); + + _table[defender] = timer; + + CheckGain(attacker); + } + + public override void OnMiss(Mobile attacker, Mobile defender) + { + ClearCurrentMove(attacker); + + attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise. + + attacker.RevealingAction(); + } + + public static bool GetMalus(Mobile target, ref int malus) + { + if (!_table.TryGetValue(target, out var info)) { - // Validates before swing - - ClearCurrentMove(attacker); - - attacker.SendLocalizedMessage(1063129); // You catch your opponent off guard with your Surprise Attack! - defender.SendLocalizedMessage(1063130); // Your defenses are lowered as your opponent surprises you! - - defender.FixedParticles(0x37B9, 1, 5, 0x26DA, 0, 3, EffectLayer.Head); - - attacker.RevealingAction(); - - StopTimer(defender); - - var ninjitsu = attacker.Skills.Ninjitsu.Fixed; - - var malus = ninjitsu / 60 + (int)Tracking.GetStalkingBonus(attacker, defender); - - var timer = new SurpriseAttackTimer(defender, malus); - timer.Start(); - - _table[defender] = timer; - - CheckGain(attacker); + return false; } - public override void OnMiss(Mobile attacker, Mobile defender) + malus = info.Malus; + return true; + } + + private static void StopTimer(Mobile m) + { + if (_table.Remove(m, out var timer)) { - ClearCurrentMove(attacker); - - attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise. - - attacker.RevealingAction(); - } - - public static bool GetMalus(Mobile target, ref int malus) - { - if (!_table.TryGetValue(target, out var info)) - { - return false; - } - - malus = info.Malus; - return true; - } - - private static void StopTimer(Mobile m) - { - if (_table.Remove(m, out var timer)) - { - timer.Stop(); - } - } - - private static void EndSurprise(SurpriseAttackTimer info) - { - StopTimer(info.Target); - info.Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal. - } - - private class SurpriseAttackTimer : Timer - { - public int Malus; - public Mobile Target; - - public SurpriseAttackTimer(Mobile target, int effect) : base(TimeSpan.FromSeconds(8.0)) - { - Target = target; - Malus = effect; - } - - protected override void OnTick() - { - StopTimer(Target); - Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal. - } + timer.Stop(); } } -} + + private static void EndSurprise(SurpriseAttackTimer info) + { + StopTimer(info.Target); + info.Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal. + } + + private class SurpriseAttackTimer : Timer + { + public int Malus; + public Mobile Target; + + public SurpriseAttackTimer(Mobile target, int effect) : base(TimeSpan.FromSeconds(8.0)) + { + Target = target; + Malus = effect; + } + + protected override void OnTick() + { + StopTimer(Target); + Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal. + } + } +} \ No newline at end of file diff --git a/Projects/UOContent/Spells/Second/Protection.cs b/Projects/UOContent/Spells/Second/Protection.cs index 3c6661118..55ef9edac 100644 --- a/Projects/UOContent/Spells/Second/Protection.cs +++ b/Projects/UOContent/Spells/Second/Protection.cs @@ -15,8 +15,8 @@ namespace Server.Spells.Second Reagent.SulfurousAsh ); - private static readonly Dictionary> _table = - new(); + // TODO: Cleanup periodically if players have logged out for a while + private static readonly Dictionary> _table = new(); public ProtectionSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) { diff --git a/Projects/UOContent/Spells/UnsummonTimer.cs b/Projects/UOContent/Spells/UnsummonTimer.cs index 18e1c96be..169517411 100644 --- a/Projects/UOContent/Spells/UnsummonTimer.cs +++ b/Projects/UOContent/Spells/UnsummonTimer.cs @@ -9,8 +9,8 @@ public class UnsummonTimer : Timer { // Track timers since some of them are really long and might hold references to long dead/deleted mobs private static readonly Dictionary _timers = new(); - private BaseCreature _creature; - private Action _onUnsummon; + private readonly BaseCreature _creature; + private readonly Action _onUnsummon; public static void StopTimer(BaseCreature creature) {