fix: Fixes various memory leaks and spells (#1786)

### Summary
- Fixes various memory leaks related to spells
- Fixes Spell Plague
- Added ability to determine if `sdi` should take effect for Spell Damage.
- Fixes animal form timer ticking non-stop while logged out.
This commit is contained in:
Kamron Batman 2024-05-21 19:44:42 -07:00 • committed by GitHub
parent 6747168db0
commit a552cf4138
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4164 additions and 4188 deletions

View file

@ -9,6 +9,12 @@ using Server.Engines.Virtues;
using Server.Logging; using Server.Logging;
using Server.Network; using Server.Network;
using Server.Regions; using Server.Regions;
using Server.Spells.Fifth;
using Server.Spells.First;
using Server.Spells.Mysticism;
using Server.Spells.Necromancy;
using Server.Spells.Ninjitsu;
using Server.Spells.Second;
namespace Server.Misc; namespace Server.Misc;
@ -235,6 +241,15 @@ public static class AccountHandler
PlayerMurderSystem.OnPlayerDeleted(m); PlayerMurderSystem.OnPlayerDeleted(m);
ChampionTitleSystem.OnPlayerDeleted(m); ChampionTitleSystem.OnPlayerDeleted(m);
// Spells
MagicReflectSpell.EndReflect(m);
ReactiveArmorSpell.EndArmor(m);
ProtectionSpell.EndProtection(m);
StoneFormSpell.RemoveEffects(m);
AnimateDeadSpell.RemoveEffects(m);
SummonFamiliarSpell.RemoveEffects(m);
AnimalForm.RemoveLastAnimalForm(m);
state.SendCharacterListUpdate(acct); state.SendCharacterListUpdate(acct);
return; return;
} }

View file

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

View file

@ -2395,13 +2395,17 @@ namespace Server.Mobiles
AnimateDeadSpell.Unregister(m_SummonMaster, this); AnimateDeadSpell.Unregister(m_SummonMaster, this);
} }
if (Summoned && SummonMaster != null)
{
SummonFamiliarSpell.Unregister(SummonMaster, this);
}
if (MLQuestSystem.Enabled) if (MLQuestSystem.Enabled)
{ {
MLQuestSystem.HandleDeletion(this); MLQuestSystem.HandleDeletion(this);
} }
UnsummonTimer.StopTimer(this); UnsummonTimer.StopTimer(this);
StaminaSystem.RemoveEntry(this as IHasSteps); StaminaSystem.RemoveEntry(this as IHasSteps);
base.OnAfterDelete(); base.OnAfterDelete();

View file

@ -1216,7 +1216,7 @@ namespace Server.Mobiles
} }
} }
public static void OnLogin(Mobile from) public static void OnLogin(PlayerMobile from)
{ {
if (AccountHandler.LockdownLevel > AccessLevel.Player) if (AccountHandler.LockdownLevel > AccessLevel.Player)
{ {
@ -1253,11 +1253,9 @@ namespace Server.Mobiles
return; return;
} }
if (from is PlayerMobile mobile) VirtueSystem.CheckAtrophies(from);
{ from.ClaimAutoStabledPets();
VirtueSystem.CheckAtrophies(mobile); AnimalForm.GetContext(from)?.Timer.Start();
mobile.ClaimAutoStabledPets();
}
} }
private class ServerLockdownNoticeGump : StaticNoticeGump<ServerLockdownNoticeGump> private class ServerLockdownNoticeGump : StaticNoticeGump<ServerLockdownNoticeGump>
@ -2561,6 +2559,7 @@ namespace Server.Mobiles
PolymorphSpell.StopTimer(this); PolymorphSpell.StopTimer(this);
IncognitoSpell.StopTimer(this); IncognitoSpell.StopTimer(this);
DisguisePersistence.RemoveTimer(this); DisguisePersistence.RemoveTimer(this);
AnimalForm.RemoveContext(this, true);
EndAction<PolymorphSpell>(); EndAction<PolymorphSpell>();
EndAction<IncognitoSpell>(); EndAction<IncognitoSpell>();

View file

@ -341,7 +341,11 @@ public static class IncomingAccountPackets
PlayerMurderSystem.OnLogin(m); PlayerMurderSystem.OnLogin(m);
AssistantHandler.OnLogin(m); AssistantHandler.OnLogin(m);
VisibilityList.OnLogin(m); VisibilityList.OnLogin(m);
PlayerMobile.OnLogin(m);
if (m is PlayerMobile pm)
{
PlayerMobile.OnLogin(pm);
}
Account.OnLogin(m); Account.OnLogin(m);
GiftGiving.OnLogin(m); GiftGiving.OnLogin(m);
PreventInaccess.OnLogin(m); PreventInaccess.OnLogin(m);

View file

@ -290,8 +290,7 @@ namespace Server.Spells
} }
} }
private static SpecialMoveContext GetContext(Mobile m) => private static SpecialMoveContext GetContext(Mobile m) => _playersTable.GetValueOrDefault(m);
_playersTable.TryGetValue(m, out var context) ? context : null;
private class SpecialMoveTimer : Timer private class SpecialMoveTimer : Timer
{ {

View file

@ -194,7 +194,10 @@ namespace Server.Spells
(m as BaseCreature)?.OnHarmfulSpell(Caster); (m as BaseCreature)?.OnHarmfulSpell(Caster);
} }
public virtual int GetNewAosDamage(int bonus, int dice, int sides, Mobile singleTarget) public int GetNewAosDamage(int bonus, int dice, int sides, Mobile singleTarget) =>
GetNewAosDamage(bonus, dice, sides, true, singleTarget);
public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool sdi = true, Mobile singleTarget = null)
{ {
if (singleTarget != null) if (singleTarget != null)
{ {
@ -203,17 +206,15 @@ namespace Server.Spells
dice, dice,
sides, sides,
Caster.Player && singleTarget.Player, Caster.Player && singleTarget.Player,
sdi,
GetDamageScalar(singleTarget) GetDamageScalar(singleTarget)
); );
} }
return GetNewAosDamage(bonus, dice, sides, false); return GetNewAosDamage(bonus, dice, sides, sdi, false);
} }
public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer) => public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer, bool sdi, double scalar = 1.0)
GetNewAosDamage(bonus, dice, sides, playerVsPlayer, 1.0);
public virtual int GetNewAosDamage(int bonus, int dice, int sides, bool playerVsPlayer, double scalar)
{ {
var damage = Utility.Dice(dice, sides, bonus) * 100; var damage = Utility.Dice(dice, sides, bonus) * 100;
@ -224,14 +225,17 @@ namespace Server.Spells
var intBonus = Caster.Int / 10; var intBonus = Caster.Int / 10;
damageBonus += intBonus; damageBonus += intBonus;
var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage); if (sdi)
// PvP spell damage increase cap of 15% from an item's magic property
if (playerVsPlayer && sdiBonus > 15)
{ {
sdiBonus = 15; var sdiBonus = AosAttributes.GetValue(Caster, AosAttribute.SpellDamage);
} // PvP spell damage increase cap of 15% from an item's magic property
if (playerVsPlayer && sdiBonus > 15)
{
sdiBonus = 15;
}
damageBonus += sdiBonus; damageBonus += sdiBonus;
}
var context = TransformationSpellHelper.GetContext(Caster); var context = TransformationSpellHelper.GetContext(Caster);

View file

@ -1022,6 +1022,7 @@ namespace Server.Spells
StaminaSystem.DFA = dfa; StaminaSystem.DFA = dfa;
var damageGiven = AOS.Damage(target, from, dmg, phys, fire, cold, pois, nrgy, chaos); var damageGiven = AOS.Damage(target, from, dmg, phys, fire, cold, pois, nrgy, chaos);
Mysticism.SpellPlagueSpell.OnMobileDamaged(target);
StaminaSystem.DFA = DFAlgorithm.Standard; StaminaSystem.DFA = DFAlgorithm.Standard;

View file

@ -58,47 +58,45 @@ namespace Server.Spells.Fifth
if (CheckSequence()) if (CheckSequence())
{ {
var targ = Caster; if (_table.Remove(Caster, out var mods))
if (_table.Remove(targ, out var mods))
{ {
targ.PlaySound(0x1ED); Caster.PlaySound(0x1ED);
targ.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
for (var i = 0; i < mods.Length; ++i) for (var i = 0; i < mods.Length; ++i)
{ {
targ.RemoveResistanceMod(mods[i]); Caster.RemoveResistanceMod(mods[i]);
} }
BuffInfo.RemoveBuff(targ, BuffIcon.MagicReflection); BuffInfo.RemoveBuff(Caster, BuffIcon.MagicReflection);
} }
else else
{ {
targ.PlaySound(0x1E9); Caster.PlaySound(0x1E9);
targ.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist); Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
var physiMod = -25 + (int)(targ.Skills.Inscribe.Value / 20); var physiMod = -25 + (int)(Caster.Skills.Inscribe.Value / 20);
const int otherMod = 10; const int otherMod = 10;
mods = new[] mods =
{ [
new ResistanceMod(ResistanceType.Physical, "PhysicalResistMagicResist", physiMod), new ResistanceMod(ResistanceType.Physical, "PhysicalResistMagicResist", physiMod),
new ResistanceMod(ResistanceType.Fire, "FireResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Fire, "FireResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Cold, "ColdResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Cold, "ColdResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Poison, "PoisonResistMagicResist", otherMod), new ResistanceMod(ResistanceType.Poison, "PoisonResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Energy, "EnergyResistMagicResist", otherMod) new ResistanceMod(ResistanceType.Energy, "EnergyResistMagicResist", otherMod)
}; ];
_table[targ] = mods; _table[Caster] = mods;
for (var i = 0; i < mods.Length; ++i) for (var i = 0; i < mods.Length; ++i)
{ {
targ.AddResistanceMod(mods[i]); Caster.AddResistanceMod(mods[i]);
} }
var buffFormat = $"{physiMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}"; var buffFormat = $"{physiMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}";
BuffInfo.AddBuff(targ, new BuffInfo(BuffIcon.MagicReflection, 1075817, buffFormat, true)); BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.MagicReflection, 1075817, buffFormat, true));
} }
} }

View file

@ -60,46 +60,44 @@ namespace Server.Spells.First
if (CheckSequence()) if (CheckSequence())
{ {
var targ = Caster; if (_table.Remove(Caster, out var mods))
if (_table.Remove(targ, out var mods))
{ {
targ.PlaySound(0x1ED); Caster.PlaySound(0x1ED);
targ.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); Caster.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist);
for (var i = 0; i < mods.Length; ++i) for (var i = 0; i < mods.Length; ++i)
{ {
targ.RemoveResistanceMod(mods[i]); Caster.RemoveResistanceMod(mods[i]);
} }
BuffInfo.RemoveBuff(Caster, BuffIcon.ReactiveArmor); BuffInfo.RemoveBuff(Caster, BuffIcon.ReactiveArmor);
} }
else else
{ {
targ.PlaySound(0x1E9); Caster.PlaySound(0x1E9);
targ.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist); Caster.FixedParticles(0x376A, 9, 32, 5008, EffectLayer.Waist);
mods = new[] mods =
{ [
new ResistanceMod( new ResistanceMod(
ResistanceType.Physical, ResistanceType.Physical,
"PhysicalResistReactiveArmorSpell", "PhysicalResistReactiveArmorSpell",
15 + (int)(targ.Skills.Inscribe.Value / 20) 15 + (int)(Caster.Skills.Inscribe.Value / 20)
), ),
new ResistanceMod(ResistanceType.Fire, "FireResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Fire, "FireResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Cold, "ColdResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Cold, "ColdResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Poison, "PoisonResistReactiveArmorSpell", -5), new ResistanceMod(ResistanceType.Poison, "PoisonResistReactiveArmorSpell", -5),
new ResistanceMod(ResistanceType.Energy, "EnergyResistReactiveArmorSpell", -5) new ResistanceMod(ResistanceType.Energy, "EnergyResistReactiveArmorSpell", -5)
}; ];
_table[targ] = mods; _table[Caster] = mods;
for (var i = 0; i < mods.Length; ++i) for (var i = 0; i < mods.Length; ++i)
{ {
targ.AddResistanceMod(mods[i]); Caster.AddResistanceMod(mods[i]);
} }
var physresist = 15 + (int)(targ.Skills.Inscribe.Value / 20); var physresist = 15 + (int)(Caster.Skills.Inscribe.Value / 20);
var args = $"{physresist}\t{5}\t{5}\t{5}\t{5}"; var args = $"{physresist}\t{5}\t{5}\t{5}\t{5}";
BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.ReactiveArmor, 1075812, 1075813, args)); BuffInfo.AddBuff(Caster, new BuffInfo(BuffIcon.ReactiveArmor, 1075812, 1075813, args));

View file

@ -117,24 +117,16 @@ namespace Server.Spells.Fourth
private class InternalTimer : Timer private class InternalTimer : Timer
{ {
private readonly Mobile m_Owner; private readonly Mobile _owner;
public InternalTimer(Mobile target, Mobile caster) : base(TimeSpan.FromSeconds(0)) public InternalTimer(Mobile target, Mobile caster) : base(GetDelay(caster)) => _owner = target;
{
var time = caster.Skills.Magery.Value * 1.2;
if (time > 144)
{
time = 144;
}
Delay = TimeSpan.FromSeconds(time); private static TimeSpan GetDelay(Mobile caster) =>
TimeSpan.FromSeconds(Math.Min(144, caster.Skills.Magery.Value * 1.2));
m_Owner = target;
}
protected override void OnTick() protected override void OnTick()
{ {
RemoveEntry(m_Owner); RemoveEntry(_owner);
} }
} }
} }

View file

@ -82,7 +82,7 @@ namespace Server.Spells.Fourth
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12); Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
} }
private void AosDelay_Callback(Mobile m, int mana) private static void AosDelay_Callback(Mobile m, int mana)
{ {
if (m.Alive && !m.IsDeadBondedPet) if (m.Alive && !m.IsDeadBondedPet)
{ {

View file

@ -1,63 +1,62 @@
using System; using System;
using Server.Mobiles; using Server.Mobiles;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class AnimatedWeaponSpell : MysticSpell, ISpellTargetingPoint3D
{ {
public class AnimatedWeaponSpell : MysticSpell, ISpellTargetingPoint3D private static readonly SpellInfo _info = new(
"Animated Weapon",
"In Jux Por Ylem",
-1,
9002,
Reagent.Bone,
Reagent.BlackPearl,
Reagent.MandrakeRoot,
Reagent.Nightshade
);
public AnimatedWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new(
"Animated Weapon",
"In Jux Por Ylem",
-1,
9002,
Reagent.Bone,
Reagent.BlackPearl,
Reagent.MandrakeRoot,
Reagent.Nightshade
);
public AnimatedWeaponSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override SpellCircle Circle => SpellCircle.Fourth;
public void Target(IPoint3D p)
{
if (Caster.Followers + 4 > Caster.FollowersMax)
{
Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature.
return;
}
var map = Caster.Map;
SpellHelper.GetSurfaceTop(ref p);
if (map == null || Caster.Player && !map.CanSpawnMobile(p.X, p.Y, p.Z))
{
Caster.SendLocalizedMessage(501942); // That location is blocked.
}
else if (SpellHelper.CheckTown(p, Caster) && CheckSequence())
{
var level = (int)((GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 2.0);
var duration = TimeSpan.FromSeconds(10 + level);
var summon = new AnimatedWeapon(Caster, level);
BaseCreature.Summon(summon, false, Caster, new Point3D(p), 0x212, duration);
summon.PlaySound(0x64A);
Effects.SendTargetParticles(summon, 0x3728, 10, 10, 0x13AA, (EffectLayer)255);
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetPoint3D(this);
}
} }
}
public override SpellCircle Circle => SpellCircle.Fourth;
public void Target(IPoint3D p)
{
if (Caster.Followers + 4 > Caster.FollowersMax)
{
Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature.
return;
}
var map = Caster.Map;
SpellHelper.GetSurfaceTop(ref p);
if (map == null || Caster.Player && !map.CanSpawnMobile(p.X, p.Y, p.Z))
{
Caster.SendLocalizedMessage(501942); // That location is blocked.
}
else if (SpellHelper.CheckTown(p, Caster) && CheckSequence())
{
var level = (int)((GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 2.0);
var duration = TimeSpan.FromSeconds(10 + level);
var summon = new AnimatedWeapon(Caster, level);
BaseCreature.Summon(summon, false, Caster, new Point3D(p), 0x212, duration);
summon.PlaySound(0x64A);
Effects.SendTargetParticles(summon, 0x3728, 10, 10, 0x13AA, (EffectLayer)255);
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetPoint3D(this);
}
}

View file

@ -1,36 +1,36 @@
using Server.Targeting; using Server.Targeting;
using System; using System;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
{
public class BombardSpell : MysticSpell, ISpellTargetingMobile
{
private static readonly SpellInfo _info = new(
"Bombard", "Corp Por Ylem",
230,
9022,
Reagent.Bloodmoss,
Reagent.Garlic,
Reagent.SulfurousAsh,
Reagent.DragonsBlood
);
public BombardSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) public class BombardSpell : MysticSpell, ISpellTargetingMobile
{ {
private static readonly SpellInfo _info = new(
"Bombard", "Corp Por Ylem",
230,
9022,
Reagent.Bloodmoss,
Reagent.Garlic,
Reagent.SulfurousAsh,
Reagent.DragonsBlood
);
public BombardSpell(Mobile caster, Item scroll) : base(caster, scroll, _info)
{
} }
public override SpellCircle Circle => SpellCircle.Sixth; public override SpellCircle Circle => SpellCircle.Sixth;
public override bool DelayedDamage => true; public override bool DelayedDamage => true;
public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf; public override Type[] DelayedDamageSpellFamilyStacking => AOSNoDelayedDamageStackingSelf;
public override void OnCast() public override void OnCast()
{ {
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful); Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful);
} }
public void Target(Mobile m) public void Target(Mobile m)
{ {
if (CheckHSequence(m)) if (CheckHSequence(m))
{ {
SpellHelper.Turn(Caster, m); SpellHelper.Turn(Caster, m);
@ -85,5 +85,4 @@ namespace Server.Spells.Mysticism
FinishSequence(); FinishSequence();
} }
} }
}

View file

@ -5,212 +5,211 @@ using Server.Spells.Necromancy;
using Server.Targeting; using Server.Targeting;
using Server.Collections; using Server.Collections;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class CleansingWindsSpell : MysticSpell, ISpellTargetingMobile
{ {
public class CleansingWindsSpell : MysticSpell, ISpellTargetingMobile public override SpellCircle Circle => SpellCircle.Sixth;
private static readonly SpellInfo _info = new(
"Cleansing Winds", "In Vas Mani Hur",
230,
9022,
Reagent.Garlic,
Reagent.Ginseng,
Reagent.MandrakeRoot,
Reagent.DragonsBlood
);
public CleansingWindsSpell(Mobile caster, Item scroll) : base(caster, scroll, _info)
{ {
public override SpellCircle Circle => SpellCircle.Sixth; }
private static readonly SpellInfo _info = new( public override void OnCast()
"Cleansing Winds", "In Vas Mani Hur", {
230, Caster.Target = new SpellTargetMobile(this, TargetFlags.Beneficial);
9022, }
Reagent.Garlic,
Reagent.Ginseng,
Reagent.MandrakeRoot,
Reagent.DragonsBlood
);
public CleansingWindsSpell(Mobile caster, Item scroll) : base(caster, scroll, _info) public void Target(Mobile m)
{
if (CheckBSequence(m))
{ {
} /**
* Soothing winds attempt to neutralize poisons, lift curses,
* and heal a valid target and up to 3 party members.
*/
public override void OnCast() Caster.PlaySound(0x64C);
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Beneficial);
}
public void Target(Mobile m) var primarySkill = GetBaseSkill(Caster);
{ var secondarySkill = GetDamageSkill(Caster);
if (CheckBSequence(m)) var cureChance = 10000 + (int)((primarySkill + secondarySkill) / 2 * 75);
using var pool = PooledRefQueue<Mobile>.Create();
pool.Enqueue(m);
var casterParty = Party.Get(Caster);
if (casterParty != null)
{ {
/** foreach (Mobile mob in Caster.Map.GetMobilesInRange(m.Location, 2))
* Soothing winds attempt to neutralize poisons, lift curses,
* and heal a valid target and up to 3 party members.
*/
Caster.PlaySound(0x64C);
var primarySkill = GetBaseSkill(Caster);
var secondarySkill = GetDamageSkill(Caster);
var cureChance = 10000 + (int)((primarySkill + secondarySkill) / 2 * 75);
using var pool = PooledRefQueue<Mobile>.Create();
pool.Enqueue(m);
var casterParty = Party.Get(Caster);
if (casterParty != null)
{ {
foreach (Mobile mob in Caster.Map.GetMobilesInRange(m.Location, 2)) if (mob != m && casterParty.Contains(mob) && Caster.CanBeBeneficial(mob, false))
{ {
if (mob != m && casterParty.Contains(mob) && Caster.CanBeBeneficial(mob, false)) pool.Enqueue(mob);
if (pool.Count == 4)
{ {
pool.Enqueue(mob); break;
if (pool.Count == 4)
{
break;
}
} }
} }
} }
}
var toHeal = ((int)((primarySkill + secondarySkill) / 4.0) + Utility.RandomMinMax(-3, 3)) / pool.Count; var toHeal = ((int)((primarySkill + secondarySkill) / 4.0) + Utility.RandomMinMax(-3, 3)) / pool.Count;
while (pool.Count > 0) while (pool.Count > 0)
{
var target = pool.Dequeue();
Caster.DoBeneficial(target);
target.FixedParticles(0x3709, 1, 30, 9963, 13, 3, EffectLayer.Head);
var from = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z - 10), target.Map);
var to = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z + 50), target.Map);
Effects.SendMovingParticles(
from,
to,
0x2255,
1,
0,
false,
false,
13,
3,
9501,
1,
0,
EffectLayer.Head,
0x100
);
var toHealMod = toHeal;
if (target.Poisoned)
{ {
var target = pool.Dequeue(); var poisonLevel = target.Poison.Level + 1;
var chanceToCure = cureChance - poisonLevel * 1750;
Caster.DoBeneficial(target); if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster))
target.FixedParticles(0x3709, 1, 30, 9963, 13, 3, EffectLayer.Head);
var from = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z - 10), target.Map);
var to = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z + 50), target.Map);
Effects.SendMovingParticles(
from,
to,
0x2255,
1,
0,
false,
false,
13,
3,
9501,
1,
0,
EffectLayer.Head,
0x100
);
var toHealMod = toHeal;
if (target.Poisoned)
{ {
var poisonLevel = target.Poison.Level + 1; toHealMod -= (int)(toHeal * poisonLevel * 0.15);
var chanceToCure = cureChance - poisonLevel * 1750;
if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster))
{
toHealMod -= (int)(toHeal * poisonLevel * 0.15);
}
else
{
toHealMod = 0;
}
} }
else
if (MortalStrike.IsWounded(target))
{ {
toHealMod = 0; toHealMod = 0;
} }
}
var curseLevel = RemoveCurses(target); if (MortalStrike.IsWounded(target))
{
toHealMod = 0;
}
if (toHealMod > 0 && curseLevel > 0) var curseLevel = RemoveCurses(target);
{
toHealMod -= curseLevel * 3;
toHealMod -= (int)(toHealMod * (curseLevel / 100.0));
}
if (toHealMod > 0) if (toHealMod > 0 && curseLevel > 0)
{ {
SpellHelper.Heal(toHealMod, target, Caster); toHealMod -= curseLevel * 3;
} toHealMod -= (int)(toHealMod * (curseLevel / 100.0));
}
if (toHealMod > 0)
{
SpellHelper.Heal(toHealMod, target, Caster);
} }
} }
FinishSequence();
} }
public static int RemoveCurses(Mobile m) FinishSequence();
{
var curseLevel = 0;
// if (SleepSpell.EndSleep(m))
// {
// curseLevel += 2;
// }
if (EvilOmenSpell.EndEffect(m))
{
curseLevel += 1;
}
if (StrangleSpell.RemoveCurse(m))
{
curseLevel += 2;
}
if (CorpseSkinSpell.RemoveCurse(m))
{
curseLevel += 3;
}
if (CurseSpell.RemoveEffect(m))
{
curseLevel += 4;
}
if (BloodOathSpell.RemoveCurse(m))
{
curseLevel += 3;
}
if (MindRotSpell.ClearMindRotScalar(m))
{
curseLevel += 2;
}
if (SpellPlagueSpell.RemoveEffect(m))
{
curseLevel += 4;
}
var mod = m.GetStatMod("[Magic] Str Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Str Curse");
}
mod = m.GetStatMod("[Magic] Dex Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Dex Curse");
}
mod = m.GetStatMod("[Magic] Int Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Int Curse");
}
if (MortalStrike.EndWound(m))
{
curseLevel += 2;
}
BuffInfo.RemoveBuff(m, BuffIcon.Clumsy);
BuffInfo.RemoveBuff(m, BuffIcon.FeebleMind);
BuffInfo.RemoveBuff(m, BuffIcon.Weaken);
BuffInfo.RemoveBuff(m, BuffIcon.Curse);
BuffInfo.RemoveBuff(m, BuffIcon.MassCurse);
BuffInfo.RemoveBuff(m, BuffIcon.MortalStrike);
BuffInfo.RemoveBuff(m, BuffIcon.CorpseSkin);
BuffInfo.RemoveBuff(m, BuffIcon.Strangle);
BuffInfo.RemoveBuff(m, BuffIcon.EvilOmen);
return curseLevel;
}
} }
}
public static int RemoveCurses(Mobile m)
{
var curseLevel = 0;
// if (SleepSpell.EndSleep(m))
// {
// curseLevel += 2;
// }
if (EvilOmenSpell.EndEffect(m))
{
curseLevel += 1;
}
if (StrangleSpell.RemoveCurse(m))
{
curseLevel += 2;
}
if (CorpseSkinSpell.RemoveCurse(m))
{
curseLevel += 3;
}
if (CurseSpell.RemoveEffect(m))
{
curseLevel += 4;
}
if (BloodOathSpell.RemoveCurse(m))
{
curseLevel += 3;
}
if (MindRotSpell.ClearMindRotScalar(m))
{
curseLevel += 2;
}
if (SpellPlagueSpell.RemoveEffect(m))
{
curseLevel += 4;
}
var mod = m.GetStatMod("[Magic] Str Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Str Curse");
}
mod = m.GetStatMod("[Magic] Dex Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Dex Curse");
}
mod = m.GetStatMod("[Magic] Int Curse");
if (mod?.Offset < 0)
{
m.RemoveStatMod("[Magic] Int Curse");
}
if (MortalStrike.EndWound(m))
{
curseLevel += 2;
}
BuffInfo.RemoveBuff(m, BuffIcon.Clumsy);
BuffInfo.RemoveBuff(m, BuffIcon.FeebleMind);
BuffInfo.RemoveBuff(m, BuffIcon.Weaken);
BuffInfo.RemoveBuff(m, BuffIcon.Curse);
BuffInfo.RemoveBuff(m, BuffIcon.MassCurse);
BuffInfo.RemoveBuff(m, BuffIcon.MortalStrike);
BuffInfo.RemoveBuff(m, BuffIcon.CorpseSkin);
BuffInfo.RemoveBuff(m, BuffIcon.Strangle);
BuffInfo.RemoveBuff(m, BuffIcon.EvilOmen);
return curseLevel;
}
}

View file

@ -1,77 +1,76 @@
using System; using System;
using Server.Targeting; using Server.Targeting;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class EagleStrikeSpell : MysticSpell, ISpellTargetingMobile
{ {
public class EagleStrikeSpell : MysticSpell, ISpellTargetingMobile private static readonly SpellInfo _info = new(
"Eagle Strike",
"Kal Por Xen",
-1,
9002,
Reagent.Bloodmoss,
Reagent.Bone,
Reagent.SpidersSilk,
Reagent.MandrakeRoot
);
public EagleStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new( }
"Eagle Strike",
"Kal Por Xen",
-1,
9002,
Reagent.Bloodmoss,
Reagent.Bone,
Reagent.SpidersSilk,
Reagent.MandrakeRoot
);
public EagleStrikeSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) public override SpellCircle Circle => SpellCircle.Third;
public void Target(Mobile m)
{
if (CheckHSequence(m))
{ {
} SpellHelper.Turn(Caster, m);
public override SpellCircle Circle => SpellCircle.Third; if (Core.SA && HasDelayedDamageContext(m))
public void Target(Mobile m)
{
if (CheckHSequence(m))
{
SpellHelper.Turn(Caster, m);
if (Core.SA && HasDelayedDamageContext(m))
{
DoHurtFizzle();
return;
}
var source = Caster;
if (SpellHelper.CheckReflect(2, ref source, ref m))
{
Timer.StartTimer(TimeSpan.FromSeconds(0.5), () =>
{
/* Conjures a magical eagle that assaults the Target with its talons, dealing energy damage. */
source.MovingEffect(m, 0x407A, 8, 1, false, true, 0, 0);
source.PlaySound(0x2EE);
});
}
Caster.MovingParticles(m, 0x407A, 7, 0, false, true, 0, 0, 0xBBE, 0xFA6, 0xFFFF, 0);
Caster.PlaySound(0x2EE);
Timer.StartTimer(TimeSpan.FromSeconds(1.0), () => Damage(m));
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful);
}
private void Damage(Mobile to)
{
if (to == null)
{ {
DoHurtFizzle();
return; return;
} }
double damage = GetNewAosDamage(19, 1, 5, to); var source = Caster;
SpellHelper.Damage(this, to, damage, 0, 0, 0, 0, 100); if (SpellHelper.CheckReflect(2, ref source, ref m))
{
Timer.StartTimer(TimeSpan.FromSeconds(0.5), () =>
{
/* Conjures a magical eagle that assaults the Target with its talons, dealing energy damage. */
source.MovingEffect(m, 0x407A, 8, 1, false, true, 0, 0);
source.PlaySound(0x2EE);
});
}
to.PlaySound(0x64D); Caster.MovingParticles(m, 0x407A, 7, 0, false, true, 0, 0, 0xBBE, 0xFA6, 0xFFFF, 0);
Caster.PlaySound(0x2EE);
Timer.StartTimer(TimeSpan.FromSeconds(1.0), () => Damage(m));
} }
FinishSequence();
} }
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful);
}
private void Damage(Mobile to)
{
if (to == null)
{
return;
}
double damage = GetNewAosDamage(19, 1, 5, to);
SpellHelper.Damage(this, to, damage, 0, 0, 0, 0, 100);
to.PlaySound(0x64D);
}
}

View file

@ -1,126 +1,125 @@
using Server.Collections; using Server.Collections;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class HailStormSpell : MysticSpell, ISpellTargetingPoint3D
{ {
public class HailStormSpell : MysticSpell, ISpellTargetingPoint3D private static readonly SpellInfo _info = new(
"Hail Storm",
"Kal Des Ylem",
-1,
9002,
Reagent.DragonsBlood,
Reagent.Bloodmoss,
Reagent.BlackPearl,
Reagent.MandrakeRoot
);
public HailStormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new( }
"Hail Storm",
"Kal Des Ylem",
-1,
9002,
Reagent.DragonsBlood,
Reagent.Bloodmoss,
Reagent.BlackPearl,
Reagent.MandrakeRoot
);
public HailStormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) public override SpellCircle Circle => SpellCircle.Seventh;
public override int GetMana() => 50;
public void Target(IPoint3D p)
{
var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p);
if (SpellHelper.CheckTown(loc, Caster) && CheckSequence())
{ {
} /* Summons a storm of hailstones that strikes all Targets within a radius around the Target's Location,
* dealing cold damage.
*/
public override SpellCircle Circle => SpellCircle.Seventh; SpellHelper.Turn(Caster, p);
public override int GetMana() => 50;
public void Target(IPoint3D p) var map = Caster.Map;
{
var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p);
if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) if (map != null)
{ {
/* Summons a storm of hailstones that strikes all Targets within a radius around the Target's Location, using var pool = PooledRefQueue<Mobile>.Create();
* dealing cold damage. var pvp = false;
*/
SpellHelper.Turn(Caster, p); PlayEffect(loc, Caster.Map);
var map = Caster.Map; foreach (var m in map.GetMobilesInRange(loc, 2))
if (map != null)
{ {
using var pool = PooledRefQueue<Mobile>.Create(); if (m == Caster || !SpellHelper.ValidIndirectTarget(Caster, m) || !Caster.CanBeHarmful(m, false)
var pvp = false; || !Caster.CanSee(m) || !Caster.InLOS(m))
PlayEffect(loc, Caster.Map);
foreach (var m in map.GetMobilesInRange(loc, 2))
{ {
if (m == Caster || !SpellHelper.ValidIndirectTarget(Caster, m) || !Caster.CanBeHarmful(m, false) continue;
|| !Caster.CanSee(m) || !Caster.InLOS(m))
{
continue;
}
pool.Enqueue(m);
if (m.Player)
{
pvp = true;
}
} }
double damage = GetNewAosDamage(51, 1, 5, pvp); pool.Enqueue(m);
while (pool.Count > 0) if (m.Player)
{ {
var m = pool.Dequeue(); pvp = true;
Caster.DoHarmful(m);
SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0);
} }
} }
double damage = GetNewAosDamage(51, 1, 5, pvp);
while (pool.Count > 0)
{
var m = pool.Dequeue();
Caster.DoHarmful(m);
SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0);
}
} }
FinishSequence();
} }
public override void OnCast() FinishSequence();
{
Caster.Target = new SpellTargetPoint3D(this);
}
private static void PlayEffect(Point3D p, Map map)
{
Effects.PlaySound(p, map, 0x64F);
PlaySingleEffect(p, map, -1, 1, -1, 1);
PlaySingleEffect(p, map, -2, 0, -3, -1);
PlaySingleEffect(p, map, -3, -1, -1, 1);
PlaySingleEffect(p, map, 1, 3, -1, 1);
PlaySingleEffect(p, map, -1, 1, 1, 3);
}
private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d)
{
var x = p.X;
var y = p.Y;
var z = p.Z + 18;
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z));
}
private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest)
{
Effects.SendMovingEffect(
p,
map,
0x36D4,
orig,
dest,
0,
0,
false,
false,
0x63,
0x4
);
}
} }
}
public override void OnCast()
{
Caster.Target = new SpellTargetPoint3D(this);
}
private static void PlayEffect(Point3D p, Map map)
{
Effects.PlaySound(p, map, 0x64F);
PlaySingleEffect(p, map, -1, 1, -1, 1);
PlaySingleEffect(p, map, -2, 0, -3, -1);
PlaySingleEffect(p, map, -3, -1, -1, 1);
PlaySingleEffect(p, map, 1, 3, -1, 1);
PlaySingleEffect(p, map, -1, 1, 1, 3);
}
private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d)
{
var x = p.X;
var y = p.Y;
var z = p.Z + 18;
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z));
}
private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest)
{
Effects.SendMovingEffect(
p,
map,
0x36D4,
orig,
dest,
0,
0,
false,
false,
0x63,
0x4
);
}
}

View file

@ -1,140 +1,139 @@
using System; using System;
using Server.Items; using Server.Items;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public abstract class MysticSpell : Spell
{ {
public abstract class MysticSpell : Spell private static int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 };
private static double[] _requiredSkill = { 0.0, 8.0, 20.0, 33.0, 45.0, 58.0, 70.0, 83.0 };
public MysticSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{ {
private static int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; }
private static double[] _requiredSkill = { 0.0, 8.0, 20.0, 33.0, 45.0, 58.0, 70.0, 83.0 };
public MysticSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) public abstract SpellCircle Circle { get; }
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.5 + 0.25 * (int)Circle);
public virtual double RequiredSkill
{
get
{ {
var circle = (int)Circle;
if (Scroll != null)
{
circle -= 2;
}
return _requiredSkill[circle];
}
}
public override SkillName CastSkill => SkillName.Mysticism;
/*
* As per OSI Publish 64:
* Imbuing is not the only skill associated with Mysticism now.
* Players can use EITHER their Focus skill or Imbuing skill.
* Evaluate Intelligence no longer has any effect on a Mystic’s spell power.
*/
public override double GetDamageSkill(Mobile m) => Math.Max(m.Skills.Imbuing.Value, m.Skills.Focus.Value);
public override int GetDamageFixed(Mobile m) => Math.Max(m.Skills.Imbuing.Fixed, m.Skills.Focus.Fixed);
public override void GetCastSkills(out double min, out double max)
{
var requiredSkill = RequiredSkill;
// As per Mysticism page at the UO Herald Playguide
// This means that we have 25% success chance at min Required Skill
min = requiredSkill - 12.5;
max = requiredSkill + 37.5;
}
public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle];
public override bool CheckCast()
{
if (!base.CheckCast())
{
return false;
} }
public abstract SpellCircle Circle { get; } var mana = ScaleMana(GetMana());
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.5 + 0.25 * (int)Circle); if (Caster.Mana < mana)
public virtual double RequiredSkill
{ {
get // You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
{ Caster.SendLocalizedMessage(1060174, mana.ToString());
var circle = (int)Circle; return false;
if (Scroll != null)
{
circle -= 2;
}
return _requiredSkill[circle];
}
} }
public override SkillName CastSkill => SkillName.Mysticism; var requiredSkill = RequiredSkill;
/* if (Caster.Skills[CastSkill].Value < requiredSkill)
* As per OSI Publish 64:
* Imbuing is not the only skill associated with Mysticism now.
* Players can use EITHER their Focus skill or Imbuing skill.
* Evaluate Intelligence no longer has any effect on a Mystic’s spell power.
*/
public override double GetDamageSkill(Mobile m) => Math.Max(m.Skills.Imbuing.Value, m.Skills.Focus.Value);
public override int GetDamageFixed(Mobile m) => Math.Max(m.Skills.Imbuing.Fixed, m.Skills.Focus.Fixed);
public override void GetCastSkills(out double min, out double max)
{ {
var requiredSkill = RequiredSkill; // You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability.
Caster.SendLocalizedMessage(1063013, $"{requiredSkill:F1}\t{CastSkill}\t ");
// As per Mysticism page at the UO Herald Playguide return false;
// This means that we have 25% success chance at min Required Skill
min = requiredSkill - 12.5;
max = requiredSkill + 37.5;
} }
public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle]; return true;
}
public override bool CheckCast() public override void OnBeginCast()
{
base.OnBeginCast();
SendCastEffect();
}
public virtual void SendCastEffect()
{
Caster.FixedEffect(0x37C4, 10, (int)(GetCastDelay().TotalSeconds * 28), 0x66C, 3);
}
public static double GetBaseSkill(Mobile m) => m.Skills.Mysticism.Value;
public virtual bool CheckResisted(Mobile target)
{
var n = GetResistPercent(target);
n /= 100.0;
if (n <= 0.0)
{ {
if (!base.CheckCast()) return false;
{ }
return false;
}
var mana = ScaleMana(GetMana());
if (Caster.Mana < mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
Caster.SendLocalizedMessage(1060174, mana.ToString());
return false;
}
var requiredSkill = RequiredSkill;
if (Caster.Skills[CastSkill].Value < requiredSkill)
{
// You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability.
Caster.SendLocalizedMessage(1063013, $"{requiredSkill:F1}\t{CastSkill}\t ");
return false;
}
if (n >= 1.0)
{
return true; return true;
} }
public override void OnBeginCast() var maxSkill = (1 + (int)Circle) * 10;
{ maxSkill += (1 + (int)Circle / 6) * 25;
base.OnBeginCast();
SendCastEffect(); if (target.Skills.MagicResist.Value < maxSkill)
{
target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap);
} }
public virtual void SendCastEffect() return n >= Utility.RandomDouble();
{
Caster.FixedEffect(0x37C4, 10, (int)(GetCastDelay().TotalSeconds * 28), 0x66C, 3);
}
public static double GetBaseSkill(Mobile m) => m.Skills.Mysticism.Value;
public virtual bool CheckResisted(Mobile target)
{
var n = GetResistPercent(target);
n /= 100.0;
if (n <= 0.0)
{
return false;
}
if (n >= 1.0)
{
return true;
}
var maxSkill = (1 + (int)Circle) * 10;
maxSkill += (1 + (int)Circle / 6) * 25;
if (target.Skills.MagicResist.Value < maxSkill)
{
target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap);
}
return n >= Utility.RandomDouble();
}
public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle)
{
var magicResist = target.Skills.MagicResist.Value;
var firstPercent = magicResist / 5.0;
var secondPercent = magicResist -
((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0);
// Seems should be about half of what stratics says.
return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0;
}
public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle);
} }
}
public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle)
{
var magicResist = target.Skills.MagicResist.Value;
var firstPercent = magicResist / 5.0;
var secondPercent = magicResist -
((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0);
// Seems should be about half of what stratics says.
return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0;
}
public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle);
}

View file

@ -1,140 +1,139 @@
using Server.Collections; using Server.Collections;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class NetherCycloneSpell : MysticSpell, ISpellTargetingPoint3D
{ {
public class NetherCycloneSpell : MysticSpell, ISpellTargetingPoint3D private static readonly SpellInfo _info = new(
"Nether Cyclone",
"Grav Hur",
-1,
9002,
Reagent.MandrakeRoot,
Reagent.Nightshade,
Reagent.SulfurousAsh,
Reagent.Bloodmoss
);
public NetherCycloneSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new( }
"Nether Cyclone",
"Grav Hur",
-1,
9002,
Reagent.MandrakeRoot,
Reagent.Nightshade,
Reagent.SulfurousAsh,
Reagent.Bloodmoss
);
public NetherCycloneSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) public override SpellCircle Circle => SpellCircle.Eighth;
public void Target(IPoint3D p)
{
var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p);
if (SpellHelper.CheckTown(loc, Caster) && CheckSequence())
{ {
} /* Summons a gale of lethal winds that strikes all Targets within a radius around
* the Target's Location, dealing chaos damage. In addition to inflicting damage,
* each Target of the Nether Cyclone temporarily loses a percentage of mana and
* stamina. The effectiveness of the Nether Cyclone is determined by a comparison
* between the Caster's Mysticism and either Focus or Imbuing (whichever is greater)
* skills and the Resisting Spells skill of the Target.
*/
public override SpellCircle Circle => SpellCircle.Eighth; SpellHelper.Turn(Caster, p);
public void Target(IPoint3D p) var map = Caster.Map;
{
var loc = (p as Item)?.GetWorldLocation() ?? new Point3D(p);
if (SpellHelper.CheckTown(loc, Caster) && CheckSequence()) if (map != null)
{ {
/* Summons a gale of lethal winds that strikes all Targets within a radius around using var pool = PooledRefQueue<Mobile>.Create();
* the Target's Location, dealing chaos damage. In addition to inflicting damage, var pvp = false;
* each Target of the Nether Cyclone temporarily loses a percentage of mana and
* stamina. The effectiveness of the Nether Cyclone is determined by a comparison
* between the Caster's Mysticism and either Focus or Imbuing (whichever is greater)
* skills and the Resisting Spells skill of the Target.
*/
SpellHelper.Turn(Caster, p); PlayEffect(loc, Caster.Map);
var map = Caster.Map; foreach (var m in map.GetMobilesInRange(loc, 2))
if (map != null)
{ {
using var pool = PooledRefQueue<Mobile>.Create(); if (m == Caster)
var pvp = false;
PlayEffect(loc, Caster.Map);
foreach (var m in map.GetMobilesInRange(loc, 2))
{ {
if (m == Caster) continue;
}
if (SpellHelper.ValidIndirectTarget(Caster, m) && Caster.CanBeHarmful(m, false) && Caster.CanSee(m))
{
if (!Caster.InLOS(m))
{ {
continue; continue;
} }
if (SpellHelper.ValidIndirectTarget(Caster, m) && Caster.CanBeHarmful(m, false) && Caster.CanSee(m)) pool.Enqueue(m);
if (m.Player)
{ {
if (!Caster.InLOS(m)) pvp = true;
{
continue;
}
pool.Enqueue(m);
if (m.Player)
{
pvp = true;
}
} }
} }
}
var damage = GetNewAosDamage(51, 1, 5, pvp); var damage = GetNewAosDamage(51, 1, 5, pvp);
var reduction = (GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 1200.0; var reduction = (GetBaseSkill(Caster) + GetDamageSkill(Caster)) / 1200.0;
while (pool.Count > 0) while (pool.Count > 0)
{ {
var m = pool.Dequeue(); var m = pool.Dequeue();
Caster.DoHarmful(m); Caster.DoHarmful(m);
SpellHelper.Damage(this, m, damage, 0, 0, 0, 0, 0, 100); SpellHelper.Damage(this, m, damage, 0, 0, 0, 0, 0, 100);
var resistedReduction = reduction - m.Skills.MagicResist.Value / 800.0; var resistedReduction = reduction - m.Skills.MagicResist.Value / 800.0;
m.Stam -= (int)(m.StamMax * resistedReduction); m.Stam -= (int)(m.StamMax * resistedReduction);
m.Mana -= (int)(m.ManaMax * resistedReduction); m.Mana -= (int)(m.ManaMax * resistedReduction);
}
} }
} }
FinishSequence();
} }
public override void OnCast() FinishSequence();
{
Caster.Target = new SpellTargetPoint3D(this);
}
private static void PlayEffect(Point3D p, Map map)
{
Effects.PlaySound(p, map, 0x64F);
PlaySingleEffect(p, map, -1, 1, -1, 1);
PlaySingleEffect(p, map, -2, 0, -3, -1);
PlaySingleEffect(p, map, -3, -1, -1, 1);
PlaySingleEffect(p, map, 1, 3, -1, 1);
PlaySingleEffect(p, map, -1, 1, 1, 3);
}
private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d)
{
int x = p.X, y = p.Y, z = p.Z + 18;
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z));
}
private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest)
{
Effects.SendMovingEffect(
p,
map,
0x375A,
orig,
dest,
0,
0,
false,
false,
0x49A,
0x4
);
}
} }
}
public override void OnCast()
{
Caster.Target = new SpellTargetPoint3D(this);
}
private static void PlayEffect(Point3D p, Map map)
{
Effects.PlaySound(p, map, 0x64F);
PlaySingleEffect(p, map, -1, 1, -1, 1);
PlaySingleEffect(p, map, -2, 0, -3, -1);
PlaySingleEffect(p, map, -3, -1, -1, 1);
PlaySingleEffect(p, map, 1, 3, -1, 1);
PlaySingleEffect(p, map, -1, 1, 1, 3);
}
private static void PlaySingleEffect(Point3D p, Map map, int a, int b, int c, int d)
{
int x = p.X, y = p.Y, z = p.Z + 18;
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + a, y + d, z));
SendEffectPacket(p, map, new Point3D(x + b, y + c, z), new Point3D(x + a, y + c, z));
SendEffectPacket(p, map, new Point3D(x + b, y + d, z), new Point3D(x + b, y + c, z));
SendEffectPacket(p, map, new Point3D(x + a, y + d, z), new Point3D(x + b, y + d, z));
SendEffectPacket(p, map, new Point3D(x + a, y + c, z), new Point3D(x + a, y + d, z));
}
private static void SendEffectPacket(Point3D p, Map map, Point3D orig, Point3D dest)
{
Effects.SendMovingEffect(
p,
map,
0x375A,
orig,
dest,
0,
0,
false,
false,
0x49A,
0x4
);
}
}

View file

@ -2,219 +2,199 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Targeting; using Server.Targeting;
namespace Server.Spells.Mysticism namespace Server.Spells.Mysticism;
public class SpellPlagueSpell : MysticSpell, ISpellTargetingMobile
{ {
public class SpellPlagueSpell : MysticSpell private static readonly SpellInfo _info = new(
"Spell Plague",
"Vas Rel Jux Ort",
-1,
9002,
Reagent.DaemonBone,
Reagent.DragonsBlood,
Reagent.Nightshade,
Reagent.SulfurousAsh
);
private static readonly Dictionary<Mobile, SpellPlagueTimer> _table = new();
public SpellPlagueSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new( }
"Spell Plague",
"Vas Rel Jux Ort",
-1,
9002,
Reagent.DaemonBone,
Reagent.DragonsBlood,
Reagent.Nightshade,
Reagent.SulfurousAsh
);
private static readonly Dictionary<Mobile, SpellPlagueTimer> _table = new(); public override SpellCircle Circle => SpellCircle.Seventh;
public SpellPlagueSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info) public static void Initialize()
{
EventSink.PlayerDeath += OnPlayerDeath;
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful);
}
public void Target(Mobile targeted)
{
if (CheckHSequence(targeted))
{ {
} SpellHelper.Turn(Caster, targeted);
public override SpellCircle Circle => SpellCircle.Seventh; SpellHelper.CheckReflect(6, Caster, ref targeted);
public static void Initialize() /* The target is hit with an explosion of chaos damage and then inflicted
{ * with the spell plague curse. Each time the target is damaged while under
EventSink.PlayerDeath += OnPlayerDeath; * the effect of the spell plague, they may suffer an explosion of chaos
} * damage. The initial chance to trigger the explosion starts at 90% and
* reduces by 30% every time an explosion occurs. Once the target is
* afflicted by 3 explosions or 8 seconds have passed, that spell plague
* is removed from the target. Spell Plague will stack with other spell
* plagues so that they are applied one after the other.
*/
public override void OnCast() VisualEffect(targeted);
{
Caster.Target = new InternalTarget(this);
}
public void Target(Mobile targeted) // Before Time of Legends, SDI was not applied to initial damage.
{ var damage = GetNewAosDamage(33, 1, 5, Core.TOL, targeted);
if (CheckHSequence(targeted)) SpellHelper.Damage(this, targeted, damage, 0, 0, 0, 0, 0);
var timer = new SpellPlagueTimer(this, targeted);
if (_table.TryGetValue(targeted, out var oldtimer))
{ {
SpellHelper.Turn(Caster, targeted); oldtimer.SetNext(timer);
SpellHelper.CheckReflect(6, Caster, ref targeted);
/* The target is hit with an explosion of chaos damage and then inflicted
* with the spell plague curse. Each time the target is damaged while under
* the effect of the spell plague, they may suffer an explosion of chaos
* damage. The initial chance to trigger the explosion starts at 90% and
* reduces by 30% every time an explosion occurs. Once the target is
* afflicted by 3 explosions or 8 seconds have passed, that spell plague
* is removed from the target. Spell Plague will stack with other spell
* plagues so that they are applied one after the other.
*/
VisualEffect(targeted);
var damage = GetNewAosDamage(33, 1, 5, targeted);
SpellHelper.Damage(this, targeted, damage, 0, 0, 0, 0, 0);
var timer = new SpellPlagueTimer(this, targeted);
if (_table.TryGetValue(targeted, out var oldtimer))
{
oldtimer.SetNext(timer);
}
else
{
_table[targeted] = timer;
timer.StartPlague();
}
} }
else
FinishSequence();
}
public static bool UnderEffect(Mobile m) => _table.ContainsKey(m);
public static bool RemoveEffect(Mobile m)
{
if (_table.Remove(m, out var context))
{ {
context.Stop(); _table[targeted] = timer;
BuffInfo.RemoveBuff(m, BuffIcon.SpellPlague); timer.StartPlague();
return true;
}
return false;
}
public static void CheckPlague(Mobile m)
{
if (_table.TryGetValue(m, out var context))
{
context.OnDamage();
} }
} }
private static void OnPlayerDeath(Mobile m) FinishSequence();
}
public static bool UnderEffect(Mobile m) => _table.ContainsKey(m);
public static bool RemoveEffect(Mobile m)
{
if (_table.Remove(m, out var timer))
{ {
RemoveEffect(m); timer.Stop();
BuffInfo.RemoveBuff(m, BuffIcon.SpellPlague);
return true;
} }
protected void VisualEffect(Mobile to) return false;
{ }
to.PlaySound(0x658);
to.FixedParticles(0x3728, 1, 13, 0x26B8, 0x47E, 7, EffectLayer.Head, 0); public static void OnMobileDamaged(Mobile m)
to.FixedParticles(0x3779, 1, 15, 0x251E, 0x43, 7, EffectLayer.Head, 0); {
if (m != null && _table.TryGetValue(m, out var context))
{
context.OnDamage();
}
}
private static void OnPlayerDeath(Mobile m) => RemoveEffect(m);
private static void VisualEffect(Mobile to)
{
to.PlaySound(0x658);
to.FixedParticles(0x3728, 1, 13, 0x26B8, 0x47E, 7, EffectLayer.Head, 0);
to.FixedParticles(0x3779, 1, 15, 0x251E, 0x43, 7, EffectLayer.Head, 0);
}
private class SpellPlagueTimer : Timer
{
private readonly SpellPlagueSpell _owner;
private readonly Mobile _target;
private int _explosions;
private DateTime _nextExplosion;
private SpellPlagueTimer _next;
public SpellPlagueTimer(SpellPlagueSpell owner, Mobile target) : base(TimeSpan.FromSeconds(8.0))
{
_owner = owner;
_target = target;
} }
private class SpellPlagueTimer : Timer public void SetNext(SpellPlagueTimer timer)
{ {
private readonly SpellPlagueSpell m_Owner; if (_next == null)
private readonly Mobile m_Target;
private int m_Explosions;
private DateTime m_LastExploded;
private SpellPlagueTimer m_Next;
public SpellPlagueTimer(SpellPlagueSpell owner, Mobile target) : base(TimeSpan.FromSeconds(8.0))
{ {
m_Owner = owner; _next = timer;
m_Target = target;
} }
else
public void SetNext(SpellPlagueTimer timer)
{ {
if (m_Next == null) _next.SetNext(timer);
{
m_Next = timer;
}
else
{
m_Next.SetNext(timer);
}
}
public void StartPlague()
{
BuffInfo.AddBuff(
m_Target,
new BuffInfo(BuffIcon.SpellPlague, 1031690, 1080167, TimeSpan.FromSeconds(8.5), m_Target)
);
Start();
}
public void OnDamage()
{
if (DateTime.Now <= m_LastExploded + TimeSpan.FromSeconds(2.0))
{
return;
}
var exploChance = 90 - m_Explosions * 30;
var resist = m_Target.Skills.MagicResist.Value;
if (resist >= 70)
{
exploChance -= (int)((resist - 70.0) * 3.0 / 10.0);
}
if (exploChance > Utility.Random(100))
{
m_Owner.VisualEffect(m_Target);
var damage = m_Owner.GetNewAosDamage(15 + m_Explosions * 3, 1, 5, m_Target);
m_Explosions++;
m_LastExploded = DateTime.Now;
SpellHelper.Damage(m_Owner, m_Target, damage, 0, 0, 0, 0, 0, 100);
if (m_Explosions >= 3)
{
EndPlague();
}
}
}
public void EndPlague()
{
if (m_Next != null)
{
_table[m_Target] = m_Next;
m_Next.StartPlague();
}
else
{
_table.Remove(m_Target);
BuffInfo.RemoveBuff(m_Target, BuffIcon.SpellPlague);
}
Stop();
} }
} }
private class InternalTarget : Target public void StartPlague()
{ {
private readonly SpellPlagueSpell _owner; BuffInfo.AddBuff(
_target,
new BuffInfo(BuffIcon.SpellPlague, 1031690, 1080167, TimeSpan.FromSeconds(8.5), _target)
);
public InternalTarget(SpellPlagueSpell owner) : base(12, false, TargetFlags.Harmful) => _nextExplosion = Core.Now + TimeSpan.FromSeconds(1);
_owner = owner; Start();
}
protected override void OnTarget(Mobile from, object o) public void OnDamage()
{
if (Core.Now < _nextExplosion)
{ {
if (o is Mobile mobile) return;
}
var exploChance = 90 - _explosions * 30;
var resist = _target.Skills.MagicResist.Fixed - 700;
if (resist > 0)
{
exploChance -= resist / 100 * 3;
}
if (exploChance > Utility.Random(100))
{
VisualEffect(_target);
var damage = _owner.GetNewAosDamage(15 + _explosions * 3, 1, 5, false, _target);
_explosions++;
_nextExplosion = Core.Now + TimeSpan.FromSeconds(1);
SpellHelper.Damage(_owner, _target, damage, 0, 0, 0, 0, 0, 100);
if (_explosions >= 3)
{ {
_owner.Target(mobile); DoNextPlague();
} }
} }
}
protected override void OnTargetFinish(Mobile from) protected override void OnTick() => DoNextPlague();
private void DoNextPlague()
{
if (_next != null)
{ {
_owner.FinishSequence(); _table[_target] = _next;
_next.StartPlague();
} }
else
{
_table.Remove(_target);
BuffInfo.RemoveBuff(_target, BuffIcon.SpellPlague);
}
_next = null;
Stop();
} }
} }
} }

View file

@ -5,419 +5,421 @@ using Server.Engines.Quests.Necro;
using Server.Items; using Server.Items;
using Server.Mobiles; 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", public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5);
203,
9031, public override double RequiredSkill => 40.0;
Reagent.GraveDust, public override int RequiredMana => 23;
Reagent.DaemonBlood
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 --mob.Hits;
new(SlayerGroup.GetEntryByName(SlayerName.Silver).Types, Array.Empty<SummonEntry>()), }
// Insects else
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.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; if (owner == null)
public override int RequiredMana => 23;
public void Target(Item item)
{ {
var comp = item as MaabusCoffinComponent; return;
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();
} }
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); var entry = entries[i];
Caster.SendLocalizedMessage(1061083); // Animate what corpse?
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) return;
{
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;
} }
public static void Unregister(Mobile master, Mobile summoned) Mobile summoned = null;
{
if (master == null || !_table.TryGetValue(master, out var list))
{
return;
}
if (list.Remove(summoned) && list.Count == 0) try
{ {
_table.Remove(master); summoned = toSummon.CreateInstance<Mobile>();
} }
catch
{
// ignored
} }
public static void Register(Mobile master, Mobile summoned) if (summoned == null)
{ {
if (master == null) return;
{
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
);
} }
private static void Summoned_Damage(Mobile mob) if (summoned is BaseCreature bc)
{ {
if (mob.Hits > 0) // to be sure
{ bc.Tamable = false;
--mob.Hits;
} bc.ControlSlots = bc is BaseMount ? 1 : 0;
else
{ Effects.PlaySound(loc, map, bc.GetAngerSound());
mob.Kill();
} 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) Scale(dragon, 50); // lose 50% hp and strength
{
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);
} }
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.HitsMaxSeed = AOS.Scale(toScale, scalar);
bc.RawStr = AOS.Scale(toScale, scalar);
toScale = bc.HitsMaxSeed;
if (toScale > 0)
{
bc.HitsMaxSeed = AOS.Scale(toScale, scalar);
}
bc.Hits = bc.Hits; // refresh hits
} }
private class CreatureGroup bc.Hits = bc.Hits; // refresh hits
{ }
public readonly SummonEntry[] m_Entries;
public readonly Type[] m_Types;
public CreatureGroup(Type[] types, SummonEntry[] entries) private class CreatureGroup
{ {
m_Types = types; public readonly SummonEntry[] _entries;
m_Entries = 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; _toSummon = toSummon;
public readonly Type[] m_ToSummon; _requirement = requirement;
public SummonEntry(int requirement, params Type[] toSummon)
{
m_ToSummon = toSummon;
m_Requirement = requirement;
}
} }
} }
} }

View file

@ -3,158 +3,157 @@ using System.Collections.Generic;
using Server.Mobiles; using Server.Mobiles;
using Server.Targeting; 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(); public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5);
private static readonly Dictionary<Mobile, ExpireTimer> _table = new();
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 void OnCast()
public override int RequiredMana => 13; {
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. caster.SendLocalizedMessage(1061620); // Your Blood Oath has been broken.
}
// 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);
} }
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; RemoveCurse(_target);
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);
}
} }
} }
} }
} }

View file

@ -2,144 +2,143 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Targeting; 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); if (CheckHSequence(m))
public override double RequiredSkill => 20.0;
public override int RequiredMana => 11;
public void Target(Mobile 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, timer = new ExpireTimer(m, mods, duration);
* making them more vulnerable to Fire and Poison damage, timer.Start();
* 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)) BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.CorpseSkin, 1075663, duration, m));
{
timer.DoExpire();
}
else
{
m.SendLocalizedMessage(1061689); // Your skin turns dry and corpselike.
}
m.Spell?.OnCasterHurt(); _table[m] = timer;
m.FixedParticles(0x373A, 1, 15, 9913, 67, 7, EffectLayer.Head); for (var i = 0; i < mods.Length; ++i)
m.PlaySound(0x1BB); {
m.AddResistanceMod(mods[i]);
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);
} }
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) m.SendLocalizedMessage(1061688); // Your skin returns to normal.
{ t?.DoExpire();
if (!_table.TryGetValue(m, out var t)) return true;
{ }
return false;
}
m.SendLocalizedMessage(1061688); // Your skin returns to normal. private class ExpireTimer : Timer
t?.DoExpire(); {
return true; 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; for (var i = 0; i < _mods.Length; ++i)
private ResistanceMod[] _mods;
public ExpireTimer(Mobile m, ResistanceMod[] mods, TimeSpan delay) : base(delay)
{ {
_mobile = m; _mobile.RemoveResistanceMod(_mods[i]);
_mods = mods;
} }
public void DoExpire() Stop();
{ BuffInfo.RemoveBuff(_mobile, BuffIcon.CorpseSkin);
for (var i = 0; i < _mods.Length; ++i) _table.Remove(_mobile);
{ }
_mobile.RemoveResistanceMod(_mods[i]);
}
Stop(); protected override void OnTick()
BuffInfo.RemoveBuff(_mobile, BuffIcon.CorpseSkin); {
_table.Remove(_mobile); _mobile.SendLocalizedMessage(1061688); // Your skin returns to normal.
} DoExpire();
protected override void OnTick()
{
_mobile.SendLocalizedMessage(1061688); // Your skin returns to normal.
DoExpire();
}
} }
} }
} }

View file

@ -2,78 +2,77 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Items; 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; private class ExpireTimer : Timer
public override int RequiredMana => 7; {
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) _weapon.Cursed = false;
{ Effects.PlaySound(_weapon.GetWorldLocation(), _weapon.Map, 0xFA);
Caster.SendLocalizedMessage(501078); // You must be holding a weapon. _table.Remove(_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);
}
} }
} }
} }

View file

@ -3,87 +3,86 @@ using System.Collections.Generic;
using Server.Mobiles; using Server.Mobiles;
using Server.Targeting; using Server.Targeting;
namespace Server.Spells.Necromancy namespace Server.Spells.Necromancy;
public class EvilOmenSpell : NecromancerSpell, ISpellTargetingMobile
{ {
public class EvilOmenSpell : NecromancerSpell, ISpellTargetingMobile private static readonly SpellInfo _info = new(
"Evil Omen",
"Pas Tym An Sanct",
203,
9031,
Reagent.BatWing,
Reagent.NoxCrystal
);
private static readonly Dictionary<Mobile, DefaultSkillMod> _table = new();
public EvilOmenSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new(
"Evil Omen",
"Pas Tym An Sanct",
203,
9031,
Reagent.BatWing,
Reagent.NoxCrystal
);
private static readonly Dictionary<Mobile, DefaultSkillMod> _table = new();
public EvilOmenSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75);
public override double RequiredSkill => 20.0;
public override int RequiredMana => 11;
public void Target(Mobile m)
{
if (m is not (BaseCreature or PlayerMobile))
{
Caster.SendLocalizedMessage(1060508); // You can't curse that.
}
else if (CheckHSequence(m))
{
SpellHelper.Turn(Caster, m);
/* Curses the target so that the next harmful event that affects them is magnified.
* Damage to the target's hit points is increased 25%,
* the poison level of the attack will be 1 higher
* and the Resist Magic skill of the target will be fixed on 50.
*
* The effect lasts for one harmful event only.
*/
m.Spell?.OnCasterHurt();
m.PlaySound(0xFC);
m.FixedParticles(0x3728, 1, 13, 9912, 1150, 7, EffectLayer.Head);
m.FixedParticles(0x3779, 1, 15, 9502, 67, 7, EffectLayer.Head);
if (!_table.ContainsKey(m))
{
var mod = new DefaultSkillMod(SkillName.MagicResist, "EvilOmen", false, m.Skills.MagicResist.Value / 2);
m.AddSkillMod(mod);
_table[m] = mod;
}
var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 12 + 1.0);
Timer.StartTimer(duration, () => EndEffect(m));
HarmfulSpell(m);
BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.EvilOmen, 1075647, 1075648, duration, m));
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
public static bool EndEffect(Mobile m)
{
if (_table.Remove(m, out var mod))
{
mod.Remove();
return true;
}
return false;
}
} }
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(0.75);
public override double RequiredSkill => 20.0;
public override int RequiredMana => 11;
public void Target(Mobile m)
{
if (m is not (BaseCreature or PlayerMobile))
{
Caster.SendLocalizedMessage(1060508); // You can't curse that.
}
else if (CheckHSequence(m))
{
SpellHelper.Turn(Caster, m);
/* Curses the target so that the next harmful event that affects them is magnified.
* Damage to the target's hit points is increased 25%,
* the poison level of the attack will be 1 higher
* and the Resist Magic skill of the target will be fixed on 50.
*
* The effect lasts for one harmful event only.
*/
m.Spell?.OnCasterHurt();
m.PlaySound(0xFC);
m.FixedParticles(0x3728, 1, 13, 9912, 1150, 7, EffectLayer.Head);
m.FixedParticles(0x3779, 1, 15, 9502, 67, 7, EffectLayer.Head);
if (!_table.ContainsKey(m))
{
var mod = new DefaultSkillMod(SkillName.MagicResist, "EvilOmen", false, m.Skills.MagicResist.Value / 2);
m.AddSkillMod(mod);
_table[m] = mod;
}
var duration = TimeSpan.FromSeconds(Caster.Skills.SpiritSpeak.Value / 12 + 1.0);
Timer.StartTimer(duration, () => EndEffect(m));
HarmfulSpell(m);
BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.EvilOmen, 1075647, 1075648, duration, m));
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
public static bool EndEffect(Mobile m)
{
if (_table.Remove(m, out var mod))
{
mod.Remove();
return true;
}
return false;
}
}

View file

@ -7,206 +7,205 @@ using Server.Guilds;
using Server.Items; using Server.Items;
using Server.Regions; 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( new(1470, 843, 0),
"Exorcism", new(1857, 865, -1),
"Ort Corp Grav", new(4220, 563, 36),
203, new(1732, 3528, 0),
9031, new(1300, 644, 8),
Reagent.NoxCrystal, new(3355, 302, 9),
Reagent.GraveDust 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 = private static readonly Point3D[] m_MalasLocs =
{ {
new(1470, 843, 0), new(976, 517, -30)
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_IllshLocs = private static readonly Point3D[] m_TokunoLocs =
{ {
new(1222, 474, -17), new(710, 1162, 25),
new(718, 1360, -60), new(1034, 515, 18),
new(297, 1014, -19), new(295, 712, 55)
new(986, 1006, -36), };
new(1180, 1288, -30),
new(1538, 1341, -3),
new(528, 223, -38)
};
private static readonly Point3D[] m_MalasLocs = public ExorcismSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
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 override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0);
public override double RequiredSkill => 80.0;
public override int RequiredMana => 40;
public override bool DelayedDamage => false;
public override bool CheckCast()
{
if (Caster.Skills.SpiritSpeak.Value < 100.0)
{
Caster.SendLocalizedMessage(1072112); // You must have GM Spirit Speak to use this spell
return false;
}
return base.CheckCast();
}
public override int ComputeKarmaAward() => 0;
public override void OnCast()
{
var r = Caster.Region.GetRegion<ChampionSpawnRegion>();
if (r == null || !Caster.InRange(r.Spawn, Range))
{
Caster.SendLocalizedMessage(1072111); // You are not in a valid exorcism region.
}
else if (CheckSequence())
{
var map = Caster.Map;
if (map != null)
{
// Cannot move a mobile while iterating mobiles in range, so use a queue
using var queue = PooledRefQueue<Mobile>.Create();
foreach (var m in r.Spawn.GetMobilesInRange(Range))
{
if (IsValidTarget(m))
{
queue.Enqueue(m);
}
}
while (queue.Count > 0)
{
var m = queue.Dequeue();
// Surprisingly, no sparkle type effects
m.Location = GetNearestShrine(m);
}
}
}
FinishSequence();
}
private bool IsValidTarget(Mobile m)
{
if (!m.Player || m.Alive)
{
return false;
}
var c = m.Corpse as Corpse;
var map = m.Map;
if (c?.Deleted == false && map != null && c.Map == map)
{
if (SpellHelper.IsAnyT2A(map, c.Location) && SpellHelper.IsAnyT2A(map, m.Location))
{
return false; // Same Map, both in T2A, ie, same 'sub server'.
}
if (m.Region.IsPartOf<DungeonRegion>() == Region.Find(c.Location, map).IsPartOf<DungeonRegion>())
{
return false; // Same Map, both in Dungeon region OR They're both NOT in a dungeon region.
}
// Just an approximation cause RunUO doesn't divide up the world the same way OSI does ;p
}
if (Party.Get(m)?.Contains(Caster) == true)
{
return false;
}
if (m.Guild != null && Caster.Guild != null)
{
var mGuild = m.Guild as Guild;
var cGuild = Caster.Guild as Guild;
if (mGuild?.IsAlly(cGuild) == true || mGuild == cGuild)
{
return false;
}
}
var f = Faction.Find(m);
return m.Map != Faction.Facet || f == null || f != Faction.Find(Caster);
}
private static Point3D GetNearestShrine(Mobile m)
{
var map = m.Map;
Point3D[] locList;
if (map == Map.Felucca || map == Map.Trammel)
{
locList = m_BritanniaLocs;
}
else if (map == Map.Ilshenar)
{
locList = m_IllshLocs;
}
else if (map == Map.Tokuno)
{
locList = m_TokunoLocs;
}
else if (map == Map.Malas)
{
locList = m_MalasLocs;
}
else
{
locList = Array.Empty<Point3D>();
}
var closest = Point3D.Zero;
var minDist = double.MaxValue;
for (var i = 0; i < locList.Length; i++)
{
var p = locList[i];
var dist = m.GetDistanceToSqrt(p);
if (minDist > dist)
{
closest = p;
minDist = dist;
}
}
return closest;
}
}

View file

@ -1,41 +1,40 @@
using System; using System;
namespace Server.Spells.Necromancy namespace Server.Spells.Necromancy;
public class HorrificBeastSpell : TransformationSpell
{ {
public class HorrificBeastSpell : TransformationSpell private static readonly SpellInfo _info = new(
"Horrific Beast",
"Rel Xen Vas Bal",
203,
9031,
Reagent.BatWing,
Reagent.DaemonBlood
);
public HorrificBeastSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new(
"Horrific Beast",
"Rel Xen Vas Bal",
203,
9031,
Reagent.BatWing,
Reagent.DaemonBlood
);
public HorrificBeastSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0);
public override double RequiredSkill => 40.0;
public override int RequiredMana => 11;
public override int Body => 746;
public override void DoEffect(Mobile m)
{
m.PlaySound(0x165);
m.FixedParticles(0x3728, 1, 13, 9918, 92, 3, EffectLayer.Head);
m.Delta(MobileDelta.WeaponDamage);
m.CheckStatTimers();
}
public override void RemoveEffect(Mobile m)
{
m.Delta(MobileDelta.WeaponDamage);
}
} }
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0);
public override double RequiredSkill => 40.0;
public override int RequiredMana => 11;
public override int Body => 746;
public override void DoEffect(Mobile m)
{
m.PlaySound(0x165);
m.FixedParticles(0x3728, 1, 13, 9918, 92, 3, EffectLayer.Head);
m.Delta(MobileDelta.WeaponDamage);
m.CheckStatTimers();
}
public override void RemoveEffect(Mobile m)
{
m.Delta(MobileDelta.WeaponDamage);
}
}

View file

@ -1,45 +1,44 @@
using System; using System;
namespace Server.Spells.Necromancy namespace Server.Spells.Necromancy;
public class LichFormSpell : TransformationSpell
{ {
public class LichFormSpell : TransformationSpell private static readonly SpellInfo _info = new(
"Lich Form",
"Rel Xen Corp Ort",
203,
9031,
Reagent.GraveDust,
Reagent.DaemonBlood,
Reagent.NoxCrystal
);
public LichFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new(
"Lich Form",
"Rel Xen Corp Ort",
203,
9031,
Reagent.GraveDust,
Reagent.DaemonBlood,
Reagent.NoxCrystal
);
public LichFormSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0);
public override double RequiredSkill => 70.0;
public override int RequiredMana => 23;
public override int Body => 749;
public override int FireResistOffset => -10;
public override int ColdResistOffset => +10;
public override int PoisResistOffset => +10;
public override double TickRate => 2.5;
public override void DoEffect(Mobile m)
{
m.PlaySound(0x19C);
m.FixedParticles(0x3709, 1, 30, 9904, 1108, 6, EffectLayer.RightFoot);
}
public override void OnTick(Mobile m)
{
--m.Hits;
}
} }
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0);
public override double RequiredSkill => 70.0;
public override int RequiredMana => 23;
public override int Body => 749;
public override int FireResistOffset => -10;
public override int ColdResistOffset => +10;
public override int PoisResistOffset => +10;
public override double TickRate => 2.5;
public override void DoEffect(Mobile m)
{
m.PlaySound(0x19C);
m.FixedParticles(0x3709, 1, 30, 9904, 1108, 6, EffectLayer.RightFoot);
}
public override void OnTick(Mobile m)
{
--m.Hits;
}
}

View file

@ -2,136 +2,135 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Targeting; 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 public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.5);
{
private DateTime _end;
private Mobile _target;
public double _double;
public MRExpireTimer(Mobile target, double scalar, TimeSpan delay) : base( public override double RequiredSkill => 30.0;
TimeSpan.FromSeconds(1.0), public override int RequiredMana => 17;
TimeSpan.FromSeconds(1.0)
) public void Target(Mobile m)
{
if (m == null)
{ {
_double = scalar; Caster.SendLocalizedMessage(1060508); // You can't curse that.
_target = target; }
_end = Core.Now + delay; 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) timer.Stop();
{ m.SendLocalizedMessage(1060872); // Your mind feels normal again.
MindRotSpell.ClearMindRotScalar(_target); BuffInfo.RemoveBuff(m, BuffIcon.Mindrot);
Stop();
} return true;
}
return false;
}
public static bool HasMindRotScalar(Mobile m) => _table.ContainsKey(m);
public static bool GetMindRotScalar(Mobile m, ref double scalar)
{
if (_table.TryGetValue(m, out var timer))
{
scalar = timer._double;
return true;
}
return false;
}
public static void SetMindRotScalar(Mobile caster, Mobile target, double scalar, TimeSpan duration)
{
if (!_table.ContainsKey(target))
{
var timer = new MRExpireTimer(target, scalar, duration);
timer.Start();
_table[target] = timer;
BuffInfo.AddBuff(target, new BuffInfo(BuffIcon.Mindrot, 1075665, duration, target));
target.SendLocalizedMessage(1074384);
} }
} }
} }
public class MRExpireTimer : Timer
{
private DateTime _end;
private Mobile _target;
public double _double;
public MRExpireTimer(Mobile target, double scalar, TimeSpan delay) : base(
TimeSpan.FromSeconds(1.0),
TimeSpan.FromSeconds(1.0)
)
{
_double = scalar;
_target = target;
_end = Core.Now + delay;
}
protected override void OnTick()
{
if (_target.Deleted || !_target.Alive || Core.Now >= _end)
{
MindRotSpell.ClearMindRotScalar(_target);
Stop();
}
}
}

View file

@ -1,47 +1,46 @@
using Server.Items; using Server.Items;
namespace Server.Spells.Necromancy namespace Server.Spells.Necromancy;
public abstract class NecromancerSpell : Spell
{ {
public abstract class NecromancerSpell : Spell public NecromancerSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{ {
public NecromancerSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{
}
public abstract double RequiredSkill { get; }
public abstract int RequiredMana { get; }
public override SkillName CastSkill => SkillName.Necromancy;
public override SkillName DamageSkill => SkillName.SpiritSpeak;
public override bool ClearHandsOnCast => false;
// Necromancer spells are not affected by fast cast items, though they are by fast cast recovery
public override double CastDelayFastScalar => Core.SE ? base.CastDelayFastScalar : 0;
public override int ComputeKarmaAward()
{
// TODO: Verify this formula being that Necro spells don't HAVE a circle.
// int karma = -(70 + (10 * (int)Circle));
var karma = -(40 + (int)(10 * (CastDelayBase.TotalSeconds / CastDelaySecondsPerTick)));
// Pub 36: "Added a new property called Increased Karma Loss which grants higher karma loss for casting necromancy spells."
if (Core.ML)
{
karma += AOS.Scale(karma, AosAttributes.GetValue(Caster, AosAttribute.IncreasedKarmaLoss));
}
return karma;
}
public override void GetCastSkills(out double min, out double max)
{
min = RequiredSkill;
max = Scroll != null ? min : RequiredSkill + 40.0;
}
public override bool ConsumeReagents() => base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, 1);
public override int GetMana() => RequiredMana;
} }
}
public abstract double RequiredSkill { get; }
public abstract int RequiredMana { get; }
public override SkillName CastSkill => SkillName.Necromancy;
public override SkillName DamageSkill => SkillName.SpiritSpeak;
public override bool ClearHandsOnCast => false;
// Necromancer spells are not affected by fast cast items, though they are by fast cast recovery
public override double CastDelayFastScalar => Core.SE ? base.CastDelayFastScalar : 0;
public override int ComputeKarmaAward()
{
// TODO: Verify this formula being that Necro spells don't HAVE a circle.
// int karma = -(70 + (10 * (int)Circle));
var karma = -(40 + (int)(10 * (CastDelayBase.TotalSeconds / CastDelaySecondsPerTick)));
// Pub 36: "Added a new property called Increased Karma Loss which grants higher karma loss for casting necromancy spells."
if (Core.ML)
{
karma += AOS.Scale(karma, AosAttributes.GetValue(Caster, AosAttribute.IncreasedKarmaLoss));
}
return karma;
}
public override void GetCastSkills(out double min, out double max)
{
min = RequiredSkill;
max = Scroll != null ? min : RequiredSkill + 40.0;
}
public override bool ConsumeReagents() => base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, 1);
public override int GetMana() => RequiredMana;
}

View file

@ -3,116 +3,115 @@ using System.Collections.Generic;
using Server.Misc; using Server.Misc;
using Server.Targeting; 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); if (CheckHSequence(m))
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) 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)) BuffInfo.AddBuff(m, new BuffInfo(BuffIcon.PainSpike, 1075667, buffTime, m, Convert.ToString((int)damage)));
{
SpellHelper.Turn(Caster, m);
// 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. // SpellHelper.Damage( this, m, damage, 100, 0, 0, 0, 0, Misc.DFAlgorithm.PainSpike );
* After 10 seconds the spell wears off, and if the target is still alive, HarmfulSpell(m);
* 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();
} }
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; _table.Remove(_mobile);
private int _toRestore;
public InternalTimer(Mobile m, double toRestore) : base(TimeSpan.FromSeconds(10.0)) if (_mobile.Alive && !_mobile.IsDeadBondedPet)
{ {
_mobile.Hits += _toRestore;
_mobile = m;
_toRestore = (int)toRestore;
} }
protected override void OnTick() BuffInfo.RemoveBuff(_mobile, BuffIcon.PainSpike);
{
_table.Remove(_mobile);
if (_mobile.Alive && !_mobile.IsDeadBondedPet)
{
_mobile.Hits += _toRestore;
}
BuffInfo.RemoveBuff(_mobile, BuffIcon.PainSpike);
}
} }
} }
} }

View file

@ -4,151 +4,150 @@ using Server.Items;
using Server.Mobiles; using Server.Mobiles;
using Server.Targeting; 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; // CheckResisted( m );
public override int RequiredMana => 17; // 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 (map != null)
{
if (CheckHSequence(m))
{ {
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. var damage = Utility.RandomMinMax(Core.ML ? 32 : 36, 40) * ((300 + GetDamageSkill(Caster) * 9) / 1000);
* 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.
*/
// CheckResisted( m ); var sdiBonus = (double)AosAttributes.GetValue(Caster, AosAttribute.SpellDamage) / 100;
// Check magic resist for skill, but do not use return value var pvmDamage = damage * (1 + sdiBonus);
// reports from OSI: Necro spells don't give Resist gain
var map = m.Map; if (Core.ML && sdiBonus > 0.15)
if (map != null)
{ {
Effects.SendLocationParticles( sdiBonus = 0.15;
EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration), }
0x36B0,
1, var pvpDamage = damage * (1 + sdiBonus);
14,
63, using var pool = PooledRefQueue<Mobile>.Create();
7,
9915, 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 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() FinishSequence();
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
} }
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
}

View file

@ -2,229 +2,228 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Targeting; 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); if (CheckHSequence(m))
public override double RequiredSkill => 65.0;
public override int RequiredMana => 29;
public void Target(Mobile 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; return;
} }
if (CheckHSequence(m)) --_count;
if (_hitDelay > 1)
{ {
SpellHelper.Turn(Caster, m); if (_maxCount < 5)
// 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(); --_hitDelay;
}
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();
} }
else else
{ {
_nextHit = Core.Now + TimeSpan.FromSeconds(_hitDelay); var delay = (int)Math.Ceiling((1.0 + 5 * _count) / _maxCount);
var damage = _minBaseDamage + Utility.RandomDouble() * (_maxBaseDamage - _minBaseDamage); if (delay <= 5)
damage *= 3 - (double)_target.Stam / _target.StamMax * 2;
if (damage < 1)
{ {
damage = 1; _hitDelay = delay;
} }
else
if (!_target.Player)
{ {
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 var damage = _minBaseDamage + Utility.RandomDouble() * (_maxBaseDamage - _minBaseDamage);
if (Utility.RandomDouble() < 0.40)
{ damage *= 3 - (double)_target.Stam / _target.StamMax * 2;
_target.RevealingAction();
} if (damage < 1)
{
damage = 1;
}
if (!_target.Player)
{
damage *= 1.75;
}
AOS.Damage(_target, _from, (int)damage, 0, 0, 0, 100, 0);
// OSI: randomly revealed between first and third damage tick, guessing 60% chance
if (Utility.RandomDouble() < 0.40)
{
_target.RevealingAction();
} }
} }
} }
} }
} }

View file

@ -4,203 +4,218 @@ using Server.Gumps;
using Server.Mobiles; using Server.Mobiles;
using Server.Network; 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 static void Unregister(Mobile master, Mobile summoned)
{
public override double RequiredSkill => 30.0; if (master != null && Table.TryGetValue(master, out var summon) && summon == summoned)
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 Table.Remove(master);
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 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. Caster.SendLocalizedMessage(1061605); // You already have a familiar.
return false; return false;
} }
public override void OnCast() return base.CheckCast();
{ }
if (CheckSequence())
{
Caster.CloseGump<SummonFamiliarGump>();
Caster.SendGump(new SummonFamiliarGump(Caster, Entries, this));
}
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; _from.SendLocalizedMessage(1061825); // You decide not to summon a familiar.
Name = name; return;
ReqNecromancy = reqNecromancy;
ReqSpiritSpeak = reqSpiritSpeak;
} }
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; } if ((_from as PlayerMobile)?.DuelContext?.AllowSpellCast(_from, _spell) == false)
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)
{ {
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)
);
}
}
} }
else if (SummonFamiliarSpell.Table.TryGetValue(_from, out var check) && check?.Deleted == false)
public override void OnResponse(NetState sender, in RelayInfo info)
{ {
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; // TODO: Is this right?
var spirit = m_From.Skills.SpiritSpeak.Value; 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)))
{ {
} _from.FixedParticles(0x3728, 1, 10, 9910, EffectLayer.Head);
else if (SummonFamiliarSpell.Table.TryGetValue(m_From, out var check) && check?.Deleted == false) bc.PlaySound(bc.GetIdleSound());
{ SummonFamiliarSpell.Table[_from] = bc;
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
}
} }
} }
else catch
{ {
m_From.SendLocalizedMessage(1061825); // You decide not to summon a familiar. // ignored
} }
} }
} }

View file

@ -1,42 +1,41 @@
namespace Server.Spells.Necromancy namespace Server.Spells.Necromancy;
public abstract class TransformationSpell : NecromancerSpell, ITransformationSpell
{ {
public abstract class TransformationSpell : NecromancerSpell, ITransformationSpell public TransformationSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{ {
public TransformationSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{
}
public override bool BlockedByHorrificBeast => false;
public abstract int Body { get; }
public virtual int Hue => 0;
public virtual int PhysResistOffset => 0;
public virtual int FireResistOffset => 0;
public virtual int ColdResistOffset => 0;
public virtual int PoisResistOffset => 0;
public virtual int NrgyResistOffset => 0;
public virtual double TickRate => 1.0;
public virtual void OnTick(Mobile m)
{
}
public virtual void DoEffect(Mobile m)
{
}
public virtual void RemoveEffect(Mobile m)
{
}
public override bool CheckCast() => TransformationSpellHelper.CheckCast(Caster, this) && base.CheckCast();
public override void OnCast()
{
TransformationSpellHelper.OnCast(Caster, this);
FinishSequence();
}
} }
}
public override bool BlockedByHorrificBeast => false;
public abstract int Body { get; }
public virtual int Hue => 0;
public virtual int PhysResistOffset => 0;
public virtual int FireResistOffset => 0;
public virtual int ColdResistOffset => 0;
public virtual int PoisResistOffset => 0;
public virtual int NrgyResistOffset => 0;
public virtual double TickRate => 1.0;
public virtual void OnTick(Mobile m)
{
}
public virtual void DoEffect(Mobile m)
{
}
public virtual void RemoveEffect(Mobile m)
{
}
public override bool CheckCast() => TransformationSpellHelper.CheckCast(Caster, this) && base.CheckCast();
public override void OnCast()
{
TransformationSpellHelper.OnCast(Caster, this);
FinishSequence();
}
}

View file

@ -1,70 +1,69 @@
using System; using System;
using Server.Items; 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;
} }
else
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) base.GetCastSkills(out min, out max);
{
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);
} }
} }
}
public override void DoEffect(Mobile m)
{
Effects.SendLocationParticles(
EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration),
0x373A,
1,
17,
1108,
7,
9914,
0
);
Effects.SendLocationParticles(
EffectItem.Create(m.Location, m.Map, EffectItem.DefaultDuration),
0x376A,
1,
22,
67,
7,
9502,
0
);
Effects.PlaySound(m.Location, m.Map, 0x4B1);
}
}

View file

@ -2,55 +2,55 @@ using System;
using Server.Mobiles; using Server.Mobiles;
using Server.Targeting; using Server.Targeting;
namespace Server.Spells.Necromancy 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
);
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); if (Caster == m)
public override double RequiredSkill => 80.0;
public override int RequiredMana => 41;
public void Target(Mobile m)
{ {
if (m == null) Caster.SendLocalizedMessage(1061832); // You cannot exact vengeance on yourself.
{ }
return; else if (CheckHSequence(m))
} {
SpellHelper.Turn(Caster, m);
if (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.
Caster.SendLocalizedMessage(1061832); // You cannot exact vengeance on yourself. * 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.
else if (CheckHSequence(m)) */
{
SpellHelper.Turn(Caster, m);
/* Summons a Revenant which haunts the target until either the target or the Revenant is dead. var duration = TimeSpan.FromSeconds(GetDamageSkill(Caster) * 80 / 120 + 10);
* 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 rev = new Revenant(Caster, m, duration);
var rev = new Revenant(Caster, m, duration); if (BaseCreature.Summon(
if (BaseCreature.Summon(
rev, rev,
false, false,
Caster, Caster,
@ -58,33 +58,32 @@ namespace Server.Spells.Necromancy
0x81, 0x81,
TimeSpan.FromSeconds(duration.TotalSeconds + 2.0) TimeSpan.FromSeconds(duration.TotalSeconds + 2.0)
)) ))
{
rev.FixedParticles(0x373A, 1, 15, 9909, EffectLayer.Waist);
}
}
FinishSequence();
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
public override bool CheckCast()
{
if (!base.CheckCast())
{ {
return false; rev.FixedParticles(0x373A, 1, 15, 9909, EffectLayer.Waist);
} }
if (Caster.Followers + 3 > Caster.FollowersMax)
{
Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature.
return false;
}
return true;
} }
FinishSequence();
} }
}
public override void OnCast()
{
Caster.Target = new SpellTargetMobile(this, TargetFlags.Harmful, Core.ML ? 10 : 12);
}
public override bool CheckCast()
{
if (!base.CheckCast())
{
return false;
}
if (Caster.Followers + 3 > Caster.FollowersMax)
{
Caster.SendLocalizedMessage(1049645); // You have too many followers to summon that creature.
return false;
}
return true;
}
}

View file

@ -3,132 +3,131 @@ using Server.Collections;
using Server.Items; using Server.Items;
using Server.Mobiles; 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 var map = Caster.Map;
{
>= Expansion.SA => 1.25,
>= Expansion.ML => 1.5,
_ => 1.0
});
public override double RequiredSkill => 60.0; if (map != null)
public override int RequiredMana => 23;
public override bool DelayedDamage => false;
public override void OnCast()
{
if (CheckSequence())
{ {
/* Creates a withering frost around the Caster, using var pool = PooledRefQueue<Mobile>.Create();
* which deals Cold Damage to all valid targets in a radius of 5 tiles.
*/
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(); if (targ == Caster
|| !Caster.InLOS(targ)
var cbc = Caster as BaseCreature; || !isMonster && !SpellHelper.ValidIndirectTarget(Caster, targ)
var isMonster = cbc?.Controlled == false && (cbc.IsAnimatedDead || !cbc.Summoned); || !Caster.CanBeHarmful(targ, false))
foreach (var targ in Caster.GetMobilesInRange(Core.ML ? 4 : 5))
{ {
if (targ == Caster continue;
|| !Caster.InLOS(targ) }
|| !isMonster && !SpellHelper.ValidIndirectTarget(Caster, targ)
|| !Caster.CanBeHarmful(targ, false)) 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; continue;
} }
if (isMonster && targ.Player) if (isMonster && (bc.Controlled || bc.Summoned || bc.Team == cbc.Team || bc.IsNecroFamiliar))
{ {
continue; 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); pool.Enqueue(targ);
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) 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(); sdiBonus = 15;
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);
} }
damage *= 100 + sdiBonus;
damage /= 100;
// TODO: cap?
// if (damage > 40)
// damage = 40;
SpellHelper.Damage(this, m, damage, 0, 0, 100, 0, 0);
} }
} }
FinishSequence();
} }
FinishSequence();
} }
} }

View file

@ -1,67 +1,66 @@
using System; using System;
using Server.Mobiles; 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)
{ {
} wraith.Mana += manaLeech;
defender.Mana -= manaLeech;
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(2.0); wraith.PlaySound(0x44D);
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;
}
} }
} }
}
public override void DoEffect(Mobile m)
{
if (m is PlayerMobile mobile)
{
mobile.IgnoreMobiles = true;
}
m.PlaySound(0x17F);
m.FixedParticles(0x374A, 1, 15, 9902, 1108, 4, EffectLayer.Waist);
}
public override void RemoveEffect(Mobile m)
{
if (m is PlayerMobile { AccessLevel: AccessLevel.Player } mobile)
{
mobile.IgnoreMobiles = false;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,71 +1,70 @@
using System; using System;
using Server.SkillHandlers; using Server.SkillHandlers;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class Backstab : NinjaMove
{ {
public class Backstab : NinjaMove public override int BaseMana => 30;
public override double RequiredSkill => Core.ML ? 40.0 : 20.0;
// You prepare to Backstab your opponent.
public override TextDefinition AbilityMessage { get; } = 1063089;
public override bool ValidatesDuringHit => false;
public override double GetDamageScalar(Mobile attacker, Mobile defender)
{ {
public override int BaseMana => 30; var ninjitsu = attacker.Skills.Ninjitsu.Value;
public override double RequiredSkill => Core.ML ? 40.0 : 20.0;
// You prepare to Backstab your opponent. return 1.0 + ninjitsu / 360 + Tracking.GetStalkingBonus(attacker, defender) / 100;
public override TextDefinition AbilityMessage { get; } = 1063089;
public override bool ValidatesDuringHit => false;
public override double GetDamageScalar(Mobile attacker, Mobile defender)
{
var ninjitsu = attacker.Skills.Ninjitsu.Value;
return 1.0 + ninjitsu / 360 + Tracking.GetStalkingBonus(attacker, defender) / 100;
}
public override bool Validate(Mobile from)
{
if (!from.Hidden || from.AllowedStealthSteps <= 0)
{
from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
return false;
}
return base.Validate(from);
}
public override bool OnBeforeSwing(Mobile attacker, Mobile defender)
{
var valid = Validate(attacker) && CheckMana(attacker, true);
if (valid)
{
attacker.BeginAction<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
return valid;
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
// Validates before swing
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063090); // You quickly stab your opponent as you come out of hiding!
defender.FixedParticles(0x37B9, 1, 5, 0x251D, 0x651, 0, EffectLayer.Waist);
attacker.RevealingAction();
CheckGain(attacker);
}
public override void OnMiss(Mobile attacker, Mobile defender)
{
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise.
attacker.RevealingAction();
}
} }
}
public override bool Validate(Mobile from)
{
if (!from.Hidden || from.AllowedStealthSteps <= 0)
{
from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
return false;
}
return base.Validate(from);
}
public override bool OnBeforeSwing(Mobile attacker, Mobile defender)
{
var valid = Validate(attacker) && CheckMana(attacker, true);
if (valid)
{
attacker.BeginAction<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
return valid;
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
// Validates before swing
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063090); // You quickly stab your opponent as you come out of hiding!
defender.FixedParticles(0x37B9, 1, 5, 0x251D, 0x651, 0, EffectLayer.Waist);
attacker.RevealingAction();
CheckGain(attacker);
}
public override void OnMiss(Mobile attacker, Mobile defender)
{
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise.
attacker.RevealingAction();
}
}

View file

@ -4,175 +4,174 @@ using System.Runtime.CompilerServices;
using Server.Items; using Server.Items;
using Server.SkillHandlers; using Server.SkillHandlers;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class DeathStrike : NinjaMove
{ {
public class DeathStrike : NinjaMove private static readonly Dictionary<Mobile, DeathStrikeTimer> _table = new();
public override int BaseMana => 30;
public override double RequiredSkill => 85.0;
// You prepare to hit your opponent with a Death Strike.
public override TextDefinition AbilityMessage { get; } = 1063091;
public override double GetDamageScalar(Mobile attacker, Mobile defender) => 0.5;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{ {
private static readonly Dictionary<Mobile, DeathStrikeTimer> _table = new(); if (!Validate(attacker) || !CheckMana(attacker, true))
public override int BaseMana => 30;
public override double RequiredSkill => 85.0;
// You prepare to hit your opponent with a Death Strike.
public override TextDefinition AbilityMessage { get; } = 1063091;
public override double GetDamageScalar(Mobile attacker, Mobile defender) => 0.5;
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{ {
if (!Validate(attacker) || !CheckMana(attacker, true)) return;
}
ClearCurrentMove(attacker);
var ninjitsu = attacker.Skills.Ninjitsu.Value;
// TODO: should be defined onHit method, what if the player hit and remove the weapon before process? ;)
var isRanged = attacker.Weapon is BaseRanged;
var chance = ninjitsu switch
{
// This formula is an approximation from OSI data. TODO: find correct formula
< 100 => 30 + (ninjitsu - 85) * 2.2,
_ => 63 + (ninjitsu - 100) * 1.1
};
if (chance / 100 < Utility.RandomDouble())
{
attacker.SendLocalizedMessage(1070779); // You missed your opponent with a Death Strike.
return;
}
var damageBonus = 0;
if (_table.Remove(defender, out var timer))
{
defender.SendLocalizedMessage(1063092); // Your opponent lands another Death Strike!
if (timer.Steps > 0)
{ {
return; damageBonus = (int)(attacker.Skills.Ninjitsu.Value / 15);
} }
ClearCurrentMove(attacker); timer.Stop();
}
else
{
defender.SendLocalizedMessage(1063093); // You have been hit by a Death Strike! Move with caution!
}
var ninjitsu = attacker.Skills.Ninjitsu.Value; attacker.SendLocalizedMessage(1063094); // You inflict a Death Strike upon your opponent!
// TODO: should be defined onHit method, what if the player hit and remove the weapon before process? ;) defender.FixedParticles(0x374A, 1, 17, 0x26BC, EffectLayer.Waist);
var isRanged = attacker.Weapon is BaseRanged; attacker.PlaySound(attacker.Female ? 0x50D : 0x50E);
var chance = ninjitsu switch var t = new DeathStrikeTimer(defender, attacker, damageBonus, isRanged);
_table[defender] = t;
t.Start();
CheckGain(attacker);
}
public static void AddStep(Mobile m)
{
if (_table.TryGetValue(m, out var timer) && ++timer.Steps >= 5)
{
timer.ProcessDeathStrike();
}
}
public static void RemoveEffect(Mobile m)
{
if (_table.Remove(m, out var timer))
{
timer.Stop();
}
}
private class DeathStrikeTimer : Timer
{
private Mobile _attacker;
private int _damageBonus;
private bool _isRanged;
private Mobile _target;
public int Steps { get; set; }
internal DeathStrikeTimer(Mobile target, Mobile attacker, int damageBonus, bool isRanged)
: base(TimeSpan.FromSeconds(5.0))
{
_target = target;
_attacker = attacker;
_damageBonus = damageBonus;
_isRanged = isRanged;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnTick()
{
ProcessDeathStrike();
}
public void ProcessDeathStrike()
{
int damage;
var ninjitsu = _attacker.Skills.Ninjitsu.Value;
var stalkingBonus = Tracking.GetStalkingBonus(_attacker, _target);
if (Core.ML)
{ {
// This formula is an approximation from OSI data. TODO: find correct formula var scalar = Math.Min(1, (_attacker.Skills.Hiding.Value + _attacker.Skills.Stealth.Value) / 220);
< 100 => 30 + (ninjitsu - 85) * 2.2,
_ => 63 + (ninjitsu - 100) * 1.1
};
if (chance / 100 < Utility.RandomDouble()) // New formula doesn't apply DamageBonus anymore, caps must be, directly, 60/30.
{ if (Steps >= 5)
attacker.SendLocalizedMessage(1070779); // You missed your opponent with a Death Strike.
return;
}
var damageBonus = 0;
if (_table.Remove(defender, out var timer))
{
defender.SendLocalizedMessage(1063092); // Your opponent lands another Death Strike!
if (timer.Steps > 0)
{ {
damageBonus = (int)(attacker.Skills.Ninjitsu.Value / 15); damage = (int)Math.Floor(Math.Min(60, ninjitsu / 3 * (0.3 + 0.7 * scalar) + stalkingBonus));
}
timer.Stop();
}
else
{
defender.SendLocalizedMessage(1063093); // You have been hit by a Death Strike! Move with caution!
}
attacker.SendLocalizedMessage(1063094); // You inflict a Death Strike upon your opponent!
defender.FixedParticles(0x374A, 1, 17, 0x26BC, EffectLayer.Waist);
attacker.PlaySound(attacker.Female ? 0x50D : 0x50E);
var t = new DeathStrikeTimer(defender, attacker, damageBonus, isRanged);
_table[defender] = t;
t.Start();
CheckGain(attacker);
}
public static void AddStep(Mobile m)
{
if (_table.TryGetValue(m, out var timer) && ++timer.Steps >= 5)
{
timer.ProcessDeathStrike();
}
}
public static void RemoveEffect(Mobile m)
{
if (_table.Remove(m, out var timer))
{
timer.Stop();
}
}
private class DeathStrikeTimer : Timer
{
private Mobile _attacker;
private int _damageBonus;
private bool _isRanged;
private Mobile _target;
public int Steps { get; set; }
internal DeathStrikeTimer(Mobile target, Mobile attacker, int damageBonus, bool isRanged)
: base(TimeSpan.FromSeconds(5.0))
{
_target = target;
_attacker = attacker;
_damageBonus = damageBonus;
_isRanged = isRanged;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnTick()
{
ProcessDeathStrike();
}
public void ProcessDeathStrike()
{
int damage;
var ninjitsu = _attacker.Skills.Ninjitsu.Value;
var stalkingBonus = Tracking.GetStalkingBonus(_attacker, _target);
if (Core.ML)
{
var scalar = Math.Min(1, (_attacker.Skills.Hiding.Value + _attacker.Skills.Stealth.Value) / 220);
// New formula doesn't apply DamageBonus anymore, caps must be, directly, 60/30.
if (Steps >= 5)
{
damage = (int)Math.Floor(Math.Min(60, ninjitsu / 3 * (0.3 + 0.7 * scalar) + stalkingBonus));
}
else
{
damage = (int)Math.Floor(Math.Min(30, ninjitsu / 9 * (0.3 + 0.7 * scalar) + stalkingBonus));
}
if (_isRanged)
{
damage /= 2;
}
_target.Damage(damage, _attacker); // Damage is direct.
} }
else else
{ {
var divisor = Steps >= 5 ? 30 : 80; damage = (int)Math.Floor(Math.Min(30, ninjitsu / 9 * (0.3 + 0.7 * scalar) + stalkingBonus));
var baseDamage = ninjitsu / divisor * 10;
var maxDamage = Steps >= 5 ? 62 : 22;
damage = Math.Clamp((int)(baseDamage + stalkingBonus), 0, maxDamage) + _damageBonus;
// Damage is physical.
AOS.Damage(
_target,
_attacker,
damage,
true,
100,
0,
0,
0,
0,
0,
0,
false,
false,
true
);
} }
RemoveEffect(_target); if (_isRanged)
{
damage /= 2;
}
_target.Damage(damage, _attacker); // Damage is direct.
} }
else
{
var divisor = Steps >= 5 ? 30 : 80;
var baseDamage = ninjitsu / divisor * 10;
var maxDamage = Steps >= 5 ? 62 : 22;
damage = Math.Clamp((int)(baseDamage + stalkingBonus), 0, maxDamage) + _damageBonus;
// Damage is physical.
AOS.Damage(
_target,
_attacker,
damage,
true,
100,
0,
0,
0,
0,
0,
0,
false,
false,
true
);
}
RemoveEffect(_target);
} }
} }
} }

View file

@ -1,64 +1,63 @@
using Server.Items; using Server.Items;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class FocusAttack : NinjaMove
{ {
public class FocusAttack : NinjaMove public override int BaseMana => Core.ML ? 10 : 20;
public override double RequiredSkill => Core.ML ? 30.0 : 60;
// You prepare to focus all of your abilities into your next strike.
public override TextDefinition AbilityMessage { get; } = 1063095;
public override bool Validate(Mobile from)
{ {
public override int BaseMana => Core.ML ? 10 : 20; var twoHanded = from.FindItemOnLayer(Layer.TwoHanded);
public override double RequiredSkill => Core.ML ? 30.0 : 60; if (twoHanded is BaseShield)
// You prepare to focus all of your abilities into your next strike.
public override TextDefinition AbilityMessage { get; } = 1063095;
public override bool Validate(Mobile from)
{ {
var twoHanded = from.FindItemOnLayer(Layer.TwoHanded); from.SendLocalizedMessage(1063096); // You cannot use this ability while holding a shield.
if (twoHanded is BaseShield)
{
from.SendLocalizedMessage(1063096); // You cannot use this ability while holding a shield.
return false;
}
var meleeWeapon =
twoHanded is BaseWeapon and not BaseRanged ||
from.FindItemOnLayer(Layer.OneHanded) is BaseWeapon and not BaseRanged;
if (meleeWeapon)
{
return base.Validate(from);
}
from.SendLocalizedMessage(1063097); // You must be wielding a melee weapon without a shield to use this ability.
return false; return false;
} }
public override double GetDamageScalar(Mobile attacker, Mobile defender) var meleeWeapon =
{ twoHanded is BaseWeapon and not BaseRanged ||
var ninjitsu = attacker.Skills.Ninjitsu.Value; from.FindItemOnLayer(Layer.OneHanded) is BaseWeapon and not BaseRanged;
return 1.0 + ninjitsu * ninjitsu / 43636; if (meleeWeapon)
{
return base.Validate(from);
} }
public override double GetPropertyBonus(Mobile attacker) from.SendLocalizedMessage(1063097); // You must be wielding a melee weapon without a shield to use this ability.
{ return false;
var ninjitsu = attacker.Skills.Ninjitsu.Value;
var bonus = ninjitsu * ninjitsu / 43636;
return 1.0 + (bonus * 3 + 0.01);
}
public override bool OnBeforeDamage(Mobile attacker, Mobile defender) =>
Validate(attacker) && CheckMana(attacker, true);
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063098); // You focus all of your abilities and strike with deadly force!
attacker.PlaySound(0x510);
CheckGain(attacker);
}
} }
}
public override double GetDamageScalar(Mobile attacker, Mobile defender)
{
var ninjitsu = attacker.Skills.Ninjitsu.Value;
return 1.0 + ninjitsu * ninjitsu / 43636;
}
public override double GetPropertyBonus(Mobile attacker)
{
var ninjitsu = attacker.Skills.Ninjitsu.Value;
var bonus = ninjitsu * ninjitsu / 43636;
return 1.0 + (bonus * 3 + 0.01);
}
public override bool OnBeforeDamage(Mobile attacker, Mobile defender) =>
Validate(attacker) && CheckMana(attacker, true);
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063098); // You focus all of your abilities and strike with deadly force!
attacker.PlaySound(0x510);
CheckGain(attacker);
}
}

View file

@ -2,125 +2,124 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Items; using Server.Items;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class KiAttack : NinjaMove
{ {
public class KiAttack : NinjaMove private static readonly Dictionary<Mobile, KiAttackTimer> _table = new();
public override int BaseMana => 25;
public override double RequiredSkill => 80.0;
// Your Ki Attack must be complete within 2 seconds for the damage bonus!
public override TextDefinition AbilityMessage { get; } = 1063099;
public override void OnUse(Mobile from)
{ {
private static readonly Dictionary<Mobile, KiAttackTimer> _table = new(); if (!Validate(from))
public override int BaseMana => 25;
public override double RequiredSkill => 80.0;
// Your Ki Attack must be complete within 2 seconds for the damage bonus!
public override TextDefinition AbilityMessage { get; } = 1063099;
public override void OnUse(Mobile from)
{ {
if (!Validate(from)) return;
{
return;
}
var t = new KiAttackTimer(from);
_table[from] = t;
t.Start();
} }
public override bool Validate(Mobile from) var t = new KiAttackTimer(from);
_table[from] = t;
t.Start();
}
public override bool Validate(Mobile from)
{
if (from.Hidden && from.AllowedStealthSteps > 0)
{ {
if (from.Hidden && from.AllowedStealthSteps > 0) from.SendLocalizedMessage(1063127); // You cannot use this ability while in stealth mode.
{ return false;
from.SendLocalizedMessage(1063127); // You cannot use this ability while in stealth mode.
return false;
}
if (Core.ML && from.Weapon is BaseRanged)
{
from.SendLocalizedMessage(1075858); // You can only use this with melee attacks.
return false;
}
return base.Validate(from);
} }
public override double GetDamageScalar(Mobile attacker, Mobile defender) if (Core.ML && from.Weapon is BaseRanged)
{ {
if (attacker.Hidden) from.SendLocalizedMessage(1075858); // You can only use this with melee attacks.
{ return false;
return 1.0;
}
/*
* Pub40 changed pvp damage max to 55%
*/
return 1.0 + GetBonus(attacker) / (Core.ML && attacker.Player && defender.Player ? 40 : 10);
} }
public override void OnHit(Mobile attacker, Mobile defender, int damage) return base.Validate(from);
}
public override double GetDamageScalar(Mobile attacker, Mobile defender)
{
if (attacker.Hidden)
{ {
if (!Validate(attacker) || !CheckMana(attacker, true)) return 1.0;
{
return;
}
ClearCurrentMove(attacker);
if (GetBonus(attacker) == 0.0)
{
attacker.SendLocalizedMessage(1063101); // You were too close to your target to cause any additional damage.
}
else
{
attacker.FixedParticles(0x37BE, 1, 5, 0x26BD, 0x0, 0x1, EffectLayer.Waist);
attacker.PlaySound(0x510);
// Your quick flight to your target causes extra damage as you strike!
attacker.SendLocalizedMessage(1063100);
defender.FixedParticles(0x37BE, 1, 5, 0x26BD, 0, 0x1, EffectLayer.Waist);
CheckGain(attacker);
}
} }
public override void OnClearMove(Mobile from) /*
* Pub40 changed pvp damage max to 55%
*/
return 1.0 + GetBonus(attacker) / (Core.ML && attacker.Player && defender.Player ? 40 : 10);
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
if (!Validate(attacker) || !CheckMana(attacker, true))
{ {
if (_table.Remove(from, out var t)) return;
{
t.Stop();
}
} }
public static double GetBonus(Mobile from) ClearCurrentMove(attacker);
if (GetBonus(attacker) == 0.0)
{ {
if (!_table.TryGetValue(from, out var t)) attacker.SendLocalizedMessage(1063101); // You were too close to your target to cause any additional damage.
{ }
return 0; else
} {
attacker.FixedParticles(0x37BE, 1, 5, 0x26BD, 0x0, 0x1, EffectLayer.Waist);
attacker.PlaySound(0x510);
var xDelta = t._location.X - from.X; // Your quick flight to your target causes extra damage as you strike!
var yDelta = t._location.Y - from.Y; attacker.SendLocalizedMessage(1063100);
defender.FixedParticles(0x37BE, 1, 5, 0x26BD, 0, 0x1, EffectLayer.Waist);
return Math.Min(Math.Sqrt(xDelta * xDelta + yDelta * yDelta), 20.0); CheckGain(attacker);
}
}
public override void OnClearMove(Mobile from)
{
if (_table.Remove(from, out var t))
{
t.Stop();
}
}
public static double GetBonus(Mobile from)
{
if (!_table.TryGetValue(from, out var t))
{
return 0;
} }
private class KiAttackTimer : Timer var xDelta = t._location.X - from.X;
var yDelta = t._location.Y - from.Y;
return Math.Min(Math.Sqrt(xDelta * xDelta + yDelta * yDelta), 20.0);
}
private class KiAttackTimer : Timer
{
public Mobile _mobile;
public Point3D _location;
public KiAttackTimer(Mobile m) : base(TimeSpan.FromSeconds(2.0))
{ {
public Mobile _mobile; _mobile = m;
public Point3D _location; _location = m.Location;
}
public KiAttackTimer(Mobile m) : base(TimeSpan.FromSeconds(2.0)) protected override void OnTick()
{ {
_mobile = m; ClearCurrentMove(_mobile);
_location = m.Location; _mobile.SendLocalizedMessage(1063102); // You failed to complete your Ki Attack in time.
}
protected override void OnTick() _table.Remove(_mobile);
{
ClearCurrentMove(_mobile);
_mobile.SendLocalizedMessage(1063102); // You failed to complete your Ki Attack in time.
_table.Remove(_mobile);
}
} }
} }
} }

View file

