Skip to content

Commit

Permalink
Merge pull request #547 from MUnique/dev/refactor-stats-update
Browse files Browse the repository at this point in the history
Refactored stats update
  • Loading branch information
sven-n authored Dec 8, 2024
2 parents f057288 + 2f3a86d commit 9442bf1
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 203 deletions.
38 changes: 36 additions & 2 deletions src/AttributeSystem/AttributeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace MUnique.OpenMU.AttributeSystem;

using System.Collections;

/// <summary>
/// The attribute system which holds all attributes of a character.
/// </summary>
public class AttributeSystem : IAttributeSystem
public class AttributeSystem : IAttributeSystem, IEnumerable<IAttribute>
{
private readonly IDictionary<AttributeDefinition, IAttribute> _attributes = new Dictionary<AttributeDefinition, IAttribute>();

Expand All @@ -16,7 +18,7 @@ public class AttributeSystem : IAttributeSystem
/// </summary>
/// <param name="statAttributes">The stat attributes. These attributes are added just as-is and are not wrapped by a <see cref="ComposableAttribute"/>.</param>
/// <param name="baseAttributes">The initial base attributes. These attributes contain the base values which will be wrapped by a <see cref="ComposableAttribute"/>, so additional elements can contribute to the attributes value. Instead of providing them here, you could also add them to the system by calling <see cref="AddElement"/> later.</param>
/// <param name="attributeRelationships">The initial attribute relationships. Instead of providing them here, you could also add them to the system by calling <see cref="AddAttributeRelationship(AttributeRelationship, IAttributeSystem)"/> later.</param>
/// <param name="attributeRelationships">The initial attribute relationships. Instead of providing them here, you could also add them to the system by calling <see cref="AddAttributeRelationship(AttributeRelationship, IAttributeSystem, AggregateType)"/> later.</param>
public AttributeSystem(IEnumerable<IAttribute> statAttributes, IEnumerable<IAttribute> baseAttributes, IEnumerable<AttributeRelationship> attributeRelationships)
{
foreach (var statAttribute in statAttributes)
Expand Down Expand Up @@ -129,6 +131,7 @@ public void AddElement(IElement element, AttributeDefinition targetAttribute)
{
attribute = new ComposableAttribute(targetAttribute);
this._attributes.Add(targetAttribute, attribute);
this.OnAttributeAdded(attribute);
}

if (attribute is IComposableAttribute composableAttribute)
Expand Down Expand Up @@ -180,6 +183,18 @@ public override string ToString()
return stringBuilder.ToString();
}

/// <inheritdoc />
public IEnumerator<IAttribute> GetEnumerator()
{
return this._attributes.Values.GetEnumerator();
}

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}

/// <summary>
/// Gets or creates the element with the specified attribute.
/// </summary>
Expand All @@ -193,11 +208,30 @@ public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
var composableAttribute = new ComposableAttribute(attributeDefinition);
element = composableAttribute;
this._attributes.Add(attributeDefinition, composableAttribute);
this.OnAttributeAdded(composableAttribute);
}

return element;
}

/// <summary>
/// Called when an attribute was added to the system after the initial construction.
/// </summary>
/// <param name="attribute">The attribute.</param>
protected virtual void OnAttributeAdded(IAttribute attribute)
{
// can be overwritten.
}

/// <summary>
/// Called when an attribute was removed from the system.
/// </summary>
/// <param name="attribute">The attribute.</param>
protected virtual void OnAttributeRemoved(IAttribute attribute)
{
// can be overwritten.
}

/// <summary>
/// Adds the attribute relationship.
/// </summary>
Expand Down
16 changes: 0 additions & 16 deletions src/GameLogic/AttackableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,6 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl

skillEntry.ThrowNotInitializedProperty(skillEntry.Skill is null, nameof(skillEntry.Skill));

var isHealthUpdated = false;
var isManaUpdated = false;
var skill = skillEntry.Skill;
foreach (var powerUpDefinition in skill.MagicEffectDef?.PowerUpDefinitions ?? Enumerable.Empty<PowerUpDefinition>())
{
Expand All @@ -187,27 +185,13 @@ public static async ValueTask ApplyRegenerationAsync(this IAttackable target, Pl
target.Attributes[regeneration.CurrentAttribute] = Math.Min(
target.Attributes[regeneration.CurrentAttribute] + value,
target.Attributes[regeneration.MaximumAttribute]);
isHealthUpdated |= regeneration.CurrentAttribute == Stats.CurrentHealth || regeneration.CurrentAttribute == Stats.CurrentShield;
isManaUpdated |= regeneration.CurrentAttribute == Stats.CurrentMana || regeneration.CurrentAttribute == Stats.CurrentAbility;
}
else
{
player.Logger.LogWarning(
$"Regeneration skill {skill.Name} is configured to regenerate a non-regeneration-able target attribute {powerUpDefinition.TargetAttribute}.");
}
}

