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
21 changes: 20 additions & 1 deletion src/GameLogic/Attributes/MonsterAttributeHolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,28 @@ public void AddAttributeRelationship(AttributeRelationship combination, IAttribu
}

/// <inheritdoc />
/// <remarks>
/// This is required when a monster attacks with a skill which has attribute relationships, e.g. a skill
/// damage multiplier which depends on the <see cref="Stats.SkillMultiplier"/> of the monster.
/// The stats of a monster are fixed, so they're returned as constant. Attributes which got elements added,
/// e.g. by a magic effect, are returned as they are, so that they stay up to date.
/// Note that the constant is a snapshot: a relationship which is created from it doesn't see elements
/// which are added to the same attribute later. So it shouldn't be used for stats which change at runtime.
/// </remarks>
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
{
throw new NotImplementedException();
IDictionary<AttributeDefinition, IComposableAttribute>? attributes;
lock (this._attributesLock)
{
attributes = this._attributes;
}

if (attributes is not null && attributes.TryGetValue(attributeDefinition, out var attribute))
{
return attribute;
}

return new ConstantElement(this.GetValueOfAttribute(attributeDefinition));
}

/// <summary>
Expand Down
32 changes: 32 additions & 0 deletions src/GameLogic/PlayerActions/WarpGateAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace MUnique.OpenMU.GameLogic.PlayerActions;

using System.ComponentModel;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;

Expand All @@ -22,6 +24,20 @@ public async ValueTask EnterGateAsync(Player player, EnterGate gate)
{
if (await this.IsWarpLegitAsync(player, gate).ConfigureAwait(false))
{
var eventArgs = new CancelEventArgs();
if (player.GameContext.PlugInManager.GetPlugInPoint<IWarpGateEnteringPlugIn>() is { } plugInPoint)
{
await plugInPoint.WarpGateEnteringAsync(player, gate.TargetGate!, eventArgs).ConfigureAwait(false);
}

if (eventArgs.Cancel)
{
// Like the original game, the player is warped to its current position.
// The game client shows its loading screen until a map change is completed.
await this.WarpToCurrentPositionAsync(player).ConfigureAwait(false);
return;
}

await player.WarpToAsync(gate.TargetGate!).ConfigureAwait(false);
}
else
Expand Down Expand Up @@ -67,6 +83,22 @@ private async ValueTask<bool> IsWarpLegitAsync(Player player, EnterGate? enterGa
return true;
}

private async ValueTask WarpToCurrentPositionAsync(Player player)
{
var position = player.IsWalking ? player.WalkTarget : player.Position;
var currentPosition = new ExitGate
{
Map = player.CurrentMap!.Definition,
X1 = position.X,
X2 = position.X,
Y1 = position.Y,
Y2 = position.Y,
Direction = player.Rotation,
};

await player.WarpToAsync(currentPosition).ConfigureAwait(false);
}

private bool IsXInRange(Point currentPosition, Gate gate, byte inaccuracy) => currentPosition.X >= gate.X1 - inaccuracy
&& currentPosition.X <= gate.X2 + inaccuracy;

Expand Down
26 changes: 26 additions & 0 deletions src/GameLogic/PlugIns/IWarpGateEnteringPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// <copyright file="IWarpGateEnteringPlugIn.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.ComponentModel;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// A plugin interface which is called when a player is about to enter a warp gate.
/// </summary>
[Guid("3F6C2B1E-9A47-4D8B-B5E2-7C0D1A9E4F36")]
[PlugInPoint("Warp gate entering", "Plugins which are called when a player is about to enter a warp gate. They can deny the entrance, e.g. while an event map is closed.")]
public interface IWarpGateEnteringPlugIn
{
/// <summary>
/// Is called when a player is about to enter a warp gate.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="targetGate">The target gate.</param>
/// <param name="eventArgs">The <see cref="CancelEventArgs"/> instance containing the event data. The entrance can be denied by setting <see cref="CancelEventArgs.Cancel"/> to <c>true</c>.
/// The plugin should then show the reason to the player. The player stays at its current position.</param>
ValueTask WarpGateEnteringAsync(Player player, ExitGate targetGate, CancelEventArgs eventArgs);
}
34 changes: 28 additions & 6 deletions src/GameLogic/Raklion/RaklionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,24 @@ public void Dispose()
}
}

/// <summary>
/// Checks whether the player may enter the map through a gate.
/// Like in the original game, the hatchery can't be entered while it's closed, and the player stays where it is.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="map">The map of the target gate.</param>
/// <returns><c>true</c>, if the player may enter the map; otherwise, <c>false</c>.</returns>
internal async ValueTask<bool> CanEnterThroughGateAsync(Player player, GameMapDefinition map)
{
if (map.Number != this._definition.HatcheryMapNumber || this.CanEnterHatchery)
{
return true;
}

await this.ShowHatcheryIsClosedAsync(player).ConfigureAwait(false);
return false;
}

/// <summary>
/// Changes the state of Selupan and shows it to the players.
/// </summary>
Expand Down Expand Up @@ -507,12 +525,7 @@ private async ValueTask OnObjectAddedToHatcheryAsync((GameMap Map, ILocateable O
else if (!this._battlePlayers.ContainsKey(player))
{
// The player is moved out of the hatchery by the next tick.
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.RaklionHatcheryIsClosed)).ConfigureAwait(false);
if (this.State == RaklionState.Notify4)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.RaklionHatcheryOpensIn), this.GetOpeningTime(player)).ConfigureAwait(false);
}

