Skip to content
Open
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
12 changes: 5 additions & 7 deletions src/GameLogic/AttackableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,11 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab
/// <param name="target">The target.</param>
/// <param name="attacker">The attacker.</param>
/// <param name="skill">The skill.</param>
/// <param name="powerUp">The power up.</param>
/// <param name="powerUps">The power ups.</param>
/// <param name="duration">The duration.</param>
/// <param name="targetAttribute">The target attribute.</param>
/// <param name="hitInfo">The hit information.</param>
/// <returns>The success of the appliance.</returns>
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IElement? powerUp, IElement? duration, AttributeDefinition? targetAttribute, HitInfo? hitInfo)
public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackable target, IAttacker attacker, Skill skill, IReadOnlyCollection<(AttributeDefinition Target, IElement Boost)> powerUps, IElement? duration, HitInfo? hitInfo)
{
if (!target.IsAlive)
{
Expand All @@ -453,12 +452,11 @@ public static async ValueTask<bool> TryApplyElementalEffectsAsync(this IAttackab

if (skill.MagicEffectDef is { } effectDefinition
&& !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number)
&& powerUp is not null
&& duration is not null
&& targetAttribute is not null)
&& powerUps.Count > 0)
{
// power-up is the wrong term here... it's more like a power-down ;-)
await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, (targetAttribute, powerUp)).ConfigureAwait(false);
await target.ApplyMagicEffectAsync(attacker, effectDefinition, duration, hitInfo, [.. powerUps]).ConfigureAwait(false);
applied = true;
}

Expand Down Expand Up @@ -984,4 +982,4 @@ private static int GetMasterSkillTreeMasteryPvpDamageBonus(IAttacker attacker)
return 0;
}
}
}
}
5 changes: 3 additions & 2 deletions src/GameLogic/Attributes/MonsterAttributeHolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class MonsterAttributeHolder : IAttributeSystem
{ Stats.DefensePvp, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
{ Stats.DamageReceiveDecrement, m => 1.0f },
{ Stats.AttackDamageIncrease, m => 1.0f },
{ Stats.MovementSpeedFactor, m => 1.0f },
{ Stats.ShieldBypassChance, m => 1.0f },
{ Stats.DefenseDecrement, m => 1.0f - m.Attributes.GetValueOfAttribute(Stats.InnovationDefDecrement) },
};
Expand Down Expand Up @@ -131,7 +132,7 @@ public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
if (attributes.TryGetValue(targetAttribute, out var attribute))
{
attribute.RemoveElement(element);
if (attribute.Elements.Skip(1).Take(1).Any())
if (!attribute.Elements.Skip(1).Any())
{
attributes.Remove(targetAttribute);
}
Expand Down Expand Up @@ -179,4 +180,4 @@ private IDictionary<AttributeDefinition, IComposableAttribute> GetAttributeDicti
return attributes;
}
}
}
}
20 changes: 20 additions & 0 deletions src/GameLogic/Attributes/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,21 @@ public class Stats
/// </summary>
public static AttributeDefinition WalkSpeed { get; } = new(new Guid("9CDDC598-E5F3-4372-9294-505455E4A40B"), "Walk Speed", string.Empty);

/// <summary>
/// Gets the maximum movement speed attribute definition.
/// </summary>
public static AttributeDefinition MaxMovementSpeed { get; } = new(new Guid("E29301BE-626B-4B42-9F68-0DFAC18B3856"), "Maximum Movement Speed", "The maximum movement speed of a character on regular terrain.");

/// <summary>
/// Gets the maximum underwater movement speed attribute definition.
/// </summary>
public static AttributeDefinition MaxMovementSpeedUnderwater { get; } = new(new Guid("12128DC7-0740-48A5-A653-E546191CD7E0"), "Maximum Underwater Movement Speed", "The maximum movement speed of a character on underwater terrain.");

/// <summary>
/// Gets the movement speed factor attribute definition.
/// </summary>
public static AttributeDefinition MovementSpeedFactor { get; } = new(new Guid("003E1F2E-661D-4258-BEF0-33111D5F4AD2"), "Movement Speed Factor", "The factor which is applied to the final movement speed of a character.");

/// <summary>
/// Gets the attack damage increase attribute definition.
/// </summary>
Expand Down Expand Up @@ -1331,6 +1346,11 @@ public class Stats
/// </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.");

/// <summary>
/// Gets the <see cref="IsUnderwater"/> attribute which defines if the character is located on an underwater game map.
/// </summary>
public static AttributeDefinition IsUnderwater { get; } = new(new Guid("72A684C1-102B-4FDE-B637-2665ADD5F4AE"), "Flag, if the character is located on an underwater game map", "Characters on underwater maps use underwater movement speed attributes.");

/// <summary>
/// Gets the attribute definition, which defines if a player has MU Helper activated.
/// </summary>
Expand Down
7 changes: 2 additions & 5 deletions src/GameLogic/InventoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,7 @@ await this._player.ForEachWorldObserverAsync<IAppearanceChangedPlugIn>(
}
}

if (item.Definition!.PossibleItemSetGroups.Count > 0)
{
this.UpdateSetPowerUps();
}
this.UpdateSetPowerUps();

var itemAdded = this.EquippedItems.Contains(item);
if (itemAdded)
Expand Down Expand Up @@ -220,4 +217,4 @@ private void UpdateSetPowerUps()
var factory = this._gameContext.ItemPowerUpFactory;
this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList();
}
}
}
58 changes: 34 additions & 24 deletions src/GameLogic/NPC/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,16 @@ public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISuppor
private readonly Walker _walker;

/// <summary>
/// The power up element of the monster skill.
/// It is a "cached" element which will be created on demand and can be applied multiple times.
/// The power up elements of the monster skill.
/// These are "cached" elements which will be created on demand and can be applied multiple times.
/// </summary>
private readonly IElement? _skillPowerUp;
private readonly (AttributeDefinition Target, IElement Boost)[] _skillPowerUps;

/// <summary>
/// The duration of the <see cref="_skillPowerUp"/>.
/// The duration of the <see cref="_skillPowerUps"/>.
/// </summary>
private readonly IElement? _skillPowerUpDuration;

/// <summary>
/// The target attribute of the <see cref="_skillPowerUp"/>.
/// </summary>
private readonly AttributeDefinition? _skillPowerUpTarget;

private readonly IObjectPool<PathFinder> _pathFinderPool;

private bool _isCalculatingPath;
Expand All @@ -59,10 +54,10 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
: base(spawnInfo, stats, map, eventStateProvider, dropGenerator, plugInManager)
{
this._pathFinderPool = pathFinderPool;
this._walker = new Walker(this, () => this.StepDelay);
this._walker = new Walker(this, this.GetStepDelay);
this._intelligence = npcIntelligence;

(this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget) = this.CreateMagicEffectPowerUp();
(this._skillPowerUps, this._skillPowerUpDuration) = this.CreateMagicEffectPowerUps();

this._intelligence.Npc = this;
}
Expand All @@ -87,7 +82,7 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
public Point WalkTarget => this._walker.CurrentTarget;

/// <inheritdoc/>
public TimeSpan StepDelay => this.Definition.MoveDelay;
public TimeSpan StepDelay => this.GetStepDelay(null);

/// <inheritdoc/>
/// <remarks>Monsters don't do combos.</remarks>
Expand Down Expand Up @@ -122,7 +117,7 @@ public async ValueTask AttackAsync(IAttackable target)
await this.ForEachWorldObserverAsync<IShowAnimationPlugIn>(p => p.ShowMonsterAttackAnimationAsync(this, target, this.GetDirectionTo(target)), true).ConfigureAwait(false);
if (this.Definition.AttackSkill is { } attackSkill)
{
await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUp, this._skillPowerUpDuration, this._skillPowerUpTarget, hitInfo).ConfigureAwait(false);
await target.TryApplyElementalEffectsAsync(this, attackSkill, this._skillPowerUps, this._skillPowerUpDuration, hitInfo).ConfigureAwait(false);

await this.ForEachWorldObserverAsync<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(this, target, attackSkill, true), true).ConfigureAwait(false);
}
Expand Down Expand Up @@ -356,27 +351,42 @@ private static WalkingStep GetStep(PathResultNode node)
};
}

private TimeSpan GetStepDelay(WalkingStep? step)
{
var tileDistance = step is { } walkingStep ? walkingStep.From.EuclideanDistanceTo(walkingStep.To) : 1.0;
var delayMilliseconds = this.Definition.MoveDelay.TotalMilliseconds * Math.Max(1.0, tileDistance);
delayMilliseconds /= this.GetMovementSpeedFactor();

return TimeSpan.FromMilliseconds(delayMilliseconds);
}

private double GetMovementSpeedFactor()
{
var movementSpeedFactor = this.Attributes[Stats.MovementSpeedFactor];

return movementSpeedFactor > 0 ? movementSpeedFactor : 1.0;
}

/// <summary>
/// Creates the magic effect power up for the given skill of a monster.
/// Creates the magic effect power ups for the given skill of a monster.
/// </summary>
/// <remarks>
/// Currently, we just support one effect for monsters.
/// </remarks>
private (IElement? PowerUp, IElement? Duration, AttributeDefinition? Target) CreateMagicEffectPowerUp()
private ((AttributeDefinition Target, IElement Boost)[] PowerUps, IElement? Duration) CreateMagicEffectPowerUps()
{
var skill = this.Definition.AttackSkill;
if (skill?.MagicEffectDef?.PowerUpDefinitions.FirstOrDefault() is not { } powerUpDefinition
|| skill.MagicEffectDef.Duration is not { } duration)
if (skill?.MagicEffectDef is not { } magicEffectDefinition
|| magicEffectDefinition.Duration is not { } duration)
{
return (null, null, null);
return ([], null);
}

if (powerUpDefinition.Boost is null)
if (magicEffectDefinition.PowerUpDefinitions.Any(p => p.Boost is null || p.TargetAttribute is null))
{
throw new InvalidOperationException($"Skill {skill.Name} ({skill.Number}) has no magic effect definition or is without a PowerUpDefinition.");
}

return (this.Attributes.CreateElement(powerUpDefinition), this.Attributes.CreateDurationElement(duration), powerUpDefinition.TargetAttribute);
return (
[.. magicEffectDefinition.PowerUpDefinitions.Select(p => (p.TargetAttribute!, this.Attributes.CreateElement(p)))],
this.Attributes.CreateDurationElement(duration));
}

private void ValidatePath(Memory<WalkingStep> steps)
Expand All @@ -401,4 +411,4 @@ private void ValidatePath(Memory<WalkingStep> steps)
}
}
}
}
}
69 changes: 58 additions & 11 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.GameLogic;
using System.Threading;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Attributes;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.GuildWar;
using MUnique.OpenMU.GameLogic.MiniGames;
Expand Down Expand Up @@ -41,6 +42,8 @@ namespace MUnique.OpenMU.GameLogic;
/// </summary>
public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacker, ITrader, IPartyMember, IRotatable, IHasBucketInformation, ISupportWalk, IMovable, ILoggerOwner<Player>
{
private const double WalkMovementSpeed = 12.0;

private static readonly MagicEffectDefinition GMEffect = new GMMagicEffectDefinition
{
InformObservers = true,
Expand Down Expand Up @@ -148,7 +151,7 @@ public Player(IGameContext gameContext)
public bool IsWalking => this._walker.CurrentTarget != default;

/// <inheritdoc />
public TimeSpan StepDelay => this.GetStepDelay();
public TimeSpan StepDelay => this.GetStepDelay(null);

/// <inheritdoc />
public Point WalkTarget => this._walker.CurrentTarget;
Expand Down Expand Up @@ -715,6 +718,11 @@ public ValueTask ShowBlueMessageAsync(string message)
throw new InvalidOperationException("AttributeSystem not set.");
}

if (this.IsAttackBlockedBySafezone(attacker))
{
return null;
}

if (!this.GameContext.PvpEnabled && this.CurrentMap?.Definition.BattleZone == null &&
this.CurrentMiniGame?.AllowPlayerKilling is false)
{
Expand Down Expand Up @@ -1450,6 +1458,17 @@ public async ValueTask WalkToAsync(Point target, Memory<WalkingStep> steps)
/// <inheritdoc />
public ValueTask StopWalkingAsync() => this._walker.StopAsync();

private bool IsAttackBlockedBySafezone(IAttacker attacker)
{
if (this.IsAtSafezone())
{
return true;
}

var attackerPlayer = attacker as Player ?? (attacker as IPlayerSurrogate)?.Owner;
return attackerPlayer?.IsAtSafezone() is true;
}

/// <summary>
/// Regenerates the attributes specified in <see cref="Stats.IntervalRegenerationAttributes"/>.
/// </summary>
Expand Down Expand Up @@ -2149,19 +2168,48 @@ private async ValueTask RegenerateHeroStateAsync()
}

/// <summary>
/// Gets the step delay depending on the equipped items.
/// Gets the step delay depending on the equipped items and current movement effects.
/// </summary>
/// <param name="step">The walking step for which the delay is calculated.</param>
/// <returns>The current step delay, depending on equipped items.</returns>
private TimeSpan GetStepDelay()
private TimeSpan GetStepDelay(WalkingStep? step)
{
if (this.Inventory?.EquippedItems.Any(item => item.Definition?.ItemSlot?.ItemSlots.Contains(7) ?? false) ?? false)
const double referenceFrameTimeMilliseconds = 40.0;
const double terrainScale = 100.0;

var speed = this.GetClientMovementSpeed(step?.From);
var tileDistance = step is { } walkingStep ? walkingStep.From.EuclideanDistanceTo(walkingStep.To) : 1.0;
var movementMilliseconds = terrainScale * Math.Max(1.0, tileDistance) / speed * referenceFrameTimeMilliseconds;

return TimeSpan.FromMilliseconds(movementMilliseconds);
}

private double GetClientMovementSpeed(Point? position = null)
{
if (this.IsInClientSafezone(position))
{
// Wings
return TimeSpan.FromMilliseconds(300);
return this.ApplyMovementSpeedFactor(WalkMovementSpeed);
}

// TODO: Consider pets etc.
return TimeSpan.FromMilliseconds(500);
var speedAttribute = this.Attributes?[Stats.IsUnderwater] > 0
? Stats.MaxMovementSpeedUnderwater
: Stats.MaxMovementSpeed;
var speed = this.Attributes?[speedAttribute] ?? 0;

return this.ApplyMovementSpeedFactor(Math.Max(WalkMovementSpeed, speed));
}

private double ApplyMovementSpeedFactor(double speed)
{
var movementSpeedFactor = this.Attributes?[Stats.MovementSpeedFactor] ?? 1.0;

return speed * (movementSpeedFactor > 0 ? movementSpeedFactor : 1.0);
}

private bool IsInClientSafezone(Point? position = null)
{
var checkedPosition = position ?? this.Position;
return this.CurrentMap?.Terrain.SafezoneMap[checkedPosition.X, checkedPosition.Y] ?? false;
}

private async ValueTask<ExitGate> GetSpawnGateOfCurrentMapAsync()
Expand Down Expand Up @@ -2455,21 +2503,20 @@ private void RaisePlayerEnteredMap(GameMap map)
{
foreach (var powerUpDefinition in powerUpDefinitions)
{
if (powerUpDefinition.TargetAttribute is not { } targetAttribute)
if (powerUpDefinition.TargetAttribute is null)
{
continue;
}

var powerUps = PowerUpWrapper.CreateByPowerUpDefinition(powerUpDefinition, attributes);
powerUps.ForEach(p =>
{
this.Attributes?.AddElement(p, targetAttribute);
this.PlayerLeftMap += OnPlayerLeftMap;

void OnPlayerLeftMap(object? o, (Player, GameMap) args)
{
this.PlayerLeftMap -= OnPlayerLeftMap;
attributes.RemoveElement(p, targetAttribute);
p.Dispose();
}
});
}
Expand Down
Loading