@ -37,7 +37,7 @@ namespace Server.Spells.Ninjitsu
{ {
if (m != null) if (m != null)
{ {
_cloneCount[m] = 1 + (_cloneCount.TryGetValue(m, out var count) ? count : 0); _cloneCount[m] = 1 + _cloneCount.GetValueOrDefault(m, 0);
} }
} }
@ -144,8 +144,10 @@ namespace Server.Mobiles
for (var i = 0; i < caster.Skills.Length; ++i) for (var i = 0; i < caster.Skills.Length; ++i)
{ {
Skills[i].Base = caster.Skills[i].Base; var skill = Skills[i];
Skills[i].Cap = caster.Skills[i].Cap; var casterSkill = caster.Skills[i];
skill.Base = casterSkill.Base;
skill.Cap = casterSkill.Cap;
} }
for (var i = 0; i < caster.Items.Count; i++) for (var i = 0; i < caster.Items.Count; i++)
@ -161,7 +163,12 @@ namespace Server.Mobiles
ControlOrder = OrderType.Follow; ControlOrder = OrderType.Follow;
ControlTarget = caster; ControlTarget = caster;
var duration = TimeSpan.FromSeconds(30.0 + caster.Skills.Ninjitsu.Value / 4.0); AddClone();
}
private void AddClone()
{
var duration = TimeSpan.FromSeconds(30.0 + _caster.Skills.Ninjitsu.Value / 4.0);
new UnsummonTimer(this, duration).Start(); new UnsummonTimer(this, duration).Start();
SummonEnd = Core.Now + duration; SummonEnd = Core.Now + duration;
@ -178,7 +185,7 @@ namespace Server.Mobiles
public override bool IsHumanInTown() => false; public override bool IsHumanInTown() => false;
private Item CloneItem(Item item) private static Item CloneItem(Item item)
{ {
var newItem = new Item(item.ItemID); var newItem = new Item(item.ItemID);
newItem.Hue = item.Hue; newItem.Hue = item.Hue;
@ -214,13 +221,10 @@ namespace Server.Mobiles
[AfterDeserialization] [AfterDeserialization]
private void AfterDeserialization() private void AfterDeserialization()
{ {
MirrorImage.AddClone(_caster); AddClone();
} }
} }
}
namespace Server.Mobiles
{
public class CloneAI : BaseAI public class CloneAI : BaseAI
{ {
public CloneAI(Clone m) : base(m) => m.SetCurrentSpeedToActive(); public CloneAI(Clone m) : base(m) => m.SetCurrentSpeedToActive();

View file

@ -1,12 +1,11 @@
namespace Server.Spells namespace Server.Spells;
{
public class NinjaMove : SpecialMove
{
public override SkillName MoveSkill => SkillName.Ninjitsu;
public override void CheckGain(Mobile m) public class NinjaMove : SpecialMove
{ {
m.CheckSkill(MoveSkill, RequiredSkill - 12.5, RequiredSkill + 37.5); // Per five on friday 02/16/07 public override SkillName MoveSkill => SkillName.Ninjitsu;
}
public override void CheckGain(Mobile m)
{
m.CheckSkill(MoveSkill, RequiredSkill - 12.5, RequiredSkill + 37.5); // Per five on friday 02/16/07
} }
} }

View file

@ -1,98 +1,97 @@
using Server.Mobiles; using Server.Mobiles;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public abstract class NinjaSpell : Spell
{ {
public abstract class NinjaSpell : Spell public NinjaSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{ {
public NinjaSpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{
}
public abstract double RequiredSkill { get; }
public abstract int RequiredMana { get; }
public override SkillName CastSkill => SkillName.Ninjitsu;
public override SkillName DamageSkill => SkillName.Ninjitsu;
public override bool RevealOnCast => false;
public override bool ClearHandsOnCast => false;
public override bool ShowHandMovement => false;
public override bool BlocksMovement => false;
// public override int CastDelayBase => 1;
public override int CastRecoveryBase => 7;
public static bool CheckExpansion(Mobile from) =>
(from as PlayerMobile)?.NetState?.SupportsExpansion(Expansion.SE) == true;
public override bool CheckCast()
{
var mana = ScaleMana(RequiredMana);
if (!base.CheckCast())
{
return false;
}
if (!CheckExpansion(Caster))
{
Caster.SendLocalizedMessage(1063456); // You must upgrade to Samurai Empire in order to use that ability.
return false;
}
if (Caster.Skills[CastSkill].Value < RequiredSkill)
{
// You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability.
Caster.SendLocalizedMessage(1063013, $"{RequiredSkill:F1}\t{CastSkill}\t ");
return false;
}
if (Caster.Mana < mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
Caster.SendLocalizedMessage(1060174, mana.ToString());
return false;
}
return true;
}
public override bool CheckFizzle()
{
var mana = ScaleMana(RequiredMana);
if (Caster.Skills[CastSkill].Value < RequiredSkill)
{
// You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack!
Caster.SendLocalizedMessage(1063352, RequiredSkill.ToString("F1"));
return false;
}
if (Caster.Mana < mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
Caster.SendLocalizedMessage(1060174, mana.ToString());
return false;
}
if (!base.CheckFizzle())
{
return false;
}
Caster.Mana -= mana;
return true;
}
public override void GetCastSkills(out double min, out double max)
{
min = RequiredSkill - 12.5; // Per 5 on friday 2/16/07
max = RequiredSkill + 37.5;
}
public override int GetMana() => 0;
} }
}
public abstract double RequiredSkill { get; }
public abstract int RequiredMana { get; }
public override SkillName CastSkill => SkillName.Ninjitsu;
public override SkillName DamageSkill => SkillName.Ninjitsu;
public override bool RevealOnCast => false;
public override bool ClearHandsOnCast => false;
public override bool ShowHandMovement => false;
public override bool BlocksMovement => false;
// public override int CastDelayBase => 1;
public override int CastRecoveryBase => 7;
public static bool CheckExpansion(Mobile from) =>
(from as PlayerMobile)?.NetState?.SupportsExpansion(Expansion.SE) == true;
public override bool CheckCast()
{
var mana = ScaleMana(RequiredMana);
if (!base.CheckCast())
{
return false;
}
if (!CheckExpansion(Caster))
{
Caster.SendLocalizedMessage(1063456); // You must upgrade to Samurai Empire in order to use that ability.
return false;
}
if (Caster.Skills[CastSkill].Value < RequiredSkill)
{
// You need at least ~1_SKILL_REQUIREMENT~ ~2_SKILL_NAME~ skill to use that ability.
Caster.SendLocalizedMessage(1063013, $"{RequiredSkill:F1}\t{CastSkill}\t ");
return false;
}
if (Caster.Mana < mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
Caster.SendLocalizedMessage(1060174, mana.ToString());
return false;
}
return true;
}
public override bool CheckFizzle()
{
var mana = ScaleMana(RequiredMana);
if (Caster.Skills[CastSkill].Value < RequiredSkill)
{
// You need ~1_SKILL_REQUIREMENT~ Ninjitsu skill to perform that attack!
Caster.SendLocalizedMessage(1063352, RequiredSkill.ToString("F1"));
return false;
}
if (Caster.Mana < mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
Caster.SendLocalizedMessage(1060174, mana.ToString());
return false;
}
if (!base.CheckFizzle())
{
return false;
}
Caster.Mana -= mana;
return true;
}
public override void GetCastSkills(out double min, out double max)
{
min = RequiredSkill - 12.5; // Per 5 on friday 2/16/07
max = RequiredSkill + 37.5;
}
public override int GetMana() => 0;
}

View file

@ -7,113 +7,112 @@ using Server.Regions;
using Server.SkillHandlers; using Server.SkillHandlers;
using Server.Targeting; using Server.Targeting;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class Shadowjump : NinjaSpell, ISpellTargetingPoint3D
{ {
public class Shadowjump : NinjaSpell, ISpellTargetingPoint3D private static readonly SpellInfo _info = new(
"Shadowjump",
null,
-1,
9002
);
public Shadowjump(Mobile caster, Item scroll) : base(caster, scroll, _info)
{ {
private static readonly SpellInfo _info = new(
"Shadowjump",
null,
-1,
9002
);
public Shadowjump(Mobile caster, Item scroll) : base(caster, scroll, _info)
{
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0);
public override double RequiredSkill => 50.0;
public override int RequiredMana => 15;
public override bool BlockedByAnimalForm => false;
public void Target(IPoint3D p)
{
var orig = p;
var map = Caster.Map;
SpellHelper.GetSurfaceTop(ref p);
var from = Caster.Location;
var to = new Point3D(p);
if ((Caster as PlayerMobile)?.IsStealthing != true)
{
Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
}
else if (Sigil.ExistsOn(Caster))
{
Caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil.
}
else if (StaminaSystem.IsOverloaded(Caster))
{
Caster.SendLocalizedMessage(502359, "", 0x22); // Thou art too encumbered to move.
}
else if (!SpellHelper.CheckTravel(Caster, TravelCheckType.TeleportFrom, out var failureMessage))
{
failureMessage.SendMessageTo(Caster);
}
else if (!SpellHelper.CheckTravel(Caster, map, to, TravelCheckType.TeleportTo, out failureMessage))
{
failureMessage.SendMessageTo(Caster);
}
else if (map?.CanSpawnMobile(p.X, p.Y, p.Z) != true)
{
Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot.
}
else if (SpellHelper.CheckMulti(to, map, true, 5))
{
Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot.
}
else if (Region.Find(to, map).IsPartOf<HouseRegion>())
{
Caster.SendLocalizedMessage(502829); // Cannot teleport to that spot.
}
else if (CheckSequence())
{
SpellHelper.Turn(Caster, orig);
var m = Caster;
m.Location = to;
m.ProcessDelta();
Effects.SendLocationParticles(
EffectItem.Create(from, m.Map, EffectItem.DefaultDuration),
0x3728,
10,
10,
2023
);
m.PlaySound(0x512);
Stealth.OnUse(m); // stealth check after the shadow jump
}
FinishSequence();
}
public override bool CheckCast()
{
// IsStealthing should be moved to Server.Mobiles
if ((Caster as PlayerMobile)?.IsStealthing != true)
{
Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
return false;
}
return base.CheckCast();
}
public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false;
public override void OnCast()
{
Caster.SendLocalizedMessage(1063088); // You prepare to perform a Shadowjump.
Caster.Target = new SpellTargetPoint3D(this, TargetFlags.None, 11);
}
} }
}
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(1.0);
public override double RequiredSkill => 50.0;
public override int RequiredMana => 15;
public override bool BlockedByAnimalForm => false;
public void Target(IPoint3D p)
{
var orig = p;
var map = Caster.Map;
SpellHelper.GetSurfaceTop(ref p);
var from = Caster.Location;
var to = new Point3D(p);
if ((Caster as PlayerMobile)?.IsStealthing != true)
{
Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
}
else if (Sigil.ExistsOn(Caster))
{
Caster.SendLocalizedMessage(1061632); // You can't do that while carrying the sigil.
}
else if (StaminaSystem.IsOverloaded(Caster))
{
Caster.SendLocalizedMessage(502359, "", 0x22); // Thou art too encumbered to move.
}
else if (!SpellHelper.CheckTravel(Caster, TravelCheckType.TeleportFrom, out var failureMessage))
{
failureMessage.SendMessageTo(Caster);
}
else if (!SpellHelper.CheckTravel(Caster, map, to, TravelCheckType.TeleportTo, out failureMessage))
{
failureMessage.SendMessageTo(Caster);
}
else if (map?.CanSpawnMobile(p.X, p.Y, p.Z) != true)
{
Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot.
}
else if (SpellHelper.CheckMulti(to, map, true, 5))
{
Caster.SendLocalizedMessage(502831); // Cannot teleport to that spot.
}
else if (Region.Find(to, map).IsPartOf<HouseRegion>())
{
Caster.SendLocalizedMessage(502829); // Cannot teleport to that spot.
}
else if (CheckSequence())
{
SpellHelper.Turn(Caster, orig);
var m = Caster;
m.Location = to;
m.ProcessDelta();
Effects.SendLocationParticles(
EffectItem.Create(from, m.Map, EffectItem.DefaultDuration),
0x3728,
10,
10,
2023
);
m.PlaySound(0x512);
Stealth.OnUse(m); // stealth check after the shadow jump
}
FinishSequence();
}
public override bool CheckCast()
{
// IsStealthing should be moved to Server.Mobiles
if ((Caster as PlayerMobile)?.IsStealthing != true)
{
Caster.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
return false;
}
return base.CheckCast();
}
public override bool CheckDisturb(DisturbType type, bool firstCircle, bool resistable) => false;
public override void OnCast()
{
Caster.SendLocalizedMessage(1063088); // You prepare to perform a Shadowjump.
Caster.Target = new SpellTargetPoint3D(this, TargetFlags.None, 11);
}
}

View file

@ -2,120 +2,119 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.SkillHandlers; using Server.SkillHandlers;
namespace Server.Spells.Ninjitsu namespace Server.Spells.Ninjitsu;
public class SurpriseAttack : NinjaMove
{ {
public class SurpriseAttack : NinjaMove private static readonly Dictionary<Mobile, SurpriseAttackTimer> _table = new();
public override int BaseMana => 20;
public override double RequiredSkill => Core.ML ? 60.0 : 30.0;
public override TextDefinition AbilityMessage { get; } = 1063128; // You prepare to surprise your prey.
public override bool ValidatesDuringHit => false;
public override bool Validate(Mobile from)
{ {
private static readonly Dictionary<Mobile, SurpriseAttackTimer> _table = new(); if (!from.Hidden || from.AllowedStealthSteps <= 0)
public override int BaseMana => 20;
public override double RequiredSkill => Core.ML ? 60.0 : 30.0;
public override TextDefinition AbilityMessage { get; } = 1063128; // You prepare to surprise your prey.
public override bool ValidatesDuringHit => false;
public override bool Validate(Mobile from)
{ {
if (!from.Hidden || from.AllowedStealthSteps <= 0) from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
{ return false;
from.SendLocalizedMessage(1063087); // You must be in stealth mode to use this ability.
return false;
}
return base.Validate(from);
} }
public override bool OnBeforeSwing(Mobile attacker, Mobile defender) return base.Validate(from);
}
public override bool OnBeforeSwing(Mobile attacker, Mobile defender)
{
var valid = Validate(attacker) && CheckMana(attacker, true);
if (valid)
{ {
var valid = Validate(attacker) && CheckMana(attacker, true); attacker.BeginAction<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
if (valid)
{
attacker.BeginAction<Stealth>();
Timer.StartTimer(TimeSpan.FromSeconds(5.0), attacker.EndAction<Stealth>);
}
return valid;
} }
public override void OnHit(Mobile attacker, Mobile defender, int damage) return valid;
}
public override void OnHit(Mobile attacker, Mobile defender, int damage)
{
// Validates before swing
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063129); // You catch your opponent off guard with your Surprise Attack!
defender.SendLocalizedMessage(1063130); // Your defenses are lowered as your opponent surprises you!
defender.FixedParticles(0x37B9, 1, 5, 0x26DA, 0, 3, EffectLayer.Head);
attacker.RevealingAction();
StopTimer(defender);
var ninjitsu = attacker.Skills.Ninjitsu.Fixed;
var malus = ninjitsu / 60 + (int)Tracking.GetStalkingBonus(attacker, defender);
var timer = new SurpriseAttackTimer(defender, malus);
timer.Start();
_table[defender] = timer;
CheckGain(attacker);
}
public override void OnMiss(Mobile attacker, Mobile defender)
{
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise.
attacker.RevealingAction();
}
public static bool GetMalus(Mobile target, ref int malus)
{
if (!_table.TryGetValue(target, out var info))
{ {
// Validates before swing return false;
ClearCurrentMove(attacker);
attacker.SendLocalizedMessage(1063129); // You catch your opponent off guard with your Surprise Attack!
defender.SendLocalizedMessage(1063130); // Your defenses are lowered as your opponent surprises you!
defender.FixedParticles(0x37B9, 1, 5, 0x26DA, 0, 3, EffectLayer.Head);
attacker.RevealingAction();
StopTimer(defender);
var ninjitsu = attacker.Skills.Ninjitsu.Fixed;
var malus = ninjitsu / 60 + (int)Tracking.GetStalkingBonus(attacker, defender);
var timer = new SurpriseAttackTimer(defender, malus);
timer.Start();
_table[defender] = timer;
CheckGain(attacker);
} }
public override void OnMiss(Mobile attacker, Mobile defender) malus = info.Malus;
return true;
}
private static void StopTimer(Mobile m)
{
if (_table.Remove(m, out var timer))
{ {
ClearCurrentMove(attacker); timer.Stop();
attacker.SendLocalizedMessage(1063161); // You failed to properly use the element of surprise.
attacker.RevealingAction();
}
public static bool GetMalus(Mobile target, ref int malus)
{
if (!_table.TryGetValue(target, out var info))
{
return false;
}
malus = info.Malus;
return true;
}
private static void StopTimer(Mobile m)
{
if (_table.Remove(m, out var timer))
{
timer.Stop();
}
}
private static void EndSurprise(SurpriseAttackTimer info)
{
StopTimer(info.Target);
info.Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal.
}
private class SurpriseAttackTimer : Timer
{
public int Malus;
public Mobile Target;
public SurpriseAttackTimer(Mobile target, int effect) : base(TimeSpan.FromSeconds(8.0))
{
Target = target;
Malus = effect;
}
protected override void OnTick()
{
StopTimer(Target);
Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal.
}
} }
} }
}
private static void EndSurprise(SurpriseAttackTimer info)
{
StopTimer(info.Target);
info.Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal.
}
private class SurpriseAttackTimer : Timer
{
public int Malus;
public Mobile Target;
public SurpriseAttackTimer(Mobile target, int effect) : base(TimeSpan.FromSeconds(8.0))
{
Target = target;
Malus = effect;
}
protected override void OnTick()
{
StopTimer(Target);
Target.SendLocalizedMessage(1063131); // Your defenses have returned to normal.
}
}
}

View file

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

View file

@ -9,8 +9,8 @@ public class UnsummonTimer : Timer
{ {
// Track timers since some of them are really long and might hold references to long dead/deleted mobs // Track timers since some of them are really long and might hold references to long dead/deleted mobs
private static readonly Dictionary<BaseCreature, UnsummonTimer> _timers = new(); private static readonly Dictionary<BaseCreature, UnsummonTimer> _timers = new();
private BaseCreature _creature; private readonly BaseCreature _creature;
private Action _onUnsummon; private readonly Action _onUnsummon;
public static void StopTimer(BaseCreature creature) public static void StopTimer(BaseCreature creature)
{ {