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:
parent
6747168db0
commit
a552cf4138
51 changed files with 4164 additions and 4188 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue