diff --git a/src/GameLogic/GameContext.cs b/src/GameLogic/GameContext.cs index 8d9d42e74..0da19768a 100644 --- a/src/GameLogic/GameContext.cs +++ b/src/GameLogic/GameContext.cs @@ -122,6 +122,11 @@ public GameContext(GameConfiguration configuration, IPersistenceContextProvider /// public IDictionary PlayersByCharacterName { get; } = new ConcurrentDictionary(); + /// + /// Gets the state of the active self defenses. + /// + public ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; } = new(); + /// public ILoggerFactory LoggerFactory { get; } diff --git a/src/GameLogic/IGameContext.cs b/src/GameLogic/IGameContext.cs index 2407b5e0a..62b839b5f 100644 --- a/src/GameLogic/IGameContext.cs +++ b/src/GameLogic/IGameContext.cs @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.Pathfinding; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.PlugIns; +using System.Collections.Concurrent; /// /// The context of the game. @@ -75,6 +76,11 @@ public interface IGameContext /// IObjectPool PathFinderPool { get; } + /// + /// Gets the state of the active self defenses. + /// + ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; } + /// /// Gets the initialized maps which are hosted on this context. /// diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 5180b22ef..f59d7e96f 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -534,6 +534,23 @@ public async ValueTask KillInstantlyAsync() await this.OnDeathAsync(null).ConfigureAwait(false); } + /// + /// Determines whether the self defense is active for the specified attacker. + /// + /// The attacker. + /// + /// true if the self defense is active for the specified attacker; otherwise, false. + /// + public bool IsSelfDefenseActive(Player attacker) + { + if (this.GameContext.SelfDefenseState.TryGetValue((attacker, this), out var timeout)) + { + return timeout > DateTime.UtcNow; + } + + return false; + } + /// public async ValueTask AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo) { @@ -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 @@ -1679,9 +1696,27 @@ async Task RespawnAsync(CancellationToken cancellationToken) /// Is called after the player killed a . /// Increment PK Level. /// - 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) diff --git a/src/GameLogic/PlugIns/SelfDefensePlugIn.cs b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs new file mode 100644 index 000000000..347b93621 --- /dev/null +++ b/src/GameLogic/PlugIns/SelfDefensePlugIn.cs @@ -0,0 +1,86 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +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; + +/// +/// Updates the state of the active self defenses on every second and every hit. +/// +[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); + + /// + 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); + } + } + } + + /// + 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(p => p.ShowMessageAsync(message, MessageType.BlueNormal)); + await attacker.InvokeViewPlugInAsync(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(p => p.ShowMessageAsync(message, MessageType.BlueNormal)).ConfigureAwait(false); + await attacker.InvokeViewPlugInAsync(p => p.ShowMessageAsync(message, MessageType.BlueNormal)).ConfigureAwait(false); + } +} \ No newline at end of file