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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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