Skip to content

Commit

Permalink
Merge pull request #417 from MUnique/dev/kalima-gates
Browse files Browse the repository at this point in the history
WIP: Kalima Gates
  • Loading branch information
sven-n authored Jun 12, 2024
2 parents c3823e4 + 8fd9834 commit 04b014e
Show file tree
Hide file tree
Showing 19 changed files with 744 additions and 71 deletions.
5 changes: 5 additions & 0 deletions src/GameLogic/DroppedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ private async ValueTask<bool> TryStackOnItemAsync(Player player, Item stackTarge

player.Logger.LogInformation("Item '{0}' got picked up by player '{1}'. Durability of available stack {2} increased to {3}", this, player, stackTarget, stackTarget.Durability);
this.DisposeAndDelete(null);
if (player.GameContext.PlugInManager.GetPlugInPoint<PlugIns.IItemStackedPlugIn>() is { } itemStackedPlugIn)
{
await itemStackedPlugIn.ItemStackedAsync(player, this.Item, stackTarget).ConfigureAwait(false);
}

return true;
}

Expand Down
9 changes: 9 additions & 0 deletions src/GameLogic/LocateableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ public static double GetDistanceTo(this ILocateable objectFrom, Point objectToPo
/// <returns><c>True</c>, if the specified coordinate is in the specified range of the object; Otherwise, <c>false</c>.</returns>
public static bool IsInRange(this ILocateable obj, Point point, int range) => obj.IsInRange(point.X, point.Y, range);

/// <summary>
/// Determines whether the specified coordinates are in the specified range of the object.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="obj2">The second object.</param>
/// <param name="range">The maximum range.</param>
/// <returns><c>True</c>, if the specified coordinate is in the specified range of the object; Otherwise, <c>false</c>.</returns>
public static bool IsInRange(this ILocateable obj, ILocateable obj2, int range) => obj.IsInRange(obj2.Position, range);

/// <summary>
/// Determines whether the specified coordinate is in the specified range of the object.
/// </summary>
Expand Down
130 changes: 130 additions & 0 deletions src/GameLogic/NPC/GateNpc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// <copyright file="GateNpc.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.NPC;

using System.Threading;

/// <summary>
/// Represents a <see cref="NonPlayerCharacter"/> which is a gate to another map.
/// When a player gets close to the gate, it will warp the player to the target gate,
/// if the player is in the same party as the summoner.
/// </summary>
public class GateNpc : NonPlayerCharacter, ISummonable
{
private const int Range = 2;
private readonly ILogger<GateNpc> _logger;
private readonly Task _warpTask;
private readonly CancellationTokenSource _cts;
private int _enterCount;

/// <summary>
/// Initializes a new instance of the <see cref="GateNpc" /> class.
/// </summary>
/// <param name="spawnInfo">The spawn information.</param>
/// <param name="stats">The stats.</param>
/// <param name="map">The map.</param>
/// <param name="summonedBy">The summoned by.</param>
/// <param name="targetGate">The target gate.</param>
/// <param name="lifespan">The lifespan.</param>
public GateNpc(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, Player summonedBy, ExitGate targetGate, TimeSpan lifespan)
: base(spawnInfo, stats, map)
{
this.SummonedBy = summonedBy;
this.TargetGate = targetGate;
this._logger = summonedBy.GameContext.LoggerFactory.CreateLogger<GateNpc>();
this._cts = new CancellationTokenSource(lifespan);
this._warpTask = Task.Run(this.RunTaskAsync);
}

/// <inheritdoc />
public Player SummonedBy { get; }

/// <summary>
/// Gets the target gate.
/// </summary>
public ExitGate TargetGate { get; }

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
this._cts.Cancel();
this._cts.Dispose();
base.Dispose(disposing);
}

/// <inheritdoc />
protected override async ValueTask DisposeAsyncCore()
{
await this._cts.CancelAsync().ConfigureAwait(false);
this._cts.Dispose();

await this._warpTask.ConfigureAwait(false);
await base.DisposeAsyncCore().ConfigureAwait(false);
}

private async Task RunTaskAsync()
{
try
{
var cancellationToken = this._cts.Token;
var playersInRange = new List<Player>();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(1000, cancellationToken).ConfigureAwait(false);

if (!this.SummonedBy.IsAlive)
{
this._logger.LogInformation("Closing the gate, player is dead or offline");
return;
}

playersInRange.Clear();
if (this.SummonedBy.Party is { } party)
{
playersInRange.AddRange(party.PartyList.OfType<Player>().Where(this.IsPlayerInRange));
}
else if (this.IsPlayerInRange(this.SummonedBy))
{
playersInRange.Add(this.SummonedBy);
}
else
{
// do nothing.
}

foreach (var player in playersInRange)
{
this._logger.LogInformation("Player {player} passes the gate ({enterCount})", player, this._enterCount);
await player.WarpToAsync(this.TargetGate).ConfigureAwait(false);
this._enterCount++;
if (this._enterCount >= this.SummonedBy.GameContext.Configuration.MaximumPartySize)
{
this._logger.LogInformation("Closing the gate, maximum entrances reached ({enterCount})", this._enterCount);
return;
}
}
}
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error in GateNpc task.");
}
finally
{
await this.DisposeAsync().ConfigureAwait(false);
}
}

private bool IsPlayerInRange(Player player)
{
return player.IsActive() && player.IsInRange(this, Range) && this.CurrentMap == player.CurrentMap;
}
}
21 changes: 21 additions & 0 deletions src/GameLogic/NPC/ISummonable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// <copyright file="ISummonable.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.NPC;

/// <summary>
/// An interface for a class which can (but not must) be summoned by a player.
/// </summary>
public interface ISummonable : IIdentifiable, ILocateable, IRotatable
{
/// <summary>
/// Gets the player which summoned this instance.
/// </summary>
Player? SummonedBy { get; }

/// <summary>
/// Gets the definition for this instance.
/// </summary>
MonsterDefinition Definition { get; }
}
2 changes: 1 addition & 1 deletion src/GameLogic/NPC/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace MUnique.OpenMU.GameLogic.NPC;
/// <summary>
/// The implementation of a monster, which can attack players.
/// </summary>
public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISupportWalk, IMovable
public sealed class Monster : AttackableNpcBase, IAttackable, IAttacker, ISupportWalk, IMovable, ISummonable
{
private readonly AsyncLock _moveLock = new();
private readonly INpcIntelligence _intelligence;
Expand Down
79 changes: 23 additions & 56 deletions src/GameLogic/PlayerActions/Items/DropItemAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace MUnique.OpenMU.GameLogic.PlayerActions.Items;

using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.GameLogic.PlugIns;
using static MUnique.OpenMU.GameLogic.PlugIns.IItemDropPlugIn;

/// <summary>
/// Action to drop an item from the inventory to the floor.
Expand All @@ -23,70 +24,36 @@ public async ValueTask DropItemAsync(Player player, byte slot, Point target)
{
var item = player.Inventory?.GetItem(slot);

if (item is not null && (player.CurrentMap?.Terrain.WalkMap[target.X, target.Y] ?? false))
{
if (item.Definition!.DropItems.Count > 0
&& item.Definition!.DropItems.Where(di => di.SourceItemLevel == item.Level) is { } itemDropGroups)
{
await this.DropRandomItemAsync(item, player, itemDropGroups).ConfigureAwait(false);
}
else
{
await this.DropItemAsync(player, item, target).ConfigureAwait(false);
}
}
else
if (item is null
|| !(player.CurrentMap?.Terrain.WalkMap[target.X, target.Y] ?? false))
{
await player.InvokeViewPlugInAsync<IItemDropResultPlugIn>(p => p.ItemDropResultAsync(slot, false)).ConfigureAwait(false);
}
}

/// <summary>
/// Drops a random item of the given <see cref="DropItemGroup"/> at the players coordinates.
/// </summary>
/// <param name="sourceItem">The source item.</param>
/// <param name="player">The player.</param>
/// <param name="dropItemGroups">The <see cref="DropItemGroup"/> from which the random item is generated.</param>
private async ValueTask DropRandomItemAsync(Item sourceItem, Player player, IEnumerable<ItemDropItemGroup> dropItemGroups)
{
if (dropItemGroups.Any(g => g.RequiredCharacterLevel > player.Level))
{
await player.InvokeViewPlugInAsync<IItemDropResultPlugIn>(p => p.ItemDropResultAsync(sourceItem.ItemSlot, false)).ConfigureAwait(false);
return;
}

var (item, droppedMoneyAmount, dropEffect) = player.GameContext.DropGenerator.GenerateItemDrop(dropItemGroups);
if (droppedMoneyAmount is not null)
if (player.GameContext.PlugInManager.GetPlugInPoint<IItemDropPlugIn>() is { } plugInPoint)
{
var droppedMoney = new DroppedMoney(droppedMoneyAmount.Value, player.Position, player.CurrentMap!);
await player.CurrentMap!.AddAsync(droppedMoney).ConfigureAwait(false);
}

if (item is not null)
{
var droppedItem = new DroppedItem(item, player.Position, player.CurrentMap!, player);
await player.CurrentMap!.AddAsync(droppedItem).ConfigureAwait(false);
}
var dropArguments = new ItemDropArguments();
await plugInPoint.HandleItemDropAsync(player, item, target, dropArguments).ConfigureAwait(false);
if (dropArguments.Cancel)
{
// If we're here, that means that a plugin has handled the drop request.
// Now, we have to decide if we should remove the item from the inventory or not.
if (dropArguments.Success)
{
await this.RemoveItemFromInventoryAsync(player, item).ConfigureAwait(false);
await player.PersistenceContext.DeleteAsync(item).ConfigureAwait(false);
}
else
{
await player.InvokeViewPlugInAsync<IItemDropResultPlugIn>(p => p.ItemDropResultAsync(slot, false)).ConfigureAwait(false);
}

if (dropEffect is not ItemDropEffect.Undefined)
{
await this.ShowDropEffectAsync(player, dropEffect).ConfigureAwait(false);
return;
}
}

await this.RemoveItemFromInventoryAsync(player, sourceItem).ConfigureAwait(false);
await player.PersistenceContext.DeleteAsync(sourceItem).ConfigureAwait(false);
}

private async ValueTask ShowDropEffectAsync(Player player, ItemDropEffect dropEffect)
{
if (dropEffect == ItemDropEffect.Swirl)
{
await player.InvokeViewPlugInAsync<IShowEffectPlugIn>(p => p.ShowEffectAsync(player, IShowEffectPlugIn.EffectType.Swirl)).ConfigureAwait(false);
}
else
{
await player.InvokeViewPlugInAsync<IShowItemDropEffectPlugIn>(p => p.ShowEffectAsync(dropEffect, player.Position)).ConfigureAwait(false);
}
await this.DropItemAsync(player, item, target).ConfigureAwait(false);
}

private async ValueTask DropItemAsync(Player player, Item item, Point target)
Expand Down
68 changes: 68 additions & 0 deletions src/GameLogic/PlayerActions/Items/ItemBoxDroppedPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// <copyright file="ItemBoxDroppedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlayerActions.Items;

using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
using static MUnique.OpenMU.GameLogic.PlugIns.IItemDropPlugIn;

/// <summary>
/// This plugin handles the drop of an item box, e.g. box of kundun.
/// </summary>
[PlugIn(nameof(ItemBoxDroppedPlugIn), "This plugin handles the drop of an item box, e.g. box of kundun.")]
[Guid("3D15D55D-EEFE-4B5F-89B1-6934AB3F0BEE")]
public sealed class ItemBoxDroppedPlugIn : IItemDropPlugIn
{
/// <inheritdoc />
public async ValueTask HandleItemDropAsync(Player player, Item sourceItem, Point target, ItemDropArguments cancelArgs)
{
if (sourceItem.Definition!.DropItems.Count <= 0
|| sourceItem.Definition!.DropItems.Where(di => di.SourceItemLevel == sourceItem.Level) is not { } itemDropGroups)
{
return;
}

cancelArgs.WasHandled = true;
if (itemDropGroups.Any(g => g.RequiredCharacterLevel > player.Level))
{
cancelArgs.Success = false;
return;
}

cancelArgs.Success = true;
var (item, droppedMoneyAmount, dropEffect) = player.GameContext.DropGenerator.GenerateItemDrop(itemDropGroups);
if (droppedMoneyAmount is not null)
{
var droppedMoney = new DroppedMoney(droppedMoneyAmount.Value, player.Position, player.CurrentMap!);
await player.CurrentMap!.AddAsync(droppedMoney).ConfigureAwait(false);
}

if (item is not null)
{
var droppedItem = new DroppedItem(item, player.Position, player.CurrentMap!, player);
await player.CurrentMap!.AddAsync(droppedItem).ConfigureAwait(false);
}

if (dropEffect is not ItemDropEffect.Undefined)
{
await this.ShowDropEffectAsync(player, dropEffect).ConfigureAwait(false);
}
}

private async ValueTask ShowDropEffectAsync(Player player, ItemDropEffect dropEffect)
{
if (dropEffect == ItemDropEffect.Swirl)
{
await player.InvokeViewPlugInAsync<IShowEffectPlugIn>(p => p.ShowEffectAsync(player, IShowEffectPlugIn.EffectType.Swirl)).ConfigureAwait(false);
}
else
{
await player.InvokeViewPlugInAsync<IShowItemDropEffectPlugIn>(p => p.ShowEffectAsync(dropEffect, player.Position)).ConfigureAwait(false);
}
}
}
Loading

0 comments on commit 04b014e

Please sign in to comment.