//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic;
using System.Collections;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Views.World;
using Nito.AsyncEx;
///
/// The list of magic effects of a player instance. Automatically applies the power-ups of the effects to the player.
///
public class MagicEffectsList : AsyncDisposable
{
private const byte InvisibleEffectStartIndex = 200;
private readonly BitArray _contains = new(0x100);
private readonly IAttackable _owner;
private readonly AsyncLock _addLock = new();
///
/// Initializes a new instance of the class.
///
/// The attackable which owns this list.
public MagicEffectsList(IAttackable owner)
{
this._owner = owner;
this.ActiveEffects = new SortedList(6);
}
///
/// Gets the active effects.
///
public IDictionary ActiveEffects { get; }
///
/// Gets the active visible effect ids.
///
public IList VisibleEffects => this.ActiveEffects.Values.Where(me => me.Definition.InformObservers).ToList();
///
/// Adds the effect and applies the power ups.
///
/// The effect.
public async ValueTask AddEffectAsync(MagicEffect effect)
{
bool added = false;
using (await this._addLock.LockAsync())
{
if (this._contains[effect.Id])
{
this.UpdateEffect(effect);
}
else
{
added = true;
this.ActiveEffects.Add(effect.Id, effect);
this._contains[effect.Id] = true;
foreach (var powerUp in effect.PowerUpElements)
{
this._owner.Attributes.AddElement(powerUp.Element, powerUp.Target);
}
}
}
if (added)
{
effect.EffectTimeOut += this.OnEffectTimeOutAsync;
if (effect.Id < InvisibleEffectStartIndex && this._owner is IWorldObserver observer)
{
await observer.InvokeViewPlugInAsync(p => p.ActivateMagicEffectAsync(effect, this._owner)).ConfigureAwait(false);
}
if (effect.Id < InvisibleEffectStartIndex && effect.Definition.InformObservers && this._owner is IObservable observable)
{
await observable.ForEachWorldObserverAsync(p => p.ActivateMagicEffectAsync(effect, this._owner), false).ConfigureAwait(false);
}
}
else
{
effect.Dispose();
}
}
///
/// Clears all active effects.
///
public async ValueTask ClearAllEffectsAsync()
{
while (this.ActiveEffects.Any())
{
await this.ActiveEffects.Values.First().DisposeAsync().ConfigureAwait(false);
}
}
///
/// Clear the effects that produce a specific stat.
///
/// The stat produced by effect.
public async ValueTask ClearAllEffectsProducingSpecificStatAsync(AttributeDefinition stat)
{
var effects = this.ActiveEffects.Values.ToArray();
foreach (var effect in effects)
{
if (effect.PowerUpElements.Any(p => p.Target == stat))
{
await effect.DisposeAsync().ConfigureAwait(false);
}
}
}
///
/// Clears the effects after death of the player.
///
public async ValueTask ClearEffectsAfterDeathAsync()
{
var effectsToRemove = this.ActiveEffects.Values.Where(effect => effect.Definition.StopByDeath).ToList();
foreach (var effect in effectsToRemove)
{
await effect.DisposeAsync().ConfigureAwait(false);
}
}
///
/// Tries to get the currently active effect of the specified .
///
/// The .
/// The effect, if found.
public async ValueTask TryGetActiveEffectOfSubTypeAsync(byte subType)
{
using var l = await this._addLock.LockAsync();
return this.ActiveEffects.Values.FirstOrDefault(e => e.Definition.SubType == subType);
}
///
protected override async ValueTask DisposeAsyncCore()
{
await this.ClearAllEffectsAsync().ConfigureAwait(false);
await base.DisposeAsyncCore().ConfigureAwait(false);
}
private async ValueTask OnEffectTimeOutAsync(MagicEffect effect)
{
using (await this._addLock.LockAsync())
{
this.ActiveEffects.Remove(effect.Id);
this._contains[effect.Id] = false;
}
foreach (var powerUp in effect.PowerUpElements)
{
this._owner.Attributes.RemoveElement(powerUp.Element, powerUp.Target);
}
if (effect.Id >= InvisibleEffectStartIndex)
{
return;
}
(this._owner as IWorldObserver)?.InvokeViewPlugInAsync(p => p.DeactivateMagicEffectAsync(effect, this._owner));
if (effect.Definition.InformObservers && this._owner.IsAlive)
{
(this._owner as IObservable)?.ForEachWorldObserverAsync(p => p.DeactivateMagicEffectAsync(effect, this._owner), false);
}
}
///
/// Updates the effect.
///
/// The effect.
private void UpdateEffect(MagicEffect effect)
{
MagicEffect magicEffect = this.ActiveEffects[effect.Id];
if (magicEffect.Value > effect.Value)
{
// no de-buffing allowed
return;
}
//// GMO behaviour would be: RemoveEffect(magicEffect.Id); AddEffectAsync(effect);
//// I change the existing Timer and Buff Value, without removing the effect itself.
//// This doesn't only save traffic, it also looks better in game.
magicEffect.Duration = effect.Duration;
magicEffect.ResetTimer();
if (magicEffect.PowerUpElements.Select(e => e.Element)
.SequenceEqual(effect.PowerUpElements.Select(e => e.Element)))
{
// if the effect power ups are the same, we can leave it like that
return;
}
foreach (var powerUp in magicEffect.PowerUpElements)
{
this._owner.Attributes.RemoveElement(powerUp.Element, powerUp.Target);
}
magicEffect.PowerUpElements = effect.PowerUpElements;
foreach (var powerUp in magicEffect.PowerUpElements)
{
this._owner.Attributes.AddElement(powerUp.Element, powerUp.Target);
}
}
}