Skip to content

Commit

Permalink
Merge pull request #381 from MUnique/self-defense
Browse files Browse the repository at this point in the history
Implemented self defense system
  • Loading branch information
sven-n authored Oct 20, 2023
2 parents 17b6ebe + c91b407 commit 3c950bb
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/GameLogic/GameContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public GameContext(GameConfiguration configuration, IPersistenceContextProvider
/// </summary>
public IDictionary<string, Player> PlayersByCharacterName { get; } = new ConcurrentDictionary<string, Player>();

/// <summary>
/// Gets the state of the active self defenses.
/// </summary>
public ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; } = new();

/// <inheritdoc />
public ILoggerFactory LoggerFactory { get; }

Expand Down
6 changes: 6 additions & 0 deletions src/GameLogic/IGameContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
using System.Collections.Concurrent;

/// <summary>
/// The context of the game.
Expand Down Expand Up @@ -75,6 +76,11 @@ public interface IGameContext
/// </summary>
IObjectPool<PathFinder> PathFinderPool { get; }

/// <summary>
/// Gets the state of the active self defenses.
/// </summary>
ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; }

/// <summary>
/// Gets the initialized maps which are hosted on this context.
/// </summary>
Expand Down
41 changes: 38 additions & 3 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,23 @@ public async ValueTask KillInstantlyAsync()
await this.OnDeathAsync(null).ConfigureAwait(false);
}

/// <summary>
/// Determines whether the self defense is active for the specified attacker.
/// </summary>
/// <param name="attacker">The attacker.</param>
/// <returns>
/// <c>true</c> if the self defense is active for the specified attacker; otherwise, <c>false</c>.
/// </returns>
public bool IsSelfDefenseActive(Player attacker)
{
if (this.GameContext.SelfDefenseState.TryGetValue((attacker, this), out var timeout))
{
return timeout > DateTime.UtcNow;
}

return false;
}

/// <inheritdoc/>
public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo)
{
Expand Down Expand Up @@ -1636,7 +1653,7 @@ private async ValueTask OnDeathAsync(IAttacker? killer)
&& !(killerAfterKilled.GuildWarContext?.Score is { } score && score == this.GuildWarContext?.Score)
&& this.CurrentMiniGame?.AllowPlayerKilling is not true)
{
await killerAfterKilled.AfterKilledPlayerAsync().ConfigureAwait(false);
await killerAfterKilled.AfterKilledPlayerAsync(this).ConfigureAwait(false);
}

// TODO: Drop items
Expand Down Expand Up @@ -1679,9 +1696,27 @@ async Task RespawnAsync(CancellationToken cancellationToken)
/// Is called after the player killed a <see cref="Player"/>.
/// Increment PK Level.
/// </summary>
private async ValueTask AfterKilledPlayerAsync()
private async ValueTask AfterKilledPlayerAsync(Player killedPlayer)
{
// TODO: Self Defense System
var killedPlayerState = killedPlayer.SelectedCharacter?.State;
if (killedPlayerState is null)
{
return;
}

if (killedPlayerState >= HeroState.PlayerKiller1stStage)
{
// Killing PKs is allowed.
return;
}

if (killedPlayerState <= HeroState.PlayerKillWarning
&& this.IsSelfDefenseActive(killedPlayer))
{
// Self defense is allowed.
return;
}

if (this._selectedCharacter!.State != HeroState.PlayerKiller2ndStage)
{
if (this._selectedCharacter.State < HeroState.Normal)
Expand Down
86 changes: 86 additions & 0 deletions src/GameLogic/PlugIns/SelfDefensePlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// <copyright file="SelfDefensePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

using MUnique.OpenMU.GameLogic.NPC;

namespace MUnique.OpenMU.GameLogic.PlugIns;

using System;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// Updates the state of the active self defenses on every second and every hit.
/// </summary>
[PlugIn(nameof(SelfDefensePlugIn), "Updates the state of the self defense system.")]
[Guid("3E702A15-653A-48EF-899C-4CDB2239A90C")]
public class SelfDefensePlugIn : IPeriodicTaskPlugIn, IAttackableGotHitPlugIn
{
private readonly TimeSpan _selfDefenseTime = TimeSpan.FromSeconds(60);

/// <inheritdoc />
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
{
var timedOut = gameContext.SelfDefenseState.Where(s => DateTime.UtcNow.Subtract(s.Value) >= _selfDefenseTime).ToList();
foreach (var (pair, lastAttack) in timedOut)
{
if (gameContext.SelfDefenseState.Remove(pair, out _))
{
await this.EndSelfDefenseAsync(pair.Attacker, pair.Defender).ConfigureAwait(false);
}
}
}

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

if (defender.SelectedCharacter?.State >= HeroState.PlayerKiller1stStage)
{
// PKs have no right to self defense.
return;
}

if (attackerPlayer.IsSelfDefenseActive(defender))
{
// Attacking during self defense period does not initiate another self defense.
return;
}

if (hitInfo is { HealthDamage: 0, ShieldDamage: 0 } || hitInfo.Attributes.HasFlag(DamageAttributes.Reflected))
{
return;
}

var now = DateTime.UtcNow;
var gameContext = defender.GameContext;
gameContext.SelfDefenseState.AddOrUpdate((attackerPlayer, defender), tuple =>
{
_ = this.BeginSelfDefenseAsync(attackerPlayer, defender);
return now;
}, (tuple, time) => now);
}

private async ValueTask BeginSelfDefenseAsync(Player attacker, Player defender)
{
var message = $"Self defense is initiated by {attacker.Name}'s attack to {defender.Name}!";
await defender.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.BlueNormal));
await attacker.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.BlueNormal));
}

private async ValueTask EndSelfDefenseAsync(Player attacker, Player defender)
{
var message = $"Self defense of {defender.Name} against {attacker.Name} diminishes.";
await defender.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.BlueNormal)).ConfigureAwait(false);
await attacker.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.BlueNormal)).ConfigureAwait(false);
}
}

0 comments on commit 3c950bb

Please sign in to comment.