Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/AttributeSystem/ComposableAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ public void RemoveElement(IElement element)
}
}

/// <summary>
/// Removes all elements from the composition.
/// </summary>
public void RemoveAllElements()
{
while (this._elementList.FirstOrDefault() is { } element)
{
this._elementList.Remove(element);
}
}

private float GetAndCacheValue()
{
// Aggregate a copy-on-write snapshot, so a concurrent AddElement/RemoveElement (e.g. an expiring
Expand Down
8 changes: 5 additions & 3 deletions src/AttributeSystem/ConstValueAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ public class ConstValueAttribute : IAttribute
/// </summary>
/// <param name="value">The value.</param>
/// <param name="definition">The definition.</param>
public ConstValueAttribute(float value, AttributeDefinition definition)
/// <param name="aggregateType">The aggregate type.</param>
public ConstValueAttribute(float value, AttributeDefinition definition, AggregateType aggregateType = AggregateType.AddRaw)
{
this.Value = value;
this._definition = definition;
this.AggregateType = aggregateType;
}

/// <summary>
Expand Down Expand Up @@ -57,11 +59,11 @@ public virtual AttributeDefinition Definition
public float Value { get; protected set; }

/// <inheritdoc/>
public AggregateType AggregateType => AggregateType.AddRaw;
public AggregateType AggregateType { get; protected set; }

/// <inheritdoc/>
public override string ToString()
{
return $"{this.Definition.Designation}: {this.Value}";
return $"{this.Definition.Designation}: {this.Value} ({this.AggregateType})";
}
}
106 changes: 98 additions & 8 deletions src/GameLogic/Attributes/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,31 @@ public class Stats
/// Gets the shield recovery everywhere attribute definition.
/// By default, shield recovery is limited to the safezone only. With this attribute (value >= 1), recovery works everywhere on a map.
/// </summary>
public static AttributeDefinition ShieldRecoveryEverywhere { get; } = new(new Guid("3D0A78FF-CCD4-442E-8B4E-64E5082ABD78"), "Is Shield Recovery Active Everwhere", "By default, shield recovery is limited to the safezone only. With this attribute (value >= 1), recovery works everywhere on a map.");
public static AttributeDefinition ShieldRecoveryEverywhere { get; } = new(new Guid("3D0A78FF-CCD4-442E-8B4E-64E5082ABD78"), "Shield Recovery Active Everywhere", "By default, shield recovery is limited to the safezone only. With this attribute (value >= 1), recovery works everywhere on a map.");

/// <summary>
/// Gets the is shield recovery active attribute definition.
/// </summary>
public static AttributeDefinition IsShieldRecoveryActive { get; } = new(new Guid("8F2C4D7E-B1A9-4E3F-9C5D-2A1B7E8F3C4D"), "Is Shield Recovery Active", string.Empty);

/// <summary>
/// Gets the shield recovery hiatus (in seconds) attribute definition.
/// </summary>
/// <remarks>
/// Must be equal or greater than <see cref="ShieldRegeneration"/>.HiatusThreshold for shield to regenerate. See <see cref="PlugIns.ShieldRecoveryHiatusPlugIn"/>.
/// </remarks>
public static AttributeDefinition ShieldRecoveryHiatus { get; } = new(new Guid("5A7E2B9C-3D1F-4A8E-B6C2-1E7D4A9F2B3C"), "Shield Recovery Hiatus", "The seconds since when shield recovery was last interrupted, either by leaving a safezone, the shield being damaged or maxing out.");

/// <summary>
/// Gets the shield recovery ramp factor attribute definition.
/// </summary>
/// <remarks>
/// The factor by which shield recovery increases. Rises linearly with <see cref="ShieldRecoveryHiatus"/>. Original range is [2,3].
/// </remarks>
public static AttributeDefinition ShieldRecoveryRampFactor { get; } = new(new Guid("C3E8F1A7-4B9D-4C5E-8A2F-3D6E1C9B5A7F"), "Shield Recovery Ramp Factor", "The factor by which shield recovery increases. Rises linearly with the uninterrupted shield recovery duration.")
{
MaximumValue = 3,
};

/// <summary>
/// Gets the ability usage reduction attribute definition. Value ranges from 0 (no reduction) to 1 (full reduction).
Expand Down Expand Up @@ -1437,7 +1461,12 @@ public class Stats
/// <summary>
/// Gets the <see cref="IsInSafezone"/> attribute which defines if the character is located in a safezone of a game map.
/// </summary>
public static AttributeDefinition IsInSafezone { get; } = new(new Guid("82044DF9-F528-4AD6-9AAA-6FEAA4C786E7"), "Flag, if the character is located in a safezone of a game map", "Characters at the safezone recover additional health and shield.");
public static AttributeDefinition IsInSafezone { get; } = new(new Guid("82044DF9-F528-4AD6-9AAA-6FEAA4C786E7"), "Flag, if the character is located in a safezone of a game map", "Characters at the safezone recover shield and additional ability.");

/// <summary>
/// Gets the <see cref="IsResting"/> attribute which defines if the character is in a resting state: sitting, leaning or hanging.
/// </summary>
public static AttributeDefinition IsResting { get; } = new(new Guid("7A4E2D9F-B1C3-48F5-9D2E-1A6F8C3E5B7D"), "Flag, if the character is resting (sitting, leaning or hanging)", "Characters resting recover additional health and mana.");

/// <summary>
/// Gets the <see cref="IsUnderwater"/> attribute which defines if the character is located on an underwater game map.
Expand Down Expand Up @@ -1588,13 +1617,43 @@ public static IEnumerable<Regeneration> AfterMonsterKillRegenerationAttributes
}
}

private static Regeneration ManaRegeneration { get; } = new(ManaRecoveryMultiplier, MaximumMana, CurrentMana, ManaRecoveryAbsolute);
/// <summary>
/// Gets the mana regeneration.
/// </summary>
public static Regeneration ManaRegeneration { get; } = new(ManaRecoveryMultiplier, MaximumMana, CurrentMana, ManaRecoveryAbsolute)
{
IntervalResting = TimeSpan.FromSeconds(5),
};

private static Regeneration HealthRegeneration { get; } = new(HealthRecoveryMultiplier, MaximumHealth, CurrentHealth, HealthRecoveryAbsolute);
/// <summary>
/// Gets the health regeneration.
/// </summary>
public static Regeneration HealthRegeneration { get; } = new(HealthRecoveryMultiplier, MaximumHealth, CurrentHealth, HealthRecoveryAbsolute)
{
Interval = TimeSpan.FromSeconds(7),
IntervalResting = TimeSpan.FromSeconds(5),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interval: zTeamS6.3, emu
IntervalResting (includes mana too): zTeamS6.3, emu

};

private static Regeneration AbilityRegeneration { get; } = new(AbilityRecoveryMultiplier, MaximumAbility, CurrentAbility, AbilityRecoveryAbsolute);
/// <summary>
/// Gets the ability regeneration.
/// </summary>
public static Regeneration AbilityRegeneration { get; } = new(AbilityRecoveryMultiplier, MaximumAbility, CurrentAbility, AbilityRecoveryAbsolute);

private static Regeneration ShieldRegeneration { get; } = new(ShieldRecoveryMultiplier, MaximumShield, CurrentShield, ShieldRecoveryAbsolute);
/// <summary>
/// Gets the shield regeneration.
/// </summary>
/// <remarks>
/// Shield recovery is only possible at safe zone, except the character has a specific attribute which has the effect that it's recovered everywhere.
/// This attribute is usually provided by level 380 armor with a Guardian Option.
/// Also, shield has a penalty (hiatus) period before the recovery starts, after which it increases linearly (see <see cref="ShieldRecoveryRampFactor"/>).
/// </remarks>
public static Regeneration ShieldRegeneration { get; } = new(ShieldRecoveryMultiplier, MaximumShield, CurrentShield, ShieldRecoveryAbsolute)
{
Interval = TimeSpan.FromSeconds(1),
IntervalResting = TimeSpan.FromSeconds(1),
EnablerAttribute = IsShieldRecoveryActive,
HiatusAttribute = ShieldRecoveryHiatus,
};

private static Regeneration ManaRegenerationAfterMonsterKill { get; } = new(ManaAfterMonsterKillMultiplier, MaximumMana, CurrentMana, ManaAfterMonsterKillAbsolute);

Expand All @@ -1613,8 +1672,8 @@ public class Regeneration
{
/// <summary>
/// Initializes a new instance of the <see cref="Regeneration" /> class.
/// At regeneration the value of <paramref name="regenerationMultiplier" /> * <paramref name="maximumAttribute" /> is getting added to
/// <paramref name="currentAttribute" />, until the value of <paramref name="maximumAttribute" /> is reached.
/// At regeneration the value of (<paramref name="maximumAttribute" /> * <paramref name="regenerationMultiplier" />) + <paramref name="absoluteAttribute"/>
/// is getting added to <paramref name="currentAttribute" />, until the value of <paramref name="maximumAttribute" /> is reached.
/// </summary>
/// <param name="regenerationMultiplier">The regeneration multiplier.</param>
/// <param name="maximumAttribute">The maximum attribute.</param>
Expand Down Expand Up @@ -1647,5 +1706,36 @@ public Regeneration(AttributeDefinition regenerationMultiplier, AttributeDefinit
/// Gets the current attribute.
/// </summary>
public AttributeDefinition CurrentAttribute { get; }

/// <summary>
/// Gets the interval at which the attribute would complete a full regeneration cycle.
/// </summary>
/// <remarks>
/// Originally, the game had different regeneration cycles depending on attributes and the player's state (<see cref="IsResting"/>).
/// To avoid having different timers each with a regeneration interval, we run just one set to <see cref="GameConfiguration.RecoveryInterval"/>.
/// Then, we apply a compensation factor which is the ratio between elapsed time and <see cref="Interval"/>.
/// This way we can mimic original regeneration over time while still keeping it configurable.
/// </remarks>
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(3);

/// <summary>
/// Gets the interval at which the attribute would complete a full regeneration cycle when the player is resting (<see cref="IsResting"/>).
/// </summary>
public TimeSpan IntervalResting { get; init; } = TimeSpan.FromSeconds(3);
Comment on lines +1719 to +1724

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default as 3 for mana and ability: zTeamS6.3, emu


/// <summary>
/// Gets the attribute which gates the regeneration, if it exists.
/// </summary>
public AttributeDefinition? EnablerAttribute { get; init; }

/// <summary>
/// Gets the attribute which keeps the regeneration hiatus duration, if it exists.
/// </summary>
public AttributeDefinition? HiatusAttribute { get; init; }

/// <summary>
/// Gets the hiatus threshold (in seconds) against wich <see cref="HiatusAttribute"/> is checked to allow the regeneration.
/// </summary>
public int HiatusThreshold { get; init; } = 10;
}
}
70 changes: 45 additions & 25 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.GameLogic.Views.MuHelper;
using MUnique.OpenMU.GameLogic.Views.Pet;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Interfaces;
Expand Down Expand Up @@ -77,6 +76,14 @@ public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacke

private DateTime _lastRegenerate = DateTime.UtcNow;

private Dictionary<Stats.Regeneration, DateTime> _lastRegeneration = new()
{
[Stats.ManaRegeneration] = DateTime.UtcNow,
[Stats.HealthRegeneration] = DateTime.UtcNow,
[Stats.AbilityRegeneration] = DateTime.UtcNow,
[Stats.ShieldRegeneration] = DateTime.UtcNow,
};

private GameMap? _currentMap;

private IDisposable? _accountLoggingScope;
Expand Down Expand Up @@ -240,8 +247,7 @@ public CharacterPose Pose

set
{
var character = this._selectedCharacter;
if (character is null || character.Pose == this.Pose)
if (this._selectedCharacter is not { } character || character.Pose == value)
{
return;
}
Expand Down Expand Up @@ -876,28 +882,42 @@ public async Task RegenerateAsync()
{
try
{
var attributes = this.Attributes;
if (attributes is null)
if (this.Attributes is not { } attributes)
{
return;
}

foreach (var r in Stats.IntervalRegenerationAttributes.Where(r =>
attributes[r.RegenerationMultiplier] > 0 || attributes[r.AbsoluteAttribute] > 0))
var now = DateTime.UtcNow;
foreach (var r in Stats.IntervalRegenerationAttributes)
{
if (r.CurrentAttribute == Stats.CurrentShield && !this.IsAtSafezone() &&
attributes[Stats.ShieldRecoveryEverywhere] < 1)
if ((r.EnablerAttribute is { } enabler && attributes[enabler] < 1)
|| (r.HiatusAttribute is { } hiatus && attributes[hiatus] < r.HiatusThreshold))
{
// Shield recovery is only possible at safe-zone, except the character has a specific attribute which has the effect that it's recovered everywhere.
// This attribute is usually provided by level 380 armor and a Guardian Option.
this._lastRegeneration[r] = now;
continue;
}

var factor = 0f;
var interval = r.Interval;
if (attributes[Stats.IsResting] > 0)
{
interval = r.IntervalResting;

if (r.CurrentAttribute == Stats.CurrentMana)
{
// Mana recovery while resting is on top of regular recovery
factor += (float)((now - this._lastRegeneration[r]) / r.Interval);
}
}

factor += (float)((now - this._lastRegeneration[r]) / interval);

attributes[r.CurrentAttribute] = Math.Min(
attributes[r.CurrentAttribute] +
((attributes[r.MaximumAttribute] * attributes[r.RegenerationMultiplier]) +
attributes[r.AbsoluteAttribute]),
(((attributes[r.MaximumAttribute] * attributes[r.RegenerationMultiplier]) + attributes[r.AbsoluteAttribute]) * factor),
attributes[r.MaximumAttribute]);

this._lastRegeneration[r] = now;
}

await this.RegenerateHeroStateAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -1202,6 +1222,17 @@ internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer)
await this.ForEachWorldObserverAsync<IUpdateCharacterHeroStatePlugIn>(o => o.UpdateCharacterHeroStateAsync(this), true).ConfigureAwait(false);
}

/// <summary>
/// Sets the current values of the regeneration attributes to their maximum values.
/// </summary>
internal void SetReclaimableAttributesToMaximum()
{
foreach (var regeneration in Stats.IntervalRegenerationAttributes)
{
this.Attributes![regeneration.CurrentAttribute] = this.Attributes[regeneration.MaximumAttribute];
}
}

/// <summary>
/// Sets the current map without raising the enter/leave map events, when the player is
/// removed from the game.
Expand Down Expand Up @@ -1594,7 +1625,7 @@ private async ValueTask OnPlayerEnteredWorldAsync()
this.AddMissingStatAttributes();

this.Attributes = new ItemAwareAttributeSystem(this.Account!, selectedCharacter, this.GameContext.Configuration);
this.Attributes[Stats.NearbyPartyMemberCount] = 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not a StatAttribute, so any assignment didn't work.

this.Attributes[Stats.IsResting] = 0;
this.LogInvalidInventoryItems();

this._storages.CreateForCharacter(selectedCharacter);
Expand Down Expand Up @@ -1669,17 +1700,6 @@ private void SetReclaimableAttributesBeforeEnterGame()
this.Attributes[Stats.CurrentHealth] = Math.Min(this.Attributes[Stats.CurrentHealth], this.Attributes[Stats.MaximumHealth]);
}

/// <summary>
/// Sets the current values of the regeneration attributes to their maximum values.
/// </summary>
internal void SetReclaimableAttributesToMaximum()
{
foreach (var regeneration in Stats.IntervalRegenerationAttributes)
{
this.Attributes![regeneration.CurrentAttribute] = this.Attributes[regeneration.MaximumAttribute];
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
private async void OnAttributeValueChanged(object? sender, IAttribute attribute)
{
Expand Down
75 changes: 75 additions & 0 deletions src/GameLogic/PlugIns/PeriodicTasks/ShieldRecoveryHiatusPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// <copyright file="ShieldRecoveryHiatusPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlugIns;

using System;
using System.Runtime.InteropServices;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// Updates and resets the shield recovery hiatus attribute.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShieldRecoveryHiatusPlugIn_Name), Description = nameof(PlugInResources.ShieldRecoveryHiatusPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("7B4A9E2D-C1F5-48E3-A6B2-1E7D4C8F3A5B")]
public class ShieldRecoveryHiatusPlugIn : IPeriodicTaskPlugIn, IAttackableMovedPlugIn, IAttackableGotHitPlugIn
{
/// <inheritdoc />
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
await gameContext.ForEachPlayerAsync(player =>
{
if (player.SelectedCharacter != null
&& !player.PlayerState.CurrentState.IsDisconnectedOrFinished()
&& player.Attributes is { } attributes
&& attributes[Stats.MaximumShield] > 0)
{
attributes.AddElement(new SimpleElement(1, AggregateType.AddRaw), Stats.ShieldRecoveryHiatus);
}

return Task.CompletedTask;
}).ConfigureAwait(false);
}

/// <inheritdoc />
public void ForceStart()
{
// do nothing.
}

/// <inheritdoc />
public void AttackableGotHit(IAttackable attackable, IAttacker attacker, HitInfo hitInfo)
{
var defender = attackable as Player ?? (attackable as Monster)?.SummonedBy;
var attackerPlayer = attacker as Player ?? (attacker as Monster)?.SummonedBy;
if (defender is null || attackerPlayer is null || defender == attackerPlayer || defender.Attributes is not { } attributes)
{
return;
}

if (attributes[Stats.MaximumShield] > 0 && hitInfo is { ShieldDamage: > 0 })
{
attributes.GetComposableAttribute(Stats.ShieldRecoveryHiatus)?.RemoveAllElements();
}
}

/// <inheritdoc />
public void AttackableMoved(IAttackable attackable)
{
if (attackable is not Player player || player.Attributes is not { } attributes)
{
return;
}

if (attributes[Stats.MaximumShield] > 0 &&
(attributes[Stats.IsShieldRecoveryActive] < 1 || attributes[Stats.CurrentShield] == attributes[Stats.MaximumShield]))
{
attributes.GetComposableAttribute(Stats.ShieldRecoveryHiatus)?.RemoveAllElements();
}
}
}
Loading
Loading