## 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
162 lines
5.2 KiB
C#
162 lines
5.2 KiB
C#
using System;
|
|
using Server.Engines.Virtues;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Spells;
|
|
using Server.Spells.Necromancy;
|
|
using Server.Spells.Ninjitsu;
|
|
|
|
namespace Server;
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
public override string Name { get; }
|
|
|
|
public override int Level { get; }
|
|
|
|
public override PoisonFamily Family { get; }
|
|
|
|
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)
|
|
{
|
|
From = m;
|
|
_mobile = m;
|
|
_poison = p;
|
|
}
|
|
|
|
public Mobile From{ get; set; }
|
|
|
|
protected override void OnTick()
|
|
{
|
|
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))
|
|
{
|
|
if (Core.SA)
|
|
{
|
|
// * You feel yourself resisting the effects of the poison *
|
|
_mobile.LocalOverheadMessage(MessageType.Emote, 0x3F, 1114441);
|
|
}
|
|
else
|
|
{
|
|
_mobile.LocalOverheadMessage(
|
|
MessageType.Emote,
|
|
0x3F,
|
|
true,
|
|
"* You feel yourself resisting the effects of the poison *"
|
|
);
|
|
}
|
|
|
|
if (Core.SA)
|
|
{
|
|
// * ~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 *"
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|