## 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
218 lines
5.9 KiB
C#
218 lines
5.9 KiB
C#
using Server.Engines.PartySystem;
|
|
using Server.Items;
|
|
using Server.Spells.Fourth;
|
|
using Server.Spells.Necromancy;
|
|
using Server.Targeting;
|
|
using Server.Collections;
|
|
using Server.Engines.BuffIcons;
|
|
using Server.Mobiles;
|
|
|
|
namespace Server.Spells.Mysticism;
|
|
|
|
public class CleansingWindsSpell : MysticSpell, ITargetingSpell<Mobile>
|
|
{
|
|
public override SpellCircle Circle => SpellCircle.Sixth;
|
|
|
|
private static readonly SpellInfo _info = new(
|
|
"Cleansing Winds", "In Vas Mani Hur",
|
|
230,
|
|
9022,
|
|
Reagent.Garlic,
|
|
Reagent.Ginseng,
|
|
Reagent.MandrakeRoot,
|
|
Reagent.DragonsBlood
|
|
);
|
|
|
|
public CleansingWindsSpell(Mobile caster, Item scroll) : base(caster, scroll, _info)
|
|
{
|
|
}
|
|
|
|
public override void OnCast()
|
|
{
|
|
Caster.Target = new SpellTarget<Mobile>(this, TargetFlags.Beneficial);
|
|
}
|
|
|
|
public void Target(Mobile m)
|
|
{
|
|
if (CheckBSequence(m))
|
|
{
|
|
/**
|
|
* Soothing winds attempt to neutralize poisons, lift curses,
|
|
* and heal a valid target and up to 3 party members.
|
|
*/
|
|
|
|
Caster.PlaySound(0x64C);
|
|
|
|
var primarySkill = GetBaseSkill(Caster);
|
|
var secondarySkill = GetDamageSkill(Caster);
|
|
var cureChance = 10000 + (int)((primarySkill + secondarySkill) / 2 * 75);
|
|
|
|
using var pool = PooledRefQueue<Mobile>.Create();
|
|
pool.Enqueue(m);
|
|
|
|
var casterParty = Party.Get(Caster);
|
|
if (casterParty != null)
|
|
{
|
|
foreach (var mob in Caster.Map.GetMobilesInRange(m.Location, 2))
|
|
{
|
|
if (mob != m && casterParty.Contains(mob) && Caster.CanBeBeneficial(mob, false))
|
|
{
|
|
pool.Enqueue(mob);
|
|
if (pool.Count == 4)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var toHeal = ((int)((primarySkill + secondarySkill) / 4.0) + Utility.RandomMinMax(-3, 3)) / pool.Count;
|
|
|
|
while (pool.Count > 0)
|
|
{
|
|
var target = pool.Dequeue();
|
|
|
|
Caster.DoBeneficial(target);
|
|
|
|
target.FixedParticles(0x3709, 1, 30, 9963, 13, 3, EffectLayer.Head);
|
|
|
|
var from = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z - 10), target.Map);
|
|
var to = new Entity(Serial.Zero, new Point3D(target.X, target.Y, target.Z + 50), target.Map);
|
|
Effects.SendMovingParticles(
|
|
from,
|
|
to,
|
|
0x2255,
|
|
1,
|
|
0,
|
|
false,
|
|
false,
|
|
13,
|
|
3,
|
|
9501,
|
|
1,
|
|
0,
|
|
EffectLayer.Head,
|
|
0x100
|
|
);
|
|
|
|
var toHealMod = toHeal;
|
|
|
|
if (target.Poisoned)
|
|
{
|
|
var poisonLevel = Poison.IncreaseLevel(target.Poison).Level;
|
|
var chanceToCure = cureChance - poisonLevel * 1750;
|
|
|
|
if (chanceToCure > 10000 || chanceToCure > Utility.Random(10000) && target.CurePoison(Caster))
|
|
{
|
|
toHealMod -= (int)(toHeal * poisonLevel * 0.15);
|
|
}
|
|
else
|
|
{
|
|
toHealMod = 0;
|
|
}
|
|
}
|
|
|
|
if (MortalStrike.IsWounded(target))
|
|
{
|
|
toHealMod = 0;
|
|
}
|
|
|
|
var curseLevel = RemoveCurses(target);
|
|
|
|
if (toHealMod > 0 && curseLevel > 0)
|
|
{
|
|
toHealMod -= curseLevel * 3;
|
|
toHealMod -= (int)(toHealMod * (curseLevel / 100.0));
|
|
}
|
|
|
|
if (toHealMod > 0)
|
|
{
|
|
SpellHelper.Heal(toHealMod, target, Caster);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static int RemoveCurses(Mobile m)
|
|
{
|
|
var curseLevel = 0;
|
|
|
|
// if (SleepSpell.EndSleep(m))
|
|
// {
|
|
// curseLevel += 2;
|
|
// }
|
|
|
|
if (EvilOmenSpell.EndEffect(m))
|
|
{
|
|
curseLevel += 1;
|
|
}
|
|
|
|
if (StrangleSpell.RemoveCurse(m))
|
|
{
|
|
curseLevel += 2;
|
|
}
|
|
|
|
if (CorpseSkinSpell.RemoveCurse(m))
|
|
{
|
|
curseLevel += 3;
|
|
}
|
|
|
|
if (CurseSpell.RemoveEffect(m))
|
|
{
|
|
curseLevel += 4;
|
|
}
|
|
|
|
if (BloodOathSpell.RemoveCurse(m))
|
|
{
|
|
curseLevel += 3;
|
|
}
|
|
|
|
if (MindRotSpell.ClearMindRotScalar(m))
|
|
{
|
|
curseLevel += 2;
|
|
}
|
|
|
|
if (SpellPlagueSpell.RemoveEffect(m))
|
|
{
|
|
curseLevel += 4;
|
|
}
|
|
|
|
var mod = m.GetStatMod("[Magic] Str Curse");
|
|
if (mod?.Offset < 0)
|
|
{
|
|
m.RemoveStatMod("[Magic] Str Curse");
|
|
}
|
|
|
|
mod = m.GetStatMod("[Magic] Dex Curse");
|
|
if (mod?.Offset < 0)
|
|
{
|
|
m.RemoveStatMod("[Magic] Dex Curse");
|
|
}
|
|
|
|
mod = m.GetStatMod("[Magic] Int Curse");
|
|
if (mod?.Offset < 0)
|
|
{
|
|
m.RemoveStatMod("[Magic] Int Curse");
|
|
}
|
|
|
|
if (MortalStrike.EndWound(m))
|
|
{
|
|
curseLevel += 2;
|
|
}
|
|
|
|
if (m is PlayerMobile pm)
|
|
{
|
|
pm.RemoveBuff(BuffIcon.Clumsy);
|
|
pm.RemoveBuff(BuffIcon.FeebleMind);
|
|
pm.RemoveBuff(BuffIcon.Weaken);
|
|
pm.RemoveBuff(BuffIcon.Curse);
|
|
pm.RemoveBuff(BuffIcon.MassCurse);
|
|
pm.RemoveBuff(BuffIcon.MortalStrike);
|
|
pm.RemoveBuff(BuffIcon.CorpseSkin);
|
|
pm.RemoveBuff(BuffIcon.Strangle);
|
|
pm.RemoveBuff(BuffIcon.EvilOmen);
|
|
}
|
|
|
|
return curseLevel;
|
|
}
|
|
}
|