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
35 changes: 32 additions & 3 deletions src/GameLogic/NPC/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ namespace MUnique.OpenMU.GameLogic.NPC;
/// </summary>
public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISupportWalk, IMovable, ISummonable
{
private const short IcedEffectNumber = 0x38;
private const short BlowOfDestructionEffectNumber = 0x56;
Comment on lines +21 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

It is recommended to use the MagicEffectNumber enum instead of hardcoded short constants for better maintainability and to ensure consistency with the rest of the project.

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.

MagicEffectNumber enum lives in Persistence.Initialization, inaccessible to GameLogic. I leave moving effect numbers to shared code to a follow-up PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can check the iced-status by this.AttributeSystem[Stats.IsIced].

private const double IcedMovementSpeedFactor = 0.5;
private const double BlowOfDestructionMovementSpeedFactor = 0.33;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add a new Stats.MovementSpeedFactor (default value: 1.0), where the ice effect multiplies the value with 0.5, and blow of descruction with 0.33.


private readonly AsyncLock _moveLock = new();
private readonly INpcIntelligence _intelligence;
private readonly Walker _walker;
Expand Down Expand Up @@ -59,7 +64,7 @@ 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();
Expand Down Expand Up @@ -87,7 +92,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 @@ -356,6 +361,30 @@ 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()
{
if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber))
{
return IcedMovementSpeedFactor;
}

if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber))
{
return BlowOfDestructionMovementSpeedFactor;
}
Comment thread
itayalroy marked this conversation as resolved.

return 1.0;
}

/// <summary>
/// Creates the magic effect power up for the given skill of a monster.
/// </summary>
Expand Down Expand Up @@ -401,4 +430,4 @@ private void ValidatePath(Memory<WalkingStep> steps)
}
}
}
}
}
160 changes: 152 additions & 8 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,17 @@ namespace MUnique.OpenMU.GameLogic;
/// </summary>
public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacker, ITrader, IPartyMember, IRotatable, IHasBucketInformation, ISupportWalk, IMovable, ILoggerOwner<Player>
{
private const short IcedEffectNumber = 0x38;
private const short BlowOfDestructionEffectNumber = 0x56;
Comment thread
itayalroy marked this conversation as resolved.
Comment thread
itayalroy marked this conversation as resolved.
private const double IcedMovementSpeedFactor = 0.5;
private const double BlowOfDestructionMovementSpeedFactor = 0.33;
Comment on lines +45 to +48
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as for the monster class.

private const byte RunningGearMinimumLevel = 5;
private const ushort AtlansMapNumber = 7;
private const ushort Kalima1MapNumber = 24;
private const ushort Kalima6MapNumber = 29;
private const ushort Kalima7MapNumber = 36;
private const ushort Doppelgaenger3MapNumber = 67;

private static readonly MagicEffectDefinition GMEffect = new GMMagicEffectDefinition
{
InformObservers = true,
Expand Down Expand Up @@ -148,7 +160,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 +727,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 @@ -1408,6 +1425,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 @@ -2107,19 +2135,135 @@ 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)
{
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.Inventory?.EquippedItems.Any(item => item.Definition?.ItemSlot?.ItemSlots.Contains(7) ?? false) ?? false)
const double walkSpeed = 12.0;
if (this.IsInClientSafezone(position))
{
return this.ApplyMovementEffects(walkSpeed);
}

return this.ApplyMovementEffects(this.GetMountedOrRunningSpeed(walkSpeed));
}

private double ApplyMovementEffects(double speed)
{
if (this.MagicEffectList.ActiveEffects.ContainsKey(IcedEffectNumber))
{
return speed * IcedMovementSpeedFactor;
}

if (this.MagicEffectList.ActiveEffects.ContainsKey(BlowOfDestructionEffectNumber))
{
return speed * BlowOfDestructionMovementSpeedFactor;
}
Comment thread
itayalroy marked this conversation as resolved.

return speed;
}

private double GetMountedOrRunningSpeed(double walkSpeed)
{
const double runSpeed = 15.0;
const double fastWingSpeed = 16.0;
const double horseOrFenrirRunSpeed = 17.0;
const double excellentFenrirRunSpeed = 19.0;

var pet = this.Inventory?.GetItem(InventoryConstants.PetSlot);
if (this.IsItem(pet, 13, 37))
{
// Wings
return TimeSpan.FromMilliseconds(300);
if (this.HasFenrirMovementOption(pet))
{
return excellentFenrirRunSpeed;
}

return horseOrFenrirRunSpeed;
}

if (this.IsItem(pet, 13, 4))
{
return horseOrFenrirRunSpeed;
}

var wings = this.Inventory?.GetItem(InventoryConstants.WingsSlot);
if (this.HasEquippedWings(wings)
|| this.IsItem(pet, 13, 2)
|| this.IsItem(pet, 13, 3))
{
return this.GetWingMovementSpeed(wings, runSpeed, fastWingSpeed);
}

// TODO: Consider pets etc.
return TimeSpan.FromMilliseconds(500);
return this.HasRunningGear() ? runSpeed : walkSpeed;
}
Comment on lines +2180 to +2212
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Imho, there are too many hardcoded values and special logic. The item definitions could hold something like a movement speed, e.g. in new Stats.MaxMovementSpeed/Stats.MaxMovementSpeedUnderwater attributes. Then the players attribute system can be asked about it.

And about atlans/kalima: You could add another new Stats.IsUnderwater which can be added to the GameMapDefinition.CharacterPowerUpDefinitions with value 1. So when the character enters the map, it gets this attribute value in it's attribute system, too.


private double GetWingMovementSpeed(Item? wings, double runSpeed, double fastWingSpeed)
{
return this.IsFastWing(wings) ? fastWingSpeed : runSpeed;
}

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

private bool HasEquippedWings(Item? item)
{
return item is { Durability: > 0.0 }
&& item.ItemSlot == InventoryConstants.WingsSlot;
}

private bool IsFastWing(Item? item)
{
return this.IsItem(item, 12, 5)
|| this.IsItem(item, 12, 36);
}

private bool HasRunningGear()
{
var slot = this.IsSwimmingMovementMap()
? InventoryConstants.GlovesSlot
: InventoryConstants.BootsSlot;
return this.Inventory?.GetItem(slot) is { Durability: > 0.0, Level: >= RunningGearMinimumLevel };
}

private bool IsSwimmingMovementMap()
{
return this.CurrentMap?.MapId is AtlansMapNumber
or >= Kalima1MapNumber and <= Kalima6MapNumber
or Kalima7MapNumber
or Doppelgaenger3MapNumber;
}

private bool IsItem(Item? item, short group, short number)
{
return item is { Durability: > 0.0 }
&& item.Definition is { } definition
&& definition.Group == group
&& definition.Number == number;
}

private bool HasFenrirMovementOption(Item? item)
{
return item?.ItemOptions.Any(option =>
option.ItemOption?.OptionType == ItemOptionTypes.BlueFenrir
|| option.ItemOption?.OptionType == ItemOptionTypes.BlackFenrir
|| option.ItemOption?.OptionType == ItemOptionTypes.GoldFenrir) ?? false;
}

private async ValueTask<ExitGate> GetSpawnGateOfCurrentMapAsync()
Expand Down
34 changes: 21 additions & 13 deletions src/GameLogic/Walker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace MUnique.OpenMU.GameLogic;
public sealed class Walker : IDisposable
{
private readonly ISupportWalk _walkSupporter;
private readonly Func<TimeSpan> _stepDelay;
private readonly Queue<WalkingStep> _nextSteps = new(5);

/// <summary>
Expand All @@ -39,10 +38,10 @@ public sealed class Walker : IDisposable
/// </summary>
/// <param name="walkSupporter">The walk supporter.</param>
/// <param name="stepDelay">The delay between performing a step.</param>
public Walker(ISupportWalk walkSupporter, Func<TimeSpan> stepDelay)
public Walker(ISupportWalk walkSupporter, Func<WalkingStep?, TimeSpan> stepDelay)
{
this._walkSupporter = walkSupporter;
this._stepDelay = stepDelay;
this.StepDelay = stepDelay;
this._walkLock = new AsyncReaderWriterLock();
}

Expand All @@ -51,6 +50,8 @@ public Walker(ISupportWalk walkSupporter, Func<TimeSpan> stepDelay)
/// </summary>
public Point CurrentTarget { get; private set; }

private Func<WalkingStep?, TimeSpan> StepDelay { get; }

/// <summary>
/// Initializes a new walk to the specified target with the specified steps.
/// </summary>
Expand Down Expand Up @@ -180,15 +181,18 @@ public void Dispose()

private async Task WalkLoopAsync(CancellationToken cancellationToken)
{
var delay = this._stepDelay().Subtract(TimeSpan.FromMilliseconds(50));

// Task.Delay might take longer than we specify. We need to compensate that.
var lastOffset = TimeSpan.Zero;
while (!cancellationToken.IsCancellationRequested)
{
var sw = Stopwatch.StartNew();
await this.WalkStepAsync(cancellationToken).ConfigureAwait(false);
var step = await this.WalkStepAsync(cancellationToken).ConfigureAwait(false);
if (step is null)
{
continue;
}

var delay = this.StepDelay(step);
var nextDelay = delay - lastOffset;
if (nextDelay > TimeSpan.Zero)
{
Expand All @@ -207,14 +211,14 @@ private async Task WalkLoopAsync(CancellationToken cancellationToken)
/// <summary>
/// Performs the next step of a walk.
/// </summary>
private async ValueTask WalkStepAsync(CancellationToken cancellationToken)
private async ValueTask<WalkingStep?> WalkStepAsync(CancellationToken cancellationToken)
{
try
{
if (this._isDisposed)
{
Debug.WriteLine("walker already disposed");
return;
return null;
}

bool stop;
Expand All @@ -226,13 +230,13 @@ private async ValueTask WalkStepAsync(CancellationToken cancellationToken)
if (stop)
{
await this.StopAsync().ConfigureAwait(false);
return;
return null;
}

// Update new coords
using (await this._walkLock.WriterLockAsync(cancellationToken))
{
this.WalkNextStepIfStepAvailable();
return this.WalkNextStepIfStepAvailable();
}
}
catch (OperationCanceledException)
Expand All @@ -243,13 +247,15 @@ private async ValueTask WalkStepAsync(CancellationToken cancellationToken)
{
Debug.Fail(ex.Message, ex.StackTrace);
}

return null;
}

private void WalkNextStepIfStepAvailable()
private WalkingStep? WalkNextStepIfStepAvailable()
{
if (this.ShouldWalkerStop())
{
return;
return null;
}

var nextStep = this._nextSteps.Dequeue();
Expand All @@ -259,7 +265,9 @@ private void WalkNextStepIfStepAvailable()
{
rotatable.Rotation = nextStep.Direction;
}

return nextStep;
}

private bool ShouldWalkerStop() => !((this._walkSupporter as IAttackable)?.IsActive() ?? false) || this._nextSteps.Count <= 0;
}
}
Loading