//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic;
using System.ComponentModel;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.Character;
using Nito.AsyncEx.Synchronous;
///
/// The implementation of the skill list, which automatically adds the passive skill power ups to the player.
///
public sealed class SkillList : ISkillList, IDisposable
{
private const ushort DurabilityReduction1SkillId = 300;
private const ushort DurabilityReduction1FistMasterSkillId = 578;
private const short TwistingSlashMasterySkillId = 332;
private const short RagefulBlowMasterySkillId = 333;
private readonly short[] _castedSkillsWithPassiveBoost = [TwistingSlashMasterySkillId, RagefulBlowMasterySkillId];
private readonly IDictionary _availableSkills;
private readonly ICollection _learnedSkills;
private readonly ICollection _itemSkills;
private readonly Player _player;
private List? _passivePowerUps;
///
/// Initializes a new instance of the class.
///
/// The player.
public SkillList(Player player)
{
if (player.SelectedCharacter is null)
{
throw new ArgumentException("SelectedCharacter must be set.");
}
if (player.Inventory is null)
{
throw new ArgumentException("Inventory must be set.");
}
this._player = player;
this._learnedSkills = this._player.SelectedCharacter.LearnedSkills ?? new List();
this._learnedSkills.Where(entry => entry.Skill is null).ForEach(entry => throw Error.NotInitializedProperty(entry, nameof(entry.Skill)));
this._availableSkills = this._learnedSkills.ToDictionary(skillEntry => skillEntry.Skill!.Number.ToUnsigned());
this._itemSkills = new List();
this._player.Inventory.EquippedItems
.Where(item => item.HasSkill)
.Where(item => (item.Definition ?? throw Error.NotInitializedProperty(item, nameof(item.Definition))).Skill != null)
.ForEach(item => this.AddItemSkillAsync(item.Definition!.Skill!).AsTask().WaitAndUnwrapException());
this._player.Inventory.EquippedItemsChanged += this.Inventory_WearingItemsChangedAsync;
foreach (var skill in this._learnedSkills
.Where(s => s.Skill!.SkillType == SkillType.PassiveBoost || this._castedSkillsWithPassiveBoost.Contains(s.Skill.Number)))
{
this.CreatePowerUpForPassiveSkill(skill);
}
}
///
public IEnumerable Skills => this._availableSkills.Values;
///
public byte SkillCount => (byte)this._availableSkills.Count;
private List PassivePowerUps => this._passivePowerUps ??= new();
///
public void Dispose()
{
this._passivePowerUps?.ForEach(p => p.Dispose());
this._passivePowerUps = null;
}
///
public SkillEntry? GetSkill(ushort skillId)
{
this._availableSkills.TryGetValue(skillId, out var result);
return result;
}
///
public async ValueTask AddLearnedSkillAsync(Skill skill)
{
var skillEntry = this._player.PersistenceContext.CreateNew();
skillEntry.Skill = skill;
skillEntry.Level = 0;
await this.AddLearnedSkillAsync(skillEntry).ConfigureAwait(false);
if (skill.MasterDefinition?.ReplacedSkill is { } replacedSkill)
{
await this._player.InvokeViewPlugInAsync(p => p.RemoveSkillAsync(replacedSkill)).ConfigureAwait(false);
}
}
///
public async ValueTask RemoveItemSkillAsync(ushort skillId)
{
this._availableSkills.TryGetValue(skillId, out var skillEntry);
if (skillEntry is null)
{
return false;
}
// We need to take into account that we there might be multiple items equipped with the same skill
var skillRemoved = this._itemSkills.Remove(skillEntry);
if (skillRemoved && this._itemSkills.All(s => s.Skill!.Number != skillId))
{
await this._player.InvokeViewPlugInAsync(p => p.RemoveSkillAsync(skillEntry.Skill!)).ConfigureAwait(false);
this._availableSkills.Remove(skillId);
}
return true;
}
///
public bool ContainsSkill(ushort skillId)
{
return this._availableSkills.ContainsKey(skillId);
}
private async ValueTask AddItemSkillAsync(Skill skill)
{
// If character is not selected (e.g., during disconnect cleanup), skill list doesn't need to be updated
if (this._player.SelectedCharacter is null)
{
return;
}
if (!skill.QualifiedCharacters.Contains(this._player.SelectedCharacter.CharacterClass!))
{
return;
}
var skillEntry = new SkillEntry
{
Skill = skill,
Level = 0,
};
this._itemSkills.Add(skillEntry);
// Item skills are always level 0, so it doesn't matter which one is added to the dictionary.
if (!this.ContainsSkill((ushort)skill.Number))
{
this._availableSkills.Add(skill.Number.ToUnsigned(), skillEntry);
await this._player.InvokeViewPlugInAsync(p => p.AddSkillAsync(skill)).ConfigureAwait(false);
}
}
private async ValueTask AddLearnedSkillAsync(SkillEntry skill)
{
this._availableSkills.Add(skill.Skill!.Number.ToUnsigned(), skill);
this._learnedSkills.Add(skill);
if (skill.Skill.SkillType == SkillType.PassiveBoost || this._castedSkillsWithPassiveBoost.Contains(skill.Skill.Number))
{
this.CreatePowerUpForPassiveSkill(skill);
}
if (skill.Skill.SkillType != SkillType.PassiveBoost)
{
await this._player.InvokeViewPlugInAsync(p => p.AddSkillAsync(skill.Skill)).ConfigureAwait(false);
}
}
private void CreatePowerUpForPassiveSkill(SkillEntry skillEntry)
{
this.CreatePowerUpWrappers(skillEntry);
}
private void CreatePowerUpWrappers(SkillEntry skillEntry)
{
var masterDefinition = skillEntry.Skill!.MasterDefinition;
if (masterDefinition is null)
{
return;
}
if (masterDefinition.TargetAttribute is null)
{
// log?
return;
}
var passiveBoost = new PassiveSkillBoostPowerUp(skillEntry);
this.PassivePowerUps.Add(passiveBoost);
this.PassivePowerUps.Add(new PowerUpWrapper(passiveBoost, masterDefinition.TargetAttribute, this._player.Attributes!));
if (skillEntry.Skill.Number == DurabilityReduction1SkillId || skillEntry.Skill.Number == DurabilityReduction1FistMasterSkillId)
{
var durabilityReductionFactorBoost = new PassiveSkillBoostPowerUp(skillEntry, true);
this.PassivePowerUps.Add(durabilityReductionFactorBoost);
this.PassivePowerUps.Add(new PowerUpWrapper(durabilityReductionFactorBoost, Stats.DurabilityReductionFactor, this._player.Attributes!));
}
}
private async ValueTask Inventory_WearingItemsChangedAsync(ItemEventArgs eventArgs)
{
var item = eventArgs.Item;
if (!item.HasSkill || item.Definition?.Skill is null)
{
return;
}
var inventory = this._player.Inventory;
if (inventory!.EquippedItems.Contains(item))
{
await this.AddItemSkillAsync(item.Definition.Skill).ConfigureAwait(false);
}
else
{
await this.RemoveItemSkillAsync(item.Definition.Skill.Number.ToUnsigned()).ConfigureAwait(false);
}
}
private sealed class PassiveSkillBoostPowerUp : IElement, IDisposable
{
private readonly SkillEntry _skillEntry;
public PassiveSkillBoostPowerUp(SkillEntry skillEntry, bool isDurabilityReductionFactor = false)
{
this._skillEntry = skillEntry;
if (isDurabilityReductionFactor)
{
this.Value = -this._skillEntry.Level / 500f;
this.AggregateType = AggregateType.AddRaw;
this._skillEntry.PropertyChanged += this.OnDurabilityReductionSkillEntryOnPropertyChanged;
}
else
{
this.Value = this._skillEntry.CalculateValue();
this.AggregateType = this._skillEntry.Skill!.MasterDefinition!.Aggregation;
this._skillEntry.PropertyChanged += this.OnSkillEntryOnPropertyChanged;
}
}
public event EventHandler? ValueChanged;
public float Value { get; private set; }
public AggregateType AggregateType { get; }
public void Dispose()
{
this._skillEntry.PropertyChanged -= this.OnSkillEntryOnPropertyChanged;
this._skillEntry.PropertyChanged -= this.OnDurabilityReductionSkillEntryOnPropertyChanged;
}
///
public override string ToString()
{
return $"{this.Value} ({this.AggregateType})";
}
private void OnSkillEntryOnPropertyChanged(object? sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == nameof(SkillEntry.Level))
{
this.Value = this._skillEntry.CalculateValue();
this.ValueChanged?.Invoke(this, EventArgs.Empty);
}
}
private void OnDurabilityReductionSkillEntryOnPropertyChanged(object? sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == nameof(SkillEntry.Level))
{
this.Value = -this._skillEntry.Level / 500f;
this.ValueChanged?.Invoke(this, EventArgs.Empty);
}
}
}
}