fix: Fixes skill requirements and cleans up magery spells (#870)

* Preps damage stacking for mysticism, necromancy fixes, and spellweaving
* Removes extra LINQ allocations
* Fixes skill requirements for magery spells.
This commit is contained in:
Kamron Batman 2021-12-05 09:40:57 -08:00 • committed by GitHub
parent 1cf7b6c056
commit 4f9714818d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 311 additions and 382 deletions

View file

@ -5,9 +5,10 @@ namespace Server.Spells
{
public abstract class MagerySpell : Spell
{
private const double ChanceOffset = 20.0, ChanceLength = 100.0 / 7.0;
private static readonly int[] m_ManaTable = { 4, 6, 9, 11, 14, 20, 40, 50 };
private static readonly int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 };
private static readonly double[] _requiredSkill = Core.ML ?
new[] { 0.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0 } :
new[] { 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0 };
public MagerySpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info)
{
@ -29,13 +30,20 @@ namespace Server.Spells
circle -= 2;
}
var avg = ChanceLength * circle;
// Original RunUO algorithm for required skill
// const double chanceOffset = 20.0
// const double chanceLength = 100.0 / 7.0
// var avg = chanceLength * circle;
// min = avg - chanceOffset;
// max = avg + chanceOffset;
min = avg - ChanceOffset;
max = avg + ChanceOffset;
// Correct algorithm according to OSI.
// TODO: Verify this algorithm on OSI for latest expansion.
min = _requiredSkill[circle];
max = min + 40;
}
public override int GetMana() => Scroll is BaseWand ? 0 : m_ManaTable[(int)Circle];
public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle];
public override double GetResistSkill(Mobile m)
{
@ -79,12 +87,13 @@ namespace Server.Spells
public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle)
{
var firstPercent = target.Skills.MagicResist.Value / 5.0;
var secondPercent = target.Skills.MagicResist.Value -
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);
return (firstPercent > secondPercent ? firstPercent : secondPercent) /
2.0; // Seems should be about half of what stratics says.
// 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

@ -10,8 +10,7 @@ namespace Server.Spells
{
public abstract class SpecialMove
{
private static readonly Dictionary<Mobile, SpecialMoveContext> m_PlayersTable =
new();
private static readonly Dictionary<Mobile, SpecialMoveContext> _playersTable = new();
public virtual int BaseMana => 0;
@ -159,40 +158,18 @@ namespace Server.Spells
return false;
}
string option = null;
if (this is Backstab)
string option = this switch
{
option = "Backstab";
}
else if (this is DeathStrike)
{
option = "Death Strike";
}
else if (this is FocusAttack)
{
option = "Focus Attack";
}
else if (this is KiAttack)
{
option = "Ki Attack";
}
else if (this is SurpriseAttack)
{
option = "Surprise Attack";
}
else if (this is HonorableExecution)
{
option = "Honorable Execution";
}
else if (this is LightningStrike)
{
option = "Lightning Strike";
}
else if (this is MomentumStrike)
{
option = "Momentum Strike";
}
Backstab => "Backstab",
DeathStrike => "Death Strike",
FocusAttack => "Focus Attack",
KiAttack => "Ki Attack",
SurpriseAttack => "Surprise Attack",
HonorableExecution => "Honorable Execution",
LightningStrike => "Lightning Strike",
MomentumStrike => "Momentum Strike",
_ => null
};
if (option != null && !DuelContext.AllowSpecialMove(from, option, this))
{
@ -303,7 +280,7 @@ namespace Server.Spells
private static void AddContext(Mobile m, SpecialMoveContext context)
{
m_PlayersTable[m] = context;
_playersTable[m] = context;
}
private static void RemoveContext(Mobile m)
@ -312,23 +289,20 @@ namespace Server.Spells
if (context != null)
{
m_PlayersTable.Remove(m);
_playersTable.Remove(m);
context.Timer.Stop();
}
}
private static SpecialMoveContext GetContext(Mobile m) =>
m_PlayersTable.TryGetValue(m, out var context) ? context : null;
_playersTable.TryGetValue(m, out var context) ? context : null;
private class SpecialMoveTimer : Timer
{
private readonly Mobile m_Mobile;
public SpecialMoveTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0))
{
m_Mobile = from;
}
public SpecialMoveTimer(Mobile from) : base(TimeSpan.FromSeconds(3.0)) => m_Mobile = from;
protected override void OnTick()
{

View file

@ -17,19 +17,16 @@ namespace Server.Spells
public abstract class Spell : ISpell
{
private static readonly TimeSpan NextSpellDelay = TimeSpan.FromSeconds(0.75);
private static readonly TimeSpan AnimateDelay = TimeSpan.FromSeconds(1.5);
// In reality, it's ANY delayed Damage spell Post-AoS that can't stack, but, only
// Expo & Magic Arrow have enough delay and a short enough cast time to bring up
// the possibility of stacking 'em. Note that a MA & an Explosion will stack, but
// of course, two MA's won't.
private static readonly Dictionary<Type, DelayedDamageContextWrapper> m_ContextTable =
new();
private static readonly Dictionary<Type, DelayedDamageContextWrapper> _contextTable = new();
private AnimTimer m_AnimTimer;
private CastTimer m_CastTimer;
private AnimTimer _animTimer;
private CastTimer _castTimer;
public Spell(Mobile caster, Item scroll, SpellInfo info)
{
@ -60,13 +57,17 @@ namespace Server.Spells
public virtual bool DelayedDamage => false;
public virtual bool DelayedDamageStacking => true;
public static readonly Type[] AOSNoDelayedDamageStackingSelf = Core.AOS ? Array.Empty<Type>() : null;
// Null means stacking is allowed while empty indicates no stacking with self
// More than zero means no stacking with self and other spells
public virtual Type[] DelayedDamageSpellFamilyStacking => null;
public virtual bool BlockedByHorrificBeast => true;
public virtual bool BlockedByAnimalForm => true;
public virtual bool BlocksMovement => true;
public virtual bool CheckNextSpellTime => !(Scroll is BaseWand);
public virtual bool CheckNextSpellTime => Scroll is not BaseWand;
public virtual int CastRecoveryBase => 6;
public virtual int CastRecoveryFastScalar => 1;
@ -149,22 +150,39 @@ namespace Server.Spells
public void StartDelayedDamageContext(Mobile m, Timer t)
{
if (DelayedDamageStacking)
var damageStacking = DelayedDamageSpellFamilyStacking;
if (damageStacking == null)
{
return; // Sanity
}
if (!m_ContextTable.TryGetValue(GetType(), out var contexts))
var type = GetType();
if (!_contextTable.TryGetValue(type, out var context))
{
m_ContextTable[GetType()] = contexts = new DelayedDamageContextWrapper();
_contextTable[type] = context = new DelayedDamageContextWrapper();
for (int i = 0; i < damageStacking.Length; i++)
{
_contextTable.Add(damageStacking[i], context);
}
}
contexts.Add(m, t);
context.Add(m, t);
}
public bool HasDelayedDamageContext(Mobile m) =>
DelayedDamageSpellFamilyStacking != null &&
_contextTable.TryGetValue(GetType(), out var context) && context.Contains(m);
public void RemoveDelayedDamageContext(Mobile m)
{
if (m_ContextTable.TryGetValue(GetType(), out var contexts))
if (m == null || DelayedDamageSpellFamilyStacking == null)
{
return; // Sanity
}
if (_contextTable.TryGetValue(GetType(), out var contexts))
{
contexts.Remove(m);
}
@ -389,8 +407,8 @@ namespace Server.Spells
{
OnDisturb(type, true);
m_CastTimer?.Stop();
m_AnimTimer?.Stop();
_castTimer?.Stop();
_animTimer?.Stop();
if (Core.AOS && Caster.Player && type == DisturbType.Hurt)
{
@ -469,7 +487,7 @@ namespace Server.Spells
{
Caster.SendLocalizedMessage(1061091); // You cannot cast that spell in this form.
}
else if (!(Scroll is BaseWand) && (Caster.Paralyzed || Caster.Frozen))
else if (Scroll is not BaseWand && (Caster.Paralyzed || Caster.Frozen))
{
Caster.SendLocalizedMessage(502643); // You can not cast a spell while frozen.
}
@ -492,7 +510,7 @@ namespace Server.Spells
State = SpellState.Casting;
Caster.Spell = this;
if (!(Scroll is BaseWand) && RevealOnCast)
if (Scroll is not BaseWand && RevealOnCast)
{
Caster.RevealingAction();
}
@ -507,8 +525,8 @@ namespace Server.Spells
if (count != 0)
{
m_AnimTimer = new AnimTimer(this, count);
m_AnimTimer.Start();
_animTimer = new AnimTimer(this, count);
_animTimer.Start();
}
if (Info.LeftHandEffect > 0)
@ -532,18 +550,18 @@ namespace Server.Spells
WeaponAbility.ClearCurrentAbility(Caster);
}
m_CastTimer = new CastTimer(this, castDelay);
_castTimer = new CastTimer(this, castDelay);
// m_CastTimer.Start();
OnBeginCast();
if (castDelay > TimeSpan.Zero)
{
m_CastTimer.Start();
_castTimer.Start();
}
else
{
m_CastTimer.Tick();
_castTimer.Tick();
}
return true;
@ -647,11 +665,6 @@ namespace Server.Spells
return TimeSpan.FromSeconds((double)delay / CastRecoveryPerSecond);
}
// public virtual int CastDelayBase{ get{ return 3; } }
// public virtual int CastDelayFastScalar{ get{ return 1; } }
// public virtual int CastDelayPerSecond{ get{ return 4; } }
// public virtual int CastDelayMinimum{ get{ return 1; } }
public virtual TimeSpan GetCastDelay()
{
if (Scroll is BaseWand)
@ -665,7 +678,7 @@ namespace Server.Spells
// Paladins with magery of 70.0 or above are subject to a faster casting cap of 2
var fcMax = 4;
if (CastSkill == SkillName.Magery || CastSkill == SkillName.Necromancy ||
if (CastSkill is SkillName.Magery or SkillName.Necromancy ||
CastSkill == SkillName.Chivalry && Caster.Skills.Magery.Value >= 70.0)
{
fcMax = 2;
@ -749,12 +762,9 @@ namespace Server.Spells
Scroll.Movable = m;
}
else
else if (ClearHandsOnCast)
{
if (ClearHandsOnCast)
{
Caster.ClearHands();
}
Caster.ClearHands();
}
var karma = ComputeKarmaAward();
@ -838,10 +848,14 @@ namespace Server.Spells
m_Contexts.Add(m, t);
}
public bool Contains(Mobile m) => m_Contexts.ContainsKey(m);
public void Remove(Mobile m)
{
m_Contexts.Remove(m);
// TODO: Should we stop the timer?
if (m_Contexts.Remove(m, out var t))
{
t.Stop();
}
}
}
@ -876,7 +890,7 @@ namespace Server.Spells
if (!Running)
{
m_Spell.m_AnimTimer = null;
m_Spell._animTimer = null;
}
}
}
@ -900,7 +914,7 @@ namespace Server.Spells
if (m_Spell.State == SpellState.Casting && m_Spell.Caster.Spell == m_Spell)
{
m_Spell.State = SpellState.Sequencing;
m_Spell.m_CastTimer = null;
m_Spell._castTimer = null;
m_Spell.Caster.OnSpellCast(m_Spell);
m_Spell.Caster.Region?.OnSpellCast(m_Spell.Caster, m_Spell);
m_Spell.Caster.NextSpellTime =
@ -915,7 +929,7 @@ namespace Server.Spells
m_Spell.Caster.Target?.BeginTimeout(m_Spell.Caster, 30000); // 30 seconds
}
m_Spell.m_CastTimer = null;
m_Spell._castTimer = null;
}
}

View file

@ -184,7 +184,7 @@ namespace Server.Spells
public static void Turn(Mobile from, object to)
{
if (!(to is IPoint3D target))
if (to is not IPoint3D target)
{
return;
}
@ -333,7 +333,7 @@ namespace Server.Spells
public static bool AddStatCurse(Mobile caster, Mobile target, StatType type, int curse, TimeSpan duration)
{
var offset = -curse;
var name = $"[Magic] {type} Offset";
var name = $"[Magic] {type} Curse";
var mod = target.GetStatMod(name);
@ -447,26 +447,8 @@ namespace Server.Spells
var bcFrom = from as BaseCreature;
var bcTarg = to as BaseCreature;
PlayerMobile pmFrom;
PlayerMobile pmTarg;
if (bcFrom?.Summoned == true)
{
pmFrom = bcFrom.SummonMaster as PlayerMobile;
}
else
{
pmFrom = from as PlayerMobile;
}
if (bcTarg?.Summoned == true)
{
pmTarg = bcTarg.SummonMaster as PlayerMobile;
}
else
{
pmTarg = to as PlayerMobile;
}
var pmFrom = (bcFrom?.Summoned == true ? bcFrom.SummonMaster : from) as PlayerMobile;
var pmTarg = (bcTarg?.Summoned == true ? bcTarg.SummonMaster : to) as PlayerMobile;
if (pmFrom?.DuelContext != null && pmFrom.DuelContext == pmTarg?.DuelContext && pmFrom.DuelContext.Started &&
pmFrom.DuelPlayer != null && pmTarg?.DuelPlayer != null)
@ -864,17 +846,6 @@ namespace Server.Spells
return x < 0 || y < 0 || x >= map.Width || y >= map.Height;
}
// towns
public static bool IsTown(IPoint3D ip, Mobile caster)
{
if (ip is Item item)
{
ip = item.GetWorldLocation();
}
return IsTown(new Point3D(ip), caster);
}
public static bool IsTown(Point3D loc, Mobile caster)
{
var map = caster.Map;
@ -897,15 +868,8 @@ namespace Server.Spells
return reg?.IsDisabled() == false;
}
public static bool CheckTown(IPoint3D ip, Mobile caster)
{
if (ip is Item item)
{
ip = item.GetWorldLocation();
}
return CheckTown(new Point3D(ip), caster);
}
public static bool CheckTown(IPoint3D ip, Mobile caster) =>
CheckTown((ip as Item)?.GetWorldLocation() ?? new Point3D(ip), caster);
public static bool CheckTown(Point3D loc, Mobile caster)
{
@ -919,13 +883,13 @@ namespace Server.Spells
}
// magic reflection
public static void CheckReflect(int circle, Mobile caster, ref Mobile target)
{
public static bool CheckReflect(int circle, Mobile caster, ref Mobile target) =>
CheckReflect(circle, ref caster, ref target);
}
public static void CheckReflect(int circle, ref Mobile caster, ref Mobile target)
public static bool CheckReflect(int circle, ref Mobile caster, ref Mobile target)
{
var reflect = false;
if (target.MagicDamageAbsorb > 0)
{
++circle;
@ -933,41 +897,26 @@ namespace Server.Spells
target.MagicDamageAbsorb -= circle;
// This order isn't very intuitive, but you have to nullify reflect before target gets switched
var reflect = target.MagicDamageAbsorb >= 0;
(target as BaseCreature)?.CheckReflect(caster, ref reflect);
reflect = target.MagicDamageAbsorb >= 0;
if (target.MagicDamageAbsorb <= 0)
{
target.MagicDamageAbsorb = 0;
DefensiveSpell.Nullify(target);
}
if (reflect)
{
target.FixedEffect(0x37B9, 10, 5);
var temp = caster;
caster = target;
target = temp;
}
}
else if (target is BaseCreature creature)
if (target is BaseCreature creature)
{
var reflect = false;
creature.CheckReflect(caster, ref reflect);
if (reflect)
{
creature.FixedEffect(0x37B9, 10, 5);
var temp = caster;
caster = creature;
target = temp;
}
}
if (reflect)
{
target.FixedEffect(0x37B9, 10, 5);
(caster, target) = (target, caster);
}
return reflect;
}
public static void Damage(Spell spell, Mobile target, double damage)
@ -995,20 +944,21 @@ namespace Server.Spells
{
(from as BaseCreature)?.AlterSpellDamageTo(target, ref iDamage);
(target as BaseCreature)?.AlterSpellDamageFrom(from, ref iDamage);
var bcTarget = target as BaseCreature;
bcTarget?.AlterSpellDamageFrom(from, ref iDamage);
target.Damage(iDamage, from);
if (from != null)
{
bcTarget?.OnHarmfulSpell(from);
bcTarget?.OnDamagedBySpell(from);
}
}
else
{
new SpellDamageTimer(spell, target, from, iDamage, delay).Start();
}
if (target is BaseCreature c && from != null && delay == TimeSpan.Zero)
{
c.OnHarmfulSpell(from);
c.OnDamagedBySpell(from);
}
}
public static void Damage(
@ -1086,6 +1036,11 @@ namespace Server.Spells
public static void DoLeech(int damageGiven, Mobile from, Mobile target)
{
if (target == null)
{
return;
}
var context = TransformationSpellHelper.GetContext(from);
if (context == null) /* cleanup */
@ -1134,7 +1089,7 @@ namespace Server.Spells
m_Damage = damage;
m_Spell = s;
if (m_Spell?.DelayedDamage == true && !m_Spell.DelayedDamageStacking)
if (m_Spell?.DelayedDamage == true)
{
m_Spell.StartDelayedDamageContext(target, this);
}
@ -1143,7 +1098,6 @@ namespace Server.Spells
protected override void OnTick()
{
(m_From as BaseCreature)?.AlterSpellDamageTo(m_Target, ref m_Damage);
(m_Target as BaseCreature)?.AlterSpellDamageFrom(m_From, ref m_Damage);
m_Target.Damage(m_Damage);
@ -1181,7 +1135,8 @@ namespace Server.Spells
m_Chaos = chaos;
m_DFA = dfa;
m_Spell = s;
if (m_Spell?.DelayedDamage == true && !m_Spell.DelayedDamageStacking)
if (m_Spell?.DelayedDamage == true)
{
m_Spell.StartDelayedDamageContext(target, this);
}
@ -1189,10 +1144,9 @@ namespace Server.Spells
protected override void OnTick()
{
var bcFrom = m_From as BaseCreature;
var bcTarg = m_Target as BaseCreature;
if (bcFrom != null && m_Target != null)
if (m_From is BaseCreature bcFrom && m_Target != null)
{
bcFrom.AlterSpellDamageTo(m_Target, ref m_Damage);
}
@ -1253,7 +1207,7 @@ namespace Server.Spells
public static bool OnCast(Mobile caster, Spell spell)
{
if (!(spell is ITransformationSpell transformSpell))
if (spell is not ITransformationSpell transformSpell)
{
return false;
}