## 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
300 lines
11 KiB
C#
300 lines
11 KiB
C#
using System;
|
|
using System.Runtime.CompilerServices;
|
|
using ModernUO.Serialization;
|
|
|
|
namespace Server.Items;
|
|
|
|
[SerializationGenerator(2, false)]
|
|
public partial class PotionKeg : Item
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
TileData.ItemTable[0x1940].Height = 4;
|
|
}
|
|
|
|
[InvalidateProperties]
|
|
[SerializableField(0)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private PotionEffect _type;
|
|
|
|
[Constructible]
|
|
public PotionKeg() : base(0x1940)
|
|
{
|
|
}
|
|
|
|
public override double DefaultWeight => 20 + Math.Clamp(_held, 0, 100) * 0.8;
|
|
|
|
[SerializableProperty(1)]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Held
|
|
{
|
|
get => _held;
|
|
set
|
|
{
|
|
if (_held != value)
|
|
{
|
|
var oldPileWeight = PileWeight;
|
|
_held = value;
|
|
var newPileWeight = PileWeight;
|
|
UpdateTotal(this, TotalType.Weight, newPileWeight - oldPileWeight);
|
|
InvalidateProperties();
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override int LabelNumber
|
|
{
|
|
get
|
|
{
|
|
if (_held <= 0)
|
|
{
|
|
return 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
|
|
};
|
|
}
|
|
}
|
|
|
|
private void Deserialize(IGenericReader reader, int version)
|
|
{
|
|
_type = (PotionEffect)reader.ReadInt();
|
|
_held = reader.ReadInt();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private int GetKegLabel() =>
|
|
_held switch
|
|
{
|
|
<= 0 => 502246, // The keg is empty.
|
|
< 5 => 502248, // The keg is nearly empty.
|
|
< 20 => 502249, // The keg is not very full.
|
|
< 30 => 502250, // The keg is about one quarter full.
|
|
< 40 => 502251, // The keg is about one third full.
|
|
< 47 => 502252, // The keg is almost half full.
|
|
< 54 => 502254, // The keg is approximately half full.
|
|
< 70 => 502253, // The keg is more than half full.
|
|
< 80 => 502255, // The keg is about three quarters full.
|
|
< 96 => 502256, // The keg is very full.
|
|
< 100 => 502257, // The liquid is almost to the top of the keg.
|
|
_ => 502258 // The keg is completely full.
|
|
};
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
list.Add(GetKegLabel());
|
|
}
|
|
|
|
public override void OnSingleClick(Mobile from)
|
|
{
|
|
base.OnSingleClick(from);
|
|
|
|
LabelTo(from, GetKegLabel());
|
|
}
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
if (!from.InRange(GetWorldLocation(), 2))
|
|
{
|
|
from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that.
|
|
return;
|
|
}
|
|
|
|
if (_held <= 0)
|
|
{
|
|
from.SendLocalizedMessage(502246); // The keg is empty.
|
|
return;
|
|
}
|
|
|
|
var pack = from.Backpack;
|
|
|
|
if (pack?.ConsumeTotal(typeof(Bottle)) == true)
|
|
{
|
|
from.SendLocalizedMessage(502242); // You pour some of the keg's contents into an empty bottle...
|
|
|
|
var pot = FillBottle(_type);
|
|
|
|
if (pack.TryDropItem(from, pot, false))
|
|
{
|
|
from.SendLocalizedMessage(502243); // ...and place it into your backpack.
|
|
from.PlaySound(0x240);
|
|
|
|
if (--Held == 0)
|
|
{
|
|
from.SendLocalizedMessage(502245); // The keg is now empty.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendLocalizedMessage(502244); // ...but there is no room for the bottle in your backpack.
|
|
pot.Delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool OnDragDrop(Mobile from, Item item)
|
|
{
|
|
if (item is not BasePotion pot)
|
|
{
|
|
from.SendLocalizedMessage(502232); // The keg is not designed to hold that type of object.
|
|
return false;
|
|
}
|
|
|
|
var toHold = Math.Min(100 - _held, pot.Amount);
|
|
|
|
if (toHold <= 0)
|
|
{
|
|
from.SendLocalizedMessage(502233); // The keg will not hold any more!
|
|
return false;
|
|
}
|
|
|
|
if (_held == 0)
|
|
{
|
|
if ((int)pot.PotionEffect >= (int)PotionEffect.Invisibility)
|
|
{
|
|
from.SendLocalizedMessage(502232); // The keg is not designed to hold that type of object.
|
|
return false;
|
|
}
|
|
|
|
if (GiveBottle(from, toHold))
|
|
{
|
|
_type = pot.PotionEffect;
|
|
Held = toHold;
|
|
|
|
from.PlaySound(0x240);
|
|
|
|
from.SendLocalizedMessage(502237); // You place the empty bottle in your backpack.
|
|
|
|
pot.Consume(toHold);
|
|
|
|
if (!pot.Deleted)
|
|
{
|
|
pot.Bounce(from);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
from.SendLocalizedMessage(502238); // You don't have room for the empty bottle in your backpack.
|
|
return false;
|
|
}
|
|
|
|
if (pot.PotionEffect != _type)
|
|
{
|
|
// You decide that it would be a bad idea to mix different types of potions.
|
|
from.SendLocalizedMessage(502236);
|
|
return false;
|
|
}
|
|
|
|
if (GiveBottle(from, toHold))
|
|
{
|
|
Held += toHold;
|
|
|
|
from.PlaySound(0x240);
|
|
|
|
from.SendLocalizedMessage(502237); // You place the empty bottle in your backpack.
|
|
|
|
pot.Consume(toHold);
|
|
|
|
if (!pot.Deleted)
|
|
{
|
|
pot.Bounce(from);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
from.SendLocalizedMessage(502238); // You don't have room for the empty bottle in your backpack.
|
|
return false;
|
|
}
|
|
|
|
public static bool GiveBottle(Mobile m, int amount)
|
|
{
|
|
var pack = m.Backpack;
|
|
|
|
var bottle = new Bottle(amount);
|
|
|
|
if (pack?.TryDropItem(m, bottle, false) != true)
|
|
{
|
|
bottle.Delete();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static BasePotion FillBottle(PotionEffect effect) =>
|
|
effect switch
|
|
{
|
|
PotionEffect.CureLesser => new LesserCurePotion(),
|
|
PotionEffect.Cure => new CurePotion(),
|
|
PotionEffect.CureGreater => new GreaterCurePotion(),
|
|
PotionEffect.Agility => new AgilityPotion(),
|
|
PotionEffect.AgilityGreater => new GreaterAgilityPotion(),
|
|
PotionEffect.Strength => new StrengthPotion(),
|
|
PotionEffect.StrengthGreater => new GreaterStrengthPotion(),
|
|
PotionEffect.PoisonLesser => new LesserPoisonPotion(),
|
|
PotionEffect.Poison => new PoisonPotion(),
|
|
PotionEffect.PoisonGreater => new GreaterPoisonPotion(),
|
|
PotionEffect.PoisonDeadly => new DeadlyPoisonPotion(),
|
|
PotionEffect.Refresh => new RefreshPotion(),
|
|
PotionEffect.RefreshTotal => new TotalRefreshPotion(),
|
|
PotionEffect.HealLesser => new LesserHealPotion(),
|
|
PotionEffect.Heal => new HealPotion(),
|
|
PotionEffect.HealGreater => new GreaterHealPotion(),
|
|
PotionEffect.ExplosionLesser => new LesserExplosionPotion(),
|
|
PotionEffect.Explosion => new ExplosionPotion(),
|
|
PotionEffect.ExplosionGreater => new GreaterExplosionPotion(),
|
|
PotionEffect.Conflagration => new ConflagrationPotion(),
|
|
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()
|
|
};
|
|
|
|
// Function to convert potion type to potion effect enum
|
|
public static PotionEffect GetPotionEffect(Type type) =>
|
|
type switch
|
|
{
|
|
_ when type == typeof(LesserCurePotion) => PotionEffect.CureLesser,
|
|
_ when type == typeof(CurePotion) => PotionEffect.Cure,
|
|
_ when type == typeof(GreaterCurePotion) => PotionEffect.CureGreater,
|
|
_ when type == typeof(AgilityPotion) => PotionEffect.Agility,
|
|
_ when type == typeof(GreaterAgilityPotion) => PotionEffect.AgilityGreater,
|
|
_ when type == typeof(StrengthPotion) => PotionEffect.Strength,
|
|
_ when type == typeof(GreaterStrengthPotion) => PotionEffect.StrengthGreater,
|
|
_ when type == typeof(LesserPoisonPotion) => PotionEffect.PoisonLesser,
|
|
_ when type == typeof(PoisonPotion) => PotionEffect.Poison,
|
|
_ when type == typeof(GreaterPoisonPotion) => PotionEffect.PoisonGreater,
|
|
_ when type == typeof(DeadlyPoisonPotion) => PotionEffect.PoisonDeadly,
|
|
_ when type == typeof(RefreshPotion) => PotionEffect.Refresh,
|
|
_ when type == typeof(TotalRefreshPotion) => PotionEffect.RefreshTotal,
|
|
_ when type == typeof(LesserHealPotion) => PotionEffect.HealLesser,
|
|
_ when type == typeof(HealPotion) => PotionEffect.Heal,
|
|
_ when type == typeof(GreaterHealPotion) => PotionEffect.HealGreater,
|
|
_ when type == typeof(LesserExplosionPotion) => PotionEffect.ExplosionLesser,
|
|
_ when type == typeof(ExplosionPotion) => PotionEffect.Explosion,
|
|
_ when type == typeof(GreaterExplosionPotion) => PotionEffect.ExplosionGreater,
|
|
_ when type == typeof(ConflagrationPotion) => PotionEffect.Conflagration,
|
|
_ when type == typeof(GreaterConflagrationPotion) => PotionEffect.ConflagrationGreater,
|
|
_ when type == typeof(ConfusionBlastPotion) => PotionEffect.ConfusionBlast,
|
|
_ when type == typeof(GreaterConfusionBlastPotion) => PotionEffect.ConfusionBlastGreater,
|
|
_ when type == typeof(InvisibilityPotion) => PotionEffect.Invisibility,
|
|
_ when type == typeof(ParasiticPotion) => PotionEffect.Parasitic,
|
|
_ when type == typeof(DarkglowPotion) => PotionEffect.Darkglow,
|
|
_ => PotionEffect.Nightsight
|
|
};
|
|
}
|