From 992bc951643fc2cfef21bde4f3769dd7f2e4d67b Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:27:22 -0700 Subject: [PATCH] feat: Refactor Poison system, implement Darkglow & Parasitic effects (#2385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Refactors the poison system to separate `Index` (globally unique ID) from `Level` (tier within a family), enabling multiple poison families (Standard, Darkglow, Parasitic) to coexist without collisions - Implements Darkglow and Parasitic poison special effects from Mondain's Legacy: Darkglow boosts damage by 10% when attacker is ranged, Parasitic heals the attacker for damage dealt in melee range - Fixes several bugs: `Register()` crashing on duplicate `Level` values across families, `IncreaseLevel()` crossing family boundaries, `InfectiousStrike` and `NinjaWeapons` stripping poison family via level-based lookups, and `ArchCure`/`CleansingWinds` using raw `Level + 1` instead of `IncreaseLevel()` ## Changes **`Projects/Server/Poison.cs`** — Adds `PoisonFamily` enum and abstract `Family` property. Adds `Index` as unique identifier. Fixes `Register()` to check `Index` uniqueness (not `Level`) and validate the new poison's name (not the existing one's). Fixes `IncreaseLevel()` to use `Index + 1`, naturally respecting family boundaries via Index gaps. Replaces linear name lookup with `Dictionary`-based `PoisonsByName`. **`Projects/UOContent/Misc/Poison.cs`** — Adds `family` parameter to `PoisonImpl`. Implements Darkglow effect (10% damage boost when `From` >1 tile, cliloc 1072850) and Parasitic effect (heals `From` for damage dealt within 1 tile, cliloc 1060203) in `PoisonTimer.OnTick()`. Renames `m_` fields to `_` convention. **`Projects/UOContent/Misc/PoisonKinds.cs`** — New file. Moves poison registration out of `PoisonImpl` into `PoisonKinds.Configure()`. Adds `PoisonFamily` to Darkglow/Parasitic registrations. Provides extension properties (`Lesser`, `Deadly`, `LesserDarkglow`, etc.), `GetPoison(int level)` (standard-only), `GetPoisonByFamilyAndLevel()`, and `IsDarkglow`/`IsParasitic` instance helpers. **`Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs`** — Family-aware poison scaling: Darkglow caps at Deadly (Poisoning/33.3), Parasitic caps at Lethal (Poisoning/25), Standard unchanged. Level bump uses `IncreaseLevel()` with family boundary check. **`Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs`** — EvilOmen level bump uses `Poison.IncreaseLevel()` instead of `Poison.GetPoison(Level + 1)`. **`Projects/UOContent/Spells/Fourth/ArchCure.cs`** and **`CleansingWindsSpell.cs`** — Replace `poison.Level + 1` with `Poison.IncreaseLevel(poison).Level` for family-safe cure chance calculation. **`Projects/Server/Serialization/SerializationExtensions.cs`** — Serializes/deserializes `Index` instead of `Level`. **`DarkglowPotion.cs`** / **`ParasiticPotion.cs`** — Point to actual Darkglow/Parasitic poisons instead of placeholder `Greater`. **`PotionKeg.cs`** / **`BasePotion.cs`** — Adds Darkglow, Parasitic, Invisibility, and FlintsPungentBrew to `PotionEffect` enum and keg label support. ## Test plan - [ ] `dotnet build` compiles cleanly (verified, 0 warnings 0 errors) - [ ] Verify `PoisonKinds.Configure()` registers all poisons without throwing (Register bug fix) - [ ] Standard poison behavior unchanged — PoisonField, PoisonSpell, SerpentArrow, SavageShaman, TrappableContainer all use `GetPoison(int level)` which now correctly filters to Standard family - [ ] Darkglow: poison tick deals +10% damage when attacker is >1 tile away, sends "Darkglow poison increases your damage!" message - [ ] Parasitic: poison tick heals attacker for damage dealt when within 1 tile, sends heal message - [ ] InfectiousStrike preserves poison family and respects family-specific skill scaling - [ ] EvilOmen + NinjaWeapons level bump stays within poison family - [ ] ArchCure/CleansingWinds cure chance calculations work correctly across all poison families - [ ] Serialization round-trips correctly using Index --- Projects/Server/Poison.cs | 73 ++--- .../Serialization/SerializationExtensions.cs | 4 +- .../Skill Items/Magical/Misc/PotionKeg.cs | 26 +- .../Skill Items/Magical/Potions/BasePotion.cs | 3 +- .../Potions/Poison Potions/DarkglowPotion.cs | 3 +- .../Potions/Poison Potions/ParasiticPotion.cs | 5 +- .../Skill Items/Ninjitsu/NinjaWeapons.cs | 2 +- .../Weapons/Abilities/InfectiousStrike.cs | 31 ++- Projects/UOContent/Misc/Poison.cs | 259 +++++++++--------- Projects/UOContent/Misc/PoisonKinds.cs | 109 ++++++++ Projects/UOContent/Spells/Fourth/ArchCure.cs | 4 +- .../Spells/Mysticism/CleansingWindsSpell.cs | 2 +- 12 files changed, 316 insertions(+), 205 deletions(-) create mode 100644 Projects/UOContent/Misc/PoisonKinds.cs diff --git a/Projects/Server/Poison.cs b/Projects/Server/Poison.cs index 64d804870..532c563dd 100644 --- a/Projects/Server/Poison.cs +++ b/Projects/Server/Poison.cs @@ -4,75 +4,56 @@ using System.Runtime.CompilerServices; namespace Server; +public enum PoisonFamily { Standard, Darkglow, Parasitic } + public abstract class Poison : ISpanParsable { - /*public abstract TimeSpan Interval{ get; } - public abstract TimeSpan Duration{ get; }*/ + public static List Poisons { get; } = []; + public static Dictionary PoisonsByName { get; } = new(StringComparer.OrdinalIgnoreCase); + + public Poison(int index) => Index = index; + + public int Index { get; } public abstract string Name { get; } public abstract int Level { get; } - - public static Poison Lesser => GetPoison("Lesser"); - public static Poison Regular => GetPoison("Regular"); - public static Poison Greater => GetPoison("Greater"); - public static Poison Deadly => GetPoison("Deadly"); - public static Poison Lethal => GetPoison("Lethal"); - - public static List Poisons { get; } = new(); + public abstract PoisonFamily Family { get; } public abstract Timer ConstructTimer(Mobile m); - /*public abstract void OnDamage( Mobile m, ref object state );*/ public override string ToString() => Name; public static void Register(Poison reg) { - var regName = reg.Name.ToLower(); + var regName = reg.Name; for (var i = 0; i < Poisons.Count; i++) { - if (reg.Level == Poisons[i].Level) + var poison = Poisons[i]; + if (reg.Index == poison.Index) { - throw new Exception("A poison with that level already exists."); + throw new Exception("A poison with that index already exists."); } - if (regName == Poisons[i].Name.ToLower()) + if (GetPoison(regName) != null) { throw new Exception("A poison with that name already exists."); } } Poisons.Add(reg); + PoisonsByName.Add(regName, reg); } - public static Poison GetPoison(int level) - { - for (var i = 0; i < Poisons.Count; ++i) - { - var p = Poisons[i]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Poison GetPoisonByIndex(int index) => index >= 0 && index < Poisons.Count ? Poisons[index] : null; - if (p.Level == level) - { - return p; - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Poison IncreaseLevel(Poison oldPoison) => + oldPoison == null ? null : GetPoisonByIndex(oldPoison.Index + 1) ?? oldPoison; - return null; - } - - public static Poison GetPoison(ReadOnlySpan name) - { - for (var i = 0; i < Poisons.Count; ++i) - { - var p = Poisons[i]; - - if (name.InsensitiveEquals(p.Name)) - { - return p; - } - } - - return null; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Poison GetPoison(ReadOnlySpan name) => + PoisonsByName.GetAlternateLookup>().TryGetValue(name, out var poison) ? poison : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Poison Parse(string s) => Parse(s, null); @@ -86,9 +67,9 @@ public abstract class Poison : ISpanParsable public static Poison Parse(ReadOnlySpan s, IFormatProvider provider) { - if (int.TryParse(s, provider, out var pLevel)) + if (int.TryParse(s, provider, out var index)) { - var result = GetPoison(pLevel); + var result = GetPoisonByIndex(index); if (result != null) { return result; @@ -106,9 +87,9 @@ public abstract class Poison : ISpanParsable public static bool TryParse(ReadOnlySpan s, IFormatProvider provider, out Poison result) { - if (int.TryParse(s, provider, out var pLevel)) + if (int.TryParse(s, provider, out var index)) { - result = GetPoison(pLevel); + result = GetPoisonByIndex(index); if (result != null) { return true; diff --git a/Projects/Server/Serialization/SerializationExtensions.cs b/Projects/Server/Serialization/SerializationExtensions.cs index 5bfcfecd3..bbf3e7748 100644 --- a/Projects/Server/Serialization/SerializationExtensions.cs +++ b/Projects/Server/Serialization/SerializationExtensions.cs @@ -182,10 +182,10 @@ public static class SerializationExtensions else { writer.Write(true); - writer.Write((byte)p.Level); + writer.Write((byte)p.Index); } } public static Poison ReadPoison(this IGenericReader reader) => - reader.ReadBool() ? Poison.GetPoison(reader.ReadByte()) : null; + reader.ReadBool() ? Poison.GetPoisonByIndex(reader.ReadByte()) : null; } diff --git a/Projects/UOContent/Items/Skill Items/Magical/Misc/PotionKeg.cs b/Projects/UOContent/Items/Skill Items/Magical/Misc/PotionKeg.cs index a7f19f399..a989655aa 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Misc/PotionKeg.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Misc/PotionKeg.cs @@ -12,7 +12,9 @@ public partial class PotionKeg : Item TileData.ItemTable[0x1940].Height = 4; } - [InvalidateProperties] [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] + [InvalidateProperties] + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] private PotionEffect _type; [Constructible] @@ -45,12 +47,20 @@ public partial class PotionKeg : Item { get { - if (_held > 0 && (int)_type >= (int)PotionEffect.Conflagration) + if (_held <= 0) { - return 1072658 + (int)_type - (int)PotionEffect.Conflagration; + return 1041641; } - return _held > 0 ? 1041620 + (int)_type : 1041641; + return _type switch + { + < PotionEffect.Nightsight or > PotionEffect.FlintsPungentBrew => 1041619, + PotionEffect.FlintsPungentBrew => 1113608, + >= PotionEffect.Parasitic => 1080069 + (int)_type - (int)PotionEffect.Parasitic, + PotionEffect.Invisibility => 1080071, + >= PotionEffect.Conflagration => 1072658 + (int)_type - (int)PotionEffect.Conflagration, + _ => 1041620 + (int)_type + }; } } @@ -249,6 +259,9 @@ public partial class PotionKeg : Item PotionEffect.ConflagrationGreater => new GreaterConflagrationPotion(), PotionEffect.ConfusionBlast => new ConfusionBlastPotion(), PotionEffect.ConfusionBlastGreater => new GreaterConfusionBlastPotion(), + PotionEffect.Invisibility => new InvisibilityPotion(), + PotionEffect.Parasitic => new ParasiticPotion(), + PotionEffect.Darkglow => new DarkglowPotion(), _ => new NightSightPotion() }; @@ -279,6 +292,9 @@ public partial class PotionKeg : Item _ when type == typeof(GreaterConflagrationPotion) => PotionEffect.ConflagrationGreater, _ when type == typeof(ConfusionBlastPotion) => PotionEffect.ConfusionBlast, _ when type == typeof(GreaterConfusionBlastPotion) => PotionEffect.ConfusionBlastGreater, - _ /* when type == typeof(NightSightPotion) */ => PotionEffect.Nightsight + _ when type == typeof(InvisibilityPotion) => PotionEffect.Invisibility, + _ when type == typeof(ParasiticPotion) => PotionEffect.Parasitic, + _ when type == typeof(DarkglowPotion) => PotionEffect.Darkglow, + _ => PotionEffect.Nightsight }; } diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs index 4e2e39a67..dbbec18a0 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs @@ -35,7 +35,8 @@ public enum PotionEffect ConfusionBlastGreater, Invisibility, Parasitic, - Darkglow + Darkglow, + FlintsPungentBrew } [SerializationGenerator(2, false)] diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/DarkglowPotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/DarkglowPotion.cs index 03cc56966..92537c1f9 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/DarkglowPotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/DarkglowPotion.cs @@ -8,8 +8,7 @@ public partial class DarkglowPotion : BasePoisonPotion [Constructible] public DarkglowPotion() : base(PotionEffect.Darkglow) => Hue = 0x96; - /* public override Poison Poison => Poison.DarkGlow; // MUST be restored when prerequisites are done */ - public override Poison Poison => Poison.Greater; + public override Poison Poison => Poison.GreaterDarkglow; public override double MinPoisoningSkill => 95.0; public override double MaxPoisoningSkill => 100.0; diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/ParasiticPotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/ParasiticPotion.cs index 460531c9e..08f481176 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/ParasiticPotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/Poison Potions/ParasiticPotion.cs @@ -8,9 +8,10 @@ public partial class ParasiticPotion : BasePoisonPotion [Constructible] public ParasiticPotion() : base(PotionEffect.Parasitic) => Hue = 0x17C; - /* public override Poison Poison => Poison.Parasitic; // MUST be restored when prerequisites are done */ - public override Poison Poison => Poison.Greater; + public override Poison Poison => Poison.DeadlyParasitic; public override double MinPoisoningSkill => 95.0; public override double MaxPoisoningSkill => 100.0; + + public override int LabelNumber => 1072848; // Parasitic Poison } diff --git a/Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs b/Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs index 498cd8c02..01d6cf67d 100644 --- a/Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs +++ b/Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs @@ -289,7 +289,7 @@ public static class NinjaWeapon { if (EvilOmenSpell.EndEffect(target)) { - target.ApplyPoison(from, Poison.GetPoison(weapon.Poison.Level + 1)); + target.ApplyPoison(from, Poison.IncreaseLevel(weapon.Poison)); } else { diff --git a/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs b/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs index 6e9b7df2b..a5196b6ec 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs @@ -46,25 +46,30 @@ namespace Server.Items --weapon.PoisonCharges; + var poisoningSkill = attacker.Skills.Poisoning.Value; + // Infectious strike special move now uses poisoning skill to help determine potency - var maxLevel = Math.Max((int)(attacker.Skills.Poisoning.Value / 20), 0); + var family = p.Family; + + int maxLevel = family switch + { + PoisonFamily.Darkglow => Math.Min((int)(poisoningSkill / 33.3), 3), + PoisonFamily.Parasitic => Math.Min((int)(poisoningSkill / 25), 4), + _ => Math.Max((int)(poisoningSkill / 20), 0) + }; + if (p.Level > maxLevel) { - p = Poison.GetPoison(maxLevel); + p = Poison.GetPoisonByFamilyAndLevel(family, maxLevel); } - if (attacker.Skills.Poisoning.Value / 100.0 > Utility.RandomDouble()) + // Check if the poison can be increased. + var newPoison = Poison.IncreaseLevel(p); + if (newPoison != p && poisoningSkill / 100.0 > Utility.RandomDouble()) { - var level = p.Level + 1; - var newPoison = Poison.GetPoison(level); - - if (newPoison != null) - { - p = newPoison; - - attacker.SendLocalizedMessage(1060080); // Your precise strike has increased the level of the poison by 1 - defender.SendLocalizedMessage(1060081); // The poison seems extra effective! - } + p = newPoison; + attacker.SendLocalizedMessage(1060080); // Your precise strike has increased the level of the poison by 1 + defender.SendLocalizedMessage(1060081); // The poison seems extra effective! } defender.PlaySound(0xDD); diff --git a/Projects/UOContent/Misc/Poison.cs b/Projects/UOContent/Misc/Poison.cs index e9d11fc72..4147fbad7 100644 --- a/Projects/UOContent/Misc/Poison.cs +++ b/Projects/UOContent/Misc/Poison.cs @@ -6,157 +6,156 @@ using Server.Spells; using Server.Spells.Necromancy; using Server.Spells.Ninjitsu; -namespace Server +namespace Server; + +public class PoisonImpl : Poison { - public class PoisonImpl : Poison + private readonly int _count; + private readonly TimeSpan _delay; + private readonly TimeSpan _interval; + private readonly int _maximum; + private readonly int _messageInterval; + private readonly int _minimum; + private readonly double _scalar; + + public PoisonImpl( + string name, int index, int level, int min, int max, double percent, double delay, double interval, int count, + int messageInterval, PoisonFamily family = PoisonFamily.Standard + ) : base(index) { - private readonly int m_Count; + Name = name; + Level = level; + Family = family; + _minimum = min; + _maximum = max; + _scalar = percent * 0.01; + _delay = TimeSpan.FromSeconds(delay); + _interval = TimeSpan.FromSeconds(interval); + _count = count; + _messageInterval = messageInterval; + } - // Timers - private readonly TimeSpan m_Delay; - private readonly TimeSpan m_Interval; - private readonly int m_Maximum; - private readonly int m_MessageInterval; + public override string Name { get; } - // Info + public override int Level { get; } - // Damage - private readonly int m_Minimum; - private readonly double m_Scalar; + public override PoisonFamily Family { get; } - public PoisonImpl( - string name, int level, int min, int max, double percent, double delay, double interval, int count, - int messageInterval - ) + public override Timer ConstructTimer(Mobile m) => new PoisonTimer(m, this); + + public class PoisonTimer : Timer + { + private readonly Mobile _mobile; + private readonly PoisonImpl _poison; + private int _index; + private int _lastDamage; + + public PoisonTimer(Mobile m, PoisonImpl p) : base(p._delay, p._interval) { - Name = name; - Level = level; - m_Minimum = min; - m_Maximum = max; - m_Scalar = percent * 0.01; - m_Delay = TimeSpan.FromSeconds(delay); - m_Interval = TimeSpan.FromSeconds(interval); - m_Count = count; - m_MessageInterval = messageInterval; + From = m; + _mobile = m; + _poison = p; } - public override string Name { get; } + public Mobile From{ get; set; } - public override int Level { get; } - - [CallPriority(10)] - public static void Configure() + protected override void OnTick() { - if (Core.AOS) + if ((Core.AOS && _poison.Level < 4 && + TransformationSpellHelper.UnderTransformation(_mobile, typeof(VampiricEmbraceSpell)) || + _poison.Level < 3 && OrangePetals.UnderEffect(_mobile) || + AnimalForm.UnderTransformation(_mobile, typeof(Unicorn))) && _mobile.CurePoison(_mobile)) { - Register(new PoisonImpl("Lesser", 0, 4, 16, 7.5, 3.0, 2.25, 10, 4)); - Register(new PoisonImpl("Regular", 1, 8, 18, 10.0, 3.0, 3.25, 10, 3)); - Register(new PoisonImpl("Greater", 2, 12, 20, 15.0, 3.0, 4.25, 10, 2)); - Register(new PoisonImpl("Deadly", 3, 16, 30, 30.0, 3.0, 5.25, 15, 2)); - Register(new PoisonImpl("Lethal", 4, 20, 50, 35.0, 3.0, 5.25, 20, 2)); - } - else - { - Register(new PoisonImpl("Lesser", 0, 4, 26, 2.500, 3.5, 3.0, 10, 2)); - Register(new PoisonImpl("Regular", 1, 5, 26, 3.125, 3.5, 3.0, 10, 2)); - Register(new PoisonImpl("Greater", 2, 6, 26, 6.250, 3.5, 3.0, 10, 2)); - Register(new PoisonImpl("Deadly", 3, 7, 26, 12.500, 3.5, 4.0, 10, 2)); - Register(new PoisonImpl("Lethal", 4, 9, 26, 25.000, 3.5, 5.0, 10, 2)); - } - } - - public static Poison IncreaseLevel(Poison oldPoison) - { - var newPoison = oldPoison == null ? null : GetPoison(oldPoison.Level + 1); - - return newPoison ?? oldPoison; - } - - public override Timer ConstructTimer(Mobile m) => new PoisonTimer(m, this); - - public class PoisonTimer : Timer - { - private readonly Mobile m_Mobile; - private readonly PoisonImpl m_Poison; - private int m_Index; - private int m_LastDamage; - - public PoisonTimer(Mobile m, PoisonImpl p) : base(p.m_Delay, p.m_Interval) - { - From = m; - m_Mobile = m; - m_Poison = p; - } - - public Mobile From { get; set; } - - protected override void OnTick() - { - if (Core.AOS && m_Poison.Level < 4 && - TransformationSpellHelper.UnderTransformation(m_Mobile, typeof(VampiricEmbraceSpell)) || - m_Poison.Level < 3 && OrangePetals.UnderEffect(m_Mobile) || - AnimalForm.UnderTransformation(m_Mobile, typeof(Unicorn))) + if (Core.SA) { - if (m_Mobile.CurePoison(m_Mobile)) - { - // * You feel yourself resisting the effects of the poison * - m_Mobile.LocalOverheadMessage(MessageType.Emote, 0x3F, 1114441); - - // * ~1_NAME~ seems resistant to the poison * - m_Mobile.NonlocalOverheadMessage(MessageType.Emote, 0x3F, 1114442, m_Mobile.Name); - - Stop(); - return; - } - } - - if (m_Index++ == m_Poison.m_Count) - { - m_Mobile.SendLocalizedMessage(502136); // The poison seems to have worn off. - m_Mobile.Poison = null; - - Stop(); - return; - } - - int damage; - - if (!Core.AOS && m_LastDamage != 0 && Utility.RandomBool()) - { - damage = m_LastDamage; + // * You feel yourself resisting the effects of the poison * + _mobile.LocalOverheadMessage(MessageType.Emote, 0x3F, 1114441); } else { - damage = 1 + (int)(m_Mobile.Hits * m_Poison.m_Scalar); - - if (damage < m_Poison.m_Minimum) - { - damage = m_Poison.m_Minimum; - } - else if (damage > m_Poison.m_Maximum) - { - damage = m_Poison.m_Maximum; - } - - m_LastDamage = damage; + _mobile.LocalOverheadMessage( + MessageType.Emote, + 0x3F, + true, + "* You feel yourself resisting the effects of the poison *" + ); } - From?.DoHarmful(m_Mobile, true); - - (m_Mobile as IHonorTarget)?.ReceivedHonorContext?.OnTargetPoisoned(); - - AOS.Damage(m_Mobile, From, damage, 0, 0, 0, 100, 0); - - // OSI: randomly revealed between first and third damage tick, guessing 60% chance - if (Utility.RandomDouble() < 0.40) + if (Core.SA) { - m_Mobile.RevealingAction(); + // * ~1_NAME~ seems resistant to the poison * + _mobile.NonlocalOverheadMessage(MessageType.Emote, 0x3F, 1114442, _mobile.Name); + } + else + { + _mobile.LocalOverheadMessage( + MessageType.Emote, + 0x3F, + true, + $"* {_mobile.Name} seems resistant to the poison *" + ); } - if (m_Index % m_Poison.m_MessageInterval == 0) - { - m_Mobile.OnPoisoned(From, m_Poison, m_Poison); - } + Stop(); + return; + } + + if (_index++ == _poison._count) + { + _mobile.SendLocalizedMessage(502136); // The poison seems to have worn off. + _mobile.Poison = null; + + Stop(); + return; + } + + int damage; + + if (!Core.AOS && _lastDamage != 0 && Utility.RandomBool()) + { + damage = _lastDamage; + } + else + { + damage = 1 + (int)(_mobile.Hits * _poison._scalar); + damage = Math.Clamp(damage, _poison._minimum, _poison._maximum); + + _lastDamage = damage; + } + + // Darkglow: 10% damage boost when attacker is more than 1 tile away + if (_poison.Family == PoisonFamily.Darkglow && From != null && From.Map == _mobile.Map && + !From.InRange(_mobile, 1)) + { + damage = (int)(damage * 1.1); + // Darkglow poison increases your damage! + From.SendLocalizedMessage(1072850); + } + + From?.DoHarmful(_mobile, true); + + (_mobile as IHonorTarget)?.ReceivedHonorContext?.OnTargetPoisoned(); + + AOS.Damage(_mobile, From, damage, 0, 0, 0, 100, 0); + + // Parasitic: heals attacker for damage dealt when within 1 tile + if (_poison.Family == PoisonFamily.Parasitic && From != null && From.Map == _mobile.Map && + From.InRange(_mobile, 1)) + { + From.Heal(damage); + // You have had ~1_HEALED_AMOUNT~ hit points healed. + From.SendLocalizedMessage(1060203, damage.ToString()); + } + + // OSI: randomly revealed between first and third damage tick, guessing 60% chance + if (Utility.RandomDouble() < 0.40) + { + _mobile.RevealingAction(); + } + + if (_index % _poison._messageInterval == 0) + { + _mobile.OnPoisoned(From, _poison, _poison); } } } diff --git a/Projects/UOContent/Misc/PoisonKinds.cs b/Projects/UOContent/Misc/PoisonKinds.cs new file mode 100644 index 000000000..45d707d41 --- /dev/null +++ b/Projects/UOContent/Misc/PoisonKinds.cs @@ -0,0 +1,109 @@ +namespace Server; + +public static class PoisonKinds +{ + private static Poison _lesser; + private static Poison _regular; + private static Poison _greater; + private static Poison _deadly; + private static Poison _lethal; + + private static Poison _lesserDarkglow; + private static Poison _regularDarkglow; + private static Poison _greaterDarkglow; + private static Poison _deadlyDarkglow; + + private static Poison _lesserParasitic; + private static Poison _regularParasitic; + private static Poison _greaterParasitic; + private static Poison _deadlyParasitic; + private static Poison _lethalParasitic; + + extension(Poison poison) + { + public static Poison Lesser => _lesser ??= Poison.GetPoison("Lesser"); + public static Poison Regular => _regular ??= Poison.GetPoison("Regular"); + public static Poison Greater => _greater ??= Poison.GetPoison("Greater"); + public static Poison Deadly => _deadly ??= Poison.GetPoison("Deadly"); + public static Poison Lethal => _lethal ??= Poison.GetPoison("Lethal"); + + public static Poison LesserDarkglow => _lesserDarkglow ??= Poison.GetPoison("LesserDarkglow"); + public static Poison RegularDarkglow => _regularDarkglow ??= Poison.GetPoison("RegularDarkglow"); + public static Poison GreaterDarkglow => _greaterDarkglow ??= Poison.GetPoison("GreaterDarkglow"); + public static Poison DeadlyDarkglow => _deadlyDarkglow ??= Poison.GetPoison("DeadlyDarkglow"); + + public static Poison LesserParasitic => _lesserParasitic ??= Poison.GetPoison("LesserParasitic"); + public static Poison RegularParasitic => _regularParasitic ??= Poison.GetPoison("RegularParasitic"); + public static Poison GreaterParasitic => _greaterParasitic ??= Poison.GetPoison("GreaterParasitic"); + public static Poison DeadlyParasitic => _deadlyParasitic ??= Poison.GetPoison("DeadlyParasitic"); + public static Poison LethalParasitic => _lethalParasitic ??= Poison.GetPoison("LethalParasitic"); + + public bool IsDarkglow => poison.Family == PoisonFamily.Darkglow; + public bool IsParasitic => poison.Family == PoisonFamily.Parasitic; + + public static Poison GetPoison(int level) + { + for (var i = 0; i < Poison.Poisons.Count; ++i) + { + var p = Poison.Poisons[i]; + + if (p.Family == PoisonFamily.Standard && p.Level == level) + { + return p; + } + } + + return null; + } + + public static Poison GetPoisonByFamilyAndLevel(PoisonFamily family, int level) + { + for (var i = 0; i < Poison.Poisons.Count; ++i) + { + var p = Poison.Poisons[i]; + + if (p.Family == family && p.Level == level) + { + return p; + } + } + + return null; + } + } + + [CallPriority(10)] + public static void Configure() + { + if (Core.AOS) + { + Poison.Register(new PoisonImpl("Lesser", 0, 0, 4, 16, 7.5, 3.0, 2.25, 10, 4)); + Poison.Register(new PoisonImpl("Regular", 1, 1, 8, 18, 10.0, 3.0, 3.25, 10, 3)); + Poison.Register(new PoisonImpl("Greater", 2, 2, 12, 20, 15.0, 3.0, 4.25, 10, 2)); + Poison.Register(new PoisonImpl("Deadly", 3, 3, 16, 30, 30.0, 3.0, 5.25, 15, 2)); + Poison.Register(new PoisonImpl("Lethal", 4, 4, 20, 50, 35.0, 3.0, 5.25, 20, 2)); + } + else + { + Poison.Register(new PoisonImpl("Lesser", 0, 0, 4, 26, 2.5, 3.5, 3.0, 10, 2)); + Poison.Register(new PoisonImpl("Regular", 1, 1, 5, 26, 3.125, 3.5, 3.0, 10, 2)); + Poison.Register(new PoisonImpl("Greater", 2, 2, 6, 26, 6.25, 3.5, 3.0, 10, 2)); + Poison.Register(new PoisonImpl("Deadly", 3, 3, 7, 26, 12.5, 3.5, 4.0, 10, 2)); + Poison.Register(new PoisonImpl("Lethal", 4, 4, 9 , 26, 25.0, 3.5, 5.0, 10, 2)); + } + + if (Core.ML) + { + Poison.Register(new PoisonImpl("LesserDarkglow", 10, 0, 4, 16, 7.5, 3.0, 2.25, 10, 4, PoisonFamily.Darkglow)); + Poison.Register(new PoisonImpl("RegularDarkglow", 11, 1, 8, 18, 10.0, 3.0, 3.25, 10, 3, PoisonFamily.Darkglow)); + Poison.Register(new PoisonImpl("GreaterDarkglow", 12, 2, 12, 20, 15.0, 3.0, 4.25, 10, 2, PoisonFamily.Darkglow)); + Poison.Register(new PoisonImpl("DeadlyDarkglow", 13, 3, 16, 30, 30.0, 3.0, 5.25, 15, 2, PoisonFamily.Darkglow)); + + Poison.Register(new PoisonImpl("LesserParasitic", 20, 0, 4, 16, 7.5, 3.0, 2.25, 10, 4, PoisonFamily.Parasitic)); + Poison.Register(new PoisonImpl("RegularParasitic", 21, 1, 8, 18, 10.0, 3.0, 3.25, 10, 3, PoisonFamily.Parasitic)); + Poison.Register(new PoisonImpl("GreaterParasitic", 22, 2, 12, 20, 15.0, 3.0, 4.25, 10, 2, PoisonFamily.Parasitic)); + Poison.Register(new PoisonImpl("DeadlyParasitic", 23, 3, 16, 30, 30.0, 3.0, 5.25, 15, 2, PoisonFamily.Parasitic)); + Poison.Register(new PoisonImpl("LethalParasitic", 24, 4, 20, 50, 35.0, 3.0, 5.25, 20, 2, PoisonFamily.Parasitic)); + } + } +} diff --git a/Projects/UOContent/Spells/Fourth/ArchCure.cs b/Projects/UOContent/Spells/Fourth/ArchCure.cs index ed5307df3..aca85af40 100644 --- a/Projects/UOContent/Spells/Fourth/ArchCure.cs +++ b/Projects/UOContent/Spells/Fourth/ArchCure.cs @@ -70,8 +70,8 @@ namespace Server.Spells.Fourth if (poison != null) { - var chanceToCure = 10000 + (int)(Caster.Skills.Magery.Value * 75) - - (poison.Level + 1) * 1750; + var poisonLevel = Poison.IncreaseLevel(poison).Level; + var chanceToCure = 10000 + (int)(Caster.Skills.Magery.Value * 75) - poisonLevel * 1750; chanceToCure /= 100; chanceToCure -= 1; diff --git a/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs b/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs index d54a33ece..744dce453 100644 --- a/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs +++ b/Projects/UOContent/Spells/Mysticism/CleansingWindsSpell.cs @@ -99,7 +99,7 @@ public class CleansingWindsSpell : MysticSpell, ITargetingSpell if (target.Poisoned) { - var poisonLevel = target.Poison.Level + 1; + var poisonLevel = Poison.IncreaseLevel(target.Poison).Level; var chanceToCure = cureChance - poisonLevel * 1750; if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster))