## 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
102 lines
3.2 KiB
C#
102 lines
3.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace Server;
|
|
|
|
public enum PoisonFamily { Standard, Darkglow, Parasitic }
|
|
|
|
public abstract class Poison : ISpanParsable<Poison>
|
|
{
|
|
public static List<Poison> Poisons { get; } = [];
|
|
public static Dictionary<string, Poison> 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 abstract PoisonFamily Family { get; }
|
|
|
|
public abstract Timer ConstructTimer(Mobile m);
|
|
|
|
public override string ToString() => Name;
|
|
|
|
public static void Register(Poison reg)
|
|
{
|
|
var regName = reg.Name;
|
|
|
|
for (var i = 0; i < Poisons.Count; i++)
|
|
{
|
|
var poison = Poisons[i];
|
|
if (reg.Index == poison.Index)
|
|
{
|
|
throw new Exception("A poison with that index already exists.");
|
|
}
|
|
|
|
if (GetPoison(regName) != null)
|
|
{
|
|
throw new Exception("A poison with that name already exists.");
|
|
}
|
|
}
|
|
|
|
Poisons.Add(reg);
|
|
PoisonsByName.Add(regName, reg);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Poison GetPoisonByIndex(int index) => index >= 0 && index < Poisons.Count ? Poisons[index] : null;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Poison IncreaseLevel(Poison oldPoison) =>
|
|
oldPoison == null ? null : GetPoisonByIndex(oldPoison.Index + 1) ?? oldPoison;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Poison GetPoison(ReadOnlySpan<char> name) =>
|
|
PoisonsByName.GetAlternateLookup<ReadOnlySpan<char>>().TryGetValue(name, out var poison) ? poison : null;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Poison Parse(string s) => Parse(s, null);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Poison Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool TryParse(string s, IFormatProvider provider, out Poison result) =>
|
|
TryParse(s.AsSpan(), provider, out result);
|
|
|
|
public static Poison Parse(ReadOnlySpan<char> s, IFormatProvider provider)
|
|
{
|
|
if (int.TryParse(s, provider, out var index))
|
|
{
|
|
var result = GetPoisonByIndex(index);
|
|
if (result != null)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
var poison = GetPoison(s.Trim());
|
|
if (poison == null)
|
|
{
|
|
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
|
}
|
|
|
|
return poison;
|
|
}
|
|
|
|
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider provider, out Poison result)
|
|
{
|
|
if (int.TryParse(s, provider, out var index))
|
|
{
|
|
result = GetPoisonByIndex(index);
|
|
if (result != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
result = GetPoison(s.Trim());
|
|
return result != null;
|
|
}
|
|
}
|