baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
283
src/GameLogic/SkillList.cs
Normal file
283
src/GameLogic/SkillList.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
// <copyright file="SkillList.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of the skill list, which automatically adds the passive skill power ups to the player.
|
||||
/// </summary>
|
||||
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<ushort, SkillEntry> _availableSkills;
|
||||
|
||||
private readonly ICollection<SkillEntry> _learnedSkills;
|
||||
|
||||
private readonly ICollection<SkillEntry> _itemSkills;
|
||||
|
||||
private readonly Player _player;
|
||||
|
||||
private List<IDisposable>? _passivePowerUps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkillList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
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<SkillEntry>();
|
||||
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<SkillEntry>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<SkillEntry> Skills => this._availableSkills.Values;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte SkillCount => (byte)this._availableSkills.Count;
|
||||
|
||||
private List<IDisposable> PassivePowerUps => this._passivePowerUps ??= new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._passivePowerUps?.ForEach(p => p.Dispose());
|
||||
this._passivePowerUps = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SkillEntry? GetSkill(ushort skillId)
|
||||
{
|
||||
this._availableSkills.TryGetValue(skillId, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask AddLearnedSkillAsync(Skill skill)
|
||||
{
|
||||
var skillEntry = this._player.PersistenceContext.CreateNew<SkillEntry>();
|
||||
skillEntry.Skill = skill;
|
||||
skillEntry.Level = 0;
|
||||
await this.AddLearnedSkillAsync(skillEntry).ConfigureAwait(false);
|
||||
|
||||
if (skill.MasterDefinition?.ReplacedSkill is { } replacedSkill)
|
||||
{
|
||||
await this._player.InvokeViewPlugInAsync<ISkillListViewPlugIn>(p => p.RemoveSkillAsync(replacedSkill)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> 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<ISkillListViewPlugIn>(p => p.RemoveSkillAsync(skillEntry.Skill!)).ConfigureAwait(false);
|
||||
this._availableSkills.Remove(skillId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<ISkillListViewPlugIn>(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<ISkillListViewPlugIn>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user