feat: add Dryad Allure spell
This commit is contained in:
parent
752bf3cc29
commit
010dbf3683
7 changed files with 508 additions and 9 deletions
|
|
@ -0,0 +1,273 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells;
|
||||
using Server.Spells.First;
|
||||
using Server.Spells.Spellweaving;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Spells.Spellweaving;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class DryadAllureSpellTests
|
||||
{
|
||||
private static Mobile NewCaster(double spellweaving = 120.0)
|
||||
{
|
||||
var caster = new Mobile(World.NewMobile);
|
||||
caster.DefaultMobileInit();
|
||||
caster.InitStats(100, 100, 100);
|
||||
caster.Mana = 100;
|
||||
caster.Skills.Spellweaving.Base = spellweaving;
|
||||
return caster;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellMetadata_MatchesDryadAllureSources()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var spell = new DryadAllureSpell(caster);
|
||||
|
||||
Assert.Equal("Dryad Allure", spell.Name);
|
||||
Assert.Equal("Rathril", spell.Mantra);
|
||||
Assert.Equal(TimeSpan.FromSeconds(3.0), spell.CastDelayBase);
|
||||
Assert.Equal(52.0, spell.RequiredSkill);
|
||||
Assert.Equal(40, spell.GetMana());
|
||||
Assert.Equal(SkillName.Spellweaving, spell.CastSkill);
|
||||
|
||||
caster.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellweavingBookAndScroll_UseDryadAllureSlot()
|
||||
{
|
||||
var book = new SpellweavingBook(1UL << (611 - 600));
|
||||
var scroll = new DryadAllureScroll();
|
||||
|
||||
Assert.Equal(600, book.BookOffset);
|
||||
Assert.Equal(16, book.BookCount);
|
||||
Assert.True(book.HasSpell(611));
|
||||
Assert.Equal(611, GetSpellScrollId(scroll));
|
||||
|
||||
book.Delete();
|
||||
scroll.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterSpellweaving_PreMl_DoesNotExposeDryadAllure()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.SE;
|
||||
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Null(SpellRegistry.NewSpell(611, caster, null));
|
||||
Assert.Equal(-1, SpellRegistry.GetRegistryNumber(typeof(DryadAllureSpell)));
|
||||
Assert.IsType<MagicArrowSpell>(SpellRegistry.NewSpell(4, caster, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterSpellweaving_Ml_ExposesDryadAllureAtSpellId611()
|
||||
{
|
||||
var previousExpansion = Core.Expansion;
|
||||
var caster = NewCaster();
|
||||
|
||||
try
|
||||
{
|
||||
ResetSpellRegistry();
|
||||
Core.Expansion = Expansion.ML;
|
||||
|
||||
Initializer.Configure();
|
||||
|
||||
Assert.Same(typeof(DryadAllureSpell), SpellRegistry.Types[611]);
|
||||
Assert.Equal(611, SpellRegistry.GetRegistryNumber(typeof(DryadAllureSpell)));
|
||||
Assert.IsType<DryadAllureSpell>(SpellRegistry.NewSpell(611, caster, null));
|
||||
Assert.IsType<EssenceOfWindSpell>(SpellRegistry.NewSpell(610, caster, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Core.Expansion = previousExpansion;
|
||||
ResetSpellRegistry();
|
||||
Initializer.Configure();
|
||||
caster.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidTarget_AllowsRepondHumanoidsAndRejectsUnsafeTargets()
|
||||
{
|
||||
var validOrc = new TestOrc();
|
||||
var controlledOrc = new TestOrc();
|
||||
var summonedOrc = new TestOrc { Summoned = true };
|
||||
var paragonOrc = new TestOrc { IsParagon = true };
|
||||
var immuneOrc = new AllureImmuneOrc();
|
||||
var nonRepond = new TestCreature();
|
||||
var master = NewCaster();
|
||||
|
||||
controlledOrc.SetControlMaster(master);
|
||||
|
||||
Assert.True(DryadAllureSpell.IsValidTarget(validOrc));
|
||||
Assert.False(DryadAllureSpell.IsValidTarget(controlledOrc));
|
||||
Assert.False(DryadAllureSpell.IsValidTarget(summonedOrc));
|
||||
Assert.False(DryadAllureSpell.IsValidTarget(paragonOrc));
|
||||
Assert.False(DryadAllureSpell.IsValidTarget(immuneOrc));
|
||||
Assert.False(DryadAllureSpell.IsValidTarget(nonRepond));
|
||||
|
||||
master.Delete();
|
||||
validOrc.Delete();
|
||||
controlledOrc.Delete();
|
||||
summonedOrc.Delete();
|
||||
paragonOrc.Delete();
|
||||
immuneOrc.Delete();
|
||||
nonRepond.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCharmChance_IncludesSpellweavingSkillAndFocusBonus()
|
||||
{
|
||||
var noFocusChance = DryadAllureSpell.GetCharmChance(75.0, 0);
|
||||
var focusChance = DryadAllureSpell.GetCharmChance(75.0, 3);
|
||||
|
||||
Assert.Equal(0.5, noFocusChance, 3);
|
||||
Assert.Equal(0.56, focusChance, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAllureAttempt_SuccessControlsTargetAsThreeSlotAlluredFollowerAndDeletesBackpackContents()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var target = new TestOrc();
|
||||
target.PackItem(new Gold(10));
|
||||
caster.Combatant = target;
|
||||
caster.Warmode = true;
|
||||
target.Combatant = caster;
|
||||
target.Warmode = true;
|
||||
|
||||
using var random = new PredictableRandom(0);
|
||||
|
||||
Assert.True(DryadAllureSpell.ApplyAllureAttempt(caster, target, focusLevel: 0));
|
||||
Assert.True(target.Controlled);
|
||||
Assert.Equal(caster, target.ControlMaster);
|
||||
Assert.True(target.Allured);
|
||||
Assert.Equal(3, target.ControlSlots);
|
||||
Assert.Equal(3, caster.Followers);
|
||||
Assert.Equal(BaseCreature.MaxLoyalty, target.Loyalty);
|
||||
Assert.Null(caster.Combatant);
|
||||
Assert.Null(target.Combatant);
|
||||
Assert.False(caster.Warmode);
|
||||
Assert.False(target.Warmode);
|
||||
Assert.Empty(target.Backpack?.Items ?? []);
|
||||
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAllureAttempt_FailureEnragesTargetWithoutFollowerLeak()
|
||||
{
|
||||
var caster = NewCaster(spellweaving: 52.0);
|
||||
var target = new TestOrc();
|
||||
|
||||
using var random = new PredictableRandom(20);
|
||||
|
||||
Assert.False(DryadAllureSpell.ApplyAllureAttempt(caster, target, focusLevel: 0));
|
||||
Assert.False(target.Controlled);
|
||||
Assert.False(target.Allured);
|
||||
Assert.Equal(caster, target.ControlTarget);
|
||||
Assert.Equal(OrderType.Attack, target.ControlOrder);
|
||||
Assert.Equal(caster, target.Combatant);
|
||||
Assert.True(target.Warmode);
|
||||
Assert.Equal(0, caster.Followers);
|
||||
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetControlMasterNull_ClearsAlluredStateOnRelease()
|
||||
{
|
||||
var caster = NewCaster();
|
||||
var target = new TestOrc();
|
||||
|
||||
target.ControlSlots = 3;
|
||||
target.SetControlMaster(caster);
|
||||
target.Allured = true;
|
||||
|
||||
target.SetControlMaster(null);
|
||||
|
||||
Assert.False(target.Controlled);
|
||||
Assert.Null(target.ControlMaster);
|
||||
Assert.False(target.Allured);
|
||||
Assert.Equal(0, caster.Followers);
|
||||
|
||||
caster.Delete();
|
||||
target.Delete();
|
||||
}
|
||||
|
||||
private static int GetSpellScrollId(SpellScroll scroll)
|
||||
{
|
||||
var field = typeof(SpellScroll).GetField("_spellID", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
return (int)field!.GetValue(scroll)!;
|
||||
}
|
||||
|
||||
private static void ResetSpellRegistry()
|
||||
{
|
||||
var types = (Type[])typeof(SpellRegistry)
|
||||
.GetField("m_Types", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.GetValue(null)!;
|
||||
Array.Clear(types);
|
||||
|
||||
var idsFromTypes = (Dictionary<Type, int>)typeof(SpellRegistry)
|
||||
.GetField("m_IDsFromTypes", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.GetValue(null)!;
|
||||
idsFromTypes.Clear();
|
||||
|
||||
typeof(SpellRegistry)
|
||||
.GetField("m_Count", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.SetValue(null, 0);
|
||||
|
||||
SpellRegistry.SpecialMoves.Clear();
|
||||
}
|
||||
|
||||
private class TestOrc : Orc
|
||||
{
|
||||
public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
|
||||
{
|
||||
activeSpeed = 0.2;
|
||||
passiveSpeed = 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
private class AllureImmuneOrc : TestOrc
|
||||
{
|
||||
public override bool AllureImmune => true;
|
||||
}
|
||||
|
||||
private class TestCreature : BaseCreature
|
||||
{
|
||||
public TestCreature() : base(AIType.AI_Animal, FightMode.Closest, 10, 1)
|
||||
{
|
||||
Body = 0xC8;
|
||||
}
|
||||
|
||||
public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
|
||||
{
|
||||
activeSpeed = 0.2;
|
||||
passiveSpeed = 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -497,6 +497,15 @@ public abstract partial class BaseAI
|
|||
{
|
||||
this.DebugSayFormatted($"Beginning transfer with {to.Name}");
|
||||
|
||||
if (Mobile.Allured)
|
||||
{
|
||||
SendTransferRefusalMessages(from, to, 1043250, 1043251);
|
||||
// 1043250: The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~
|
||||
// 1043251: The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~
|
||||
ResumePersistentOrder();
|
||||
return true;
|
||||
}
|
||||
|
||||
var youngFrom = from is PlayerMobile mobile && mobile.Young;
|
||||
var youngTo = to is PlayerMobile playerMobile && playerMobile.Young;
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ internal sealed partial class TransferItem : Item
|
|||
|
||||
Delete();
|
||||
|
||||
if (!accepted || !_creature.SetControlMaster(to))
|
||||
if (!accepted || _creature.Allured || !_creature.SetControlMaster(to))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ namespace Server.Mobiles
|
|||
};
|
||||
|
||||
private bool _summoned;
|
||||
private bool _allured;
|
||||
|
||||
private bool m_bTamable;
|
||||
private int m_ColdResistance;
|
||||
|
|
@ -494,6 +495,30 @@ namespace Server.Mobiles
|
|||
|
||||
public virtual bool DeathAdderCharmable => false;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool Allured
|
||||
{
|
||||
get => _allured;
|
||||
set
|
||||
{
|
||||
if (_allured == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_allured = value;
|
||||
|
||||
if (_allured)
|
||||
{
|
||||
Loyalty = MaxLoyalty;
|
||||
}
|
||||
|
||||
InvalidateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool AllureImmune => BardImmune || IsInvulnerable;
|
||||
|
||||
// TODO: Find the pub 31 tweaks to the DispelDifficulty and apply them of course.
|
||||
// at this skill level we dispel 50% chance
|
||||
public virtual double DispelDifficulty => 0.0;
|
||||
|
|
@ -1843,7 +1868,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
base.Serialize(writer);
|
||||
|
||||
writer.Write(20); // version
|
||||
writer.Write(21); // version
|
||||
|
||||
writer.Write((int)m_CurrentAI);
|
||||
writer.Write((int)m_DefaultAI);
|
||||
|
|
@ -1963,6 +1988,9 @@ namespace Server.Mobiles
|
|||
|
||||
// Version 19
|
||||
writer.Write(HomeMap);
|
||||
|
||||
// Version 21
|
||||
writer.Write(_allured);
|
||||
}
|
||||
|
||||
public override void Deserialize(IGenericReader reader)
|
||||
|
|
@ -2166,6 +2194,11 @@ namespace Server.Mobiles
|
|||
HomeMap = reader.ReadMap();
|
||||
}
|
||||
|
||||
if (version >= 21)
|
||||
{
|
||||
_allured = reader.ReadBool();
|
||||
}
|
||||
|
||||
if (version <= 14 && m_Paragon && Hue == 0x31)
|
||||
{
|
||||
Hue = Paragon.Hue; // Paragon hue fixed, should now be 0x501.
|
||||
|
|
@ -3471,6 +3504,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
if (m == null)
|
||||
{
|
||||
Allured = false;
|
||||
ControlMaster = null;
|
||||
Controlled = false;
|
||||
ControlTarget = null;
|
||||
|
|
@ -5621,6 +5655,12 @@ namespace Server.Mobiles
|
|||
{
|
||||
c.OwnerAbandonTime = DateTime.MinValue;
|
||||
|
||||
if (c.Allured)
|
||||
{
|
||||
c.Loyalty = BaseCreature.MaxLoyalty;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.Map != Map.Internal)
|
||||
{
|
||||
c.Loyalty -= BaseCreature.MaxLoyalty / 10;
|
||||
|
|
|
|||
|
|
@ -227,12 +227,10 @@ namespace Server.Mobiles
|
|||
{
|
||||
SayTo(from, 502673); // I can not stable summoned creatures.
|
||||
}
|
||||
/*
|
||||
else if (pet.Allured)
|
||||
{
|
||||
SayTo( from, 1048053 ); // You can't stable that!
|
||||
}
|
||||
*/
|
||||
else if (pet.Allured)
|
||||
{
|
||||
SayTo(from, 1048053); // You can't stable that!
|
||||
}
|
||||
else if (pet is PackLlama or PackHorse or Beetle && pet.Backpack?.Items.Count > 0)
|
||||
{
|
||||
SayTo(from, 1042563); // You need to unload your pet.
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ namespace Server.Spells
|
|||
Register(608, typeof(ReaperFormSpell));
|
||||
// Register(609, typeof(WildfireSpell));
|
||||
Register(610, typeof(EssenceOfWindSpell));
|
||||
// Register(611, typeof(DryadAllureSpell));
|
||||
Register(611, typeof(DryadAllureSpell));
|
||||
Register(612, typeof(EtherealVoyageSpell));
|
||||
Register(613, typeof(WordOfDeathSpell));
|
||||
Register(614, typeof(GiftOfLifeSpell));
|
||||
|
|
|
|||
179
Projects/UOContent/Spells/Spellweaving/DryadAllure.cs
Normal file
179
Projects/UOContent/Spells/Spellweaving/DryadAllure.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Spells.Spellweaving
|
||||
{
|
||||
public class DryadAllureSpell : ArcanistSpell
|
||||
{
|
||||
private const int ControlSlotCost = 3;
|
||||
|
||||
private static readonly SpellInfo _info = new(
|
||||
"Dryad Allure",
|
||||
"Rathril",
|
||||
-1
|
||||
);
|
||||
|
||||
public DryadAllureSpell(Mobile caster, Item scroll = null) : base(caster, scroll, _info)
|
||||
{
|
||||
}
|
||||
|
||||
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds(3.0);
|
||||
public override double RequiredSkill => 52.0;
|
||||
public override int RequiredMana => 40;
|
||||
|
||||
public static bool IsValidTarget(BaseCreature creature)
|
||||
{
|
||||
if (creature == null || creature.IsParagon || creature.Summoned || creature.AllureImmune ||
|
||||
creature.Controlled && !creature.Allured)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SlayerGroup.GetEntryByName(SlayerName.Repond)?.Slays(creature) == true;
|
||||
}
|
||||
|
||||
internal static double GetCharmChance(double spellweaving, int focusLevel) =>
|
||||
spellweaving / 150.0 + focusLevel / 50.0;
|
||||
|
||||
internal static bool ApplyAllureAttempt(Mobile caster, BaseCreature creature, int focusLevel)
|
||||
{
|
||||
if (GetCharmChance(caster.Skills.Spellweaving.Value, focusLevel) <= Utility.RandomDouble())
|
||||
{
|
||||
EnrageTarget(caster, creature);
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldControlSlots = creature.ControlSlots;
|
||||
|
||||
creature.ControlSlots = ControlSlotCost;
|
||||
creature.Combatant = null;
|
||||
creature.Warmode = false;
|
||||
creature.RemoveAggressor(caster);
|
||||
creature.RemoveAggressed(caster);
|
||||
caster.RemoveAggressor(creature);
|
||||
caster.RemoveAggressed(creature);
|
||||
|
||||
if (caster.Combatant == creature)
|
||||
{
|
||||
caster.Combatant = null;
|
||||
caster.Warmode = false;
|
||||
}
|
||||
|
||||
if (!creature.SetControlMaster(caster))
|
||||
{
|
||||
creature.ControlSlots = oldControlSlots;
|
||||
return false;
|
||||
}
|
||||
|
||||
creature.PlaySound(0x5C4);
|
||||
creature.Allured = true;
|
||||
creature.Loyalty = BaseCreature.MaxLoyalty;
|
||||
DeleteBackpackContents(creature);
|
||||
|
||||
caster.SendLocalizedMessage(1074377); // You allure the humanoid to follow and protect you.
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EnrageTarget(Mobile caster, BaseCreature creature)
|
||||
{
|
||||
creature.PlaySound(0x5C5);
|
||||
creature.ControlTarget = caster;
|
||||
creature.ControlOrder = OrderType.Attack;
|
||||
creature.Combatant = caster;
|
||||
creature.Warmode = true;
|
||||
|
||||
caster.SendLocalizedMessage(1074378); // The humanoid becomes enraged by your charming attempt and attacks you.
|
||||
}
|
||||
|
||||
private static void DeleteBackpackContents(BaseCreature creature)
|
||||
{
|
||||
var pack = creature.Backpack;
|
||||
|
||||
if (pack == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = pack.Items.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (i >= pack.Items.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
pack.Items[i].Delete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Target(BaseCreature creature)
|
||||
{
|
||||
if (creature == null || creature.Deleted)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1074379); // You cannot charm that!
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Caster.CanSee(creature.Location) || !Caster.InLOS(creature))
|
||||
{
|
||||
Caster.SendLocalizedMessage(500237); // Target can not be seen.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsValidTarget(creature))
|
||||
{
|
||||
Caster.SendLocalizedMessage(1074379); // You cannot charm that!
|
||||
return;
|
||||
}
|
||||
|
||||
if (creature.Allured)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1074380); // This humanoid is already controlled by someone else.
|
||||
return;
|
||||
}
|
||||
|
||||
if (Caster.Followers + ControlSlotCost > Caster.FollowersMax)
|
||||
{
|
||||
Caster.SendLocalizedMessage(1049607); // You have too many followers to control that creature.
|
||||
return;
|
||||
}
|
||||
|
||||
if (CheckSequence())
|
||||
{
|
||||
SpellHelper.Turn(Caster, creature);
|
||||
ApplyAllureAttempt(Caster, creature, FocusLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnCast()
|
||||
{
|
||||
Caster.Target = new InternalTarget(this);
|
||||
}
|
||||
|
||||
private class InternalTarget : Target
|
||||
{
|
||||
private readonly DryadAllureSpell _spell;
|
||||
|
||||
public InternalTarget(DryadAllureSpell spell) : base(12, false, TargetFlags.None) =>
|
||||
_spell = spell;
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is BaseCreature creature)
|
||||
{
|
||||
_spell.Target(creature);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1074379); // You cannot charm that!
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTargetFinish(Mobile from)
|
||||
{
|
||||
_spell.FinishSequence();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue