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