if (target is IWorldObserver observer)
{
var updatedStats =
(isHealthUpdated ? IUpdateStatsPlugIn.UpdatedStats.Health : IUpdateStatsPlugIn.UpdatedStats.Undefined)
| (isManaUpdated ? IUpdateStatsPlugIn.UpdatedStats.Mana : IUpdateStatsPlugIn.UpdatedStats.Undefined);

if (updatedStats != IUpdateStatsPlugIn.UpdatedStats.Undefined)
{
await observer.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(updatedStats)).ConfigureAwait(false);
}
}
}

/// <summary>
Expand Down
42 changes: 42 additions & 0 deletions src/GameLogic/Attributes/ItemAwareAttributeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ public ItemAwareAttributeSystem(Account account, Character character)
character.CharacterClass.AttributeCombinations)
{
this.ItemPowerUps = new Dictionary<Item, IReadOnlyList<PowerUpWrapper>>();
foreach (var attribute in this)
{
this.OnAttributeAdded(attribute);
}
}

/// <summary>
/// Occurs when an attribute value changed.
/// </summary>
public event EventHandler<IAttribute>? AttributeValueChanged;

/// <summary>
/// Gets the item power ups.
/// </summary>
Expand All @@ -38,6 +47,12 @@ public ItemAwareAttributeSystem(Account account, Character character)
/// <inheritdoc />
public void Dispose()
{
this.AttributeValueChanged = null;
foreach (var attribute in this)
{
attribute.ValueChanged -= this.OnAttributeValueChanged;
}

foreach (var powerUpWrapper in this.ItemPowerUps.SelectMany(p => p.Value))
{
powerUpWrapper.Dispose();
Expand Down Expand Up @@ -82,4 +97,31 @@ public override string ToString()

return stringBuilder.ToString();
}

/// <inheritdoc />
protected override void OnAttributeAdded(IAttribute attribute)
{
base.OnAttributeAdded(attribute);
attribute.ValueChanged += this.OnAttributeValueChanged;
}

/// <inheritdoc />
protected override void OnAttributeRemoved(IAttribute attribute)
{
attribute.ValueChanged -= this.OnAttributeValueChanged;
base.OnAttributeRemoved(attribute);
}

/// <summary>
/// Called when an attribute value changed. Forwards the event to <see cref="AttributeValueChanged"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
private void OnAttributeValueChanged(object? sender, EventArgs e)
{
if (sender is IAttribute attribute)
{
this.AttributeValueChanged?.Invoke(this, attribute);
}
}
}
70 changes: 20 additions & 50 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.GameLogic.Views.Duel;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.GameLogic.Views.MuHelper;
using MUnique.OpenMU.GameLogic.Views.Pet;
Expand All @@ -30,7 +29,6 @@ namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx;
using static MUnique.OpenMU.GameLogic.Views.Character.IUpdateStatsPlugIn;

/// <summary>
/// The base implementation of a player.
Expand Down Expand Up @@ -626,13 +624,11 @@ public bool IsAnySelfDefenseActive()
if (Rand.NextRandomBool(this.Attributes[Stats.FullyRecoverHealthAfterHitChance]))
{
this.Attributes[Stats.CurrentHealth] = this.Attributes[Stats.MaximumHealth];
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Health)).ConfigureAwait(false);
}

if (Rand.NextRandomBool(this.Attributes[Stats.FullyRecoverManaAfterHitChance]))
{
this.Attributes[Stats.CurrentMana] = this.Attributes[Stats.MaximumMana];
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false);
}

await this.HitAsync(hitInfo, attacker, skill?.Skill).ConfigureAwait(false);
Expand Down Expand Up @@ -777,8 +773,6 @@ public async ValueTask AfterKilledMonsterAsync()
var additionalValue = (uint)((this.Attributes![recoverAfterMonsterKill.RegenerationMultiplier] * this.Attributes[recoverAfterMonsterKill.MaximumAttribute]) + this.Attributes[recoverAfterMonsterKill.AbsoluteAttribute]);
this.Attributes[recoverAfterMonsterKill.CurrentAttribute] = (uint)Math.Min(this.Attributes[recoverAfterMonsterKill.MaximumAttribute], this.Attributes[recoverAfterMonsterKill.CurrentAttribute] + additionalValue);
}

await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync()).ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -1276,8 +1270,6 @@ public async Task RegenerateAsync()
attributes[r.MaximumAttribute]);
}

await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync()).ConfigureAwait(false);

await this.RegenerateHeroStateAsync().ConfigureAwait(false);
}
catch (InvalidOperationException)
Expand Down Expand Up @@ -1385,8 +1377,6 @@ public async ValueTask<bool> TryConsumeForSkillAsync(Skill skill)
this.Attributes![requirement.Attribute] -= this.GetRequiredValue(requirement);
}

await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false);

return true;
}

Expand Down Expand Up @@ -1688,6 +1678,7 @@ protected override async ValueTask DisposeAsyncCore()
this._walker.Dispose();
await this.MagicEffectList.DisposeAsync().ConfigureAwait(false);
this._respawnAfterDeathCts?.Dispose();
(this._viewPlugIns as IDisposable)?.Dispose();

this.PlayerDisconnected = null;
this.PlayerEnteredWorld = null;
Expand Down Expand Up @@ -2138,12 +2129,8 @@ private async ValueTask OnPlayerEnteredWorldAsync()
await this.InvokeViewPlugInAsync<IQuestStateResponsePlugIn>(p => p.ShowQuestStateAsync(null)).ConfigureAwait(false); // Legacy quest system
await this.InvokeViewPlugInAsync<ICurrentlyActiveQuestsPlugIn>(p => p.ShowActiveQuestsAsync()).ConfigureAwait(false); // New quest system

this.Attributes.GetOrCreateAttribute(Stats.MaximumMana).ValueChanged += this.OnMaximumManaOrAbilityChanged;
this.Attributes.GetOrCreateAttribute(Stats.MaximumAbility).ValueChanged += this.OnMaximumManaOrAbilityChanged;
this.Attributes.GetOrCreateAttribute(Stats.MaximumHealth).ValueChanged += this.OnMaximumHealthOrShieldChanged;
this.Attributes.GetOrCreateAttribute(Stats.MaximumShield).ValueChanged += this.OnMaximumHealthOrShieldChanged;
this.Attributes.AttributeValueChanged += this.OnAttributeValueChanged;
this.Attributes.GetOrCreateAttribute(Stats.TransformationSkin).ValueChanged += this.OnTransformationSkinChanged;
this.Attributes.GetOrCreateAttribute(Stats.AttackSpeed).ValueChanged += this.OnAttackSpeedChanged;

var ammoAttribute = this.Attributes.GetOrCreateAttribute(Stats.AmmunitionAmount);
this.Attributes[Stats.AmmunitionAmount] = (float)(this.Inventory?.EquippedAmmunitionItem?.Durability ?? 0);
Expand Down Expand Up @@ -2232,19 +2219,31 @@ private void SetReclaimableAttributesToMaximum()
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
private async void OnMaximumHealthOrShieldChanged(object? sender, EventArgs args)
private async void OnAttributeValueChanged(object? sender, IAttribute attribute)
{
try
{
this.Attributes![Stats.CurrentHealth] = Math.Min(this.Attributes[Stats.CurrentHealth], this.Attributes[Stats.MaximumHealth]);
this.Attributes[Stats.CurrentShield] = Math.Min(this.Attributes[Stats.CurrentShield], this.Attributes[Stats.MaximumShield]);
_ = LimitCurrentAttribute(Stats.MaximumHealth, Stats.CurrentHealth)
|| LimitCurrentAttribute(Stats.MaximumMana, Stats.CurrentMana)
|| LimitCurrentAttribute(Stats.MaximumShield, Stats.CurrentShield)
|| LimitCurrentAttribute(Stats.MaximumAbility, Stats.CurrentAbility);

await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateMaximumStatsAsync(UpdatedStats.Health)).ConfigureAwait(false);
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Health)).ConfigureAwait(false);
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateStatsAsync(attribute.Definition, attribute.Value)).ConfigureAwait(false);
}
catch (Exception ex)
{
this.Logger.LogError(ex, nameof(this.OnMaximumHealthOrShieldChanged));
this.Logger.LogError(ex, $"{nameof(this.OnAttributeValueChanged)} failed for attribute {attribute.Definition}.");
}

