// // 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); } } }