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.
This commit is contained in:
Kamron Batman 2024-05-21 19:44:42 -07:00 committed by GitHub
parent 6747168db0
commit a552cf4138
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4164 additions and 4188 deletions

View file

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

View file

@ -1216,7 +1216,7 @@ namespace Server
public override string ToString() => "...";
public void CheckCancelMorph(Mobile m)
public static void CheckCancelMorph(Mobile m)
{
if (m == null)
{

View file

@ -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();

View file

@ -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<ServerLockdownNoticeGump>
@ -2561,6 +2559,7 @@ namespace Server.Mobiles
PolymorphSpell.StopTimer(this);
IncognitoSpell.StopTimer(this);
DisguisePersistence.RemoveTimer(this);
AnimalForm.RemoveContext(this, true);
EndAction<PolymorphSpell>();
EndAction<IncognitoSpell>();

View file

@ -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);

View file

@ -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
{

View file

@ -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);

View file

@ -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;

View file

@ -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));
}
}

View file

@ -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));

View file

@ -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);
}
}
}

View file

@ -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)
{

View file

@ -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);
}
}

View file

@ -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();
}
}
}
}

View file

@ -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<Mobile>.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<Mobile>.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;
}
}

View file

@ -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);
}
}

View file

@ -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<Mobile>.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<Mobile>.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
);
}
}

View file

@ -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 Mystics 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 Mystics 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);
}

View file

@ -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<Mobile>.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<Mobile>.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
);
}
}

View file

@ -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<Mobile, SpellPlagueTimer> _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<Mobile, SpellPlagueTimer> _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();
}
}
}

View file

@ -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<Mobile, List<Mobile>> _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<AnimateMaabusCorpseObjective>();
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<SummonEntry>()),
// 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<Type>(),
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<Mobile, List<Mobile>> _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<AnimateMaabusCorpseObjective>();
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<Mobile>();
}
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<Mobile>();
}
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<Mobile>();
}
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;
}
}
}

View file

@ -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<Mobile, Mobile> _oathTable = new();
private static readonly Dictionary<Mobile, ExpireTimer> _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<Mobile, Mobile> _oathTable = new();
private static readonly Dictionary<Mobile, ExpireTimer> _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);
}
}
}
}
}

View file

@ -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<Mobile, ExpireTimer> _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<Mobile, ExpireTimer> _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();
}
}
}
}

View file

@ -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<BaseWeapon, ExpireTimer> _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<BaseWeapon, ExpireTimer> _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);
}
}
}
}

View file

@ -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<Mobile, DefaultSkillMod> _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<Mobile, DefaultSkillMod> _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;
}
}

View file

@ -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<ChampionSpawnRegion>();
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<Mobile>.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<DungeonRegion>() == Region.Find(c.Location, map).IsPartOf<DungeonRegion>())
{
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<Point3D>();
}
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<ChampionSpawnRegion>();
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<Mobile>.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<DungeonRegion>() == Region.Find(c.Location, map).IsPartOf<DungeonRegion>())
{
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<Point3D>();
}
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;
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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<Mobile, MRExpireTimer> _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<Mobile, MRExpireTimer> _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();
}
}
}

View file

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

View file

@ -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<Mobile, InternalTimer> _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<Mobile, InternalTimer> _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);
}
}
}
}

View file

@ -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<Mobile>.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<Mobile>.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);
}
}

View file

@ -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<Mobile, InternalTimer> _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<Mobile, InternalTimer> _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();
}
}
}
}
}
}

View file

@ -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<Mobile, BaseCreature> 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<Mobile, BaseCreature> 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<SummonFamiliarGump>();
Caster.SendGump(new SummonFamiliarGump(Caster, Entries, this));
}
return base.CheckCast();
}
FinishSequence();
public override void OnCast()
{
if (CheckSequence())
{
Caster.CloseGump<SummonFamiliarGump>();
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<SummonFamiliarGump>();
_from.SendGump(new SummonFamiliarGump(_from, SummonFamiliarSpell.Entries, _spell));
}
else if (entry.Type == null)
{
_from.SendMessage("That familiar has not yet been defined.");
_from.CloseGump<SummonFamiliarGump>();
_from.SendGump(new SummonFamiliarGump(_from, SummonFamiliarSpell.Entries, _spell));
}
else
{
try
{
var entry = m_Entries[index];
var bc = entry.Type.CreateInstance<BaseCreature>();
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<SummonFamiliarGump>();
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<SummonFamiliarGump>();
m_From.SendGump(new SummonFamiliarGump(m_From, SummonFamiliarSpell.Entries, m_Spell));
}
else
{
try
{
var bc = entry.Type.CreateInstance<BaseCreature>();
// 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
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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<Mobile>.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<Mobile>.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();
}
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
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<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
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();
}
}

View file

@ -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<Mobile, DeathStrikeTimer> _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<Mobile, DeathStrikeTimer> _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);
}
}
}

View file

@ -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);
}
}

View file

@ -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<Mobile, KiAttackTimer> _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<Mobile, KiAttackTimer> _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);
}
}
}

View file

@ -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();

View file

@ -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
}
}
}

View file

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

View file

@ -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<HouseRegion>())
{
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<HouseRegion>())
{
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);
}
}

View file

@ -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<Mobile, SurpriseAttackTimer> _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<Mobile, SurpriseAttackTimer> _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<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
return valid;
attacker.BeginAction<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
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.
}
}
}

View file

@ -15,8 +15,8 @@ namespace Server.Spells.Second
Reagent.SulfurousAsh
);
private static readonly Dictionary<Mobile, Tuple<ResistanceMod, DefaultSkillMod>> _table =
new();
// TODO: Cleanup periodically if players have logged out for a while
private static readonly Dictionary<Mobile, Tuple<ResistanceMod, DefaultSkillMod>> _table = new();
public ProtectionSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{

View file

@ -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<BaseCreature, UnsummonTimer> _timers = new();
private BaseCreature _creature;
private Action _onUnsummon;
private readonly BaseCreature _creature;
private readonly Action _onUnsummon;
public static void StopTimer(BaseCreature creature)
{