## 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
247 lines
5.6 KiB
C#
247 lines
5.6 KiB
C#
using System;
|
|
using ModernUO.Serialization;
|
|
using Server.Engines.ConPVP;
|
|
using Server.Engines.Craft;
|
|
|
|
namespace Server.Items;
|
|
|
|
public enum PotionEffect
|
|
{
|
|
Nightsight,
|
|
CureLesser,
|
|
Cure,
|
|
CureGreater,
|
|
Agility,
|
|
AgilityGreater,
|
|
Strength,
|
|
StrengthGreater,
|
|
PoisonLesser,
|
|
Poison,
|
|
PoisonGreater,
|
|
PoisonDeadly,
|
|
Refresh,
|
|
RefreshTotal,
|
|
HealLesser,
|
|
Heal,
|
|
HealGreater,
|
|
ExplosionLesser,
|
|
Explosion,
|
|
ExplosionGreater,
|
|
Conflagration,
|
|
ConflagrationGreater,
|
|
MaskOfDeath, // Mask of Death is not available in OSI but does exist in cliloc files
|
|
MaskOfDeathGreater, // included in enumeration for compatibility if later enabled by OSI
|
|
ConfusionBlast,
|
|
ConfusionBlastGreater,
|
|
Invisibility,
|
|
Parasitic,
|
|
Darkglow,
|
|
FlintsPungentBrew
|
|
}
|
|
|
|
[SerializationGenerator(2, false)]
|
|
public abstract partial class BasePotion : Item, ICraftable, ICommodity
|
|
{
|
|
[InvalidateProperties]
|
|
[SerializableField(0)]
|
|
private PotionEffect _potionEffect;
|
|
|
|
public BasePotion(int itemID, PotionEffect effect) : base(itemID)
|
|
{
|
|
_potionEffect = effect;
|
|
|
|
Stackable = Core.ML;
|
|
}
|
|
|
|
public override double DefaultWeight => 1.0;
|
|
|
|
public override int LabelNumber => 1041314 + (int)_potionEffect;
|
|
|
|
public virtual bool RequireFreeHand => true;
|
|
|
|
public virtual bool IsThrowablePotion => false;
|
|
|
|
int ICommodity.DescriptionNumber => LabelNumber;
|
|
bool ICommodity.IsDeedable => Core.ML;
|
|
|
|
public int OnCraft(
|
|
int quality,
|
|
bool makersMark,
|
|
Mobile from,
|
|
CraftSystem craftSystem,
|
|
Type typeRes,
|
|
BaseTool tool,
|
|
CraftItem craftItem,
|
|
int resHue
|
|
)
|
|
{
|
|
if (craftSystem is DefAlchemy)
|
|
{
|
|
var pack = from.Backpack;
|
|
|
|
if (pack != null)
|
|
{
|
|
if ((int)PotionEffect >= (int)PotionEffect.Invisibility)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
using var queue = pack.EnumerateItemsByType<PotionKeg>();
|
|
foreach (var keg in queue)
|
|
{
|
|
if (keg.Held is <= 0 or >= 100)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (keg.Type != PotionEffect)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
++keg.Held;
|
|
|
|
Consume();
|
|
from.AddToBackpack(new Bottle());
|
|
|
|
return -1; // signal placed in keg
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
public static bool HasFreeHand(Mobile m)
|
|
{
|
|
var handOne = m.FindItemOnLayer(Layer.OneHanded);
|
|
var handTwo = m.FindItemOnLayer(Layer.TwoHanded);
|
|
|
|
if (handTwo is BaseWeapon)
|
|
{
|
|
handOne = handTwo;
|
|
}
|
|
|
|
if (handTwo is BaseRanged ranged && ranged.Balanced)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return handOne == null || handTwo == null;
|
|
}
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
if (!CanDrink(from))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var pot = this;
|
|
|
|
if (IsThrowablePotion && Amount > 1)
|
|
{
|
|
pot = GetType().CreateInstance<BasePotion>();
|
|
|
|
Amount--;
|
|
|
|
if (from.Backpack?.Deleted == false)
|
|
{
|
|
from.Backpack.DropItem(pot);
|
|
}
|
|
else
|
|
{
|
|
pot.MoveToWorld(from.Location, from.Map);
|
|
}
|
|
}
|
|
|
|
pot.Drink(from);
|
|
}
|
|
|
|
private void Deserialize(IGenericReader reader, int version)
|
|
{
|
|
_potionEffect = (PotionEffect)reader.ReadInt();
|
|
}
|
|
|
|
public virtual bool CanDrink(Mobile from)
|
|
{
|
|
if (!Movable)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!from.InRange(GetWorldLocation(), 1))
|
|
{
|
|
from.SendLocalizedMessage(502138); // That is too far away for you to use
|
|
return false;
|
|
}
|
|
|
|
if (RequireFreeHand && !HasFreeHand(from))
|
|
{
|
|
from.SendLocalizedMessage(502172); // You must have a free hand to drink a potion.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public abstract void Drink(Mobile from);
|
|
|
|
public void PlayDrinkEffect(Mobile m)
|
|
{
|
|
m.RevealingAction();
|
|
m.PlaySound(0x2D6);
|
|
|
|
if (!DuelContext.IsFreeConsume(m))
|
|
{
|
|
Consume();
|
|
m.AddToBackpack(new Bottle());
|
|
}
|
|
|
|
if (m.Body.IsHuman && !m.Mounted)
|
|
{
|
|
m.Animate(34, 5, 1, true, false, 0);
|
|
}
|
|
}
|
|
|
|
public static int EnhancePotions(Mobile m)
|
|
{
|
|
var EP = AosAttributes.GetValue(m, AosAttribute.EnhancePotions);
|
|
var skillBonus = (int)(m.Skills.Alchemy.Value * 10 / 33);
|
|
|
|
if (Core.ML && EP > 50 && m.AccessLevel <= AccessLevel.Player)
|
|
{
|
|
EP = 50;
|
|
}
|
|
|
|
return EP + skillBonus;
|
|
}
|
|
|
|
public static TimeSpan Scale(Mobile m, TimeSpan v)
|
|
{
|
|
if (!Core.AOS)
|
|
{
|
|
return v;
|
|
}
|
|
|
|
return v * (1.0 + 0.01 * EnhancePotions(m));
|
|
}
|
|
|
|
public static double Scale(Mobile m, double v)
|
|
{
|
|
if (!Core.AOS)
|
|
{
|
|
return v;
|
|
}
|
|
|
|
var scalar = 1.0 + 0.01 * EnhancePotions(m);
|
|
|
|
return v * scalar;
|
|
}
|
|
|
|
public static int Scale(Mobile m, int v) => !Core.AOS ? v : AOS.Scale(v, 100 + EnhancePotions(m));
|
|
|
|
public override bool StackWith(Mobile from, Item dropped, bool playSound) =>
|
|
dropped is BasePotion potion && potion._potionEffect == _potionEffect &&
|
|
base.StackWith(from, potion, playSound);
|
|
}
|