using System; using System.Collections.Generic; using Server.Engines.Craft; using Server.Mobiles; using Server.Network; using Server.Targeting; namespace Server.Items { public delegate void InstrumentPickedCallback(Mobile from, BaseInstrument instrument); public enum InstrumentQuality { Low, Regular, Exceptional } public abstract class BaseInstrument : Item, ICraftable, ISlayer { private static readonly Dictionary m_Instruments = new(); private Mobile m_Crafter; private DateTime m_LastReplenished; private InstrumentQuality m_Quality; private bool m_ReplenishesCharges; private SlayerName m_Slayer, m_Slayer2; private int m_UsesRemaining; public BaseInstrument(int itemID, int wellSound, int badlySound) : base(itemID) { SuccessSound = wellSound; FailureSound = badlySound; UsesRemaining = Utility.RandomMinMax(InitMinUses, InitMaxUses); } public BaseInstrument(Serial serial) : base(serial) { } [CommandProperty(AccessLevel.GameMaster)] public int SuccessSound { get; set; } [CommandProperty(AccessLevel.GameMaster)] public int FailureSound { get; set; } [CommandProperty(AccessLevel.GameMaster)] public InstrumentQuality Quality { get => m_Quality; set { UnscaleUses(); m_Quality = value; InvalidateProperties(); ScaleUses(); } } [CommandProperty(AccessLevel.GameMaster)] public Mobile Crafter { get => m_Crafter; set { m_Crafter = value; InvalidateProperties(); } } public virtual int InitMinUses => 350; public virtual int InitMaxUses => 450; public virtual TimeSpan ChargeReplenishRate => TimeSpan.FromMinutes(5.0); [CommandProperty(AccessLevel.GameMaster)] public int UsesRemaining { get { CheckReplenishUses(); return m_UsesRemaining; } set { m_UsesRemaining = value; InvalidateProperties(); } } [CommandProperty(AccessLevel.GameMaster)] public DateTime LastReplenished { get => m_LastReplenished; set { m_LastReplenished = value; CheckReplenishUses(); } } [CommandProperty(AccessLevel.GameMaster)] public bool ReplenishesCharges { get => m_ReplenishesCharges; set { if (value != m_ReplenishesCharges && value) { m_LastReplenished = Core.Now; } m_ReplenishesCharges = value; } } public int OnCraft( int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool, CraftItem craftItem, int resHue ) { Quality = (InstrumentQuality)quality; if (makersMark) { Crafter = from; } return quality; } [CommandProperty(AccessLevel.GameMaster)] public SlayerName Slayer { get => m_Slayer; set { m_Slayer = value; InvalidateProperties(); } } [CommandProperty(AccessLevel.GameMaster)] public SlayerName Slayer2 { get => m_Slayer2; set { m_Slayer2 = value; InvalidateProperties(); } } public void CheckReplenishUses(bool invalidate = true) { if (!m_ReplenishesCharges || m_UsesRemaining >= InitMaxUses) { return; } if (m_LastReplenished + ChargeReplenishRate < Core.Now) { var timeDifference = Core.Now - m_LastReplenished; m_UsesRemaining = Math.Min( m_UsesRemaining + (int)(timeDifference.Ticks / ChargeReplenishRate.Ticks), InitMaxUses ); // How rude of TimeSpan to not allow timespan division. m_LastReplenished = Core.Now; if (invalidate) { InvalidateProperties(); } } } public void ScaleUses() { UsesRemaining = UsesRemaining * GetUsesScalar() / 100; // InvalidateProperties(); } public void UnscaleUses() { UsesRemaining = UsesRemaining * 100 / GetUsesScalar(); } public int GetUsesScalar() => m_Quality == InstrumentQuality.Exceptional ? 200 : 100; public void ConsumeUse(Mobile from) { // TODO: Confirm what must happen here? if (UsesRemaining > 1) { --UsesRemaining; } else { from?.SendLocalizedMessage(502079); // The instrument played its last tune. Delete(); } } public static BaseInstrument GetInstrument(Mobile from) { if (m_Instruments.TryGetValue(from, out var instrument) && instrument.IsChildOf(from.Backpack)) { return instrument; } m_Instruments.Remove(from); return null; } public static int GetBardRange(Mobile bard, SkillName skill) => 8 + (int)(bard.Skills[skill].Value / 15); public static void PickInstrument(Mobile from, InstrumentPickedCallback callback) { var instrument = GetInstrument(from); if (instrument != null) { callback?.Invoke(from, instrument); } else { from.SendLocalizedMessage(500617); // What instrument shall you play? from.BeginTarget(1, false, TargetFlags.None, OnPickedInstrument, callback); } } public static void OnPickedInstrument(Mobile from, object targeted, InstrumentPickedCallback callback) { if (!(targeted is BaseInstrument instrument)) { from.SendLocalizedMessage(500619); // That is not a musical instrument. } else { SetInstrument(from, instrument); callback?.Invoke(from, instrument); } } public static bool IsMageryCreature(BaseCreature bc) => bc?.AI == AIType.AI_Mage && bc.Skills.Magery.Base > 5.0; public static bool IsFireBreathingCreature(BaseCreature bc) => bc?.HasBreath == true; public static bool IsPoisonImmune(BaseCreature bc) => bc?.PoisonImmune != null; public static int GetPoisonLevel(BaseCreature bc) => (bc?.HitPoison?.Level ?? -1) + 1; public static double GetBaseDifficulty(Mobile targ) { /* Difficulty TODO: Add another 100 points for each of the following abilities: - Radiation or Aura Damage (Heat, Cold etc.) - Summoning Undead */ var val = targ.HitsMax * 1.6 + targ.StamMax + targ.ManaMax; val += targ.SkillsTotal / 10.0; if (val > 700) { val = 700 + (int)((val - 700) * (3.0 / 11)); } var bc = targ as BaseCreature; if (IsMageryCreature(bc)) { val += 100; } if (IsFireBreathingCreature(bc)) { val += 100; } if (IsPoisonImmune(bc)) { val += 100; } if (targ is VampireBat || targ is VampireBatFamiliar) { val += 100; } val += GetPoisonLevel(bc) * 20; val /= 10; if (bc?.IsParagon == true) { val += 40.0; } if (Core.SE && val > 160.0) { val = 160.0; } return val; } public double GetDifficultyFor(Mobile targ) { var val = GetBaseDifficulty(targ); if (m_Quality == InstrumentQuality.Exceptional) { val -= 5.0; // 10% } if (m_Slayer != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer); if (entry != null) { if (entry.Slays(targ)) { val -= 10.0; // 20% } else if (entry.Group.OppositionSuperSlays(targ)) { val += 10.0; // -20% } } } if (m_Slayer2 != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer2); if (entry != null) { if (entry.Slays(targ)) { val -= 10.0; // 20% } else if (entry.Group.OppositionSuperSlays(targ)) { val += 10.0; // -20% } } } return val; } public static void SetInstrument(Mobile from, BaseInstrument item) { m_Instruments[from] = item; } public override void GetProperties(ObjectPropertyList list) { var oldUses = m_UsesRemaining; CheckReplenishUses(false); base.GetProperties(list); if (m_Crafter != null) { list.Add(1050043, m_Crafter.Name); // crafted by ~1_NAME~ } if (m_Quality == InstrumentQuality.Exceptional) { list.Add(1060636); // exceptional } list.Add(1060584, m_UsesRemaining.ToString()); // uses remaining: ~1_val~ if (m_ReplenishesCharges) { list.Add(1070928); // Replenish Charges } if (m_Slayer != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer); if (entry != null) { list.Add(entry.Title); } } if (m_Slayer2 != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer2); if (entry != null) { list.Add(entry.Title); } } if (m_UsesRemaining != oldUses) { Timer.StartTimer(InvalidateProperties); } } public override void OnSingleClick(Mobile from) { var attrs = new List(); if (DisplayLootType) { if (LootType == LootType.Blessed) { attrs.Add(new EquipInfoAttribute(1038021)); // blessed } else if (LootType == LootType.Cursed) { attrs.Add(new EquipInfoAttribute(1049643)); // cursed } } if (m_Quality == InstrumentQuality.Exceptional) { attrs.Add(new EquipInfoAttribute(1018305 - (int)m_Quality)); } if (m_ReplenishesCharges) { attrs.Add(new EquipInfoAttribute(1070928)); // Replenish Charges } // TODO: Must this support item identification? if (m_Slayer != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer); if (entry != null) { attrs.Add(new EquipInfoAttribute(entry.Title)); } } if (m_Slayer2 != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(m_Slayer2); if (entry != null) { attrs.Add(new EquipInfoAttribute(entry.Title)); } } int number; if (Name == null) { number = LabelNumber; } else { LabelTo(from, Name); number = 1041000; } if (attrs.Count == 0 && Crafter == null && Name != null) { return; } from.NetState.SendDisplayEquipmentInfo(Serial, number, m_Crafter?.RawName, false, attrs); } public override void Serialize(IGenericWriter writer) { base.Serialize(writer); writer.Write(3); // version writer.Write(m_ReplenishesCharges); if (m_ReplenishesCharges) { writer.Write(m_LastReplenished); } writer.Write(m_Crafter); writer.WriteEncodedInt((int)m_Quality); writer.WriteEncodedInt((int)m_Slayer); writer.WriteEncodedInt((int)m_Slayer2); writer.WriteEncodedInt(UsesRemaining); writer.WriteEncodedInt(SuccessSound); writer.WriteEncodedInt(FailureSound); } public override void Deserialize(IGenericReader reader) { base.Deserialize(reader); var version = reader.ReadInt(); switch (version) { case 3: { m_ReplenishesCharges = reader.ReadBool(); if (m_ReplenishesCharges) { m_LastReplenished = reader.ReadDateTime(); } goto case 2; } case 2: { m_Crafter = reader.ReadEntity(); m_Quality = (InstrumentQuality)reader.ReadEncodedInt(); m_Slayer = (SlayerName)reader.ReadEncodedInt(); m_Slayer2 = (SlayerName)reader.ReadEncodedInt(); UsesRemaining = reader.ReadEncodedInt(); SuccessSound = reader.ReadEncodedInt(); FailureSound = reader.ReadEncodedInt(); break; } case 1: { m_Crafter = reader.ReadEntity(); m_Quality = (InstrumentQuality)reader.ReadEncodedInt(); m_Slayer = (SlayerName)reader.ReadEncodedInt(); UsesRemaining = reader.ReadEncodedInt(); SuccessSound = reader.ReadEncodedInt(); FailureSound = reader.ReadEncodedInt(); break; } case 0: { SuccessSound = reader.ReadInt(); FailureSound = reader.ReadInt(); UsesRemaining = Utility.RandomMinMax(InitMinUses, InitMaxUses); break; } } CheckReplenishUses(); } public override void OnDoubleClick(Mobile from) { if (!from.InRange(GetWorldLocation(), 1)) { from.SendLocalizedMessage(500446); // That is too far away. } else if (from.BeginAction()) { SetInstrument(from, this); // Delay of 6 second before being able to play another instrument again Timer.StartTimer(TimeSpan.FromSeconds(6), from.EndAction); if (CheckMusicianship(from)) { PlayInstrumentWell(from); } else { PlayInstrumentBadly(from); } } else { from.SendLocalizedMessage(500119); // You must wait to perform another action } } public static bool CheckMusicianship(Mobile m) { m.CheckSkill(SkillName.Musicianship, 0.0, 120.0); return m.Skills.Musicianship.Value / 100 > Utility.RandomDouble(); } public void PlayInstrumentWell(Mobile from) { from.PlaySound(SuccessSound); } public void PlayInstrumentBadly(Mobile from) { from.PlaySound(FailureSound); } } }