ModernUO/Projects/UOContent/Spells/Fifth/MagicReflect.cs
Jack 7be8b5f7f6
feat: T2A defensive spells (#2378)
## Summary

Implements T2A-accurate mechanics for the three defensive spells based on UO98 demo scripts. Pre-UOR (`!Core.UOR`) triggers the new behavior; UOR and AOS paths are unchanged.

- **Reactive Armor**: percentage-based melee damage reflection (10-35% based on target Magery), targeted, timed (25-75s). Guards exempt, ranged attacks beyond 1 tile unaffected. Reflected damage is sourceless.
- **Protection**: temporary AC bonus (casterMagery/10, 1-10 AR) via VirtualArmorMod, targeted, timed (12-120s). Shared table with Arch Protection — only one protection buff per mobile.
- **Magic Reflect**: single-use spell reflection using MagicDamageAbsorb as a flag. No timer, no DefensiveSpell lock. Consumed on first reflected spell.
- **Arch Protection**: T2A path applies Protection via shared table with area sound (0x1F7 vs 0x1ED).
- No DefensiveSpell mutual exclusion for T2A — all three spells operate independently.

## Test plan

- [x] Set expansion to T2A
- [x] Cast Reactive Armor on self → verify particles (0x376A) and sound (0x1F2)
- [x] Get hit in melee → verify attacker takes reflected damage with spark effect and sound
- [x] Get hit by ranged weapon from > 1 tile → verify no reflection
- [x] Wait for RA to expire → verify effect ends silently
- [x] Cast RA on target that already has it → verify "This spell is already in effect"
- [x] Cast Protection on another player → verify particles (0x375A) and sound (0x1ED)
- [x] Check target's AR → verify it increased by casterMagery/10
- [x] Wait for Protection to expire → verify AR returns to normal
- [x] Cast Protection on self, then cast Protection on another player → verify both succeed
- [x] Cast Arch Protection on ground near allies → verify each gets AR bonus with sound 0x1F7
- [x] Cast Arch Protection near a target already protected → verify that target is skipped
- [x] Cast Magic Reflect on self → verify particles (0x375A) and sound (0x1E9)
- [x] Have an enemy cast a harmful spell at you → verify spell reflects back, effect consumed
- [x] Cast Magic Reflect again → verify it can be recast after consumption
- [x] Cast Magic Reflect when already active → verify "This spell is already in effect"
- [x] Verify all three spells can be active simultaneously on the same mobile
- [x] Switch expansion to UOR → verify existing UOR behavior unchanged
- [x] Switch expansion to AOS → verify existing AOS behavior unchanged
2026-03-22 11:18:34 -07:00

175 lines
6.3 KiB
C#

using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Engines.BuffIcons;
using Server.Mobiles;
namespace Server.Spells.Fifth
{
public class MagicReflectSpell : MagerySpell
{
private static readonly SpellInfo _info = new(
"Magic Reflection",
"In Jux Sanct",
242,
9012,
Reagent.Garlic,
Reagent.MandrakeRoot,
Reagent.SpidersSilk
);
private static readonly Dictionary<Mobile, ResistanceMod[]> _table = new();
public MagicReflectSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
{
}
public override SpellCircle Circle => SpellCircle.Fifth;
public override bool CheckCast()
{
if (Core.AOS)
{
return true;
}
if (Caster.MagicDamageAbsorb > 0)
{
Caster.SendLocalizedMessage(1005559); // This spell is already in effect.
return false;
}
if (!Core.UOR)
{
return true;
}
if (!Caster.CanBeginAction<DefensiveSpell>())
{
Caster.SendLocalizedMessage(1005385); // The spell will not adhere to you at this time.
return false;
}
return true;
}
public override void OnCast()
{
if (Core.AOS)
{
/* The magic reflection spell decreases the caster's physical resistance, while increasing the caster's elemental resistances.
* Physical decrease = 25 - (Inscription/20).
* Elemental resistance = +10 (-20 physical, +10 elemental at GM Inscription)
* The magic reflection spell has an indefinite duration, becoming active when cast, and deactivated when re-cast.
* Reactive Armor, Protection, and Magic Reflection will stay on even after logging out,
* even after dying, until you turn them off by casting them again.
*/
if (CheckSequence())
{
if (_table.Remove(Caster, out var mods))
{
Caster.PlaySound(0x1ED);
Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
for (var i = 0; i < mods.Length; ++i)
{
Caster.RemoveResistanceMod(mods[i]);
}
(Caster as PlayerMobile)?.RemoveBuff(BuffIcon.MagicReflection);
}
else
{
Caster.PlaySound(0x1E9);
Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
var physiMod = -25 + (int)(Caster.Skills.Inscribe.Value / 20);
const int otherMod = 10;
mods =
[
new ResistanceMod(ResistanceType.Physical, "PhysicalResistMagicResist", physiMod),
new ResistanceMod(ResistanceType.Fire, "FireResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Cold, "ColdResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Poison, "PoisonResistMagicResist", otherMod),
new ResistanceMod(ResistanceType.Energy, "EnergyResistMagicResist", otherMod)
];
_table[Caster] = mods;
for (var i = 0; i < mods.Length; ++i)
{
Caster.AddResistanceMod(mods[i]);
}
var buffFormat = $"{physiMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}\t+{otherMod}";
(Caster as PlayerMobile)?.AddBuff(
new BuffInfo(BuffIcon.MagicReflection, 1075817, args: buffFormat, retainThroughDeath: true)
);
}
}
}
else if (!Core.UOR)
{
if (Caster.MagicDamageAbsorb > 0)
{
Caster.SendLocalizedMessage(1005559); // This spell is already in effect.
}
else if (CheckSequence())
{
Caster.MagicDamageAbsorb = 1;
Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
Caster.PlaySound(0x1E9);
}
}
else
{
if (Caster.MagicDamageAbsorb > 0)
{
Caster.SendLocalizedMessage(1005559); // This spell is already in effect.
}
else if (!Caster.CanBeginAction<DefensiveSpell>())
{
Caster.SendLocalizedMessage(1005385); // The spell will not adhere to you at this time.
}
else if (CheckSequence())
{
if (Caster.BeginAction<DefensiveSpell>())
{
var value = (int)(Caster.Skills.Magery.Value + Caster.Skills.Inscribe.Value);
value = (int)(8 + value / 200.0 * 7.0); // absorb from 8 to 15 "circles"
Caster.MagicDamageAbsorb = value;
Caster.FixedParticles(0x375A, 10, 15, 5037, EffectLayer.Waist);
Caster.PlaySound(0x1E9);
}
else
{
Caster.SendLocalizedMessage(1005385); // The spell will not adhere to you at this time.
}
}
}
FinishSequence();
}
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
public static void EndReflect(Mobile m)
{
if (!_table.Remove(m, out var mods))
{
return;
}
for (var i = 0; i < mods?.Length; ++i)
{
m.RemoveResistanceMod(mods[i]);
}
(m as PlayerMobile)?.RemoveBuff(BuffIcon.MagicReflection);
}
}
}