await this.ShowHatcheryIsClosedAsync(player).ConfigureAwait(false);
return;
}
else
Expand All @@ -533,6 +546,15 @@ private ValueTask OnObjectRemovedFromHatcheryAsync((GameMap Map, ILocateable Obj
return ValueTask.CompletedTask;
}

private async ValueTask ShowHatcheryIsClosedAsync(Player player)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.RaklionHatcheryIsClosed)).ConfigureAwait(false);
if (this.State == RaklionState.Notify4)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.RaklionHatcheryOpensIn), this.GetOpeningTime(player)).ConfigureAwait(false);
}
}

private async ValueTask ShowCurrentStateAsync(Player player)
{
await player.InvokeViewPlugInAsync<IRaklionEventViewPlugIn>(p => p.ShowCurrentStateAsync(this.State)).ConfigureAwait(false);
Expand Down
41 changes: 41 additions & 0 deletions src/GameLogic/Raklion/RaklionEventDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,34 @@ public class RaklionEventDefinition
/// </summary>
public int AreaSkillRadius { get; set; } = 6;

/// <summary>
/// Gets or sets the radius around Selupan, in which its fall hits players when it appears.
/// Only players which are closer than this radius are hit.
/// </summary>
public int FallRadius { get; set; } = 4;

/// <summary>
/// Gets or sets the number of fields, by which the fall of Selupan pushes the hit players away.
/// </summary>
public int FallPushDistance { get; set; } = 4;

/// <summary>
/// Gets or sets the number of fields, by which the ice strike (frost shock) of Selupan pushes the hit players away.
/// </summary>
public int IceStrikePushDistance { get; set; } = 10;

/// <summary>
/// Gets or sets the maximum number of fields, by which the skills of Selupan push the players
/// of the character classes which aren't contained in <see cref="FullyPushedCharacterClassNumbers"/>.
/// </summary>
public int LimitedPushDistance { get; set; } = 2;

/// <summary>
/// Gets or sets the numbers of the character classes, which are pushed by the full distance of the skills of Selupan.
/// Like in the original game, these are the dark knights, magic gladiators and dark lords.
/// </summary>
public IList<byte> FullyPushedCharacterClassNumbers { get; set; } = new List<byte> { 4, 6, 7, 12, 13, 16, 17 };

/// <summary>
/// Gets the pattern (1 to 7) of Selupan by its remaining health.
/// </summary>
Expand Down Expand Up @@ -172,6 +200,19 @@ public int GetBerserkLevel(int pattern)
return this.PatternBerserkLevels[Math.Clamp(pattern, 1, this.PatternBerserkLevels.Count) - 1];
}

/// <summary>
/// Gets the number of fields, by which a player of the character class is pushed by a skill of Selupan.
/// </summary>
/// <param name="characterClassNumber">The number of the character class of the player.</param>
/// <param name="distance">The push distance of the skill.</param>
/// <returns>The distance, limited by <see cref="LimitedPushDistance"/> for the classes which aren't pushed by the full distance.</returns>
public int GetPushDistance(byte? characterClassNumber, int distance)
{
return characterClassNumber is { } number && this.FullyPushedCharacterClassNumbers.Contains(number)
? distance
: Math.Min(distance, this.LimitedPushDistance);
}

/// <summary>
/// Gets the skills which Selupan may use in the pattern, except the <see cref="SelupanSkill.Fall"/>.
/// </summary>
Expand Down
14 changes: 13 additions & 1 deletion src/GameLogic/Raklion/RaklionPlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace MUnique.OpenMU.GameLogic.Raklion;

using System.Collections.Concurrent;
using System.ComponentModel;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
Expand All @@ -19,7 +20,7 @@ namespace MUnique.OpenMU.GameLogic.Raklion;
[PlugIn]
[Display(Name = nameof(PlugInResources.RaklionPlugIn_Name), Description = nameof(PlugInResources.RaklionPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("C5A1E7D3-8B24-4F69-9E0A-6D3B2F7C4E18")]
public sealed class RaklionPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISupportCustomConfiguration<RaklionEventDefinition>, ISupportDefaultCustomConfiguration, IDisposable
public sealed class RaklionPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, IWarpGateEnteringPlugIn, ISupportCustomConfiguration<RaklionEventDefinition>, ISupportDefaultCustomConfiguration, IDisposable
{
private readonly ConcurrentDictionary<IGameContext, RaklionContext> _contexts = new();
private readonly ConcurrentDictionary<IGameContext, int> _runningTicks = new();
Expand Down Expand Up @@ -82,6 +83,17 @@ public void ForceStart()
}
}

/// <inheritdoc />
public async ValueTask WarpGateEnteringAsync(Player player, ExitGate targetGate, CancelEventArgs eventArgs)
{
if (targetGate.Map is { } map
&& this._contexts.TryGetValue(player.GameContext, out var context)
&& !await context.CanEnterThroughGateAsync(player, map).ConfigureAwait(false))
{
eventArgs.Cancel = true;
}
}

/// <inheritdoc />
public object CreateDefaultConfig()
{
Expand Down
Loading