ModernUO/Projects/UOContent/Items/Weapons/SplinteringWeapon.cs

297 lines
8.2 KiB
C#

using System;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Collections;
using Server.Engines.BuffIcons;
using Server.Mobiles;
using Server.Network;
namespace Server.Items;
public static class SplinteringWeapon
{
internal const int DurabilityLoss = 10;
internal const int TickCount = 5;
internal static readonly TimeSpan ForcedWalkDuration = TimeSpan.FromSeconds(4.0);
internal static readonly TimeSpan BleedTickInterval = TimeSpan.FromSeconds(2.0);
internal static readonly TimeSpan PlayerImmunityDuration = TimeSpan.FromSeconds(15.0);
private static readonly Dictionary<(Mobile Attacker, Mobile Defender), SplinteringContext> _contexts = [];
private static readonly Dictionary<Mobile, TimerExecutionToken> _playerImmunity = [];
public static void Initialize()
{
EventSink.Logout += Clear;
EventSink.Movement += OnMovement;
}
public static bool TryProcOnEligibleHit(
Mobile attacker,
Mobile defender,
BaseWeapon weapon,
WeaponAbility ability,
int splinteringChance
)
{
if (!Core.SA || splinteringChance <= 0 || weapon == null || attacker == null || defender == null)
{
return false;
}
if (!IsValidCombatant(attacker) || !IsValidCombatant(defender))
{
return false;
}
if (IsExcludedAbility(ability) || IsPlayerImmune(defender))
{
return false;
}
if (splinteringChance <= Utility.Random(100))
{
return false;
}
var key = (attacker, defender);
if (_contexts.ContainsKey(key))
{
return false;
}
_contexts[key] = new SplinteringContext(attacker, defender);
if (defender is PlayerMobile)
{
AddPlayerImmunity(defender);
}
return true;
}
internal static bool IsForceWalking(Mobile defender)
{
if (defender == null)
{
return false;
}
foreach (var context in _contexts.Values)
{
if (context.Defender == defender && context.ForceWalking)
{
return true;
}
}
return false;
}
internal static bool IsPlayerImmune(Mobile defender) =>
defender is PlayerMobile && _playerImmunity.ContainsKey(defender);
internal static int ActiveContextCount => _contexts.Count;
internal static bool EndForceWalkForTests(Mobile attacker, Mobile defender)
{
if (attacker == null || defender == null || !_contexts.TryGetValue((attacker, defender), out var context))
{
return false;
}
context.EndForceWalk();
return true;
}
internal static bool TickForTests(Mobile attacker, Mobile defender)
{
if (attacker == null || defender == null || !_contexts.TryGetValue((attacker, defender), out var context))
{
return false;
}
context.OnBleedTick();
return true;
}
internal static void ExpirePlayerImmunityForTests(Mobile mobile) => RemovePlayerImmunity(mobile);
internal static void ClearAll()
{
foreach (var context in _contexts.Values)
{
context.Stop();
}
_contexts.Clear();
foreach (var token in _playerImmunity.Values)
{
token.Cancel();
}
_playerImmunity.Clear();
}
internal static void ClearEffects(Mobile mobile)
{
if (mobile == null)
{
return;
}
using var pairs = PooledRefList<(Mobile Attacker, Mobile Defender)>.Create();
foreach (var pair in _contexts.Keys)
{
if (pair.Attacker == mobile || pair.Defender == mobile)
{
pairs.Add(pair);
}
}
for (var i = 0; i < pairs.Count; i++)
{
StopContext(pairs[i]);
}
}
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
[OnEvent(nameof(PlayerMobile.PlayerDeletedEvent))]
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
public static void Clear(Mobile mobile)
{
ClearEffects(mobile);
RemovePlayerImmunity(mobile);
}
internal static void OnMovement(MovementEventArgs e)
{
if ((e.Direction & Direction.Running) != 0 && IsForceWalking(e.Mobile) && e.Mobile.AccessLevel < AccessLevel.GameMaster)
{
e.Blocked = true;
}
}
private static bool IsExcludedAbility(WeaponAbility ability) => ability is Disarm or InfectiousStrike;
private static bool IsValidCombatant(Mobile mobile) =>
mobile is { Deleted: false, Alive: true } && mobile.Map != null && mobile.Map != Map.Internal;
private static void AddPlayerImmunity(Mobile defender)
{
if (defender is not PlayerMobile || _playerImmunity.ContainsKey(defender))
{
return;
}
Timer.StartTimer(PlayerImmunityDuration, () => RemovePlayerImmunity(defender), out var token);
_playerImmunity[defender] = token;
}
private static void RemovePlayerImmunity(Mobile mobile)
{
if (mobile != null && _playerImmunity.Remove(mobile, out var token))
{
token.Cancel();
}
}
private static void StopContext((Mobile Attacker, Mobile Defender) key)
{
if (_contexts.Remove(key, out var context))
{
context.Stop();
}
}
private sealed class SplinteringContext
{
private readonly Mobile _attacker;
private readonly Mobile _defender;
private TimerExecutionToken _forcedWalkTimerToken;
private TimerExecutionToken _bleedTimerToken;
private int _bleedLevel = TickCount;
public SplinteringContext(Mobile attacker, Mobile defender)
{
_attacker = attacker;
_defender = defender;
ForceWalking = true;
StartForceWalk();
defender.SendLocalizedMessage(1112486); // A shard of the brittle weapon has become lodged in you!
attacker.SendLocalizedMessage(1113077); // A shard of your blade breaks off and sticks in your opponent!
Effects.PlaySound(defender.Location, defender.Map, 0x1DF);
if (defender is PlayerMobile pm && BuffInfo.Enabled)
{
pm.AddBuff(new BuffInfo(BuffIcon.Unknown2, 1154670, 1152144, ForcedWalkDuration));
}
Timer.StartTimer(ForcedWalkDuration, EndForceWalk, out _forcedWalkTimerToken);
Timer.StartTimer(BleedTickInterval, BleedTickInterval, OnBleedTick, out _bleedTimerToken);
}
public Mobile Defender => _defender;
public bool ForceWalking { get; private set; }
public void Stop()
{
_forcedWalkTimerToken.Cancel();
_bleedTimerToken.Cancel();
EndForceWalk();
}
private void StartForceWalk()
{
if (_defender.NetState != null && _defender.AccessLevel < AccessLevel.GameMaster)
{
_defender.NetState.SendSpeedControl(SpeedControlSetting.Walk);
}
}
public void EndForceWalk()
{
if (!ForceWalking)
{
return;
}
ForceWalking = false;
if (_defender.NetState != null)
{
_defender.NetState.SendSpeedControl(SpeedControlSetting.Disable);
}
if (_defender is PlayerMobile pm && BuffInfo.Enabled)
{
pm.RemoveBuff(BuffIcon.Unknown2);
}
_defender.SendLocalizedMessage(1112487); // The shard is successfully removed.
}
public void OnBleedTick()
{
if (!IsValidCombatant(_attacker) || !IsValidCombatant(_defender))
{
StopContext((_attacker, _defender));
return;
}
BleedAttack.DoBleed(_defender, _attacker, _bleedLevel, bloodDrinker: false);
_bleedLevel--;
if (_bleedLevel <= 0)
{
StopContext((_attacker, _defender));
}
}
}
}