bool LimitCurrentAttribute(AttributeDefinition maximumDefinition, AttributeDefinition currentDefinition)
{
if (attribute.Definition == maximumDefinition && attribute.Value < this.Attributes![currentDefinition])
{
this.Attributes![currentDefinition] = attribute.Value;
return true;
}

return false;
}
}

Expand Down Expand Up @@ -2274,35 +2273,6 @@ private async void OnAmmunitionAmountChanged(object? sender, EventArgs args)
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
private async void OnMaximumManaOrAbilityChanged(object? sender, EventArgs args)
{
try
{
this.Attributes![Stats.CurrentMana] = Math.Min(this.Attributes[Stats.CurrentMana], this.Attributes[Stats.MaximumMana]);
this.Attributes[Stats.CurrentAbility] = Math.Min(this.Attributes[Stats.CurrentAbility], this.Attributes[Stats.MaximumAbility]);
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateMaximumStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false);
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Mana)).ConfigureAwait(false);
}
catch (Exception ex)
{
this.Logger.LogError(ex, nameof(this.OnMaximumManaOrAbilityChanged));
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
private async void OnAttackSpeedChanged(object? sender, EventArgs args)
{
try
{
await this.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(UpdatedStats.Speed)).ConfigureAwait(false);
}
catch (Exception ex)
{
this.Logger.LogError(ex, nameof(this.OnMaximumManaOrAbilityChanged));
}
}

private async ValueTask DecreaseItemDurabilityAfterHitAsync(HitInfo hitInfo)
{
var randomArmorItem = this.Inventory?.EquippedItems.Where(ItemExtensions.IsDefensiveItem).SelectRandom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;

using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.Character;

/// <summary>
/// The consume handler for a potion that recovers health.
Expand All @@ -20,11 +19,4 @@ public abstract class HealthPotionConsumeHandlerPlugIn : RecoverConsumeHandlerPl

/// <inheritdoc/>
protected override AttributeDefinition CurrentAttribute => Stats.CurrentHealth;

/// <inheritdoc />
protected override async ValueTask OnAfterRecoverAsync(Player player)
{
// maybe instead of calling UpdateCurrentHealth etc. provide a more general method where we pass this.CurrentAttribute. The view can then decide what to do with it.
await player.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Health)).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,4 @@ public abstract class ManaPotionConsumeHandler : RecoverConsumeHandlerPlugIn.Man

/// <inheritdoc/>
protected override AttributeDefinition CurrentAttribute => Stats.CurrentMana;

/// <inheritdoc />
protected override async ValueTask OnAfterRecoverAsync(Player player)
{
await player.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Mana)).ConfigureAwait(false);
}
}
14 changes: 6 additions & 8 deletions src/GameLogic/PlayerActions/Skills/DrainLifeSkillPlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ public class DrainLifeSkillPlugIn : IAreaSkillPlugIn
/// <inheritdoc/>
public async ValueTask AfterTargetGotAttackedAsync(IAttacker attacker, IAttackable target, SkillEntry skillEntry, Point targetAreaCenter, HitInfo? hitInfo)
{
if (attacker is Player attackerPlayer && hitInfo != null && hitInfo.Value.HealthDamage > 0)
if (attacker is not Player attackerPlayer
|| hitInfo is not { HealthDamage: > 0 }
|| attackerPlayer.Attributes is not { } playerAttributes)
{
var playerAttributes = attackerPlayer.Attributes;

if (playerAttributes != null)
{
playerAttributes[Stats.CurrentHealth] = (uint)Math.Min(playerAttributes[Stats.MaximumHealth], playerAttributes[Stats.CurrentHealth] + hitInfo.Value.HealthDamage);
await attackerPlayer.InvokeViewPlugInAsync<IUpdateStatsPlugIn>(p => p.UpdateCurrentStatsAsync(IUpdateStatsPlugIn.UpdatedStats.Health)).ConfigureAwait(false);
}
return;
}

playerAttributes[Stats.CurrentHealth] = (uint)Math.Min(playerAttributes[Stats.MaximumHealth], playerAttributes[Stats.CurrentHealth] + hitInfo.Value.HealthDamage);
}
}
Loading

0 comments on commit 9442bf1

Please sign in to comment.