baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
462
src/GameLogic/NPC/AttackableNpcBase.cs
Normal file
462
src/GameLogic/NPC/AttackableNpcBase.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
// <copyright file="AttackableNpcBase.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.Diagnostics;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns;
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// An abstract base class for an <see cref="IAttackable"/> <see cref="NonPlayerCharacter"/>.
|
||||
/// </summary>
|
||||
public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
{
|
||||
private const byte MaximumDropDistance = 2;
|
||||
|
||||
private readonly IEventStateProvider? _eventStateProvider;
|
||||
private readonly IDropGenerator _dropGenerator;
|
||||
private readonly PlugInManager _plugInManager;
|
||||
private readonly List<IDisposable> _registrations = new();
|
||||
|
||||
private int _health;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AttackableNpcBase" /> class.
|
||||
/// </summary>
|
||||
/// <param name="spawnInfo">The spawn information.</param>
|
||||
/// <param name="stats">The stats.</param>
|
||||
/// <param name="map">The map.</param>
|
||||
/// <param name="eventStateProvider">The event state provider.</param>
|
||||
/// <param name="dropGenerator">The drop generator.</param>
|
||||
/// <param name="plugInManager">The plug in manager.</param>
|
||||
protected AttackableNpcBase(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, IEventStateProvider? eventStateProvider, IDropGenerator dropGenerator, PlugInManager plugInManager)
|
||||
: base(spawnInfo, stats, map)
|
||||
{
|
||||
this._eventStateProvider = eventStateProvider;
|
||||
this._dropGenerator = dropGenerator;
|
||||
this._plugInManager = plugInManager;
|
||||
this.MagicEffectList = new MagicEffectsList(this);
|
||||
this.Attributes = new MonsterAttributeHolder(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when this instance died.
|
||||
/// </summary>
|
||||
public event EventHandler<DeathInformation>? Died;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAttributeSystem Attributes { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MagicEffectsList MagicEffectList { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAlive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this <see cref="IAttackable" /> is currently teleporting and can't be directly targeted.
|
||||
/// It can still receive damage, if the teleport target coordinates are within an target skill area for area attacks.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if teleporting; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
/// <remarks>Teleporting for monsters or npcs is not implemented yet.</remarks>
|
||||
public bool IsTeleporting => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeathInformation? LastDeath { get; protected set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Point Position
|
||||
{
|
||||
get => base.Position;
|
||||
set
|
||||
{
|
||||
if (base.Position != value)
|
||||
{
|
||||
base.Position = value;
|
||||
this._plugInManager?.GetPlugInPoint<IAttackableMovedPlugIn>()?.AttackableMoved(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current health.
|
||||
/// </summary>
|
||||
public int Health
|
||||
{
|
||||
get => Math.Max(this._health, 0);
|
||||
set => this._health = value;
|
||||
}
|
||||
|
||||
private bool ShouldRespawn => this.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic
|
||||
|| (this.SpawnArea.SpawnTrigger == SpawnTrigger.AutomaticDuringEvent && (this._eventStateProvider?.IsEventRunning ?? false))
|
||||
|| (this.SpawnArea.SpawnTrigger == SpawnTrigger.AutomaticDuringWave && (this._eventStateProvider?.IsSpawnWaveActive(this.SpawnArea.WaveNumber) ?? false));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null)
|
||||
{
|
||||
if (this.Definition.ObjectKind == NpcObjectKind.Guard || this.IsAttackBlockedBySafezone(attacker))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var hitInfo = await attacker.CalculateDamageAsync(this, skill, isCombo, damageFactor).ConfigureAwait(false);
|
||||
|
||||
if (skill?.Skill is not { } attackSkill || attackSkill.DamageType != DamageType.Fenrir)
|
||||
{
|
||||
attacker.ApplyAmmunitionConsumption(hitInfo);
|
||||
}
|
||||
|
||||
await this.HitAsync(hitInfo, attacker, skill?.Skill, isFinalStreakHit).ConfigureAwait(false);
|
||||
|
||||
if (hitInfo.HealthDamage > 0)
|
||||
{
|
||||
if (this.Attributes[Stats.IsAsleep] > 0)
|
||||
{
|
||||
await this.MagicEffectList.ClearAllEffectsProducingSpecificStatAsync(Stats.IsAsleep).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (attacker is Player player)
|
||||
{
|
||||
await player.AfterHitTargetAsync().ConfigureAwait(false);
|
||||
|
||||
if (this.IsAlive && Rand.NextRandomBool(player.Attributes![Stats.MaceMasteryStunChance]))
|
||||
{
|
||||
await player.ApplyMaceMasteryStunEffectAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (attacker as IPlayerSurrogate is { } playerSurrogate)
|
||||
{
|
||||
await playerSurrogate.Owner.AfterHitTargetAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return hitInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask ReflectDamageAsync(IAttacker reflector, uint damage);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask KillInstantlyAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
this.Health = this.SpawnArea.MaximumHealthOverride ?? (int)this.Attributes[Stats.MaximumHealth];
|
||||
this.IsAlive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the attributes from the <see cref="NonPlayerCharacter.Definition"/>, so that changes
|
||||
/// to the monster definition take effect on this already spawned instance.
|
||||
/// </summary>
|
||||
public void ReloadAttributes()
|
||||
{
|
||||
(this.Attributes as MonsterAttributeHolder)?.ApplyChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a disposable (e.g. a configuration change registration) to be disposed
|
||||
/// together with this instance.
|
||||
/// </summary>
|
||||
/// <param name="disposable">The disposable.</param>
|
||||
public void RegisterDisposable(IDisposable disposable)
|
||||
{
|
||||
this._registrations.Add(disposable);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool managed)
|
||||
{
|
||||
if (managed)
|
||||
{
|
||||
this.Died = null;
|
||||
this.IsAlive = false;
|
||||
foreach (var registration in this._registrations)
|
||||
{
|
||||
registration.Dispose();
|
||||
}
|
||||
|
||||
this._registrations.Clear();
|
||||
}
|
||||
|
||||
base.Dispose(managed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the object is removed from the map.
|
||||
/// </summary>
|
||||
protected virtual void OnRemoveFromMap()
|
||||
{
|
||||
// can be overwritten to do additional stuff.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hits this instance with the specified hit information.
|
||||
/// </summary>
|
||||
/// <param name="hitInfo">The hit information.</param>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
/// <param name="skill">The skill.</param>
|
||||
/// <param name="isFinalStreakHit">
|
||||
/// Not <c>null</c> when it's a rage fighter multiple hit skill:
|
||||
/// <c>true</c>, if it's the final hit;
|
||||
/// <c>false</c>, for other hits.
|
||||
/// </param>
|
||||
protected async ValueTask HitAsync(HitInfo hitInfo, IAttacker attacker, Skill? skill, bool? isFinalStreakHit = null)
|
||||
{
|
||||
if (!this.IsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var killed = this.TryHit(hitInfo.HealthDamage + hitInfo.ShieldDamage, attacker);
|
||||
|
||||
var player = this.GetHitNotificationTarget(attacker);
|
||||
if (player is not null)
|
||||
{
|
||||
if (isFinalStreakHit.HasValue)
|
||||
{
|
||||
hitInfo.Attributes |= DamageAttributes.RageFighterStreakHit;
|
||||
|
||||
if (isFinalStreakHit.Value || killed)
|
||||
{
|
||||
hitInfo.Attributes |= DamageAttributes.RageFighterStreakFinalHit;
|
||||
}
|
||||
}
|
||||
|
||||
await player.InvokeViewPlugInAsync<IShowHitPlugIn>(p => p.ShowHitAsync(this, hitInfo)).ConfigureAwait(false);
|
||||
player.GameContext.PlugInManager.GetPlugInPoint<IAttackableGotHitPlugIn>()?.AttackableGotHit(this, attacker, hitInfo);
|
||||
}
|
||||
|
||||
if (killed)
|
||||
{
|
||||
this.LastDeath = new DeathInformation(attacker.Id, attacker.GetName(), hitInfo, skill?.Number ?? 0);
|
||||
await this.OnDeathAsync(attacker).ConfigureAwait(false);
|
||||
this.Died?.Invoke(this, this.LastDeath);
|
||||
if (!this.ShouldRespawn)
|
||||
{
|
||||
await this.RemoveFromMapAndDisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of a hit notification.
|
||||
/// </summary>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
/// <returns>The target player of a hit notification.</returns>
|
||||
protected virtual Player? GetHitNotificationTarget(IAttacker attacker)
|
||||
{
|
||||
return attacker as Player ?? (attacker as IPlayerSurrogate)?.Owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the hit.
|
||||
/// </summary>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
protected virtual void RegisterHit(IAttacker attacker)
|
||||
{
|
||||
// can be overwritten
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this instance died.
|
||||
/// </summary>
|
||||
/// <param name="attacker">The attacker which killed this instance.</param>
|
||||
protected virtual async ValueTask OnDeathAsync(IAttacker attacker)
|
||||
{
|
||||
if (this.ShouldRespawn)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(this.Definition.RespawnDelay).ConfigureAwait(false);
|
||||
await this.RespawnAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail($"Unexpected error during respawning the attackable npc {this}: {ex}", ex.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await this.ForEachWorldObserverAsync<IObjectGotKilledPlugIn>(p => p.ObjectGotKilledAsync(this, attacker), true).ConfigureAwait(false);
|
||||
|
||||
var player = this.GetHitNotificationTarget(attacker);
|
||||
if (player is { })
|
||||
{
|
||||
int exp = await (player.Party?.DistributeExperienceAfterKillAsync(this, player) ?? player.AddExpAfterKillAsync(this)).ConfigureAwait(false);
|
||||
if (attacker == player)
|
||||
{
|
||||
await player.AfterKilledMonsterAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (player.GameContext.PlugInManager.GetPlugInPoint<IAttackableGotKilledPlugIn>() is { } plugInPoint)
|
||||
{
|
||||
await plugInPoint.AttackableGotKilledAsync(this, attacker).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!this.IsSummonedMonster && player.SelectedCharacter is { } selectedCharacter)
|
||||
{
|
||||
if (selectedCharacter.State > HeroState.Normal)
|
||||
{
|
||||
selectedCharacter.StateRemainingSeconds -= (int)this.Attributes[Stats.Level];
|
||||
}
|
||||
|
||||
_ = this.DropItemDelayedAsync(player, exp); // don't wait for completion.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask RemoveFromMapAndDisposeAsync()
|
||||
{
|
||||
await this.CurrentMap.RemoveAsync(this).ConfigureAwait(false);
|
||||
this.Dispose();
|
||||
this.OnRemoveFromMap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Respawns this instance on the map.
|
||||
/// </summary>
|
||||
private async ValueTask RespawnAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this.ShouldRespawn)
|
||||
{
|
||||
await this.RemoveFromMapAndDisposeAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.Initialize();
|
||||
await this.CurrentMap.RespawnAsync(this).ConfigureAwait(false);
|
||||
this.OnSpawn();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail(ex.Message, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryHit(uint damage, IAttacker attacker)
|
||||
{
|
||||
if (damage > 0)
|
||||
{
|
||||
this.RegisterHit(attacker);
|
||||
}
|
||||
|
||||
if (damage >= this.Health)
|
||||
{
|
||||
this.IsAlive = false;
|
||||
this.Health = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Interlocked.Add(ref this._health, -(int)damage);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleMoneyDropAsync(uint amount, Player killer)
|
||||
{
|
||||
// We don't drop money in Devil Square, etc.
|
||||
var shouldDropMoney = killer.GameContext.Configuration.ShouldDropMoney && killer.CurrentMiniGame is null;
|
||||
if (!shouldDropMoney)
|
||||
{
|
||||
var party = killer.Party;
|
||||
if (party is null)
|
||||
{
|
||||
killer.TryAddMoney((int)amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
await party.DistributeMoneyAfterKillAsync(this, killer, amount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var droppedMoney = new DroppedMoney((uint)(amount * (killer.Attributes?[Stats.MoneyAmountRate] ?? 1.0f)), this.Position, this.CurrentMap);
|
||||
await this.CurrentMap.AddAsync(droppedMoney).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask DropItemAsync(int exp, Player killer)
|
||||
{
|
||||
// When the killer is in a party, DistributeExperienceAfterKillAsync returns a
|
||||
// total party experience that does NOT include game rate (ExperienceRate) or
|
||||
// personal experience rate multipliers. Since the money drop amount is
|
||||
// derived from this experience value, party money drops were dramatically
|
||||
// lower than solo drops. We recalculate the experience for money purposes
|
||||
// using the solo formula so money is consistent regardless of party state.
|
||||
if (killer.Party is not null)
|
||||
{
|
||||
exp = killer.CalculateExpAfterKill(this);
|
||||
}
|
||||
|
||||
var (generatedItems, droppedMoney) = await this._dropGenerator.GenerateItemDropsAsync(this.Definition, exp, killer).ConfigureAwait(false);
|
||||
if (droppedMoney > 0)
|
||||
{
|
||||
await this.HandleMoneyDropAsync(droppedMoney.Value, killer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var firstItem = !droppedMoney.HasValue;
|
||||
foreach (var item in generatedItems)
|
||||
{
|
||||
Point dropCoordinates;
|
||||
if (firstItem)
|
||||
{
|
||||
dropCoordinates = this.Position;
|
||||
firstItem = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
dropCoordinates = this.CurrentMap.Terrain.GetRandomCoordinate(this.Position, MaximumDropDistance);
|
||||
}
|
||||
|
||||
var owners = killer.Party?.PartyList.AsEnumerable() ?? killer.GetAsEnumerable();
|
||||
var droppedItem = new DroppedItem(item, dropCoordinates, this.CurrentMap, null, owners);
|
||||
await this.CurrentMap.AddAsync(droppedItem).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask DropItemDelayedAsync(Player player, int gainedExp)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
await this.DropItemAsync(gainedExp, player).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
player.Logger.LogDebug(ex, "Dropping an item failed after killing '{this}': {ex}", this, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user