baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
40
src/GameLogic/ArrayExtensions.cs
Normal file
40
src/GameLogic/ArrayExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// <copyright file="ArrayExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for arrays.
|
||||
/// </summary>
|
||||
public static class ArrayExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears the elements of an array to the default values of the element type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The array element type.</typeparam>
|
||||
/// <param name="array">The array.</param>
|
||||
public static void ClearToDefaults<T>(this T[] array)
|
||||
{
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
array[i] = default!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the elements of an array to the default values of the element type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The array element type.</typeparam>
|
||||
/// <param name="array">The array.</param>
|
||||
public static void ClearToDefaults<T>(this T[,] array)
|
||||
{
|
||||
for (int i = 0; i < array.GetLength(0); i++)
|
||||
{
|
||||
for (int j = 0; j < array.GetLength(1); j++)
|
||||
{
|
||||
array[i, j] = default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/GameLogic/AsyncDisposable.cs
Normal file
54
src/GameLogic/AsyncDisposable.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// <copyright file="AsyncDisposable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for classes which want to implement <see cref="IAsyncDisposable"/>.
|
||||
/// </summary>
|
||||
public class AsyncDisposable : Disposable, IAsyncDisposable
|
||||
{
|
||||
private bool _isDisposing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposing.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is disposing; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public new bool IsDisposing
|
||||
{
|
||||
get => this._isDisposing || base.IsDisposing;
|
||||
private set => this._isDisposing = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!this.IsDisposed && !this.IsDisposing)
|
||||
{
|
||||
this.IsDisposing = true;
|
||||
try
|
||||
{
|
||||
await this.DisposeAsyncCore().ConfigureAwait(false);
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.IsDisposing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or
|
||||
/// resetting unmanaged resources asynchronously.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods", Justification = "This naming is suggested by microsoft itself.")]
|
||||
protected virtual ValueTask DisposeAsyncCore()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
1023
src/GameLogic/AttackableExtensions.cs
Normal file
1023
src/GameLogic/AttackableExtensions.cs
Normal file
File diff suppressed because it is too large
Load Diff
102
src/GameLogic/Attributes/AttributeSystemExtensions.cs
Normal file
102
src/GameLogic/Attributes/AttributeSystemExtensions.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
// <copyright file="AttributeSystemExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Attributes;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="IAttributeSystem"/>s.
|
||||
/// </summary>
|
||||
public static class AttributeSystemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the attribute for a dummy attribute to be used internally for durations.
|
||||
/// </summary>
|
||||
private static AttributeDefinition DurationDummy { get; } = new(new Guid("23D069C3-24D8-4277-8FDC-D82F0AF64037"), "Duration Dummy", "A dummy attribute to be used internally for durations.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attribute for a dummy attribute to be used internally for chances.
|
||||
/// </summary>
|
||||
private static AttributeDefinition ChanceDummy { get; } = new(new Guid("E6B9E6A5-5800-40EA-80B7-14C2C06392A6"), "Chance Dummy", "A dummy attribute to be used internally for chances.");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new element on this attribute system with the specified power up value.
|
||||
/// </summary>
|
||||
/// <param name="attributeSystem">The attribute system.</param>
|
||||
/// <param name="powerUpDefinition">The power up definition.</param>
|
||||
/// <returns>The added element.</returns>
|
||||
public static IElement CreateDurationElement(this IAttributeSystem attributeSystem, PowerUpDefinitionValue powerUpDefinition)
|
||||
{
|
||||
return attributeSystem.CreateElement(powerUpDefinition, DurationDummy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new element on this attribute system with the specified power up value.
|
||||
/// </summary>
|
||||
/// <param name="attributeSystem">The attribute system.</param>
|
||||
/// <param name="powerUpDefinition">The power up definition.</param>
|
||||
/// <returns>The added element.</returns>
|
||||
public static IElement CreateChanceElement(this IAttributeSystem attributeSystem, PowerUpDefinitionValue powerUpDefinition)
|
||||
{
|
||||
return attributeSystem.CreateElement(powerUpDefinition, ChanceDummy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new element on this attribute system with the specified power up value.
|
||||
/// </summary>
|
||||
/// <param name="attributeSystem">The attribute system.</param>
|
||||
/// <param name="powerUpDefinition">The power up definition.</param>
|
||||
/// <returns>The added element.</returns>
|
||||
public static IElement CreateElement(this IAttributeSystem attributeSystem, PowerUpDefinition powerUpDefinition)
|
||||
{
|
||||
var value = powerUpDefinition.Boost ?? throw Error.NotInitializedProperty(powerUpDefinition, nameof(powerUpDefinition.Boost));
|
||||
var targetDefinition = powerUpDefinition.TargetAttribute ?? throw Error.NotInitializedProperty(powerUpDefinition, nameof(powerUpDefinition.TargetAttribute));
|
||||
return attributeSystem.CreateElement(value, targetDefinition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new element on this attribute system with the specified power up value.
|
||||
/// </summary>
|
||||
/// <param name="attributeSystem">The attribute system.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="targetDefinition">The attribute.</param>
|
||||
/// <returns>The added element.</returns>
|
||||
public static IElement CreateElement(this IAttributeSystem attributeSystem, PowerUpDefinitionValue value, AttributeDefinition targetDefinition)
|
||||
{
|
||||
var relations = value.RelatedValues;
|
||||
var result = value.ConstantValue;
|
||||
if (relations?.Any() ?? false)
|
||||
{
|
||||
var elements = relations
|
||||
.Select(r => new AttributeRelationshipElement(
|
||||
new[] { attributeSystem.GetOrCreateAttribute(r.InputAttribute ?? throw new InvalidOperationException($"InputAttribute value not set for AttributeRelationship {r.GetId()}.")) },
|
||||
r.GetOperandElement(attributeSystem),
|
||||
r.InputOperator)
|
||||
{
|
||||
AggregateType = result.AggregateType,
|
||||
})
|
||||
.Cast<IElement>();
|
||||
|
||||
if (value.ConstantValue is not null)
|
||||
{
|
||||
elements = elements.Concat(value.ConstantValue.GetAsEnumerable());
|
||||
}
|
||||
|
||||
var composableResult = new ComposableAttribute(targetDefinition, result.AggregateType, value.MaximumValue);
|
||||
|
||||
elements.ForEach(element => composableResult.AddElement(element));
|
||||
return composableResult;
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new ArgumentException($"The passed {nameof(PowerUpDefinitionValue)} doesn't have a constant value or related values.", nameof(value));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
129
src/GameLogic/Attributes/ItemAwareAttributeSystem.cs
Normal file
129
src/GameLogic/Attributes/ItemAwareAttributeSystem.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
// <copyright file="ItemAwareAttributeSystem.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute system which considers items of a character.
|
||||
/// </summary>
|
||||
public sealed class ItemAwareAttributeSystem : AttributeSystem, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemAwareAttributeSystem" /> class.
|
||||
/// </summary>
|
||||
/// <param name="account">The account.</param>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <param name="gameConfiguration">The game configuration with global attributes.</param>
|
||||
public ItemAwareAttributeSystem(Account account, Character character, GameConfiguration gameConfiguration)
|
||||
: base(
|
||||
character.Attributes.Concat(account.Attributes),
|
||||
character.CharacterClass!.BaseAttributeValues.Concat(gameConfiguration.GlobalBaseAttributeValues),
|
||||
character.CharacterClass.AttributeCombinations.Concat(gameConfiguration.GlobalAttributeCombinations))
|
||||
{
|
||||
this.ItemPowerUps = new Dictionary<Item, IReadOnlyList<PowerUpWrapper>>();
|
||||
foreach (var attribute in this)
|
||||
{
|
||||
this.OnAttributeAdded(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an attribute value changed.
|
||||
/// </summary>
|
||||
public event EventHandler<IAttribute>? AttributeValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item power ups.
|
||||
/// </summary>
|
||||
public IDictionary<Item, IReadOnlyList<PowerUpWrapper>> ItemPowerUps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the item set power ups.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PowerUpWrapper>? ItemSetPowerUps { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.AttributeValueChanged = null;
|
||||
foreach (var attribute in this)
|
||||
{
|
||||
attribute.ValueChanged -= this.OnAttributeValueChanged;
|
||||
}
|
||||
|
||||
foreach (var powerUpWrapper in this.ItemPowerUps.SelectMany(p => p.Value))
|
||||
{
|
||||
powerUpWrapper.Dispose();
|
||||
}
|
||||
|
||||
this.ItemPowerUps.Clear();
|
||||
|
||||
if (this.ItemSetPowerUps is { } itemSetPowerUps)
|
||||
{
|
||||
foreach (var powerUp in itemSetPowerUps)
|
||||
{
|
||||
powerUp.Dispose();
|
||||
}
|
||||
|
||||
this.ItemSetPowerUps = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.Append(base.ToString());
|
||||
stringBuilder.AppendLine("Item Power Ups:");
|
||||
foreach (var (item, powerUps) in this.ItemPowerUps)
|
||||
{
|
||||
stringBuilder.Append(" ").AppendLine(item.ToString());
|
||||
foreach (var powerUp in powerUps)
|
||||
{
|
||||
stringBuilder.Append(" ").AppendLine(powerUp.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ItemSetPowerUps != null)
|
||||
{
|
||||
stringBuilder.AppendLine("Item Set Power Ups:");
|
||||
foreach (var attribute in this.ItemSetPowerUps)
|
||||
{
|
||||
stringBuilder.Append(" ").AppendLine(attribute.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttributeAdded(IAttribute attribute)
|
||||
{
|
||||
base.OnAttributeAdded(attribute);
|
||||
attribute.ValueChanged += this.OnAttributeValueChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttributeRemoved(IAttribute attribute)
|
||||
{
|
||||
attribute.ValueChanged -= this.OnAttributeValueChanged;
|
||||
base.OnAttributeRemoved(attribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an attribute value changed. Forwards the event to <see cref="AttributeValueChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
|
||||
private void OnAttributeValueChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is IAttribute attribute)
|
||||
{
|
||||
this.AttributeValueChanged?.Invoke(this, attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
src/GameLogic/Attributes/MonsterAttributeHolder.cs
Normal file
208
src/GameLogic/Attributes/MonsterAttributeHolder.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
// <copyright file="MonsterAttributeHolder.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
|
||||
/// <summary>
|
||||
/// The attribute system for monsters, which is considering monster definitions.
|
||||
/// </summary>
|
||||
public class MonsterAttributeHolder : IAttributeSystem
|
||||
{
|
||||
private static readonly IDictionary<AttributeDefinition, Func<AttackableNpcBase, float>> StatMapping =
|
||||
new Dictionary<AttributeDefinition, Func<AttackableNpcBase, float>>
|
||||
{
|
||||
{ Stats.CurrentHealth, m => m.Health },
|
||||
{ Stats.DefensePvm, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
|
||||
{ Stats.DefensePvp, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0) },
|
||||
{ Stats.DamageReceiveDecrement, m => 1.0f },
|
||||
{ Stats.AttackDamageIncrease, m => 1.0f },
|
||||
{ Stats.MovementSpeedFactor, m => 1.0f },
|
||||
{ Stats.ShieldBypassChance, m => 1.0f },
|
||||
{ Stats.DefenseDecrement, m => 1.0f - m.Attributes.GetValueOfAttribute(Stats.InnovationDefDecrement) },
|
||||
};
|
||||
|
||||
private static readonly IDictionary<AttributeDefinition, Action<AttackableNpcBase, float>> SetterMapping =
|
||||
new Dictionary<AttributeDefinition, Action<AttackableNpcBase, float>>
|
||||
{
|
||||
{ Stats.CurrentHealth, (m, v) => m.Health = (int)v },
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<MonsterDefinition, IDictionary<AttributeDefinition, float>> MonsterStatAttributesCache = new();
|
||||
|
||||
private readonly AttackableNpcBase _monster;
|
||||
|
||||
private readonly object _attributesLock = new();
|
||||
|
||||
private IDictionary<AttributeDefinition, float> _statAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute dictionary of a monster instance.
|
||||
/// Most monster instances don't have additional attributes, so we just instantiate one if needed.
|
||||
/// </summary>
|
||||
private IDictionary<AttributeDefinition, IComposableAttribute>? _attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MonsterAttributeHolder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="monster">The monster.</param>
|
||||
public MonsterAttributeHolder(AttackableNpcBase monster)
|
||||
{
|
||||
this._monster = monster;
|
||||
this._statAttributes = GetStatAttributeOfMonster(monster.Definition);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float this[AttributeDefinition attributeDefinition]
|
||||
{
|
||||
get => this.GetValueOfAttribute(attributeDefinition);
|
||||
|
||||
set
|
||||
{
|
||||
if (SetterMapping.TryGetValue(attributeDefinition, out var setAction))
|
||||
{
|
||||
setAction(this._monster, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float GetValueOfAttribute(AttributeDefinition attributeDefinition)
|
||||
{
|
||||
IDictionary<AttributeDefinition, IComposableAttribute>? attributes;
|
||||
lock (this._attributesLock)
|
||||
{
|
||||
attributes = this._attributes;
|
||||
}
|
||||
|
||||
if (attributes is not null
|
||||
&& attributes.TryGetValue(attributeDefinition, out var attribute))
|
||||
{
|
||||
return attribute.Value;
|
||||
}
|
||||
|
||||
if (this._statAttributes.TryGetValue(attributeDefinition, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (StatMapping.TryGetValue(attributeDefinition, out var mappingFunction))
|
||||
{
|
||||
return mappingFunction(this._monster);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddElement(IElement element, AttributeDefinition targetAttribute)
|
||||
{
|
||||
var attributeDictionary = this.GetAttributeDictionary();
|
||||
if (!attributeDictionary.TryGetValue(targetAttribute, out var attribute))
|
||||
{
|
||||
attribute = new ComposableAttribute(targetAttribute);
|
||||
var attrValue = this.GetValueOfAttribute(targetAttribute);
|
||||
var nullValue = element.AggregateType == AggregateType.Multiplicate ? 1 : 0;
|
||||
attrValue = Math.Abs(attrValue) < 0.01f ? nullValue : attrValue;
|
||||
attribute.AddElement(new SimpleElement { Value = attrValue });
|
||||
attributeDictionary.Add(targetAttribute, attribute);
|
||||
}
|
||||
|
||||
attribute.AddElement(element);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
|
||||
{
|
||||
IDictionary<AttributeDefinition, IComposableAttribute>? attributes;
|
||||
lock (this._attributesLock)
|
||||
{
|
||||
attributes = this._attributes;
|
||||
}
|
||||
|
||||
if (attributes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributes.TryGetValue(targetAttribute, out var attribute))
|
||||
{
|
||||
attribute.RemoveElement(element);
|
||||
if (!attribute.Elements.Skip(1).Any())
|
||||
{
|
||||
attributes.Remove(targetAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes.Count == 0)
|
||||
{
|
||||
lock (this._attributesLock)
|
||||
{
|
||||
this._attributes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddAttributeRelationship(AttributeRelationship combination, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the stat attributes from the (possibly changed) <see cref="MonsterDefinition"/>,
|
||||
/// so that configuration changes take effect on the running game server.
|
||||
/// </summary>
|
||||
public void ApplyChanges()
|
||||
{
|
||||
// When many instances of the same monster are spawned, all of them get notified about the
|
||||
// same change. The first one rebuilds the shared cache; the others just adopt the new one.
|
||||
if (MonsterStatAttributesCache.TryGetValue(this._monster.Definition, out var cached)
|
||||
&& !ReferenceEquals(cached, this._statAttributes))
|
||||
{
|
||||
this._statAttributes = cached;
|
||||
return;
|
||||
}
|
||||
|
||||
var statAttributes = BuildStatAttributes(this._monster.Definition);
|
||||
MonsterStatAttributesCache[this._monster.Definition] = statAttributes;
|
||||
this._statAttributes = statAttributes;
|
||||
}
|
||||
|
||||
private static IDictionary<AttributeDefinition, float> GetStatAttributeOfMonster(MonsterDefinition monsterDefinition)
|
||||
{
|
||||
return MonsterStatAttributesCache.GetOrAdd(monsterDefinition, BuildStatAttributes);
|
||||
}
|
||||
|
||||
private static IDictionary<AttributeDefinition, float> BuildStatAttributes(MonsterDefinition monsterDefinition)
|
||||
{
|
||||
return monsterDefinition.Attributes.ToDictionary(
|
||||
m => m.AttributeDefinition ?? throw Error.NotInitializedProperty(m, nameof(m.AttributeDefinition)),
|
||||
m => m.Value);
|
||||
}
|
||||
|
||||
private IDictionary<AttributeDefinition, IComposableAttribute> GetAttributeDictionary()
|
||||
{
|
||||
lock (this._attributesLock)
|
||||
{
|
||||
var attributes = this._attributes;
|
||||
if (attributes is null)
|
||||
{
|
||||
attributes = new Dictionary<AttributeDefinition, IComposableAttribute>();
|
||||
this._attributes = attributes;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/GameLogic/Attributes/PowerUpWrapper.cs
Normal file
105
src/GameLogic/Attributes/PowerUpWrapper.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
// <copyright file="PowerUpWrapper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper class which adapts power ups to <see cref="IElement"/> instances.
|
||||
/// </summary>
|
||||
public sealed class PowerUpWrapper : IElement, IDisposable
|
||||
{
|
||||
private readonly IElement _element;
|
||||
|
||||
private readonly AggregateType? _aggregateType;
|
||||
|
||||
private ComposableAttribute? _parentAttribute;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PowerUpWrapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="element">The element.</param>
|
||||
/// <param name="targetAttribute">The target attribute.</param>
|
||||
/// <param name="attributeHolder">The attribute holder.</param>
|
||||
/// <param name="aggregateType">The aggregate type that should override the <paramref name="element"/>'s.</param>
|
||||
public PowerUpWrapper(IElement element, AttributeDefinition targetAttribute, AttributeSystem attributeHolder, AggregateType? aggregateType = null)
|
||||
{
|
||||
this._parentAttribute = attributeHolder.GetComposableAttribute(targetAttribute);
|
||||
if (this._parentAttribute is null)
|
||||
{
|
||||
throw new ArgumentException($"Target attribute [{targetAttribute}] is not composable", nameof(targetAttribute));
|
||||
}
|
||||
|
||||
this._element = element;
|
||||
this._aggregateType = aggregateType;
|
||||
this._parentAttribute.AddElement(this);
|
||||
this._element.ValueChanged += this.OnValueChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? ValueChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Value => this._element.Value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AggregateType AggregateType => this._aggregateType ?? this._element.AggregateType;
|
||||
|
||||
/// <summary>
|
||||
/// Creates elements by a <see cref="PowerUpDefinition"/>.
|
||||
/// </summary>
|
||||
/// <param name="powerUpDef">The power up definition.</param>
|
||||
/// <param name="attributeHolder">The attribute holder.</param>
|
||||
/// <param name="aggregateType">The specific aggregate type. If not specified, the aggregate type of the <paramref name="powerUpDef"/>'s constant value will be used.</param>
|
||||
/// <returns>The elements which represent the power-up.</returns>
|
||||
public static IEnumerable<PowerUpWrapper> CreateByPowerUpDefinition(PowerUpDefinition powerUpDef, AttributeSystem attributeHolder, AggregateType? aggregateType = null)
|
||||
{
|
||||
if (powerUpDef.Boost?.ConstantValue != null)
|
||||
{
|
||||
yield return new PowerUpWrapper(
|
||||
powerUpDef.Boost.ConstantValue,
|
||||
powerUpDef.TargetAttribute ?? throw Error.NotInitializedProperty(powerUpDef, nameof(PowerUpDefinition.TargetAttribute)),
|
||||
attributeHolder,
|
||||
aggregateType);
|
||||
}
|
||||
|
||||
if (powerUpDef.Boost?.RelatedValues != null)
|
||||
{
|
||||
foreach (var relationship in powerUpDef.Boost.RelatedValues)
|
||||
{
|
||||
var aggregType = powerUpDef.Boost?.ConstantValue.AggregateType ?? AggregateType.AddRaw;
|
||||
yield return new PowerUpWrapper(
|
||||
attributeHolder.CreateRelatedAttribute(relationship, attributeHolder, aggregType),
|
||||
powerUpDef.TargetAttribute ?? throw Error.NotInitializedProperty(powerUpDef, nameof(PowerUpDefinition.TargetAttribute)),
|
||||
attributeHolder,
|
||||
aggregateType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this._parentAttribute != null)
|
||||
{
|
||||
this._parentAttribute.RemoveElement(this);
|
||||
this.ValueChanged = null;
|
||||
this._parentAttribute = null;
|
||||
this._element.ValueChanged -= this.OnValueChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? ToString()
|
||||
{
|
||||
return $"{this._parentAttribute?.Definition.Designation}: {this._element}{(this._aggregateType is { } aggreg ? $"->({aggreg})" : string.Empty)}";
|
||||
}
|
||||
|
||||
private void OnValueChanged(object? sender, EventArgs eventArgs)
|
||||
{
|
||||
this.ValueChanged?.Invoke(sender, eventArgs);
|
||||
}
|
||||
}
|
||||
1608
src/GameLogic/Attributes/Stats.cs
Normal file
1608
src/GameLogic/Attributes/Stats.cs
Normal file
File diff suppressed because it is too large
Load Diff
102
src/GameLogic/Attributes/TrapAttributeHolder.cs
Normal file
102
src/GameLogic/Attributes/TrapAttributeHolder.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
// <copyright file="TrapAttributeHolder.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
|
||||
/// <summary>
|
||||
/// The attribute system for traps, which is considering monster definitions.
|
||||
/// </summary>
|
||||
public class TrapAttributeHolder : IAttributeSystem
|
||||
{
|
||||
private static readonly IDictionary<AttributeDefinition, Func<Trap, float>> StatMapping =
|
||||
new Dictionary<AttributeDefinition, Func<Trap, float>>
|
||||
{
|
||||
{ Stats.AttackDamageIncrease, _ => 1.0f },
|
||||
{ Stats.ShieldBypassChance, _ => 1.0f },
|
||||
};
|
||||
|
||||
private static readonly IDictionary<MonsterDefinition, IDictionary<AttributeDefinition, float>> MonsterStatAttributesCache = new Dictionary<MonsterDefinition, IDictionary<AttributeDefinition, float>>();
|
||||
|
||||
private readonly Trap _trap;
|
||||
|
||||
private readonly IDictionary<AttributeDefinition, float> _statAttributes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TrapAttributeHolder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="trap">The trap.</param>
|
||||
public TrapAttributeHolder(Trap trap)
|
||||
{
|
||||
this._trap = trap;
|
||||
this._statAttributes = GetStatAttributeOfMonster(trap.Definition);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float this[AttributeDefinition attributeDefinition]
|
||||
{
|
||||
get => this.GetValueOfAttribute(attributeDefinition);
|
||||
|
||||
set
|
||||
{
|
||||
// can't set anything for Traps.
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float GetValueOfAttribute(AttributeDefinition attributeDefinition)
|
||||
{
|
||||
if (this._statAttributes.TryGetValue(attributeDefinition, out float value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (StatMapping.TryGetValue(attributeDefinition, out var mappingFunction))
|
||||
{
|
||||
return mappingFunction(this._trap);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddElement(IElement element, AttributeDefinition targetAttribute)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddAttributeRelationship(AttributeRelationship combination, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static IDictionary<AttributeDefinition, float> GetStatAttributeOfMonster(MonsterDefinition monsterDef)
|
||||
{
|
||||
if (!MonsterStatAttributesCache.TryGetValue(monsterDef, out var result))
|
||||
{
|
||||
result = monsterDef.Attributes.ToDictionary(
|
||||
m => m.AttributeDefinition ?? throw Error.NotInitializedProperty(m, nameof(PowerUpDefinition.TargetAttribute)),
|
||||
m => m.Value);
|
||||
MonsterStatAttributesCache.Add(monsterDef, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
44
src/GameLogic/BackupItemStorage.cs
Normal file
44
src/GameLogic/BackupItemStorage.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="BackupItemStorage.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// A storage which keeps items backed up - to be able to revert them after cancellation of an action.
|
||||
/// </summary>
|
||||
public class BackupItemStorage
|
||||
{
|
||||
private readonly IList<(Item Item, byte Slot)> _initialItemStates;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackupItemStorage"/> class.
|
||||
/// </summary>
|
||||
/// <param name="itemStorage">The item storage.</param>
|
||||
public BackupItemStorage(ItemStorage itemStorage)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
items.AddRange(itemStorage.Items);
|
||||
this.Items = items;
|
||||
this.Money = itemStorage.Money;
|
||||
this._initialItemStates = this.Items.Select(item => (item, item.ItemSlot)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the items.
|
||||
/// </summary>
|
||||
public ICollection<Item> Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the money.
|
||||
/// </summary>
|
||||
public int Money { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Restores the initial item states.
|
||||
/// </summary>
|
||||
public void RestoreItemStates()
|
||||
{
|
||||
this._initialItemStates.ForEach(state => state.Item.ItemSlot = state.Slot);
|
||||
}
|
||||
}
|
||||
73
src/GameLogic/BleedingMagicEffect.cs
Normal file
73
src/GameLogic/BleedingMagicEffect.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// <copyright file="BleedingMagicEffect.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Timers;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The magic effect for bleeding, which will damage the character every second until the effect ends.
|
||||
/// </summary>
|
||||
public sealed class BleedingMagicEffect : MagicEffect
|
||||
{
|
||||
private readonly Timer _damageTimer;
|
||||
private readonly float _damage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BleedingMagicEffect"/> class.
|
||||
/// </summary>
|
||||
/// <param name="powerUp">The power up.</param>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <param name="duration">The duration.</param>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <param name="damage">The bleeding damage.</param>
|
||||
public BleedingMagicEffect(IElement powerUp, MagicEffectDefinition definition, TimeSpan duration, IAttacker attacker, IAttackable owner, float damage)
|
||||
: base(powerUp, definition, duration)
|
||||
{
|
||||
this.Attacker = attacker;
|
||||
this.Owner = owner;
|
||||
this._damage = damage;
|
||||
this._damageTimer = new Timer(1000);
|
||||
this._damageTimer.Elapsed += this.OnDamageTimerElapsed;
|
||||
this._damageTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the owner of the effect.
|
||||
/// </summary>
|
||||
public IAttackable Owner { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attacker which applied the effect.
|
||||
/// </summary>
|
||||
public IAttacker Attacker { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
this._damageTimer.Stop();
|
||||
this._damageTimer.Dispose();
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void OnDamageTimerElapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this.Owner.IsAlive || this.IsDisposed || this.IsDisposing || this._damage <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this.Owner.ApplyBleedingDamageAsync(this.Attacker, (uint)this._damage).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
(this.Owner as ILoggerOwner)?.Logger.LogError(ex, "Error when applying bleeding damage");
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/GameLogic/BucketAreaOfInterestManager.cs
Normal file
205
src/GameLogic/BucketAreaOfInterestManager.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
// <copyright file="BucketAreaOfInterestManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// A area of interest manager which works with a bucket map.
|
||||
/// </summary>
|
||||
internal class BucketAreaOfInterestManager : IAreaOfInterestManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BucketAreaOfInterestManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="chunkSize">Size of the chunk.</param>
|
||||
public BucketAreaOfInterestManager(int chunkSize)
|
||||
{
|
||||
this.Map = new BucketMap<ILocateable>(0x100, true, chunkSize / 2, chunkSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying bucket map.
|
||||
/// </summary>
|
||||
protected BucketMap<ILocateable> Map { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask AddObjectAsync(ILocateable obj)
|
||||
{
|
||||
var newBucket = this.Map[obj.Position];
|
||||
if (obj is IHasBucketInformation bucketInfo)
|
||||
{
|
||||
bucketInfo.OldBucket = null;
|
||||
bucketInfo.NewBucket = newBucket;
|
||||
}
|
||||
|
||||
await newBucket.AddAsync(obj).ConfigureAwait(false);
|
||||
|
||||
if (obj is IBucketMapObserver observingPlayer)
|
||||
{
|
||||
await this.UpdateObservingBucketsAsync(obj.Position, observingPlayer).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// This needs to take into account, that the <see cref="Walker"/> might change the <see cref="ILocateable.Position"/>,
|
||||
/// but not updating the buckets. So accessing the bucket of the current coordinates might not be the current bucket!.
|
||||
/// </remarks>
|
||||
public async ValueTask RemoveObjectAsync(ILocateable obj)
|
||||
{
|
||||
if (obj is IHasBucketInformation bucketInfo)
|
||||
{
|
||||
// we remove the object from all known buckets and set old/new bucket to null
|
||||
var oldBucket = bucketInfo.OldBucket;
|
||||
var newBucket = bucketInfo.NewBucket;
|
||||
|
||||
bucketInfo.OldBucket = newBucket;
|
||||
bucketInfo.NewBucket = null;
|
||||
newBucket?.RemoveAsync(obj);
|
||||
|
||||
bucketInfo.OldBucket = newBucket;
|
||||
oldBucket?.RemoveAsync(obj);
|
||||
|
||||
bucketInfo.OldBucket = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.Map[obj.Position].RemoveAsync(obj).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (obj is IBucketMapObserver observingPlayer)
|
||||
{
|
||||
foreach (var bucket in observingPlayer.ObservingBuckets)
|
||||
{
|
||||
bucket.ItemAdded -= observingPlayer.LocateableAddedAsync;
|
||||
bucket.ItemRemoved -= observingPlayer.LocateableRemovedAsync;
|
||||
}
|
||||
|
||||
if (observingPlayer.ObservingBuckets.Any(b => b.Count > 0))
|
||||
{
|
||||
await observingPlayer.LocateablesOutOfScopeAsync(observingPlayer.ObservingBuckets.SelectMany(o => o)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
observingPlayer.ObservingBuckets.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask MoveObjectAsync(ILocateable obj, Point target, AsyncLock moveLock, MoveType moveType)
|
||||
{
|
||||
var differentBucket = await this.MoveObjectOnMapAsync(obj, target, moveLock, moveType).ConfigureAwait(false);
|
||||
|
||||
if (obj is IObservable observable)
|
||||
{
|
||||
await observable.ForEachWorldObserverAsync<IObjectMovedPlugIn>(p => p.ObjectMovedAsync(obj, moveType), true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var observingPlayer = obj as IBucketMapObserver;
|
||||
if (differentBucket && observingPlayer != null)
|
||||
{
|
||||
await this.UpdateObservingBucketsAsync(target, observingPlayer).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ILocateable> GetInRange(Point point, int range)
|
||||
{
|
||||
return this.Map.GetInRange(point, range);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the observing buckets, and notifies the <paramref name="player"/> about new objects which are in scope, and old objects which are out of scope.
|
||||
/// </summary>
|
||||
/// <param name="newPoint">The new coordinates on the map.</param>
|
||||
/// <param name="player">The player.</param>
|
||||
protected async ValueTask UpdateObservingBucketsAsync(Point newPoint, IBucketMapObserver player)
|
||||
{
|
||||
var curbuckets = this.Map.GetBucketsInRange(newPoint, player.InfoRange).ToList(); // All buckets in range
|
||||
var oldbuckets = player.ObservingBuckets.Where(i => !curbuckets.Contains(i)).ToList(); // Buckets which are not meant to be observed anymore
|
||||
var newbuckets = curbuckets.Where(i => !player.ObservingBuckets.Contains(i)).ToList(); // New buckets for observation
|
||||
|
||||
oldbuckets.ForEach(i =>
|
||||
{
|
||||
i.ItemAdded -= player.LocateableAddedAsync;
|
||||
i.ItemRemoved -= player.LocateableRemovedAsync;
|
||||
});
|
||||
newbuckets.ForEach(i =>
|
||||
{
|
||||
i.ItemAdded += player.LocateableAddedAsync;
|
||||
i.ItemRemoved += player.LocateableRemovedAsync;
|
||||
});
|
||||
|
||||
oldbuckets.ForEach(b => player.ObservingBuckets.Remove(b));
|
||||
newbuckets.ForEach(player.ObservingBuckets.Add);
|
||||
if (oldbuckets.Any(b => b.Count > 0))
|
||||
{
|
||||
await player.LocateablesOutOfScopeAsync(oldbuckets.SelectMany(o => o)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (newbuckets.Any(b => b.Count > 0))
|
||||
{
|
||||
await player.NewLocateablesInScopeAsync(newbuckets.SelectMany(o => o)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> MoveObjectOnMapAsync(ILocateable obj, Point target, AsyncLock moveLock, MoveType moveType)
|
||||
{
|
||||
if (moveType == MoveType.Walk)
|
||||
{
|
||||
if (obj is ISupportWalk supportWalk)
|
||||
{
|
||||
target = supportWalk.WalkTarget;
|
||||
}
|
||||
}
|
||||
|
||||
var differentBucket = obj.Position.X / this.Map.BucketSideLength != target.X / this.Map.BucketSideLength
|
||||
|| obj.Position.Y / this.Map.BucketSideLength != target.Y / this.Map.BucketSideLength;
|
||||
|
||||
if (!differentBucket)
|
||||
{
|
||||
if (moveType != MoveType.Walk)
|
||||
{
|
||||
obj.Position = target;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
using (await moveLock.LockAsync())
|
||||
{
|
||||
var oldPosition = obj.Position;
|
||||
Bucket<ILocateable>? oldBucket;
|
||||
Bucket<ILocateable> newBucket = this.Map[target];
|
||||
if (obj is IHasBucketInformation bucketInfo)
|
||||
{
|
||||
oldBucket = bucketInfo.NewBucket;
|
||||
bucketInfo.NewBucket = newBucket;
|
||||
bucketInfo.OldBucket = oldBucket;
|
||||
}
|
||||
else
|
||||
{
|
||||
oldBucket = this.Map[oldPosition];
|
||||
}
|
||||
|
||||
// only set x and y of the object if it's not walking - the Walker sets these coordinates!
|
||||
if (moveType != MoveType.Walk)
|
||||
{
|
||||
obj.Position = target;
|
||||
}
|
||||
|
||||
if (oldBucket is not null)
|
||||
{
|
||||
await oldBucket.RemoveAsync(obj).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await newBucket.AddAsync(obj).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
26
src/GameLogic/BucketItemEventArgs.cs
Normal file
26
src/GameLogic/BucketItemEventArgs.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="BucketItemEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Event args which includes the involved bucket item.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the bucket item.</typeparam>
|
||||
public class BucketItemEventArgs<T> : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BucketItemEventArgs{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The bucket item.</param>
|
||||
public BucketItemEventArgs(T item)
|
||||
{
|
||||
this.Item = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bucket item which is involved in the event.
|
||||
/// </summary>
|
||||
public T Item { get; }
|
||||
}
|
||||
129
src/GameLogic/BucketMap{T}.cs
Normal file
129
src/GameLogic/BucketMap{T}.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
// <copyright file="BucketMap{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// A two-dimensional map of buckets.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects which should be hold.</typeparam>
|
||||
internal class BucketMap<T>
|
||||
{
|
||||
private const int DefaultListCapacity = 4;
|
||||
|
||||
private readonly Bucket<T>[] _list;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BucketMap{T}"/> class.
|
||||
/// </summary>
|
||||
public BucketMap()
|
||||
{
|
||||
this.BucketSideLength = 4;
|
||||
this._list = new Bucket<T>[0x100 / this.BucketSideLength * 0x100 / this.BucketSideLength];
|
||||
this.InitList(DefaultListCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BucketMap{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sideLength">Length of the side.</param>
|
||||
/// <param name="createLists">if set to <c>true</c> the all lists are created now, and not by demand.</param>
|
||||
/// <param name="listCapacity">The list capacity.</param>
|
||||
/// <param name="bucketSideLength">Length of the bucket side.</param>
|
||||
/// <exception cref="System.ArgumentException">SideLength must be a multiple of BucketSideLength.</exception>
|
||||
public BucketMap(int sideLength, bool createLists, int listCapacity, int bucketSideLength)
|
||||
{
|
||||
if (sideLength % bucketSideLength != 0)
|
||||
{
|
||||
throw new ArgumentException($"SideLength ({sideLength}) must be a multiple of BucketSideLength ({bucketSideLength}).");
|
||||
}
|
||||
|
||||
this.BucketSideLength = bucketSideLength;
|
||||
this.SideLength = sideLength / bucketSideLength;
|
||||
this._list = new Bucket<T>[this.SideLength * this.SideLength];
|
||||
if (createLists)
|
||||
{
|
||||
this.InitList(listCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the length of a side of the map. This value should be a multiple of <see cref="BucketSideLength"/>.
|
||||
/// </summary>
|
||||
public int SideLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the length of the bucket side.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The length of the bucket side.
|
||||
/// </value>
|
||||
public int BucketSideLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Bucket{T}"/> at the specified coordinates.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The <see cref="Bucket{T}"/>.
|
||||
/// </value>
|
||||
/// <param name="point">The coordinates.</param>
|
||||
/// <returns>The <see cref="Bucket{T}"/> at the specified coordinates.</returns>
|
||||
public Bucket<T> this[Point point]
|
||||
{
|
||||
get => this._list[this.GetListIndex(point)];
|
||||
|
||||
set => this._list[this.GetListIndex(point)] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items which are in range of the specified coordinates and range.
|
||||
/// </summary>
|
||||
/// <param name="point">The coordinates.</param>
|
||||
/// <param name="range">The maximum range.</param>
|
||||
/// <returns>The items which are in range of the specified coordinate and range.</returns>
|
||||
public IEnumerable<ILocateable> GetInRange(Point point, int range)
|
||||
{
|
||||
var result = new List<ILocateable>();
|
||||
var buckets = this.GetBucketsInRange(point, range);
|
||||
foreach (var bucket in buckets)
|
||||
{
|
||||
result.AddRange(bucket.OfType<ILocateable>().Where(obj => obj.IsInRange(point, range)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the buckets in the specified range of the specified coordinates.
|
||||
/// </summary>
|
||||
/// <param name="point">The coordinates.</param>
|
||||
/// <param name="range">The range.</param>
|
||||
/// <returns>The buckets in the specified range of the specified coordinate.</returns>
|
||||
public IEnumerable<Bucket<T>> GetBucketsInRange(Point point, int range)
|
||||
{
|
||||
int maxX = Math.Min(point.X + range, (this.SideLength - 1) * this.BucketSideLength) / this.BucketSideLength;
|
||||
int maxY = Math.Min(point.Y + range, (this.SideLength - 1) * this.BucketSideLength) / this.BucketSideLength;
|
||||
int minX = Math.Max(point.X - range, 0) / this.BucketSideLength;
|
||||
int minY = Math.Max(point.Y - range, 0) / this.BucketSideLength;
|
||||
for (int i = minX; maxX >= i; ++i)
|
||||
{
|
||||
for (int j = minY; maxY >= j; ++j)
|
||||
{
|
||||
yield return this._list[i + (j * this.SideLength)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GetListIndex(Point point) => (point.X / this.BucketSideLength) + ((point.Y / this.BucketSideLength) * this.SideLength);
|
||||
|
||||
private void InitList(int listCapacity)
|
||||
{
|
||||
for (int i = 0; i < this._list.Length; ++i)
|
||||
{
|
||||
this._list[i] = new Bucket<T>(listCapacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
142
src/GameLogic/Bucket{T}.cs
Normal file
142
src/GameLogic/Bucket{T}.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
// <copyright file="Bucket{T}.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// A bucket, which can be observed for added and removed items.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type which should be hold by this bucket.</typeparam>
|
||||
public sealed class Bucket<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly List<T> _innerList;
|
||||
|
||||
private readonly AsyncReaderWriterLock _locker = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Bucket{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial capacity.</param>
|
||||
public Bucket(int capacity)
|
||||
{
|
||||
this._innerList = new List<T>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an item has been added.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<T>? ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an item has been removed.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<T>? ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
public int Count => this._innerList.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
public async ValueTask AddAsync(T item)
|
||||
{
|
||||
using (await this._locker.WriterLockAsync())
|
||||
{
|
||||
this._innerList.Add(item);
|
||||
}
|
||||
|
||||
if (this.ItemAdded is { } eventHandler)
|
||||
{
|
||||
await eventHandler(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The success.</returns>
|
||||
public async ValueTask<bool> RemoveAsync(T item)
|
||||
{
|
||||
bool result;
|
||||
using (await this._locker.WriterLockAsync())
|
||||
{
|
||||
result = this._innerList.Remove(item);
|
||||
}
|
||||
|
||||
if (result && this.ItemRemoved is { } eventHandler)
|
||||
{
|
||||
await eventHandler(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return new LockingEnumerator<T>(this._locker, this._innerList);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
private sealed class LockingEnumerator<TEnumerated> : IEnumerator<TEnumerated>
|
||||
{
|
||||
private readonly AsyncReaderWriterLock _locker;
|
||||
private readonly IEnumerable<TEnumerated> _enumerable;
|
||||
private IEnumerator<TEnumerated>? _enumerator;
|
||||
private IDisposable? _lockRelease;
|
||||
|
||||
public LockingEnumerator(AsyncReaderWriterLock locker, IEnumerable<TEnumerated> enumerable)
|
||||
{
|
||||
this._locker = locker;
|
||||
this._enumerable = enumerable;
|
||||
}
|
||||
|
||||
public TEnumerated Current => this.Enumerator.Current;
|
||||
|
||||
object IEnumerator.Current => this.Enumerator.Current!;
|
||||
|
||||
private IEnumerator<TEnumerated> Enumerator
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this._enumerator is { })
|
||||
{
|
||||
return this._enumerator;
|
||||
}
|
||||
|
||||
this._lockRelease = this._locker.ReaderLock();
|
||||
return this._enumerator ??= this._enumerable.GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
return this.Enumerator.MoveNext();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.Enumerator.Reset();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._enumerator?.Dispose();
|
||||
this._lockRelease?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/GameLogic/CharacterExtensions.cs
Normal file
155
src/GameLogic/CharacterExtensions.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
// <copyright file="CharacterExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.DataModel.Configuration.Quests;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using Nito.Disposables.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="Character"/>.
|
||||
/// </summary>
|
||||
public static class CharacterExtensions
|
||||
{
|
||||
private const int MaxLevel = 400;
|
||||
private static readonly ushort[] FruitPointsPerLevel = GetFruitPoints(400).ToArray();
|
||||
private static readonly ushort[] FruitPointsPerLevelMagicGladiator = GetFruitPoints(700).ToArray();
|
||||
private static readonly ushort[] FruitPointsPerLevelDarkLord = GetFruitPoints(500).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum fruit points of the character.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <returns>The maximum fruit points of the character.</returns>
|
||||
public static ushort GetMaximumFruitPoints(this Character character)
|
||||
{
|
||||
var index = (int)character.Attributes.First(a => a.Definition == Stats.Level).Value - 1;
|
||||
return character.CharacterClass?.FruitCalculation switch
|
||||
{
|
||||
FruitCalculationStrategy.DarkLord => FruitPointsPerLevelDarkLord[index],
|
||||
FruitCalculationStrategy.MagicGladiator => FruitPointsPerLevelMagicGladiator[index],
|
||||
_ => FruitPointsPerLevel[index],
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the character has a full ancient set equipped.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the character has a full ancient set equipped; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool HasFullAncientSetEquipped(this Character character)
|
||||
{
|
||||
if (character?.Inventory is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var equippedAncientSetItems = character.Inventory.Items.Where(i =>
|
||||
i.ItemSlot <= InventoryConstants.LastEquippableItemSlotIndex
|
||||
&& i.ItemSlot >= InventoryConstants.FirstEquippableItemSlotIndex
|
||||
&& i.ItemSetGroups.Any(group => group.AncientSetDiscriminator > 0))
|
||||
.Select(i =>
|
||||
new
|
||||
{
|
||||
Item = i.Definition,
|
||||
Set = i.ItemSetGroups.First(s => s.AncientSetDiscriminator > 0),
|
||||
});
|
||||
var ancientSets = equippedAncientSetItems.Select(i => i.Set.ItemSetGroup).WhereNotNull().Distinct();
|
||||
return ancientSets.Any(set =>
|
||||
set.Items.All(setItem => equippedAncientSetItems.Any(i =>
|
||||
object.Equals(i.Item, setItem.ItemDefinition)
|
||||
&& object.Equals(i.Set.ItemSetGroup, set))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the character is able to increase StatAttributes.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the character can increase StatAttributes; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool CanIncreaseStats(this Character character)
|
||||
{
|
||||
return character.CharacterStatus == CharacterStatus.GameMaster || character.LevelUpPoints > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the character is able to increase StatAttributes.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <param name="amount">The amount of points which should be added.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the character can increase StatAttributes; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool CanIncreaseStats(this Character character, ushort amount)
|
||||
{
|
||||
return character.CharacterStatus == CharacterStatus.GameMaster || character.LevelUpPoints >= amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the character is a "special" character with reduced level requirements for maps and events.
|
||||
/// Special characters have a <see cref="CharacterClass.LevelWarpRequirementReductionPercent"/> of 33.
|
||||
/// Usually, this includes the classes Magic Gladiator, Dark Lord, Rage Fighter and Summoner.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the character is a "special" character with reduced level requirements for maps and events; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsSpecialCharacter(this Character character)
|
||||
{
|
||||
return character.CharacterClass?.LevelWarpRequirementReductionPercent > 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective move level requirement by considering <see cref="CharacterClass.LevelWarpRequirementReductionPercent"/>.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <param name="levelRequirement">The level requirement.</param>
|
||||
/// <returns>The effective move level requirement.</returns>
|
||||
public static int GetEffectiveMoveLevelRequirement(this Character character, int levelRequirement)
|
||||
{
|
||||
if (levelRequirement == 400)
|
||||
{
|
||||
return levelRequirement;
|
||||
}
|
||||
|
||||
if (character.CharacterClass?.LevelWarpRequirementReductionPercent is { } reduction and > 0)
|
||||
{
|
||||
levelRequirement = levelRequirement * (100 - reduction) / 100;
|
||||
}
|
||||
|
||||
return levelRequirement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quest drop item groups of a character.
|
||||
/// </summary>
|
||||
/// <param name="character">The character.</param>
|
||||
/// <returns>The quest drop item groups of a character.</returns>
|
||||
public static IEnumerable<DropItemGroup> GetQuestDropItemGroups(this Character character)
|
||||
{
|
||||
return character.QuestStates?
|
||||
.SelectMany(q => q.ActiveQuest?.RequiredItems ?? Enumerable.Empty<QuestItemRequirement>())
|
||||
.Select(i => i.DropItemGroup)
|
||||
.WhereNotNull()
|
||||
?? Enumerable.Empty<DropItemGroup>();
|
||||
}
|
||||
|
||||
private static IEnumerable<ushort> GetFruitPoints(int divisor)
|
||||
{
|
||||
var current = 2;
|
||||
for (int i = 0; i < MaxLevel; i++)
|
||||
{
|
||||
if (((i + 1) % 10) == 0)
|
||||
{
|
||||
current += (3 * (i + 11) / divisor) + 2;
|
||||
}
|
||||
|
||||
yield return (ushort)current;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/GameLogic/ComboState.cs
Normal file
29
src/GameLogic/ComboState.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="ComboState.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a state in the <see cref="ComboStateMachine"/>.
|
||||
/// It includes data about the <see cref="RequiredSkill"/> to achieve this state.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.GameLogic.State" />
|
||||
public class ComboState : State
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComboState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="requiredSkill">The required skill.</param>
|
||||
public ComboState(Guid id, Skill? requiredSkill)
|
||||
: base(id)
|
||||
{
|
||||
this.RequiredSkill = requiredSkill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the required skill to achieve this state.
|
||||
/// </summary>
|
||||
public Skill? RequiredSkill { get; }
|
||||
}
|
||||
152
src/GameLogic/ComboStateMachine.cs
Normal file
152
src/GameLogic/ComboStateMachine.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
// <copyright file="ComboStateMachine.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// A state machine which is dynamically built, based on a <see cref="SkillComboDefinition"/>.
|
||||
/// For creation, use the factory method <see cref="Create"/>.
|
||||
/// </summary>
|
||||
public sealed class ComboStateMachine : StateMachine
|
||||
{
|
||||
private static readonly ConcurrentDictionary<SkillComboDefinition, (ComboState Inital, ComboState Final)> StateCache = new();
|
||||
|
||||
private readonly TimeSpan _maximumCompletionTime;
|
||||
private DateTime _lastStartedCombo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComboStateMachine"/> class.
|
||||
/// </summary>
|
||||
/// <param name="initial">The initial state.</param>
|
||||
/// <param name="final">The final state.</param>
|
||||
/// <param name="maximumCompletionTime">The maximum completion time for combos.</param>
|
||||
private ComboStateMachine(ComboState initial, ComboState final, TimeSpan maximumCompletionTime)
|
||||
: base(initial)
|
||||
{
|
||||
this._maximumCompletionTime = maximumCompletionTime;
|
||||
this.InitialState = initial;
|
||||
this.FinalState = final;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the final state.
|
||||
/// When this state is achieved, the state machine will proceed to the <see cref="InitialState"/>
|
||||
/// to handle the next combo attempt.
|
||||
/// </summary>
|
||||
public ComboState FinalState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial state.
|
||||
/// </summary>
|
||||
public ComboState InitialState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the specified combo definition, based on the <see cref="SkillComboDefinition"/>.
|
||||
/// </summary>
|
||||
/// <param name="comboDefinition">The combo definition.</param>
|
||||
/// <returns>The created <see cref="ComboStateMachine"/>.</returns>.
|
||||
public static ComboStateMachine Create(SkillComboDefinition comboDefinition)
|
||||
{
|
||||
var states = GetOrCreateStates(comboDefinition);
|
||||
return new ComboStateMachine(states.Initial, states.Final, comboDefinition.MaximumCompletionTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the skill to trigger potential state advancements.
|
||||
/// </summary>
|
||||
/// <param name="skill">The performed skill.</param>
|
||||
/// <returns><see langword="true"/>, if the combo completed; otherwise, <see langword="false"/>.</returns>
|
||||
public async ValueTask<bool> RegisterSkillAsync(Skill skill)
|
||||
{
|
||||
if (DateTime.UtcNow - this._lastStartedCombo > this._maximumCompletionTime)
|
||||
{
|
||||
// If it took to long, reset to initial state.
|
||||
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var nextPossibleSkillState = this.CurrentState?.PossibleTransitions?.OfType<ComboState>().FirstOrDefault(t => t.RequiredSkill == skill.GetBaseSkill());
|
||||
if (nextPossibleSkillState is null)
|
||||
{
|
||||
// If it's the wrong skill, reset to initial state.
|
||||
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.CurrentState == this.InitialState)
|
||||
{
|
||||
this._lastStartedCombo = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await this.TryAdvanceToAsync(nextPossibleSkillState).ConfigureAwait(false);
|
||||
|
||||
var canComplete = this.CurrentState?.PossibleTransitions?.Contains(this.FinalState) ?? false;
|
||||
if (canComplete && await this.TryAdvanceToAsync(this.FinalState).ConfigureAwait(false))
|
||||
{
|
||||
await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static (ComboState Initial, ComboState Final) GetOrCreateStates(SkillComboDefinition comboDefinition)
|
||||
{
|
||||
return StateCache.GetOrAdd(comboDefinition, BuildStates);
|
||||
}
|
||||
|
||||
private static (ComboState Initial, ComboState Final) BuildStates(SkillComboDefinition comboDefinition)
|
||||
{
|
||||
var initialState = new ComboState(Guid.NewGuid(), null) { Name = "Initial", PossibleTransitions = new List<State>() };
|
||||
var finalState = new ComboState(Guid.NewGuid(), null) { Name = "Finished", PossibleTransitions = new List<State> { initialState } };
|
||||
|
||||
var statesPerStep = new Dictionary<int, List<State>>();
|
||||
foreach (var groupedSteps in comboDefinition.Steps.GroupBy(s => s.Order).OrderByDescending(s => s.Key))
|
||||
{
|
||||
foreach (var step in groupedSteps)
|
||||
{
|
||||
var stepState = new ComboState(Guid.NewGuid(), step.Skill);
|
||||
stepState.Name = $"Step {step.Order}: {step.Skill?.Name}";
|
||||
stepState.PossibleTransitions = new List<State>();
|
||||
stepState.PossibleTransitions.Add(initialState);
|
||||
|
||||
if (step.Order == 1)
|
||||
{
|
||||
initialState.PossibleTransitions.Add(stepState);
|
||||
}
|
||||
|
||||
if (step.IsFinalStep)
|
||||
{
|
||||
stepState.PossibleTransitions.Add(finalState);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (statesPerStep.TryGetValue(step.Order + 1, out var nextSteps))
|
||||
{
|
||||
nextSteps
|
||||
.OfType<ComboState>()
|
||||
.Where(p => p.RequiredSkill != step.Skill)
|
||||
.ForEach(stepState.PossibleTransitions.Add);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Fail("Inconsistent combo data");
|
||||
}
|
||||
}
|
||||
|
||||
if (!statesPerStep.TryGetValue(step.Order, out var list))
|
||||
{
|
||||
list = new List<State>();
|
||||
statesPerStep[step.Order] = list;
|
||||
}
|
||||
|
||||
list.Add(stepState);
|
||||
}
|
||||
}
|
||||
|
||||
return (initialState, finalState);
|
||||
}
|
||||
}
|
||||
230
src/GameLogic/ConfigurationChangeMediator.cs
Normal file
230
src/GameLogic/ConfigurationChangeMediator.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
// <copyright file="ConfigurationChangeMediator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A mediator which notifies about configuration changes for registered instances.
|
||||
/// </summary>
|
||||
public class ConfigurationChangeMediator : IConfigurationChangeMediator, IConfigurationChangeMediatorListener
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, IChangeRegistration> _registrations = new();
|
||||
private readonly ConcurrentDictionary<Type, ICreateRegistration> _createRegistrations = new();
|
||||
|
||||
/// <summary>
|
||||
/// A non-generic interface for the change-registration.
|
||||
/// </summary>
|
||||
private interface IChangeRegistration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Raises the <see cref="ChangeRegistration{TConfig}"/> event.
|
||||
/// </summary>
|
||||
ValueTask RaiseOnChangeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="ChangeRegistration{TConfig}"/> event.
|
||||
/// </summary>
|
||||
ValueTask RaiseOnDeleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An non-generic interface for the <see cref="CreateRegistration{TConfig}"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
private interface ICreateRegistration : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Raises the <see cref="ChangeRegistration{TConfig}"/> event.
|
||||
/// </summary>
|
||||
/// <param name="created">The created configuration.</param>
|
||||
ValueTask RaiseOnCreateAsync(object created);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable RegisterObject<TConfig, T>(TConfig config, T obj, Func<Action, TConfig, T, ValueTask>? onChange = null, Func<TConfig, T, ValueTask>? onDelete = null)
|
||||
where T : class
|
||||
where TConfig : class
|
||||
{
|
||||
var registration = (ChangeRegistration<TConfig>)this._registrations.AddOrUpdate(
|
||||
config.GetId(),
|
||||
_ => new ChangeRegistration<TConfig>(config),
|
||||
(_, value) => value);
|
||||
|
||||
if (onChange is not null)
|
||||
{
|
||||
registration.OnChange += InvokeOnChangeAsync;
|
||||
}
|
||||
|
||||
if (onDelete is not null)
|
||||
{
|
||||
registration.OnDelete += InvokeOnDeleteAsync;
|
||||
}
|
||||
|
||||
var disposable = new Nito.Disposables.Disposable(() =>
|
||||
{
|
||||
if (onChange is not null)
|
||||
{
|
||||
registration.OnChange -= InvokeOnChangeAsync;
|
||||
}
|
||||
|
||||
if (onDelete is not null)
|
||||
{
|
||||
registration.OnDelete -= InvokeOnDeleteAsync;
|
||||
}
|
||||
});
|
||||
|
||||
return disposable;
|
||||
|
||||
async ValueTask InvokeOnChangeAsync(TConfig changedConfig)
|
||||
{
|
||||
await onChange(
|
||||
() =>
|
||||
{
|
||||
registration.OnChange -= InvokeOnChangeAsync;
|
||||
if (onDelete is not null)
|
||||
{
|
||||
registration.OnDelete -= InvokeOnDeleteAsync;
|
||||
}
|
||||
},
|
||||
changedConfig,
|
||||
obj).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async ValueTask InvokeOnDeleteAsync(TConfig changedConfig)
|
||||
{
|
||||
await onDelete(changedConfig, obj).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable RegisterForNew<TConfig, T>(T obj, Func<TConfig, T, ValueTask> onNewConfig)
|
||||
{
|
||||
var registration = (CreateRegistration<TConfig>)this._createRegistrations.AddOrUpdate(
|
||||
typeof(TConfig),
|
||||
_ => new CreateRegistration<TConfig>(),
|
||||
(_, value) => value);
|
||||
registration.OnCreate += InvokeOnCreateAsync;
|
||||
|
||||
return registration;
|
||||
async ValueTask InvokeOnCreateAsync(TConfig config)
|
||||
{
|
||||
await onNewConfig(config, obj).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask HandleConfigurationChangedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
if (this._registrations.TryGetValue(id, out var registration))
|
||||
{
|
||||
await registration.RaiseOnChangeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask HandleConfigurationAddedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
if (this._createRegistrations.TryGetValue(type, out var createRegistration)
|
||||
|| this._createRegistrations.TryGetValue(type.BaseType!, out createRegistration))
|
||||
{
|
||||
await createRegistration.RaiseOnCreateAsync(configuration).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask HandleConfigurationRemovedAsync(Type type, Guid id)
|
||||
{
|
||||
if (this._registrations.Remove(id, out var registration))
|
||||
{
|
||||
await registration.RaiseOnDeleteAsync().ConfigureAwait(false);
|
||||
registration.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A registration for a change of a configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfig">The type of the configuration.</typeparam>
|
||||
private class ChangeRegistration<TConfig> : Disposable, IChangeRegistration
|
||||
where TConfig : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChangeRegistration{TConfig}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration.</param>
|
||||
public ChangeRegistration(TConfig config)
|
||||
{
|
||||
this.Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a config has been changed.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<TConfig>? OnChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a config has been deleted.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<TConfig>? OnDelete;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration in which the registration is interested in.
|
||||
/// </summary>
|
||||
private TConfig Configuration { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RaiseOnChangeAsync()
|
||||
{
|
||||
await this.OnChange.SafeInvokeAsync(this.Configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RaiseOnDeleteAsync()
|
||||
{
|
||||
await this.OnDelete.SafeInvokeAsync(this.Configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
this.OnDelete = null;
|
||||
this.OnChange = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A registration for a creation of a configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfig">The type of the configuration.</typeparam>
|
||||
private class CreateRegistration<TConfig> : Disposable, ICreateRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when a new config of <typeparamref name="TConfig"/> is created.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<TConfig>? OnCreate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RaiseOnCreateAsync(object created)
|
||||
{
|
||||
if (created is not TConfig typed)
|
||||
{
|
||||
throw new InvalidOperationException("created object is of wrong type");
|
||||
}
|
||||
|
||||
await this.OnCreate.SafeInvokeAsync(typed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
this.OnCreate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/GameLogic/DamageAttributes.cs
Normal file
67
src/GameLogic/DamageAttributes.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// <copyright file="DamageAttributes.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// The attributes of a damage.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum DamageAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// No defined attribute.
|
||||
/// </summary>
|
||||
Undefined = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The damage is critical (means 100% of the possible damage between minimum and maximum).
|
||||
/// </summary>
|
||||
Critical = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The damage is excellent (20% higher base damage than critical).
|
||||
/// </summary>
|
||||
Excellent = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The damage ignored the defense of the victim.
|
||||
/// </summary>
|
||||
IgnoreDefense = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The damage is caused by poison.
|
||||
/// </summary>
|
||||
Poison = 8,
|
||||
|
||||
/// <summary>
|
||||
/// The damage was doubled.
|
||||
/// </summary>
|
||||
Double = 16,
|
||||
|
||||
/// <summary>
|
||||
/// The damage was tripled (e.g. combo skill).
|
||||
/// </summary>
|
||||
Triple = 32,
|
||||
|
||||
/// <summary>
|
||||
/// The damage was reflected.
|
||||
/// </summary>
|
||||
Reflected = 64,
|
||||
|
||||
/// <summary>
|
||||
/// The damage includes the combo bonus.
|
||||
/// </summary>
|
||||
Combo = 128,
|
||||
|
||||
/// <summary>
|
||||
/// The damage is a non-final hit of a quick sequence from a rage fighter skill.
|
||||
/// </summary>
|
||||
RageFighterStreakHit = 256,
|
||||
|
||||
/// <summary>
|
||||
/// The damage is the final hit of a quick sequence from a rage fighter skill.
|
||||
/// </summary>
|
||||
RageFighterStreakFinalHit = 512,
|
||||
}
|
||||
593
src/GameLogic/DefaultDropGenerator.cs
Normal file
593
src/GameLogic/DefaultDropGenerator.cs
Normal file
@@ -0,0 +1,593 @@
|
||||
// <copyright file="DefaultDropGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The default drop generator.
|
||||
/// </summary>
|
||||
public class DefaultDropGenerator : IDropGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of money which is dropped at least, and added to the gained experience.
|
||||
/// </summary>
|
||||
private const int BaseMoneyDrop = 7;
|
||||
private const int DropLevelMaxGap = 12;
|
||||
private const int SkillDropChancePercent = 50;
|
||||
|
||||
private const byte DefaultMaxItemOptionLevelDrop = 3;
|
||||
private const byte MinItemOptionLevelDrop = 1;
|
||||
private const byte MaxItemOptionLevelDrop = 4;
|
||||
|
||||
/// <summary>
|
||||
/// A re-usable list of drop item groups.
|
||||
/// </summary>
|
||||
private readonly List<DropItemGroup> _chanceDropGroups = new(64);
|
||||
private readonly List<DropItemGroup> _guaranteedDropGroups = new(16);
|
||||
|
||||
private readonly AsyncLock _lock = new();
|
||||
private readonly IRandomizer _randomizer;
|
||||
private readonly IList<ItemDefinition> _ancientItems;
|
||||
private readonly IList<ItemDefinition> _droppableItems;
|
||||
private readonly IList<ItemDefinition>?[] _droppableItemsPerMonsterLevel = new IList<ItemDefinition>?[byte.MaxValue + 1];
|
||||
private readonly IList<ItemDefinition>?[] _droppableSocketItemsPerMonsterLevel = new IList<ItemDefinition>?[byte.MaxValue + 1];
|
||||
|
||||
private readonly byte _maxItemOptionLevelDrop;
|
||||
private readonly byte _excellentItemDropLevelDelta;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDropGenerator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration.</param>
|
||||
/// <param name="randomizer">The randomizer.</param>
|
||||
public DefaultDropGenerator(GameConfiguration config, IRandomizer randomizer)
|
||||
{
|
||||
this._excellentItemDropLevelDelta = config.ExcellentItemDropLevelDelta;
|
||||
this._randomizer = randomizer;
|
||||
this._maxItemOptionLevelDrop = IsValidOptionLevelDrop(config.MaximumItemOptionLevelDrop)
|
||||
? config.MaximumItemOptionLevelDrop
|
||||
: DefaultMaxItemOptionLevelDrop;
|
||||
this._droppableItems = config.Items.Where(i => i.DropsFromMonsters).ToList();
|
||||
this._ancientItems = this._droppableItems.Where(
|
||||
i => i.PossibleItemSetGroups.Any(
|
||||
g => g.Options?.PossibleOptions.Any(
|
||||
o => object.Equals(o.OptionType, ItemOptionTypes.AncientOption)) ?? false))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<(IEnumerable<Item> Items, uint? Money)> GenerateItemDropsAsync(MonsterDefinition monster, int gainedExperience, Player player)
|
||||
{
|
||||
var character = player.SelectedCharacter;
|
||||
var map = player.CurrentMap?.Definition;
|
||||
if (map is null || character is null)
|
||||
{
|
||||
return ([], null);
|
||||
}
|
||||
|
||||
using var l = await this._lock.LockAsync();
|
||||
this._guaranteedDropGroups.Clear();
|
||||
this._chanceDropGroups.Clear();
|
||||
|
||||
if (monster.ObjectKind == NpcObjectKind.Destructible)
|
||||
{
|
||||
this.PartitionDropGroups(monster.DropItemGroups ?? []);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.PartitionDropGroups(monster.DropItemGroups ?? []);
|
||||
this.PartitionDropGroups(character.DropItemGroups ?? [], monster);
|
||||
this.PartitionDropGroups(map.DropItemGroups ?? [], monster);
|
||||
this.PartitionDropGroups(await GetQuestItemGroupsAsync(player).ConfigureAwait(false) ?? [], monster);
|
||||
}
|
||||
|
||||
uint money = 0;
|
||||
var (droppedItems, moneyResult) = this.GenerateDrops(monster, gainedExperience);
|
||||
if (moneyResult > 0)
|
||||
{
|
||||
money = moneyResult;
|
||||
}
|
||||
|
||||
this._guaranteedDropGroups.Clear();
|
||||
this._chanceDropGroups.Clear();
|
||||
return (droppedItems ?? Enumerable.Empty<Item>(), money > 0 ? money : null);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Item? GenerateItemDrop(DropItemGroup selectedGroup)
|
||||
{
|
||||
return this.GenerateItemDrop(selectedGroup, selectedGroup.PossibleItems);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public (Item? Item, uint? Money, ItemDropEffect DropEffect) GenerateItemDrop(IEnumerable<DropItemGroup> groups)
|
||||
{
|
||||
var group = this.SelectRandomGroup(groups.OrderBy(group => group.Chance), 1.0);
|
||||
if (group is null)
|
||||
{
|
||||
return (null, null, ItemDropEffect.Undefined);
|
||||
}
|
||||
|
||||
var dropEffect = ItemDropEffect.Undefined;
|
||||
if (group is ItemDropItemGroup itemDropItemGroup)
|
||||
{
|
||||
dropEffect = itemDropItemGroup.DropEffect;
|
||||
|
||||
if (group.ItemType == SpecialItemType.Money)
|
||||
{
|
||||
return (null, (uint)itemDropItemGroup.MoneyAmount, dropEffect);
|
||||
}
|
||||
}
|
||||
|
||||
return (this.GenerateItemDrop(group), null, dropEffect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random item.
|
||||
/// </summary>
|
||||
/// <param name="monsterLevel">The monster level.</param>
|
||||
/// <param name="isSocketItem">If set to <c>true</c>, it selects only socket items.</param>
|
||||
/// <returns>A random item.</returns>
|
||||
protected Item? GenerateRandomItem(int monsterLevel, bool isSocketItem)
|
||||
{
|
||||
var possible = this.GetPossibleList(monsterLevel, isSocketItem);
|
||||
var item = this.GenerateRandomItem(possible);
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
item.Level = GetItemLevelByMonsterLevel(item.Definition!, monsterLevel);
|
||||
item.Durability = item.GetMaximumDurabilityOfOnePiece();
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies random options to the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
protected void ApplyRandomOptions(Item item)
|
||||
{
|
||||
foreach (var option in item.Definition!.PossibleItemOptions.Where(o =>
|
||||
o.AddsRandomly &&
|
||||
!o.PossibleOptions.Any(po => object.Equals(po.OptionType, ItemOptionTypes.Excellent))))
|
||||
{
|
||||
this.ApplyOption(item, option);
|
||||
}
|
||||
|
||||
if (item.Definition.MaximumSockets > 0)
|
||||
{
|
||||
item.SocketCount = this._randomizer.NextInt(1, item.Definition.MaximumSockets + 1);
|
||||
}
|
||||
|
||||
if (item.CanHaveSkill())
|
||||
{
|
||||
item.HasSkill = this._randomizer.NextRandomBool(SkillDropChancePercent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random excellent item.
|
||||
/// </summary>
|
||||
/// <param name="monsterLevel">The monster level, if it's a monster drop.</param>
|
||||
/// <param name="possibleItems">The possible items, if the drop is from an item box (e.g. box of kundun).</param>
|
||||
/// <returns>A random excellent item.</returns>
|
||||
protected Item? GenerateRandomExcellentItem(int monsterLevel = 0, ICollection<ItemDefinition>? possibleItems = null)
|
||||
{
|
||||
if (monsterLevel < this._excellentItemDropLevelDelta && possibleItems is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var possible = possibleItems ?? this.GetPossibleList(monsterLevel - this._excellentItemDropLevelDelta);
|
||||
var item = this.GenerateRandomItem(possible);
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
item.HasSkill = item.CanHaveSkill(); // every excellent item got skill
|
||||
|
||||
this.AddRandomExcOptions(item);
|
||||
item.Durability = item.GetMaximumDurabilityOfOnePiece();
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random ancient item.
|
||||
/// </summary>
|
||||
/// <returns>A random ancient item.</returns>
|
||||
protected Item? GenerateRandomAncient()
|
||||
{
|
||||
var item = this.GenerateRandomItem(this._ancientItems);
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
item.HasSkill = item.CanHaveSkill(); // every ancient item got skill
|
||||
|
||||
this.ApplyRandomAncientOption(item);
|
||||
item.Durability = item.GetMaximumDurabilityOfOnePiece();
|
||||
return item;
|
||||
}
|
||||
|
||||
private static byte GetItemLevelByMonsterLevel(ItemDefinition itemDefinition, int monsterLevel)
|
||||
{
|
||||
return Math.Min((byte)((monsterLevel - itemDefinition.DropLevel) / 3), itemDefinition.MaximumItemLevel);
|
||||
}
|
||||
|
||||
private static async ValueTask<IEnumerable<DropItemGroup>> GetQuestItemGroupsAsync(Player player)
|
||||
{
|
||||
if (player.SelectedCharacter is not { } character)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (player.Party is { } party)
|
||||
{
|
||||
return await party.GetQuestDropItemGroupsAsync(player).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return character.GetQuestDropItemGroups();
|
||||
}
|
||||
|
||||
private static bool IsGroupRelevant(MonsterDefinition monsterDefinition, DropItemGroup group)
|
||||
{
|
||||
if (group.MinimumMonsterLevel.HasValue && monsterDefinition[Stats.Level] < group.MinimumMonsterLevel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group.MaximumMonsterLevel.HasValue && monsterDefinition[Stats.Level] > group.MaximumMonsterLevel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group.Monster is { } monster && !monster.Equals(monsterDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidOptionLevelDrop(byte value)
|
||||
=> value is >= MinItemOptionLevelDrop and <= MaxItemOptionLevelDrop;
|
||||
|
||||
private static bool CanDropAtMonsterLevel(ItemDefinition itemDefinition, int monsterLevel)
|
||||
{
|
||||
if (itemDefinition.DropLevel > monsterLevel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return itemDefinition.MaximumDropLevel is not { } maxDropLevel || monsterLevel <= maxDropLevel;
|
||||
}
|
||||
|
||||
private (IList<Item>? Items, uint Money) GenerateDrops(MonsterDefinition monster, int gainedExperience)
|
||||
{
|
||||
uint money = 0;
|
||||
List<Item>? droppedItems = null;
|
||||
var remainingDrops = monster.NumberOfMaximumItemDrops;
|
||||
|
||||
// Guaranteed groups.
|
||||
foreach (var group in this._guaranteedDropGroups)
|
||||
{
|
||||
if (remainingDrops <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var item = this.GenerateItemDropOrMoney(monster, group, gainedExperience, out var droppedMoney);
|
||||
if (item is not null)
|
||||
{
|
||||
droppedItems ??= new List<Item>(monster.NumberOfMaximumItemDrops);
|
||||
droppedItems.Add(item);
|
||||
}
|
||||
|
||||
if (droppedMoney is not null)
|
||||
{
|
||||
money += droppedMoney.Value;
|
||||
}
|
||||
|
||||
remainingDrops--;
|
||||
}
|
||||
|
||||
// Chance based groups.
|
||||
if (remainingDrops > 0 && this._chanceDropGroups.Count > 0)
|
||||
{
|
||||
double totalChance = 0;
|
||||
foreach (var group in this._chanceDropGroups)
|
||||
{
|
||||
totalChance += group.Chance;
|
||||
}
|
||||
|
||||
for (int i = 0; i < remainingDrops; i++)
|
||||
{
|
||||
var group = this.SelectRandomGroup(this._chanceDropGroups, totalChance);
|
||||
if (group is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = this.GenerateItemDropOrMoney(monster, group, gainedExperience, out var droppedMoney);
|
||||
if (item is not null)
|
||||
{
|
||||
droppedItems ??= new List<Item>(monster.NumberOfMaximumItemDrops);
|
||||
droppedItems.Add(item);
|
||||
}
|
||||
|
||||
if (droppedMoney is not null)
|
||||
{
|
||||
money += droppedMoney.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (droppedItems, money);
|
||||
}
|
||||
|
||||
private void PartitionDropGroups(IEnumerable<DropItemGroup> groups, MonsterDefinition? monster = null)
|
||||
{
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (monster is not null && !IsGroupRelevant(monster, group))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group.Chance >= 1.0)
|
||||
{
|
||||
this._guaranteedDropGroups.Add(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._chanceDropGroups.Add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Item? GenerateItemDrop(DropItemGroup selectedGroup, ICollection<ItemDefinition> possibleItems)
|
||||
{
|
||||
var item = selectedGroup.ItemType switch
|
||||
{
|
||||
SpecialItemType.Ancient => this.GenerateRandomAncient(),
|
||||
SpecialItemType.Excellent => this.GenerateRandomExcellentItem(possibleItems: possibleItems),
|
||||
_ => this.GenerateRandomItem(possibleItems),
|
||||
};
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.Durability == 0)
|
||||
{
|
||||
item.Durability = item.GetMaximumDurabilityOfOnePiece();
|
||||
}
|
||||
|
||||
if (selectedGroup is ItemDropItemGroup itemDropItemGroup)
|
||||
{
|
||||
item.Level = (byte)this._randomizer.NextInt(itemDropItemGroup.MinimumLevel, itemDropItemGroup.MaximumLevel + 1);
|
||||
}
|
||||
else if (selectedGroup.ItemLevel is { } itemLevel)
|
||||
{
|
||||
item.Level = itemLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no level defined, so it stays at 0.
|
||||
}
|
||||
|
||||
item.Level = Math.Min(item.Level, item.Definition!.MaximumItemLevel);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private void ApplyOption(Item item, ItemOptionDefinition option)
|
||||
{
|
||||
for (int i = 0; i < option.MaximumOptionsPerItem; i++)
|
||||
{
|
||||
if (this._randomizer.NextRandomBool(option.AddChance))
|
||||
{
|
||||
var remainingOptions = option.PossibleOptions.Where(possibleOption => item.ItemOptions.All(link => link.ItemOption != possibleOption));
|
||||
var newOption = remainingOptions.SelectRandom(this._randomizer);
|
||||
if (newOption is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var itemOptionLink = new ItemOptionLink
|
||||
{
|
||||
ItemOption = newOption,
|
||||
Level = newOption.LevelDependentOptions
|
||||
.Select(ldo => ldo.Level)
|
||||
.Concat(newOption.LevelDependentOptions.Count > 0 ? [1] : []) // For base def/dmg opts level 1 is not an ItemOptionOfLevel entry
|
||||
.Distinct()
|
||||
.Where(l => l <= this._maxItemOptionLevelDrop)
|
||||
.DefaultIfEmpty(0)
|
||||
.SelectRandom(),
|
||||
};
|
||||
item.ItemOptions.Add(itemOptionLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Item? GenerateRandomItem(ICollection<ItemDefinition>? possibleItems)
|
||||
{
|
||||
if (possibleItems is null || possibleItems.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = new TemporaryItem
|
||||
{
|
||||
Definition = possibleItems.ElementAt(this._randomizer.NextInt(0, possibleItems.Count)),
|
||||
};
|
||||
|
||||
this.ApplyRandomOptions(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private void ApplyRandomAncientOption(Item item)
|
||||
{
|
||||
var ancientSet = item.Definition?.PossibleItemSetGroups
|
||||
.Where(g => g!.Options?.PossibleOptions.Any(o => object.Equals(o.OptionType, ItemOptionTypes.AncientOption)) ?? false)
|
||||
.SelectRandom(this._randomizer);
|
||||
if (ancientSet is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var itemOfSet = ancientSet.Items.First(i => object.Equals(i.ItemDefinition, item.Definition));
|
||||
item.ItemSetGroups.Add(itemOfSet);
|
||||
|
||||
// For example: +5str or +10str.
|
||||
if (itemOfSet.BonusOption is { } bonusOption)
|
||||
{
|
||||
var bonusOptionLink = new ItemOptionLink();
|
||||
bonusOptionLink.ItemOption = bonusOption;
|
||||
bonusOptionLink.Level = bonusOption.LevelDependentOptions.Select(o => o.Level).SelectRandom();
|
||||
item.ItemOptions.Add(bonusOptionLink);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRandomExcOptions(Item item)
|
||||
{
|
||||
var excellentOptions = item.Definition!.PossibleItemOptions.FirstOrDefault(
|
||||
o => o.PossibleOptions.Any(p => object.Equals(p.OptionType, ItemOptionTypes.Excellent)));
|
||||
|
||||
if (excellentOptions is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingOptionCount = item.ItemOptions.Count(o => object.Equals(o.ItemOption?.OptionType, ItemOptionTypes.Excellent));
|
||||
|
||||
for (int i = existingOptionCount; i < excellentOptions.MaximumOptionsPerItem; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
// The first option is always added without a chance
|
||||
var newOption = excellentOptions.PossibleOptions.SelectRandom(this._randomizer);
|
||||
if (newOption is not null)
|
||||
{
|
||||
item.ItemOptions.Add(new ItemOptionLink { ItemOption = newOption });
|
||||
existingOptionCount++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._randomizer.NextRandomBool(excellentOptions.AddChance))
|
||||
{
|
||||
var newOption = excellentOptions.PossibleOptions.SelectRandom(this._randomizer);
|
||||
while (item.ItemOptions.Any(o => object.Equals(o.ItemOption, newOption)))
|
||||
{
|
||||
newOption = excellentOptions.PossibleOptions.SelectRandom(this._randomizer);
|
||||
}
|
||||
|
||||
if (newOption is not null)
|
||||
{
|
||||
item.ItemOptions.Add(new ItemOptionLink { ItemOption = newOption });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Item? GenerateItemDropOrMoney(MonsterDefinition monster, DropItemGroup selectedGroup, int gainedExperience, out uint? droppedMoney)
|
||||
{
|
||||
droppedMoney = null;
|
||||
|
||||
if (selectedGroup.PossibleItems?.Count > 0)
|
||||
{
|
||||
return this.GenerateItemFromGroup(monster, selectedGroup);
|
||||
}
|
||||
|
||||
var item = this.GenerateSpecialItem(monster, selectedGroup);
|
||||
if (item is null && selectedGroup.ItemType == SpecialItemType.Money)
|
||||
{
|
||||
droppedMoney = (uint)(gainedExperience + BaseMoneyDrop);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private Item? GenerateItemFromGroup(MonsterDefinition monster, DropItemGroup selectedGroup)
|
||||
{
|
||||
var isDropSpecificForMonster = monster.DropItemGroups.Contains(selectedGroup);
|
||||
if (isDropSpecificForMonster)
|
||||
{
|
||||
return this.GenerateItemDrop(selectedGroup, selectedGroup.PossibleItems!);
|
||||
}
|
||||
|
||||
var monsterLevel = (int)monster[Stats.Level];
|
||||
var isJewel = selectedGroup.ItemType == SpecialItemType.Jewel;
|
||||
|
||||
var filteredPossibleItems = selectedGroup.PossibleItems!
|
||||
.Where(it => CanDropAtMonsterLevel(it, monsterLevel)
|
||||
&& (isJewel || it.DropLevel == 0 || it.DropLevel > monsterLevel - DropLevelMaxGap))
|
||||
.ToList();
|
||||
|
||||
return this.GenerateItemDrop(selectedGroup, filteredPossibleItems);
|
||||
}
|
||||
|
||||
private Item? GenerateSpecialItem(MonsterDefinition monster, DropItemGroup selectedGroup)
|
||||
{
|
||||
var monsterLevel = (int)monster[Stats.Level];
|
||||
return selectedGroup.ItemType switch
|
||||
{
|
||||
SpecialItemType.Ancient => this.GenerateRandomAncient(),
|
||||
SpecialItemType.Excellent => this.GenerateRandomExcellentItem(monsterLevel),
|
||||
SpecialItemType.RandomItem => this.GenerateRandomItem(monsterLevel, false),
|
||||
SpecialItemType.SocketItem => this.GenerateRandomItem(monsterLevel, true),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private DropItemGroup? SelectRandomGroup(IEnumerable<DropItemGroup> groups, double totalChance)
|
||||
{
|
||||
var remainingThreshold = this._randomizer.NextDouble();
|
||||
if (totalChance > 1.0)
|
||||
{
|
||||
remainingThreshold *= totalChance;
|
||||
}
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (remainingThreshold > group.Chance)
|
||||
{
|
||||
remainingThreshold -= group.Chance;
|
||||
}
|
||||
else
|
||||
{
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IList<ItemDefinition>? GetPossibleList(int monsterLevel, bool isSocketItem = false)
|
||||
{
|
||||
if (monsterLevel is < byte.MinValue or > byte.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cache = isSocketItem ? this._droppableSocketItemsPerMonsterLevel : this._droppableItemsPerMonsterLevel;
|
||||
return cache[monsterLevel]
|
||||
??= (from it in this._droppableItems
|
||||
where CanDropAtMonsterLevel(it, monsterLevel)
|
||||
&& (it.DropLevel > monsterLevel - DropLevelMaxGap)
|
||||
&& (!isSocketItem || it.MaximumSockets > 0)
|
||||
select it).ToList();
|
||||
}
|
||||
}
|
||||
70
src/GameLogic/DirectionExtensions.cs
Normal file
70
src/GameLogic/DirectionExtensions.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
// <copyright file="DirectionExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods which have to do with handling <see cref="Direction"/>s.
|
||||
/// </summary>
|
||||
public static class DirectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the target point based on the origin and the specified direction.
|
||||
/// </summary>
|
||||
/// <param name="origin">From point.</param>
|
||||
/// <param name="direction">The direction.</param>
|
||||
/// <returns>The target point based on the origin and the specified direction.</returns>
|
||||
/// <exception cref="ArgumentException">direction.</exception>
|
||||
public static Point CalculateTargetPoint(this Point origin, Direction direction)
|
||||
{
|
||||
return direction switch
|
||||
{
|
||||
Direction.Undefined => origin,
|
||||
Direction.North => new Point((byte)(origin.X - 1), (byte)(origin.Y + 1)),
|
||||
Direction.South => new Point((byte)(origin.X + 1), (byte)(origin.Y - 1)),
|
||||
Direction.East => new Point((byte)(origin.X + 1), (byte)(origin.Y + 1)),
|
||||
Direction.West => new Point((byte)(origin.X - 1), (byte)(origin.Y - 1)),
|
||||
Direction.NorthEast => new Point(origin.X, (byte)(origin.Y + 1)),
|
||||
Direction.NorthWest => new Point((byte)(origin.X - 1), origin.Y),
|
||||
Direction.SouthEast => new Point((byte)(origin.X + 1), origin.Y),
|
||||
Direction.SouthWest => new Point(origin.X, (byte)(origin.Y - 1)),
|
||||
_ => throw new ArgumentException($"Direction value {direction} is not defined", nameof(direction)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the direction from one point to another.
|
||||
/// </summary>
|
||||
/// <param name="from">The origin point.</param>
|
||||
/// <param name="to">The target point.</param>
|
||||
/// <returns>The direction from the origin to the target.</returns>
|
||||
public static Direction GetDirectionTo(this Point from, Point to)
|
||||
{
|
||||
if (from == to)
|
||||
{
|
||||
return Direction.Undefined;
|
||||
}
|
||||
|
||||
double angle = Math.Atan2(to.Y - from.Y, to.X - from.X);
|
||||
angle += Math.PI;
|
||||
angle /= Math.PI / 4;
|
||||
int halfQuarter = Convert.ToInt32(angle);
|
||||
halfQuarter %= 8;
|
||||
|
||||
return halfQuarter switch
|
||||
{
|
||||
7 => Direction.North,
|
||||
0 => Direction.NorthWest,
|
||||
1 => Direction.West,
|
||||
2 => Direction.SouthWest,
|
||||
3 => Direction.South,
|
||||
4 => Direction.SouthEast,
|
||||
5 => Direction.East,
|
||||
6 => Direction.NorthEast,
|
||||
_ => Direction.Undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
66
src/GameLogic/Disposable.cs
Normal file
66
src/GameLogic/Disposable.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// <copyright file="Disposable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for a disposable class.
|
||||
/// </summary>
|
||||
public class Disposable : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="Disposable"/> class.
|
||||
/// </summary>
|
||||
~Disposable()
|
||||
{
|
||||
this.Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disposing.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance is disposing; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsDisposing { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this.IsDisposed && !this.IsDisposing)
|
||||
{
|
||||
this.IsDisposing = true;
|
||||
try
|
||||
{
|
||||
this.Dispose(true);
|
||||
|
||||
this.IsDisposed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.IsDisposing = false;
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
298
src/GameLogic/DroppedItem.cs
Normal file
298
src/GameLogic/DroppedItem.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
// <copyright file="DroppedItem.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// An item which got dropped on the ground of a map.
|
||||
/// </summary>
|
||||
public sealed class DroppedItem : AsyncDisposable, ILocateable
|
||||
{
|
||||
private static readonly TimeSpan TimeUntilDropIsFree = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pickup lock. Used to synchronize pick up requests from the players.
|
||||
/// </summary>
|
||||
private readonly AsyncLock _pickupLock = new();
|
||||
|
||||
private readonly DateTime _dropTimestamp = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the item was persistent (exists on the database) when it was dropped.
|
||||
/// If it wasn't and we clean it up, then we don't need to delete it.
|
||||
/// </summary>
|
||||
private readonly bool _wasItemPersisted;
|
||||
|
||||
private Player? _dropper;
|
||||
|
||||
private IEnumerable<object>? _owners;
|
||||
|
||||
private Timer? _removeTimer;
|
||||
|
||||
private bool _availableToPick = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DroppedItem" /> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="position">The position where the item was dropped on the map.</param>
|
||||
/// <param name="map">The map.</param>
|
||||
/// <param name="dropper">The dropper.</param>
|
||||
public DroppedItem(Item item, Point position, GameMap map, Player dropper)
|
||||
: this(item, position, map, dropper, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DroppedItem" /> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item, which should be detached from any player persistence context.</param>
|
||||
/// <param name="position">The position where the item was dropped on the map.</param>
|
||||
/// <param name="map">The map.</param>
|
||||
/// <param name="dropper">The dropper.</param>
|
||||
/// <param name="owners">The owners.</param>
|
||||
/// <param name="wasItemPersisted">If set to <c>true</c>, the item was persisted before and exists on the database.</param>
|
||||
public DroppedItem(Item item, Point position, GameMap map, Player? dropper, IEnumerable<object>? owners, bool wasItemPersisted = false)
|
||||
{
|
||||
this.Item = item;
|
||||
this.Position = position;
|
||||
this.CurrentMap = map;
|
||||
this._dropper = dropper;
|
||||
this._owners = owners;
|
||||
this._wasItemPersisted = wasItemPersisted;
|
||||
this._removeTimer = new Timer(this.DisposeAndDelete, null, (int)map.ItemDropDuration.TotalMilliseconds, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item.
|
||||
/// </summary>
|
||||
public Item Item { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
public ushort Id { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameMap CurrentMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to pick the item up by the specified player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>
|
||||
/// The success.
|
||||
/// StackTarget: If the success is <c>true</c>, and this is not <c>null</c>, this dropped item was stacked on an existing item of the players inventory.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Can be overwritten, for example for quest items which dropped only for a specific player.
|
||||
/// </remarks>
|
||||
public async ValueTask<(bool Success, Item? StackTarget)> TryPickUpByAsync(Player player)
|
||||
{
|
||||
Item? stackTarget = null;
|
||||
if (!this._availableToPick)
|
||||
{
|
||||
return (false, stackTarget);
|
||||
}
|
||||
|
||||
if (this.Item.IsStackable())
|
||||
{
|
||||
stackTarget = player.Inventory?.Items.FirstOrDefault(i => i.CanCompletelyStackOn(this.Item));
|
||||
}
|
||||
|
||||
if (stackTarget != null)
|
||||
{
|
||||
if (await this.TryStackOnItemAsync(player, stackTarget).ConfigureAwait(false))
|
||||
{
|
||||
return (true, stackTarget);
|
||||
}
|
||||
|
||||
return (false, stackTarget);
|
||||
}
|
||||
|
||||
if (this.Item.Definition!.IsBoundToCharacter && !this.IsPlayerAnOwner(player))
|
||||
{
|
||||
return (false, stackTarget);
|
||||
}
|
||||
|
||||
if (!this.IsPlayerAnOwner(player)
|
||||
&& DateTime.UtcNow < this._dropTimestamp.Add(TimeUntilDropIsFree))
|
||||
{
|
||||
return (false, stackTarget);
|
||||
}
|
||||
|
||||
return (await this.TryPickUpAsync(player).ConfigureAwait(false), stackTarget);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Id}: {this.Item} at {this.CurrentMap.Definition.Name} ({this.Position})";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
var timer = this._removeTimer;
|
||||
if (timer != null)
|
||||
{
|
||||
this._removeTimer = null;
|
||||
await timer.DisposeAsync().ConfigureAwait(false);
|
||||
await this.CurrentMap.RemoveAsync(this).ConfigureAwait(false);
|
||||
this._dropper = null;
|
||||
this._owners = null;
|
||||
}
|
||||
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void DisposeAndDelete(object? state)
|
||||
{
|
||||
var player = this._dropper;
|
||||
try
|
||||
{
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
if (player != null)
|
||||
{
|
||||
await this.DeleteItemAsync(player).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// we have to catch all errors, because it runs under a pooled thread without an additional safety net ;-)
|
||||
player?.Logger.LogError(ex, "Error during DroppedItem.DisposeAndDeleteAsync");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the owner-pickup priority period is still active.
|
||||
/// </summary>
|
||||
public bool IsOwnerPickupPriorityActive => DateTime.UtcNow < this._dropTimestamp.Add(TimeUntilDropIsFree);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified player is an owner of this dropped item.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns><c>true</c> if the player is an owner; otherwise, <c>false</c>.</returns>
|
||||
public bool IsPlayerAnOwner(Player player)
|
||||
{
|
||||
return this._owners?.Contains(player) ?? true;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> TryPickUpAsync(Player player)
|
||||
{
|
||||
player.Logger.LogDebug("Player {0} tries to pick up {1}", player, this);
|
||||
var slot = player.Inventory?.CheckInvSpace(this.Item);
|
||||
if (!slot.HasValue || slot < InventoryConstants.LastEquippableItemSlotIndex)
|
||||
{
|
||||
player.Logger.LogDebug("Inventory full, Player {0}, Item {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemWasTemporary = this.Item is TemporaryItem;
|
||||
using (await this._pickupLock.LockAsync())
|
||||
{
|
||||
if (!this._availableToPick)
|
||||
{
|
||||
player.Logger.LogDebug("Picked up by another player in the mean time, Player {0}, Item {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!itemWasTemporary)
|
||||
{
|
||||
// We already attach it here, so that the next changes are in the context.
|
||||
player.PersistenceContext.Attach(this.Item);
|
||||
}
|
||||
|
||||
if (!await player.Inventory!.AddItemAsync((byte)slot, this.Item).ConfigureAwait(false))
|
||||
{
|
||||
player.Logger.LogDebug("Item could not be added to the inventory, Player {0}, Item {1}", player, this);
|
||||
|
||||
if (!itemWasTemporary)
|
||||
{
|
||||
player.PersistenceContext.Detach(this.Item);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (itemWasTemporary)
|
||||
{
|
||||
// We set the item slot in a temporary item manually, so the further logic can report to the client where the item has been added.
|
||||
// If it's temporary, it has been converted into a persistent item before it was added to the inventory. So it's not the same instance.
|
||||
// If it's not temporary, this step is not required, because the inventory already set the ItemSlot in the same instance we're holding here.
|
||||
this.Item.ItemSlot = (byte)slot;
|
||||
}
|
||||
|
||||
this._availableToPick = false;
|
||||
}
|
||||
|
||||
player.Logger.LogDebug("Item '{0}' was picked up by player '{1}' and added to his inventory.", this, player);
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> TryStackOnItemAsync(Player player, Item stackTarget)
|
||||
{
|
||||
player.Logger.LogDebug("Player {0} tries to pick up {1}, trying to add to an existing item at slot {2}", player, this, stackTarget.ItemSlot);
|
||||
using (await this._pickupLock.LockAsync())
|
||||
{
|
||||
if (!this._availableToPick)
|
||||
{
|
||||
player.Logger.LogDebug("Picked up by another player in the mean time, Player {0}, Item {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
stackTarget.Durability += this.Item.Durability;
|
||||
this._availableToPick = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async ValueTask DeleteItemAsync(Player player)
|
||||
{
|
||||
player.Logger.LogDebug("Item '{0}' which was dropped by player '{1}' is getting deleted.", this, player);
|
||||
if (!this._wasItemPersisted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var repositoryManager = player.GameContext.PersistenceContextProvider;
|
||||
|
||||
// We could use here the persistence context of the dropper - but if it logged out and is not saving anymore, the deletion would not be saved.
|
||||
// So we use a new temporary persistence context instead.
|
||||
// We use a trade-context as it just focuses on the items. Otherwise, we would track a lot more items.
|
||||
using var context = repositoryManager.CreateNewTradeContext();
|
||||
context.Attach(this.Item);
|
||||
await context.DeleteAsync(this.Item).ConfigureAwait(false);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
player.Logger.LogWarning("Exception during deleting of the item {0}: {1}\n{2}", this, e.Message, e.StackTrace);
|
||||
}
|
||||
|
||||
player.GameContext.PlugInManager.GetPlugInPoint<IItemDestroyedPlugIn>()?.ItemDestroyed(this.Item);
|
||||
}
|
||||
}
|
||||
173
src/GameLogic/DroppedMoney.cs
Normal file
173
src/GameLogic/DroppedMoney.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
// <copyright file="DroppedMoney.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// Money which got dropped on the ground of a map.
|
||||
/// </summary>
|
||||
public sealed class DroppedMoney : AsyncDisposable, ILocateable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the pickup lock. Used to synchronize pick up requests from the players.
|
||||
/// </summary>
|
||||
private readonly AsyncLock _pickupLock;
|
||||
|
||||
private Timer? _removeTimer;
|
||||
|
||||
private bool _availableToPick = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DroppedMoney" /> class.
|
||||
/// </summary>
|
||||
/// <param name="amount">The amount.</param>
|
||||
/// <param name="position">The position where the item was dropped on the map.</param>
|
||||
/// <param name="map">The map.</param>
|
||||
public DroppedMoney(uint amount, Point position, GameMap map)
|
||||
{
|
||||
this.Amount = amount;
|
||||
this._pickupLock = new();
|
||||
this.Position = position;
|
||||
this.CurrentMap = map;
|
||||
this._removeTimer = new Timer(this.OnTimerTimeout, null, (int)map.ItemDropDuration.TotalMilliseconds, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the money item.
|
||||
/// </summary>
|
||||
public uint Amount { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
public ushort Id { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameMap CurrentMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to pick the money by the specified player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>
|
||||
/// The success.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Can be overwritten, for example for quest items which dropped only for a specific player.
|
||||
/// </remarks>
|
||||
public async ValueTask<bool> TryPickUpByAsync(Player player)
|
||||
{
|
||||
player.Logger.LogDebug("Player {0} tries to pick up {1}", player, this);
|
||||
|
||||
using (await this._pickupLock.LockAsync())
|
||||
{
|
||||
if (!this._availableToPick)
|
||||
{
|
||||
player.Logger.LogDebug("Picked up by another player in the mean time, Player {0}, Money {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._availableToPick = false;
|
||||
}
|
||||
|
||||
if (player.Party is { } party)
|
||||
{
|
||||
var partyMembers = party.PartyList
|
||||
.OfType<Player>()
|
||||
.Where(p => p.CurrentMap == player.CurrentMap && !p.IsAtSafezone() && p.Attributes is { })
|
||||
.ToList();
|
||||
|
||||
if (partyMembers.Count > 0)
|
||||
{
|
||||
var share = (int)(this.Amount / partyMembers.Count);
|
||||
foreach (var member in partyMembers)
|
||||
{
|
||||
member.TryAddMoney((int)(share * member.Attributes![Stats.MoneyAmountRate]));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var clampMoneyOnPickup = player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false;
|
||||
if (clampMoneyOnPickup)
|
||||
{
|
||||
var maxMoney = player.GameContext?.Configuration?.MaximumInventoryMoney ?? int.MaxValue;
|
||||
var currentMoney = player.Money;
|
||||
var amountToAdd = (int)Math.Min(this.Amount, (uint)Math.Max(0, maxMoney - currentMoney));
|
||||
|
||||
if (amountToAdd <= 0)
|
||||
{
|
||||
player.Logger.LogDebug("Player is at maximum money limit, Player {0}, Money {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!player.TryAddMoney(amountToAdd))
|
||||
{
|
||||
player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!player.TryAddMoney((int)this.Amount))
|
||||
{
|
||||
player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Money: {this.Amount} at {this.CurrentMap.Definition.Name} ({this.Position})";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (this._removeTimer is { } timer)
|
||||
{
|
||||
try
|
||||
{
|
||||
this._removeTimer = null;
|
||||
await timer.DisposeAsync().ConfigureAwait(false);
|
||||
await this.CurrentMap.RemoveAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Fail(e.Message, e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void OnTimerTimeout(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail(ex.Message, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
489
src/GameLogic/DuelRoom.cs
Normal file
489
src/GameLogic/DuelRoom.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
// <copyright file="DuelRoom.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.Views.Duel;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The state of the duel.
|
||||
/// </summary>
|
||||
public enum DuelState
|
||||
{
|
||||
/// <summary>
|
||||
/// Duel state is undefined.
|
||||
/// </summary>
|
||||
Undefined,
|
||||
|
||||
/// <summary>
|
||||
/// A duel was requested.
|
||||
/// </summary>
|
||||
DuelRequested,
|
||||
|
||||
/// <summary>
|
||||
/// Duel request was refused.
|
||||
/// </summary>
|
||||
DuelRefused,
|
||||
|
||||
/// <summary>
|
||||
/// Duel failed to start.
|
||||
/// </summary>
|
||||
DuelStartFailed,
|
||||
|
||||
/// <summary>
|
||||
/// Duel request was accepted.
|
||||
/// </summary>
|
||||
DuelAccepted,
|
||||
|
||||
/// <summary>
|
||||
/// Duel has started.
|
||||
/// </summary>
|
||||
DuelStarted,
|
||||
|
||||
/// <summary>
|
||||
/// Dual was cancelled.
|
||||
/// </summary>
|
||||
DuelCancelled,
|
||||
|
||||
/// <summary>
|
||||
/// Duel has finished.
|
||||
/// </summary>
|
||||
DuelFinished,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class that manages a duel between two players.
|
||||
/// </summary>
|
||||
public sealed class DuelRoom : AsyncDisposable
|
||||
{
|
||||
private readonly AsyncLock _spectatorLock = new();
|
||||
private CancellationTokenSource? _cts = new();
|
||||
private byte _scoreRequester;
|
||||
private byte _scoreOpponent;
|
||||
private int _maximumScore;
|
||||
private int _maximumSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DuelRoom" /> class.
|
||||
/// </summary>
|
||||
/// <param name="area">The duel area.</param>
|
||||
/// <param name="requester">The requester.</param>
|
||||
/// <param name="opponent">The opponent.</param>
|
||||
public DuelRoom(DuelArea area, Player requester, Player opponent)
|
||||
{
|
||||
this.Area = area;
|
||||
this.Index = area.Index;
|
||||
this.Requester = requester;
|
||||
this.Opponent = opponent;
|
||||
this.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
this._maximumScore = requester.GameContext.Configuration.DuelConfiguration?.MaximumScore ?? 10;
|
||||
this._maximumSpectators = requester.GameContext.Configuration.DuelConfiguration?.MaximumSpectatorsPerDuelRoom ?? 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the area of the duel.
|
||||
/// </summary>
|
||||
public DuelArea Area { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the area of the duel.
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="DateTime"/> of the start of the duel.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player that requested the duel.
|
||||
/// </summary>
|
||||
public Player Requester { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player that accepted the duel.
|
||||
/// </summary>
|
||||
public Player Opponent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the score of the player that requested the duel.
|
||||
/// </summary>
|
||||
public byte ScoreRequester
|
||||
{
|
||||
get => this._scoreRequester;
|
||||
set
|
||||
{
|
||||
this._scoreRequester = value;
|
||||
if (value >= this._maximumScore)
|
||||
{
|
||||
this.State = DuelState.DuelFinished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the score of the player that accepted the duel.
|
||||
/// </summary>
|
||||
public byte ScoreOpponent
|
||||
{
|
||||
get => this._scoreOpponent;
|
||||
set
|
||||
{
|
||||
this._scoreOpponent = value;
|
||||
if (value >= this._maximumScore)
|
||||
{
|
||||
this.State = DuelState.DuelFinished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// public bool IsAccepted { get; set; }
|
||||
|
||||
// public bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state of the duel.
|
||||
/// </summary>
|
||||
public DuelState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a lock object used to update the score of the duel.
|
||||
/// </summary>
|
||||
public AsyncLock Lock { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duel room spectators list.
|
||||
/// </summary>
|
||||
public List<Player> Spectators { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the players taking part in the duel, either as duelists or spectators.
|
||||
/// </summary>
|
||||
public IEnumerable<Player> AllPlayers
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return this.Requester;
|
||||
|
||||
yield return this.Opponent;
|
||||
|
||||
for (var index = this.Spectators.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var spectator = this.Spectators[index];
|
||||
yield return spectator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the duel room still has spectator slots.
|
||||
/// </summary>
|
||||
public bool IsOpen => this.Spectators.Count < this._maximumSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the player is participating in the duel as a duelist.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
public bool IsDuelist(Player player)
|
||||
{
|
||||
return this.Requester == player || this.Opponent == player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the spectator from the room.
|
||||
/// </summary>
|
||||
/// <param name="spectator">The spectator which should be removed.</param>
|
||||
public async ValueTask RemoveSpectatorAsync(Player spectator)
|
||||
{
|
||||
using (await this._spectatorLock.LockAsync())
|
||||
{
|
||||
if (!this.Spectators.Remove(spectator))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = this.Spectators.Count; j >= 0; --j)
|
||||
{
|
||||
var player = this.Spectators[j];
|
||||
await player.InvokeViewPlugInAsync<IDuelSpectatorRemovedPlugIn>(p => p.SpectatorRemovedAsync(spectator)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await spectator.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
|
||||
await spectator.RemoveInvisibleEffectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the duel.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Occurs when one of the players has left the duel map.</exception>
|
||||
public async Task RunDuelAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cancellationToken = this._cts?.Token ?? default;
|
||||
|
||||
// We first wait until both players are on the map
|
||||
while (this.Requester.Id == default || this.Opponent.Id == default)
|
||||
{
|
||||
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (this.Requester.CurrentMap?.Definition != this.Area.FirstPlayerGate?.Map
|
||||
|| this.Opponent.CurrentMap?.Definition != this.Area.SecondPlayerGate?.Map)
|
||||
{
|
||||
throw new InvalidOperationException("Duel cannot start when any of the players left the duel map");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
this.State = DuelState.DuelAccepted;
|
||||
|
||||
for (int i = 5; i > 0; i--)
|
||||
{
|
||||
var seconds = i;
|
||||
await this.AllPlayers.ForEachAsync(p => p.ShowLocalizedGoldenMessageAsync(nameof(PlayerMessage.DuelBattleBeginsInSecondsFormat), seconds)).ConfigureAwait(false);
|
||||
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this.Requester.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(DuelStartResult.Success, this.Opponent)).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(DuelStartResult.Success, this.Requester)).ConfigureAwait(false);
|
||||
|
||||
await this.Requester.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(p => p.UpdateScoreAsync(this)).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(p => p.UpdateScoreAsync(this)).ConfigureAwait(false);
|
||||
|
||||
await this.Requester.InvokeViewPlugInAsync<IInitializeDuelPlugIn>(p => p.InitializeDuelAsync(this)).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IInitializeDuelPlugIn>(p => p.InitializeDuelAsync(this)).ConfigureAwait(false);
|
||||
|
||||
this.State = DuelState.DuelStarted;
|
||||
|
||||
while (this.State is not (DuelState.DuelFinished or DuelState.DuelCancelled))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||
await this.SendCurrentStateToAllPlayersAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (this.State is DuelState.DuelFinished)
|
||||
{
|
||||
await this.FinishDuelAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (this.State is not DuelState.DuelFinished)
|
||||
{
|
||||
this.State = DuelState.DuelCancelled;
|
||||
await this.StopDuelAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels and stops the duel.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ValueTask"/>.</returns>
|
||||
public async ValueTask CancelDuelAsync()
|
||||
{
|
||||
if (this._cts is { } cts)
|
||||
{
|
||||
await cts.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the spawn gate of the player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>The gate where the player is teleported to.</returns>
|
||||
public ExitGate? GetSpawnGate(Player player)
|
||||
{
|
||||
if (this.Opponent == player)
|
||||
{
|
||||
return this.Area.SecondPlayerGate;
|
||||
}
|
||||
|
||||
if (this.Requester == player)
|
||||
{
|
||||
return this.Area.FirstPlayerGate;
|
||||
}
|
||||
|
||||
return this.Area.SpectatorsGate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add the player as a spectator to the duel.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> with the result.</returns>
|
||||
public async ValueTask<bool> TryAddSpectatorAsync(Player player)
|
||||
{
|
||||
if (this.Area.SpectatorsGate is not { } spectatorsGate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Player[] spectators;
|
||||
using (await this._spectatorLock.LockAsync())
|
||||
{
|
||||
if (this.Spectators.Count >= this._maximumSpectators)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
spectators = this.Spectators.ToArray();
|
||||
this.Spectators.Add(player);
|
||||
}
|
||||
|
||||
await player.AddInvisibleEffectAsync().ConfigureAwait(false);
|
||||
await player.WarpToAsync(spectatorsGate).ConfigureAwait(false);
|
||||
|
||||
await player.InvokeViewPlugInAsync<IInitializeDuelPlugIn>(p => p.InitializeDuelAsync(this)).ConfigureAwait(false);
|
||||
await player.InvokeViewPlugInAsync<IDuelHealthUpdatePlugIn>(p => p.UpdateHealthAsync(this)).ConfigureAwait(false);
|
||||
await player.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(p => p.UpdateScoreAsync(this)).ConfigureAwait(false);
|
||||
|
||||
// send spectator list to this player
|
||||
await player.InvokeViewPlugInAsync<IDuelSpectatorListUpdatePlugIn>(p => p.UpdateSpectatorListAsync(spectators)).ConfigureAwait(false);
|
||||
|
||||
// send spectator added to all spectators of the same room
|
||||
for (int i = spectators.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (spectators[i] is { } spectator)
|
||||
{
|
||||
await spectator.InvokeViewPlugInAsync<IDuelSpectatorAddedPlugIn>(p => p.SpectatorAddedAsync(player)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets and disposes of the duel room.
|
||||
/// </summary>
|
||||
/// <param name="startResult">The resuls of the duel start request.</param>
|
||||
/// <returns>A <see cref="ValueTask"/>.</returns>
|
||||
public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult)
|
||||
{
|
||||
this.Requester.DuelRoom = null;
|
||||
this.Opponent.DuelRoom = null;
|
||||
await this.Requester.GameContext.DuelRoomManager.GiveBackDuelRoomAsync(this).ConfigureAwait(false);
|
||||
if (startResult == DuelStartResult.Refused)
|
||||
{
|
||||
this.State = DuelState.DuelRefused;
|
||||
await this.Requester.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false);
|
||||
}
|
||||
else if (startResult != DuelStartResult.Undefined)
|
||||
{
|
||||
this.State = DuelState.DuelStartFailed;
|
||||
await this.Requester.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
|
||||
|
||||
await this.Requester.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Requester)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.Requester.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
|
||||
await this.Opponent.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (Interlocked.Exchange(ref this._cts, null) is not { } cts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await cts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
if (this.State >= DuelState.DuelAccepted)
|
||||
{
|
||||
await this.MovePlayersToExitAsync().ConfigureAwait(false);
|
||||
|
||||
await this.Requester.GameContext.DuelRoomManager.GiveBackDuelRoomAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this.AllPlayers.ForEach(p => p.DuelRoom = null);
|
||||
cts.Dispose();
|
||||
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask MovePlayersToExitAsync()
|
||||
{
|
||||
var duelConfig = this.Requester.GameContext.Configuration.DuelConfiguration;
|
||||
var exitGate = duelConfig?.Exit;
|
||||
var duelArenaMapNumber = this.Area.FirstPlayerGate?.Map?.Number;
|
||||
|
||||
var players = this.AllPlayers
|
||||
.Where(p => p.CurrentMap?.MapId == duelArenaMapNumber);
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (exitGate is not null && !this.IsDuelist(player))
|
||||
{
|
||||
await player.WarpToAsync(exitGate).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await player.WarpToSafezoneAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
player.Logger.LogError(ex, "Unexpected error when moving player away from duel arena.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask NotifyDuelFinishedAsync()
|
||||
{
|
||||
var winner = this.ScoreRequester > this.ScoreOpponent ? this.Requester : this.Opponent;
|
||||
var loser = this.Requester == winner ? this.Opponent : this.Requester;
|
||||
await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync<IDuelFinishedPlugIn>(p => p.DuelFinishedAsync(winner, loser))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask SendCurrentStateToAllPlayersAsync()
|
||||
{
|
||||
await this.AllPlayers.ForEachAsync(p => p.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(p => p.UpdateScoreAsync(this))).ConfigureAwait(false);
|
||||
|
||||
for (var index = this.Spectators.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var spectator = this.Spectators[index];
|
||||
await spectator.InvokeViewPlugInAsync<IDuelHealthUpdatePlugIn>(p => p.UpdateHealthAsync(this)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask StopDuelAsync()
|
||||
{
|
||||
await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false);
|
||||
await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false);
|
||||
|
||||
await this.ResetAndDisposeAsync(DuelStartResult.Undefined).ConfigureAwait(false);
|
||||
await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask FinishDuelAsync()
|
||||
{
|
||||
await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false);
|
||||
await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false);
|
||||
|
||||
await this.NotifyDuelFinishedAsync().ConfigureAwait(false);
|
||||
await Task.Delay(10000, default).ConfigureAwait(false);
|
||||
await this.MovePlayersToExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
93
src/GameLogic/DuelRoomManager.cs
Normal file
93
src/GameLogic/DuelRoomManager.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
// <copyright file="DuelRoomManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.Views.Duel;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// A class that manages several instances of <see cref="DuelRoom"/>.
|
||||
/// </summary>
|
||||
public class DuelRoomManager
|
||||
{
|
||||
private readonly DuelConfiguration _configuration;
|
||||
private readonly AsyncLock _lock = new AsyncLock();
|
||||
private readonly DuelRoom?[] _duelRooms;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DuelRoomManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
public DuelRoomManager(DuelConfiguration configuration)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
this._duelRooms = new DuelRoom?[this.MaxRoomCount];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum <see cref="DuelRoom"/>s count.
|
||||
/// </summary>
|
||||
public int MaxRoomCount => this._configuration?.DuelAreas.Count ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DuelRoom"/> for the two duelist players.
|
||||
/// </summary>
|
||||
/// <param name="player1">The first player.</param>
|
||||
/// <param name="player2">The second player.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> with a free <see cref="DuelRoom"/>.</returns>
|
||||
public async ValueTask<DuelRoom?> GetFreeDuelRoomAsync(Player player1, Player player2, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var l = await this._lock.LockAsync(cancellationToken);
|
||||
for (int i = 0; i < this._duelRooms.Length; i++)
|
||||
{
|
||||
if (this._duelRooms[i] is null)
|
||||
{
|
||||
var area = this._configuration.DuelAreas.First(a => a.Index == i);
|
||||
|
||||
return this._duelRooms[i] = new DuelRoom(area, player1, player2);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discards a <see cref="DuelRoom"/>.
|
||||
/// </summary>
|
||||
/// <param name="duelRoom">The <see cref="DuelRoom"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/>.</returns>
|
||||
public async ValueTask GiveBackDuelRoomAsync(DuelRoom duelRoom)
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
this._duelRooms[duelRoom.Index] = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DuelRoom"/> by its index number.
|
||||
/// </summary>
|
||||
/// <param name="requestedDuelIndex">The index of the <see cref="DuelRoom"/>.</param>
|
||||
/// <returns>A <see cref="DuelRoom"/>.</returns>
|
||||
public DuelRoom? GetRoomByIndex(byte requestedDuelIndex)
|
||||
{
|
||||
if (this.MaxRoomCount <= requestedDuelIndex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._duelRooms[requestedDuelIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the <see cref="DuelRoom"/>s to a player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>A <see cref="ValueTask"/>.</returns>
|
||||
public async ValueTask ShowRoomsAsync(Player player)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IDuelStatusUpdatePlugIn>(p => p.UpdateStatusAsync(this._duelRooms)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
134
src/GameLogic/EnumerableExtensions.cs
Normal file
134
src/GameLogic/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// <copyright file="EnumerableExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="IEnumerable{T}"/>.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the <paramref name="action"/> for each element of <paramref name="enumerable"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="action">The action which should be executed for each element.</param>
|
||||
/// <exception cref="System.ArgumentNullException">action.</exception>
|
||||
public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
|
||||
{
|
||||
if (action is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the <paramref name="action"/> for each element of <paramref name="enumerable"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="action">The action which should be executed for each element.</param>
|
||||
/// <exception cref="System.ArgumentNullException">action.</exception>
|
||||
public static async ValueTask ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, ValueTask> action)
|
||||
{
|
||||
if (action is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
await action(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the enumerable as list, by either casting it or creating it.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the list elements.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <returns>The list.</returns>
|
||||
public static IList<T> AsList<T>(this IEnumerable<T> enumerable)
|
||||
{
|
||||
if (enumerable is IList<T> list)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
return enumerable.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random element of an enumerable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="randomizer">The randomizer.</param>
|
||||
/// <returns>The randomly selected element.</returns>
|
||||
public static T? SelectRandom<T>(this IEnumerable<T> enumerable, IRandomizer randomizer)
|
||||
{
|
||||
var list = enumerable as IList<T> ?? enumerable.ToList();
|
||||
if (list.Count > 0)
|
||||
{
|
||||
var index = randomizer.NextInt(0, list.Count);
|
||||
return list[index];
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random element of an enumerable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <returns>The randomly selected element.</returns>
|
||||
public static T? SelectRandom<T>(this IEnumerable<T> enumerable) => SelectRandom(enumerable, Rand.GetRandomizer());
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random weighted element of an enumerable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="weights">The weights associated with <paramref name="enumerable" />, respectively.</param>
|
||||
/// <param name="randomizer">The randomizer.</param>
|
||||
/// <returns>The randomly selected weighted element.</returns>
|
||||
public static T? SelectWeightedRandom<T>(this IEnumerable<T> enumerable, IEnumerable<int> weights, IRandomizer randomizer)
|
||||
{
|
||||
var list = enumerable as IList<T> ?? enumerable.ToList();
|
||||
var weightList = weights as IList<int> ?? weights.ToList();
|
||||
if (list.Count > 0 && weightList.Count == list.Count)
|
||||
{
|
||||
var roll = randomizer.NextInt(0, weights.Sum());
|
||||
int inc = 0;
|
||||
for (int i = 0; i < weightList.Count; i++)
|
||||
{
|
||||
inc += weightList[i];
|
||||
if (roll < inc)
|
||||
{
|
||||
return list[i];
|
||||
}
|
||||
}
|
||||
|
||||
return SelectRandom(enumerable, randomizer); // Fallback in case there are no weights assigned (>0)
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random weighted element of an enumerable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The generic type of the enumerable.</typeparam>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="weights">The weights associated with <paramref name="enumerable" />, respectively.</param>
|
||||
/// <returns>The randomly selected weighted element.</returns>
|
||||
public static T? SelectWeightedRandom<T>(this IEnumerable<T> enumerable, IEnumerable<int> weights) => SelectWeightedRandom(enumerable, weights, Rand.GetRandomizer());
|
||||
}
|
||||
23
src/GameLogic/ExitGateExtensions.cs
Normal file
23
src/GameLogic/ExitGateExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="ExitGateExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="ExitGate"/>.
|
||||
/// </summary>
|
||||
public static class ExitGateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a random point at the exit gate.
|
||||
/// </summary>
|
||||
/// <param name="gate">The gate.</param>
|
||||
/// <returns>The random point.</returns>
|
||||
public static Point GetRandomPoint(this ExitGate gate)
|
||||
{
|
||||
return new Point((byte)Rand.NextInt(gate.X1, gate.X2), (byte)Rand.NextInt(gate.Y1, gate.Y2));
|
||||
}
|
||||
}
|
||||
76
src/GameLogic/FeaturePlugInContainer.cs
Normal file
76
src/GameLogic/FeaturePlugInContainer.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="FeaturePlugInContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A plugin container for <see cref="IFeaturePlugIn"/>s.
|
||||
/// </summary>
|
||||
public class FeaturePlugInContainer : PlugInContainerBase<IFeaturePlugIn>, ICustomPlugInContainer<IFeaturePlugIn>
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, IFeaturePlugIn> _currentlyEffectivePlugIns = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeaturePlugInContainer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The plugin manager which manages this instance.</param>
|
||||
public FeaturePlugInContainer(PlugInManager manager)
|
||||
: base(manager)
|
||||
{
|
||||
foreach (var plugIn in this.Manager.GetActivePlugInsOf<IFeaturePlugIn>())
|
||||
{
|
||||
if (!this._currentlyEffectivePlugIns.ContainsKey(plugIn.GetType()))
|
||||
{
|
||||
this.AddPlugIn(plugIn, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetPlugIn<T>()
|
||||
where T : class, IFeaturePlugIn
|
||||
{
|
||||
if (this._currentlyEffectivePlugIns.TryGetValue(typeof(T), out var plugIn) && plugIn is T t)
|
||||
{
|
||||
return t;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ActivatePlugIn(IFeaturePlugIn plugIn)
|
||||
{
|
||||
var plugInType = plugIn.GetType();
|
||||
if (this._currentlyEffectivePlugIns.ContainsKey(plugInType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.ActivatePlugIn(plugIn);
|
||||
this._currentlyEffectivePlugIns.TryAdd(plugInType, plugIn);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DeactivatePlugIn(IFeaturePlugIn plugIn)
|
||||
{
|
||||
base.DeactivatePlugIn(plugIn);
|
||||
this._currentlyEffectivePlugIns.TryRemove(plugIn.GetType(), out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void BeforeActivatePlugInType(Type plugInType)
|
||||
{
|
||||
base.BeforeActivatePlugInType(plugInType);
|
||||
|
||||
var knownPlugIn = this.FindKnownPlugin(plugInType);
|
||||
if (knownPlugIn is null)
|
||||
{
|
||||
this.AddPlugIn((IFeaturePlugIn)Activator.CreateInstance(plugInType)!, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
535
src/GameLogic/GameContext.cs
Normal file
535
src/GameLogic/GameContext.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
// <copyright file="GameContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.MiniGames;
|
||||
using MUnique.OpenMU.GameLogic.PlugIns;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
|
||||
/// <summary>
|
||||
/// The game context which holds all data of the game together.
|
||||
/// </summary>
|
||||
public class GameContext : AsyncDisposable, IGameContext
|
||||
{
|
||||
private const string DefaultExperienceFormula = "if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))";
|
||||
private const string DefaultMasterExperienceFormula = "(505 * level * level * level) + (35278500 * level) + (228045 * level * level)";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
private static readonly Counter<int> PlayerCounter = Meter.CreateCounter<int>("PlayerCount");
|
||||
|
||||
private static readonly Counter<int> MapCounter = Meter.CreateCounter<int>("MapCount");
|
||||
|
||||
private static readonly Counter<int> MiniGameCounter = Meter.CreateCounter<int>("MiniGameCount");
|
||||
|
||||
private static readonly IObjectPool<PathFinder> PathFinderPoolInstance = new LimitedObjectPool<PathFinder>(new PathFinderPoolingPolicy());
|
||||
|
||||
private readonly Dictionary<ushort, GameMap> _mapList = new();
|
||||
|
||||
private readonly Dictionary<MiniGameMapKey, MiniGameContext> _miniGames = new();
|
||||
|
||||
private readonly Timer _recoverTimer;
|
||||
|
||||
private readonly IMapInitializer _mapInitializer;
|
||||
|
||||
private readonly AsyncLock _mapInitializerLock = new();
|
||||
|
||||
private readonly Timer _tasksTimer;
|
||||
|
||||
private readonly AsyncReaderWriterLock _playerListLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the list of all players.
|
||||
/// </summary>
|
||||
private readonly List<Player> _playerList = new();
|
||||
|
||||
private readonly IDisposable _configChangeHandlerRegistration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameContext" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="persistenceContextProvider">The persistence context provider.</param>
|
||||
/// <param name="mapInitializer">The map initializer.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="plugInManager">The plug in manager.</param>
|
||||
/// <param name="dropGenerator">The drop generator.</param>
|
||||
/// <param name="changeMediator">The cange mediator.</param>
|
||||
public GameContext(GameConfiguration configuration, IPersistenceContextProvider persistenceContextProvider, IMapInitializer mapInitializer, ILoggerFactory loggerFactory, PlugInManager plugInManager, IDropGenerator dropGenerator, IConfigurationChangeMediator changeMediator)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Configuration = configuration;
|
||||
this.PersistenceContextProvider = persistenceContextProvider;
|
||||
this.PlugInManager = plugInManager;
|
||||
this._mapInitializer = mapInitializer;
|
||||
this.LoggerFactory = loggerFactory;
|
||||
this.DropGenerator = dropGenerator;
|
||||
this.ConfigurationChangeMediator = changeMediator;
|
||||
this.ItemPowerUpFactory = new ItemPowerUpFactory(loggerFactory.CreateLogger<ItemPowerUpFactory>());
|
||||
this.PartyManager = new PartyManager(configuration.MaximumPartySize, loggerFactory.CreateLogger<Party>());
|
||||
this._recoverTimer = new Timer(this.RecoverTimerElapsed, null, this.Configuration.RecoveryInterval, this.Configuration.RecoveryInterval);
|
||||
this._tasksTimer = new Timer(this.ExecutePeriodicTasks, null, 1000, 1000);
|
||||
this.FeaturePlugIns = new FeaturePlugInContainer(this.PlugInManager);
|
||||
this._configChangeHandlerRegistration = this.ConfigurationChangeMediator.RegisterObject(this.Configuration, this, this.OnGameConfigurationChangeAsync);
|
||||
this.DuelRoomManager = new DuelRoomManager(this.Configuration.DuelConfiguration!);
|
||||
this.ExperienceTable = CreateExpTable(this.Configuration.ExperienceFormula ?? DefaultExperienceFormula, this.Configuration.MaximumLevel);
|
||||
this.MasterExperienceTable = CreateExpTable(this.Configuration.MasterExperienceFormula ?? DefaultMasterExperienceFormula, this.Configuration.MaximumMasterLevel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<GameContext>().LogError(ex, "Unexpected error in constructor of GameContext.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a game map got created.
|
||||
/// </summary>
|
||||
public event EventHandler<GameMap>? GameMapCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a game map got removed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Currently, maps are never removed.
|
||||
/// It may make sense to remove unused maps after a certain period.
|
||||
/// </remarks>
|
||||
public event EventHandler<GameMap>? GameMapRemoved;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual float ExperienceRate => this.Configuration.ExperienceRate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual float MasterExperienceRate => this.Configuration.MasterExperienceRate;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool PvpEnabled { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfiguration Configuration { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long[] ExperienceTable { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long[] MasterExperienceTable { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IConfigurationChangeMediator ConfigurationChangeMediator { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PlugInManager PlugInManager { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDropGenerator DropGenerator { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public FeaturePlugInContainer FeaturePlugIns { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Offline.OfflinePlayerManager OfflinePlayerManager { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IItemPowerUpFactory ItemPowerUpFactory { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPersistenceContextProvider PersistenceContextProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players by character name dictionary.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<string, Player> PlayersByCharacterName { get; } = new ConcurrentDictionary<string, Player>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public DuelRoomManager DuelRoomManager { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPartyManager PartyManager { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path finder pool.
|
||||
/// </summary>
|
||||
public IObjectPool<PathFinder> PathFinderPool => PathFinderPoolInstance;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int PlayerCount => this._playerList.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the meter of this class.
|
||||
/// </summary>
|
||||
internal static string MeterName => typeof(GameContext).FullName ?? nameof(GameContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialized maps which are hosted on this context.
|
||||
/// </summary>
|
||||
public async ValueTask<IEnumerable<GameMap>> GetMapsAsync()
|
||||
{
|
||||
using var l = await this._mapInitializerLock.LockAsync();
|
||||
return this._mapList.Values.Concat(this._miniGames.Values.Select(g => g.Map)).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<GameMap?> GetMapAsync(ushort mapId, bool createIfNotExists = true)
|
||||
{
|
||||
if (this._mapList.TryGetValue(mapId, out var map))
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
if (!createIfNotExists)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
GameMap? createdMap;
|
||||
using (await this._mapInitializerLock.LockAsync())
|
||||
{
|
||||
if (this._mapList.TryGetValue(mapId, out map))
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
createdMap = this._mapInitializer.CreateGameMap(mapId);
|
||||
if (createdMap is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
this._mapList.Add(mapId, createdMap);
|
||||
createdMap.ObjectAdded += async args =>
|
||||
{
|
||||
if (this.PlugInManager.GetPlugInPoint<IObjectAddedToMapPlugIn>() is { } plugInPoint)
|
||||
{
|
||||
await plugInPoint.ObjectAddedToMapAsync(args.Map, args.Object).ConfigureAwait(false);
|
||||
}
|
||||
};
|
||||
createdMap.ObjectRemoved += async args =>
|
||||
{
|
||||
if (this.PlugInManager.GetPlugInPoint<IObjectRemovedFromMapPlugIn>() is { } plugInPoint)
|
||||
{
|
||||
await plugInPoint.ObjectRemovedFromMapAsync(args.Map, args.Object).ConfigureAwait(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField it's desired behavior to initialize the map outside the lock to keep locked timespan short.
|
||||
await this._mapInitializer.InitializeStateAsync(createdMap).ConfigureAwait(false);
|
||||
this.GameMapCreated?.Invoke(this, createdMap);
|
||||
MapCounter.Add(1);
|
||||
|
||||
return createdMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mini game map which is meant to be hosted by the game.
|
||||
/// </summary>
|
||||
/// <param name="miniGameDefinition">The mini game definition.</param>
|
||||
/// <param name="requester">The requesting player.</param>
|
||||
/// <returns>The hosted mini game instance.</returns>
|
||||
public async ValueTask<MiniGameContext> GetMiniGameAsync(MiniGameDefinition miniGameDefinition, Player requester)
|
||||
{
|
||||
var miniGameKey = MiniGameMapKey.Create(miniGameDefinition, requester);
|
||||
|
||||
if (this._miniGames.TryGetValue(miniGameKey, out var miniGameContext) && miniGameContext is { IsDisposed: false, IsDisposing: false })
|
||||
{
|
||||
return miniGameContext;
|
||||
}
|
||||
|
||||
using (await this._mapInitializerLock.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (this._miniGames.TryGetValue(miniGameKey, out miniGameContext))
|
||||
{
|
||||
if (miniGameContext.IsDisposed)
|
||||
{
|
||||
this._miniGames.Remove(miniGameKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
return miniGameContext;
|
||||
}
|
||||
}
|
||||
|
||||
switch (miniGameDefinition.Type)
|
||||
{
|
||||
case MiniGameType.ChaosCastle:
|
||||
miniGameContext = new ChaosCastleContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
||||
break;
|
||||
case MiniGameType.DevilSquare:
|
||||
miniGameContext = new DevilSquareContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
||||
break;
|
||||
case MiniGameType.BloodCastle:
|
||||
miniGameContext = new BloodCastleContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
||||
break;
|
||||
default:
|
||||
miniGameContext = new MiniGameContext(miniGameKey, miniGameDefinition, this, this._mapInitializer);
|
||||
break;
|
||||
}
|
||||
|
||||
this._miniGames.Add(miniGameKey, miniGameContext);
|
||||
}
|
||||
|
||||
var createdMap = miniGameContext.Map;
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField it's desired behavior to initialize the map outside the lock to keep locked timespan short.
|
||||
await this._mapInitializer.InitializeStateAsync(createdMap).ConfigureAwait(false);
|
||||
this.GameMapCreated?.Invoke(this, createdMap);
|
||||
MiniGameCounter.Add(1);
|
||||
return miniGameContext;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RemoveMiniGameAsync(MiniGameContext miniGameContext)
|
||||
{
|
||||
using var l = await this._mapInitializerLock.LockAsync().ConfigureAwait(false);
|
||||
MiniGameCounter.Add(-1);
|
||||
miniGameContext.Dispose();
|
||||
this._miniGames.Remove(miniGameContext.Key);
|
||||
this.GameMapRemoved?.Invoke(this, miniGameContext.Map);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the player to the game.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
public virtual async ValueTask AddPlayerAsync(Player player)
|
||||
{
|
||||
player.PlayerLeftWorld += this.PlayerLeftWorldAsync;
|
||||
player.PlayerEnteredWorld += this.PlayerEnteredWorldAsync;
|
||||
player.PlayerDisconnected += this.RemovePlayerAsync;
|
||||
|
||||
using (await this._playerListLock.WriterLockAsync())
|
||||
{
|
||||
this._playerList.Add(player);
|
||||
}
|
||||
|
||||
PlayerCounter.Add(1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IList<Player>> GetPlayersAsync()
|
||||
{
|
||||
using var l = await this._playerListLock.ReaderLockAsync();
|
||||
if (this._playerList.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return this._playerList.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the player from the game.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
public virtual async ValueTask RemovePlayerAsync(Player player)
|
||||
{
|
||||
bool removed;
|
||||
using (await this._playerListLock.WriterLockAsync())
|
||||
{
|
||||
removed = this._playerList.Remove(player);
|
||||
}
|
||||
|
||||
if (!removed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerCounter.Add(-1);
|
||||
if (player.SelectedCharacter != null)
|
||||
{
|
||||
this.PlayersByCharacterName.TryRemove(player.SelectedCharacter.Name, out _);
|
||||
}
|
||||
|
||||
player.CurrentMap?.RemoveAsync(player);
|
||||
|
||||
player.PlayerDisconnected -= this.RemovePlayerAsync;
|
||||
player.PlayerEnteredWorld -= this.PlayerEnteredWorldAsync;
|
||||
player.PlayerLeftWorld -= this.PlayerLeftWorldAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player by the character name.
|
||||
/// </summary>
|
||||
/// <param name="name">The character name.</param>
|
||||
/// <returns>The player by character name.</returns>
|
||||
public Player? GetPlayerByCharacterName(string name)
|
||||
{
|
||||
this.PlayersByCharacterName.TryGetValue(name, out var player);
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ForEachPlayerAsync(Func<Player, Task> action)
|
||||
{
|
||||
if (this._playerList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerList = await this.GetPlayersAsync().ConfigureAwait(false);
|
||||
await playerList.Select(action).WhenAll().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action for each player, grouped by their culture.
|
||||
/// </summary>
|
||||
/// <param name="stateFactory">The state factory which creates a state for each culture group.</param>
|
||||
/// <param name="action">The action to execute for each player and culture state.</param>
|
||||
/// <typeparam name="TCultureState">The type of the culture state.</typeparam>
|
||||
public async ValueTask ForEachPlayerGroupedByCultureAsync<TCultureState>(Func<CultureInfo, TCultureState> stateFactory, Func<Player, TCultureState, Task> action)
|
||||
{
|
||||
if (this._playerList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerList = await this.GetPlayersAsync().ConfigureAwait(false);
|
||||
await playerList
|
||||
.GroupBy(p => p.Culture)
|
||||
.SelectMany(g =>
|
||||
{
|
||||
var state = stateFactory(g.Key);
|
||||
return g.Select(player => action(player, state));
|
||||
})
|
||||
.WhenAll().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ShowGlobalLocalizedMessageAsync(MessageType messageType, string messageKey, params object?[] formatArguments)
|
||||
{
|
||||
await this.ForEachPlayerGroupedByCultureAsync<string>(
|
||||
cultureInfo =>
|
||||
{
|
||||
if (formatArguments.Length > 0)
|
||||
{
|
||||
return string.Format(PlayerMessage.ResourceManager.GetString(messageKey, cultureInfo) ?? string.Empty, formatArguments);
|
||||
}
|
||||
|
||||
return PlayerMessage.ResourceManager.GetString(messageKey, cultureInfo) ?? string.Empty;
|
||||
},
|
||||
(player, message) => player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, messageType)).AsTask())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendGlobalMessageAsync(string message, MessageType messageType)
|
||||
{
|
||||
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, messageType)).AsTask()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendGlobalChatMessageAsync(string sender, string message, ChatMessageType messageType)
|
||||
{
|
||||
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IChatViewPlugIn>(p => p.ChatMessageAsync(message, sender, messageType)).AsTask()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SendGlobalNotificationAsync(string message)
|
||||
{
|
||||
var sendingMessage = message.TrimStart('!');
|
||||
await this.SendGlobalMessageAsync(sendingMessage, MessageType.GoldenCenter).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
this._configChangeHandlerRegistration.Dispose();
|
||||
await this._recoverTimer.DisposeAsync().ConfigureAwait(false);
|
||||
await this._tasksTimer.DisposeAsync().ConfigureAwait(false);
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static long[] CreateExpTable(string experienceFormula, short maximumLevel)
|
||||
{
|
||||
var argument = new Argument("level");
|
||||
var expression = new Expression(experienceFormula);
|
||||
expression.addArguments(argument);
|
||||
|
||||
return Enumerable.Range(0, maximumLevel + 2)
|
||||
.Select(level =>
|
||||
{
|
||||
argument.setArgumentValue(level);
|
||||
return (long)expression.calculate();
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998
|
||||
private async ValueTask OnGameConfigurationChangeAsync(Action unregisterAction, GameConfiguration gameConfiguration, GameContext context)
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
this._recoverTimer.Change(gameConfiguration.RecoveryInterval, gameConfiguration.RecoveryInterval);
|
||||
this.ExperienceTable = CreateExpTable(gameConfiguration.ExperienceFormula ?? DefaultExperienceFormula, gameConfiguration.MaximumLevel);
|
||||
this.MasterExperienceTable = CreateExpTable(gameConfiguration.MasterExperienceFormula ?? DefaultMasterExperienceFormula, gameConfiguration.MaximumMasterLevel);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void ExecutePeriodicTasks(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.PlugInManager.GetPlugInPoint<IPeriodicTaskPlugIn>() is { } plugInPoint)
|
||||
{
|
||||
await plugInPoint.ExecuteTaskAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail(ex.Message, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void RecoverTimerElapsed(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.ForEachPlayerAsync(player =>
|
||||
{
|
||||
if (player.SelectedCharacter != null && !player.PlayerState.CurrentState.IsDisconnectedOrFinished())
|
||||
{
|
||||
return player.RegenerateAsync();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// This should never happen as we already handle Exceptions in player.RegenerateAsync.
|
||||
// However, if the player disconnects in the meantime, it could happen :-).
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask PlayerEnteredWorldAsync(Player player)
|
||||
{
|
||||
this.PlayersByCharacterName.TryAdd(player.SelectedCharacter!.Name, player);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private ValueTask PlayerLeftWorldAsync(Player player)
|
||||
{
|
||||
this.PlayersByCharacterName.TryRemove(player.SelectedCharacter!.Name, out _);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
276
src/GameLogic/GameMap.cs
Normal file
276
src/GameLogic/GameMap.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
// <copyright file="GameMap.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.PlayerActions;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The game map which contains instances of players, npcs, drops, and more.
|
||||
/// </summary>
|
||||
public class GameMap
|
||||
{
|
||||
private readonly IDictionary<ushort, ILocateable> _objectsInMap = new ConcurrentDictionary<ushort, ILocateable>();
|
||||
|
||||
private readonly IAreaOfInterestManager _areaOfInterestManager;
|
||||
|
||||
private readonly IdGenerator _objectIdGenerator;
|
||||
|
||||
private readonly IdGenerator _dropIdGenerator;
|
||||
|
||||
private readonly ExitGate? _safezoneSpawnGate;
|
||||
|
||||
private int _playerCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameMap" /> class.
|
||||
/// </summary>
|
||||
/// <param name="mapDefinition">The map definition.</param>
|
||||
/// <param name="itemDropDuration">Duration of the item drop.</param>
|
||||
/// <param name="chunkSize">Size of the chunk.</param>
|
||||
public GameMap(GameMapDefinition mapDefinition, TimeSpan itemDropDuration, byte chunkSize)
|
||||
{
|
||||
this.Id = Guid.NewGuid();
|
||||
this.Definition = mapDefinition;
|
||||
this.ItemDropDuration = itemDropDuration;
|
||||
this.Terrain = new GameMapTerrain(this.Definition);
|
||||
|
||||
this._areaOfInterestManager = new BucketAreaOfInterestManager(chunkSize);
|
||||
this._objectIdGenerator = new IdGenerator(ViewExtensions.ConstantPlayerId + 1, 0x7FFF);
|
||||
this._dropIdGenerator = new IdGenerator(0, ViewExtensions.ConstantPlayerId - 1);
|
||||
|
||||
this._safezoneSpawnGate = this.Definition.GetSafezoneGate(this.Terrain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an object was added to the map.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<(GameMap Map, ILocateable Object)>? ObjectAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an object was removed from the map.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<(GameMap Map, ILocateable Object)>? ObjectRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the map identifier.
|
||||
/// </summary>
|
||||
public ushort MapId => this.Definition.Number.ToUnsigned();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terrain of the map.
|
||||
/// </summary>
|
||||
public GameMapTerrain Terrain { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the safe zone spawn gate.
|
||||
/// </summary>
|
||||
public ExitGate? SafeZoneSpawnGate => this._safezoneSpawnGate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration about how long drops are laying on the ground until they are disappearing.
|
||||
/// </summary>
|
||||
public TimeSpan ItemDropDuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the definition of the map.
|
||||
/// </summary>
|
||||
public GameMapDefinition Definition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of this map instance.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object with the specified identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>The object with the specified identifier.</returns>
|
||||
public ILocateable? GetObject(ushort id)
|
||||
{
|
||||
this._objectsInMap.TryGetValue(id, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attackables in range of the specified coordinates.
|
||||
/// </summary>
|
||||
/// <param name="point">The coordinates.</param>
|
||||
/// <param name="range">The range.</param>
|
||||
/// <returns>The attackables in range of the specified coordinate.</returns>
|
||||
public IList<IAttackable> GetAttackablesInRange(Point point, int range)
|
||||
{
|
||||
return this._areaOfInterestManager.GetInRange(point, range).OfType<IAttackable>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all dropped items and money within the specified range of a point.
|
||||
/// </summary>
|
||||
/// <param name="point">The coordinates.</param>
|
||||
/// <param name="range">The range.</param>
|
||||
/// <returns>Dropped items and money in range.</returns>
|
||||
public IList<ILocateable> GetDropsInRange(Point point, int range)
|
||||
{
|
||||
return this._areaOfInterestManager.GetInRange(point, range)
|
||||
.Where(l => l is DroppedItem or DroppedMoney)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drop by id.
|
||||
/// </summary>
|
||||
/// <param name="dropId">The drop identifier.</param>
|
||||
/// <returns>The dropped item.</returns>
|
||||
public ILocateable? GetDrop(ushort dropId)
|
||||
{
|
||||
this._objectsInMap.TryGetValue(dropId, out var item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the locateable from the map.
|
||||
/// </summary>
|
||||
/// <param name="locateable">The locateable.</param>
|
||||
public async ValueTask RemoveAsync(ILocateable locateable)
|
||||
{
|
||||
await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false);
|
||||
if (this._objectsInMap.Remove(locateable.Id) && locateable.Id != 0)
|
||||
{
|
||||
if (locateable is DroppedItem
|
||||
|| locateable is DroppedMoney)
|
||||
{
|
||||
this._dropIdGenerator.GiveBack(locateable.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._objectIdGenerator.GiveBack(locateable.Id);
|
||||
}
|
||||
|
||||
if (locateable is Player player)
|
||||
{
|
||||
player.Id = 0;
|
||||
Interlocked.Decrement(ref this._playerCount);
|
||||
}
|
||||
|
||||
if (this.ObjectRemoved is { } eventHandler)
|
||||
{
|
||||
await eventHandler((this, locateable)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the locateable to the map.
|
||||
/// </summary>
|
||||
/// <param name="locateable">The locateable object.</param>
|
||||
public async ValueTask AddAsync(ILocateable locateable)
|
||||
{
|
||||
if (!this._objectsInMap.TryGetValue(locateable.Id, out var existing)
|
||||
|| existing != locateable)
|
||||
{
|
||||
switch (locateable)
|
||||
{
|
||||
case DroppedItem droppedItem:
|
||||
droppedItem.Id = (ushort)this._dropIdGenerator.GenerateId();
|
||||
break;
|
||||
case DroppedMoney droppedMoney:
|
||||
droppedMoney.Id = (ushort)this._dropIdGenerator.GenerateId();
|
||||
break;
|
||||
case Player player:
|
||||
player.Id = (ushort)this._objectIdGenerator.GenerateId();
|
||||
Interlocked.Increment(ref this._playerCount);
|
||||
break;
|
||||
case NonPlayerCharacter npc:
|
||||
npc.Id = (ushort)this._objectIdGenerator.GenerateId();
|
||||
break;
|
||||
case ISupportIdUpdate idUpdate:
|
||||
idUpdate.Id = (ushort)this._objectIdGenerator.GenerateId();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Adding an object of type {locateable.GetType()} is not supported.");
|
||||
}
|
||||
|
||||
this._objectsInMap.Add(locateable.Id, locateable);
|
||||
}
|
||||
|
||||
await this._areaOfInterestManager.AddObjectAsync(locateable).ConfigureAwait(false);
|
||||
if (this.ObjectAdded is { } eventHandler)
|
||||
{
|
||||
await eventHandler((this, locateable)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the locatable on the map.
|
||||
/// </summary>
|
||||
/// <param name="locatable">The monster.</param>
|
||||
/// <param name="target">The new coordinates.</param>
|
||||
/// <param name="moveLock">The move lock.</param>
|
||||
/// <param name="moveType">Type of the move.</param>
|
||||
public ValueTask MoveAsync(ILocateable locatable, Point target, AsyncLock moveLock, MoveType moveType)
|
||||
{
|
||||
return this._areaOfInterestManager.MoveObjectAsync(locatable, target, moveLock, moveType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a respawn for the specified locateable.
|
||||
/// </summary>
|
||||
/// <param name="locateable">The locateable.</param>
|
||||
public async ValueTask InitRespawnAsync(ILocateable locateable)
|
||||
{
|
||||
await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Respawns the specified locateable.
|
||||
/// </summary>
|
||||
/// <param name="locateable">The locateable.</param>
|
||||
public async ValueTask RespawnAsync(ILocateable locateable)
|
||||
{
|
||||
await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false);
|
||||
await this._areaOfInterestManager.AddObjectAsync(locateable).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears event NPCs.
|
||||
/// </summary>
|
||||
public async ValueTask ClearEventSpawnedNpcsAsync()
|
||||
{
|
||||
var eventMonsters = this._objectsInMap.Values
|
||||
.OfType<NonPlayerCharacter>()
|
||||
.Where(n => n.SpawnArea.SpawnTrigger is not SpawnTrigger.Automatic)
|
||||
.ToList();
|
||||
foreach (var monster in eventMonsters)
|
||||
{
|
||||
await monster.CurrentMap.RemoveAsync(monster).ConfigureAwait(false);
|
||||
monster.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the drops on invalid terrain.
|
||||
/// </summary>
|
||||
public async ValueTask ClearDropsOnInvalidTerrainAsync()
|
||||
{
|
||||
var drops = this._objectsInMap.Values
|
||||
.OfType<DroppedItem>()
|
||||
.Where(d => !this.Terrain.WalkMap[d.Position.X, d.Position.Y])
|
||||
.ToList();
|
||||
|
||||
foreach (var drop in drops)
|
||||
{
|
||||
await drop.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/GameLogic/GameMapTerrain.cs
Normal file
167
src/GameLogic/GameMapTerrain.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
// <copyright file="GameMapTerrain.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// The terrain of a map.
|
||||
/// </summary>
|
||||
public class GameMapTerrain
|
||||
{
|
||||
/// <summary>
|
||||
/// The size of the map in each dimension (byte range: 0–255).
|
||||
/// </summary>
|
||||
private const int MapSize = 256;
|
||||
|
||||
/// <summary>
|
||||
/// The default terrain where all coordinates are walkable and not a safezone.
|
||||
/// </summary>
|
||||
private static readonly byte[] DefaultTerrain = Enumerable.Repeat<byte>(0, short.MaxValue).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Pre-computed array of walkable, non-safezone points.
|
||||
/// Built once during construction for O(1) random spawn lookups.
|
||||
/// </summary>
|
||||
private readonly Point[] _spawnPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameMapTerrain"/> class.
|
||||
/// </summary>
|
||||
/// <param name="definition">The game map definition.</param>
|
||||
public GameMapTerrain(GameMapDefinition definition)
|
||||
: this(definition?.TerrainData)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameMapTerrain"/> class.
|
||||
/// </summary>
|
||||
/// <param name="terrainData">The terrain data.</param>
|
||||
public GameMapTerrain(byte[]? terrainData)
|
||||
{
|
||||
if (terrainData is { })
|
||||
{
|
||||
this.ReadTerrainData(terrainData.AsSpan(3));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ReadTerrainData(DefaultTerrain);
|
||||
}
|
||||
|
||||
this._spawnPoints = this.BuildSpawnPoints();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a grid of all safezone coordinates.
|
||||
/// </summary>
|
||||
public bool[,] SafezoneMap { get; } = new bool[MapSize, MapSize];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a grid of all walkable coordinates.
|
||||
/// </summary>
|
||||
public bool[,] WalkMap { get; } = new bool[MapSize, MapSize];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a grid of the walkable coordinates of monsters.
|
||||
/// </summary>
|
||||
public byte[,] AIgrid { get; } = new byte[MapSize, MapSize];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random walkable, non-safezone point anywhere on the map.
|
||||
/// Samples from a pre-computed array in O(1) per call.
|
||||
/// </summary>
|
||||
public Point? RandomWalkableCoordinate
|
||||
{
|
||||
get
|
||||
{
|
||||
var points = this._spawnPoints;
|
||||
if (points.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return points[Random.Shared.Next(points.Length)];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random drop coordinate at the specified point in the specified radius.
|
||||
/// </summary>
|
||||
/// <param name="point">The target point.</param>
|
||||
/// <param name="maximumRadius">The maximum radius around the specified coordinate.</param>
|
||||
/// <returns>The random drop coordinate.</returns>
|
||||
public Point GetRandomCoordinate(Point point, byte maximumRadius)
|
||||
{
|
||||
byte tempx = (byte)Rand.NextInt(Math.Max(0, point.X - maximumRadius), Math.Min(255, point.X + maximumRadius + 1));
|
||||
byte tempy = (byte)Rand.NextInt(Math.Max(0, point.Y - maximumRadius), Math.Min(255, point.Y + maximumRadius + 1));
|
||||
int i = 0;
|
||||
while (!this.WalkMap[tempx, tempy] && i < 20)
|
||||
{
|
||||
tempx = (byte)Rand.NextInt(Math.Max(0, point.X - maximumRadius), Math.Min(255, point.X + maximumRadius + 1));
|
||||
tempy = (byte)Rand.NextInt(Math.Max(0, point.Y - maximumRadius), Math.Min(255, point.Y + maximumRadius + 1));
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == 20)
|
||||
{
|
||||
return point;
|
||||
}
|
||||
|
||||
return new Point(tempx, tempy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the ai grid value at the specified coordinate.
|
||||
/// </summary>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateAiGridValue(byte x, byte y)
|
||||
{
|
||||
this.AIgrid[x, y] = (byte)((this.WalkMap[x, y] ? 1 : 0) | (this.SafezoneMap[x, y] ? 0b1000_0000 : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the terrain data from a stream.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
private void ReadTerrainData(ReadOnlySpan<byte> data)
|
||||
{
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
byte x = (byte)(i & 0xFF);
|
||||
byte y = (byte)((i >> 8) & 0xFF);
|
||||
byte value = data[i];
|
||||
this.WalkMap[x, y] = value == 0 || value == 1;
|
||||
this.SafezoneMap[x, y] = value == 1;
|
||||
this.UpdateAiGridValue(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the array of valid spawn points.
|
||||
/// A valid spawn point is walkable and not in a safezone.
|
||||
/// </summary>
|
||||
/// <returns>Array of valid spawn points.</returns>
|
||||
private Point[] BuildSpawnPoints()
|
||||
{
|
||||
var result = new List<Point>(MapSize * MapSize);
|
||||
|
||||
for (var x = 0; x < MapSize; x++)
|
||||
{
|
||||
for (var y = 0; y < MapSize; y++)
|
||||
{
|
||||
if (this.WalkMap[x, y] && !this.SafezoneMap[x, y])
|
||||
{
|
||||
result.Add(new Point((byte)x, (byte)y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
14
src/GameLogic/GlobalUsings.cs
Normal file
14
src/GameLogic/GlobalUsings.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// <copyright file="GlobalUsings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
#pragma warning disable SA1200 // Using directives should be placed correctly
|
||||
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
|
||||
global using Microsoft.Extensions.Logging;
|
||||
|
||||
global using MUnique.OpenMU.DataModel;
|
||||
global using MUnique.OpenMU.DataModel.Configuration;
|
||||
global using MUnique.OpenMU.DataModel.Entities;
|
||||
global using MUnique.OpenMU.GameLogic.Properties;
|
||||
28
src/GameLogic/GuildEventArgs.cs
Normal file
28
src/GameLogic/GuildEventArgs.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="GuildEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for a deleted guild.
|
||||
/// </summary>
|
||||
public class GuildEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild identifier of the deleted guild.</param>
|
||||
public GuildEventArgs(uint guildId)
|
||||
{
|
||||
this.GuildId = guildId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild identifier of the deleted guild.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The guild identifier of the deleted guild.
|
||||
/// </value>
|
||||
public uint GuildId { get; }
|
||||
}
|
||||
66
src/GameLogic/GuildWar/GuildWarContext.cs
Normal file
66
src/GameLogic/GuildWar/GuildWarContext.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// <copyright file="GuildWarContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.GuildWar;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the information about an ongoing guild war.
|
||||
/// </summary>
|
||||
public class GuildWarContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildWarContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="warType">Type of the war.</param>
|
||||
/// <param name="score">The score.</param>
|
||||
/// <param name="team">The team.</param>
|
||||
/// <param name="requester">The requester.</param>
|
||||
public GuildWarContext(GuildWarType warType, GuildWarScore score, GuildWarTeam team, Player? requester)
|
||||
{
|
||||
this.WarType = warType;
|
||||
this.Score = score;
|
||||
this.Team = team;
|
||||
this.Requester = requester;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the war.
|
||||
/// </summary>
|
||||
public GuildWarType WarType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the score.
|
||||
/// </summary>
|
||||
public GuildWarScore Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the team.
|
||||
/// </summary>
|
||||
public GuildWarTeam Team { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial requester.
|
||||
/// </summary>
|
||||
public Player? Requester { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
public GuildWarState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the enemy team.
|
||||
/// </summary>
|
||||
public string EnemyTeamName => this.Team == GuildWarTeam.First ? this.Score.SecondGuildName : this.Score.FirstGuildName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the score for this guild.
|
||||
/// </summary>
|
||||
public byte ThisScore => this.Team == GuildWarTeam.First ? this.Score.FirstGuildScore : this.Score.SecondGuildScore;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the score of the enemy guild.
|
||||
/// </summary>
|
||||
public byte EnemyScore => this.Team == GuildWarTeam.First ? this.Score.SecondGuildScore : this.Score.FirstGuildScore;
|
||||
}
|
||||
103
src/GameLogic/GuildWar/GuildWarScore.cs
Normal file
103
src/GameLogic/GuildWar/GuildWarScore.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
// <copyright file="GuildWarScore.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.GuildWar;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// The score of a guild war.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.ComponentModel.INotifyPropertyChanged" />
|
||||
public class GuildWarScore : INotifyPropertyChanged
|
||||
{
|
||||
private uint _firstGuildScore;
|
||||
|
||||
private uint _secondGuildScore;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a property value changes.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the first guild.
|
||||
/// </summary>
|
||||
public string FirstGuildName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the second guild.
|
||||
/// </summary>
|
||||
public string SecondGuildName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the score of the first guild.
|
||||
/// </summary>
|
||||
public byte FirstGuildScore => (byte)Math.Min(byte.MaxValue, this._firstGuildScore);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the score of the second guild.
|
||||
/// </summary>
|
||||
public byte SecondGuildScore => (byte)Math.Min(byte.MaxValue, this._secondGuildScore);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the guild war has ended.
|
||||
/// </summary>
|
||||
public bool HasEnded => this._firstGuildScore >= this.MaximumScore || this._secondGuildScore >= this.MaximumScore;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum score.
|
||||
/// </summary>
|
||||
public byte MaximumScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the winners of the guild war.
|
||||
/// </summary>
|
||||
public GuildWarTeam? Winners { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Increases the score of the first guild.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public void IncreaseFirstGuildScore(byte value = 1)
|
||||
{
|
||||
if (!this.HasEnded)
|
||||
{
|
||||
Interlocked.Add(ref this._firstGuildScore, value);
|
||||
this.RaisePropertyChanged(nameof(this.FirstGuildScore));
|
||||
if (this.HasEnded)
|
||||
{
|
||||
this.Winners = GuildWarTeam.First;
|
||||
this.RaisePropertyChanged(nameof(this.HasEnded));
|
||||
this.PropertyChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increases the score of the first guild.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public void IncreaseSecondGuildScore(byte value = 1)
|
||||
{
|
||||
if (!this.HasEnded)
|
||||
{
|
||||
Interlocked.Add(ref this._secondGuildScore, value);
|
||||
this.RaisePropertyChanged(nameof(this.SecondGuildScore));
|
||||
if (this.HasEnded)
|
||||
{
|
||||
this.Winners = GuildWarTeam.Second;
|
||||
this.RaisePropertyChanged(nameof(this.HasEnded));
|
||||
this.PropertyChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
26
src/GameLogic/GuildWar/GuildWarState.cs
Normal file
26
src/GameLogic/GuildWar/GuildWarState.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="GuildWarState.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.GuildWar;
|
||||
|
||||
/// <summary>
|
||||
/// The state of a guild war.
|
||||
/// </summary>
|
||||
public enum GuildWarState
|
||||
{
|
||||
/// <summary>
|
||||
/// The guild war was requested.
|
||||
/// </summary>
|
||||
Requested,
|
||||
|
||||
/// <summary>
|
||||
/// The guild war was started and is ongoing.
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// The guild war has ended.
|
||||
/// </summary>
|
||||
Ended,
|
||||
}
|
||||
21
src/GameLogic/GuildWar/GuildWarTeam.cs
Normal file
21
src/GameLogic/GuildWar/GuildWarTeam.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="GuildWarTeam.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.GuildWar;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the team of a guild war.
|
||||
/// </summary>
|
||||
public enum GuildWarTeam
|
||||
{
|
||||
/// <summary>
|
||||
/// The first team.
|
||||
/// </summary>
|
||||
First,
|
||||
|
||||
/// <summary>
|
||||
/// The second team.
|
||||
/// </summary>
|
||||
Second,
|
||||
}
|
||||
21
src/GameLogic/GuildWar/GuildWarType.cs
Normal file
21
src/GameLogic/GuildWar/GuildWarType.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// <copyright file="GuildWarType.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.GuildWar;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the type of a guild war.
|
||||
/// </summary>
|
||||
public enum GuildWarType
|
||||
{
|
||||
/// <summary>
|
||||
/// A normal guild war, where two parties fight against each other on a normal game map.
|
||||
/// </summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>
|
||||
/// A battle soccer match, where two parties play against each other on a <see cref="SoccerGameMap"/>.
|
||||
/// </summary>
|
||||
Soccer,
|
||||
}
|
||||
10
src/GameLogic/HitInfo.cs
Normal file
10
src/GameLogic/HitInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="HitInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// The information about a hit.
|
||||
/// </summary>
|
||||
public record struct HitInfo(uint HealthDamage, uint ShieldDamage, DamageAttributes Attributes, uint ManaToll = 0);
|
||||
44
src/GameLogic/IAreaOfInterestManager.cs
Normal file
44
src/GameLogic/IAreaOfInterestManager.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="IAreaOfInterestManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// A manager of an area of interest.
|
||||
/// </summary>
|
||||
public interface IAreaOfInterestManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the object to the area of interest.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
ValueTask AddObjectAsync(ILocateable obj);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the object from the area of interest.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
ValueTask RemoveObjectAsync(ILocateable obj);
|
||||
|
||||
/// <summary>
|
||||
/// Moves the object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="target">The new coordinates.</param>
|
||||
/// <param name="moveLock">The move lock.</param>
|
||||
/// <param name="moveType">Type of the move.</param>
|
||||
ValueTask MoveObjectAsync(ILocateable obj, Point target, AsyncLock moveLock, MoveType moveType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object in range.
|
||||
/// </summary>
|
||||
/// <param name="point">The point at which the objects are searched in the specified range.</param>
|
||||
/// <param name="range">The range.</param>
|
||||
/// <returns>The objects in range.</returns>
|
||||
IEnumerable<ILocateable> GetInRange(Point point, int range);
|
||||
}
|
||||
101
src/GameLogic/IAttackable.cs
Normal file
101
src/GameLogic/IAttackable.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// <copyright file="IAttackable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which is attackable.
|
||||
/// </summary>
|
||||
public interface IAttackable : IIdentifiable, ILocateable
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when this instance died.
|
||||
/// </summary>
|
||||
event EventHandler<DeathInformation>? Died;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attributes.
|
||||
/// </summary>
|
||||
IAttributeSystem Attributes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the magic effect list which contains buffs and de-buffs.
|
||||
/// </summary>
|
||||
MagicEffectsList MagicEffectList { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this <see cref="IAttackable"/> is alive.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if alive; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool IsAlive { get; }
|
||||
|
||||
/// <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>
|
||||
bool IsTeleporting { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the information about the last death.
|
||||
/// </summary>
|
||||
DeathInformation? LastDeath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attacks this object by the attacker with the specified skill.
|
||||
/// </summary>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
/// <param name="skill">The skill.</param>
|
||||
/// <param name="isCombo">If set to <c>true</c>, the attacker did a combination of skills.</param>
|
||||
/// <param name="damageFactor">The damage factor.</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>
|
||||
/// <returns>Returns information about the damage inflicted.</returns>
|
||||
ValueTask<HitInfo?> AttackByAsync(IAttacker attacker, SkillEntry? skill, bool isCombo, double damageFactor = 1.0, bool? isFinalStreakHit = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reflects the damage which was done previously with <see cref="AttackByAsync" /> or even <see cref="ReflectDamageAsync" /> to the <paramref name="reflector" />.
|
||||
/// </summary>
|
||||
/// <param name="reflector">The reflector.</param>
|
||||
/// <param name="damage">The damage.</param>
|
||||
ValueTask ReflectDamageAsync(IAttacker reflector, uint damage);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the poison damage.
|
||||
/// </summary>
|
||||
/// <param name="initialAttacker">The initial attacker.</param>
|
||||
/// <param name="damage">The damage.</param>
|
||||
ValueTask ApplyPoisonDamageAsync(IAttacker initialAttacker, uint damage);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the bleeding damage.
|
||||
/// </summary>
|
||||
/// <param name="initialAttacker">The initial attacker.</param>
|
||||
/// <param name="damage">The damage.</param>
|
||||
ValueTask ApplyBleedingDamageAsync(IAttacker initialAttacker, uint damage);
|
||||
|
||||
/// <summary>
|
||||
/// Kills the attackable instantly.
|
||||
/// </summary>
|
||||
ValueTask KillInstantlyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about the last death of the object.
|
||||
/// </summary>
|
||||
/// <param name="KillerId">The id of the killer.</param>
|
||||
/// <param name="KillerName">The name of the killer.</param>
|
||||
/// <param name="FinalHit">The hit info of the final/lethal hit.</param>
|
||||
/// <param name="SkillNumber">The number of the used skill.</param>
|
||||
// [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "The names are affecting the property names of the record. We want upper case there.")]
|
||||
public record DeathInformation(ushort KillerId, string KillerName, HitInfo FinalHit, short SkillNumber);
|
||||
23
src/GameLogic/IAttacker.cs
Normal file
23
src/GameLogic/IAttacker.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="IAttacker.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which can attack.
|
||||
/// </summary>
|
||||
public interface IAttacker : IIdentifiable, ILocateable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the attributes.
|
||||
/// </summary>
|
||||
IAttributeSystem Attributes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of the combo, if the attacker supports combos.
|
||||
/// </summary>
|
||||
ComboStateMachine? ComboState { get; }
|
||||
}
|
||||
45
src/GameLogic/IBucketMapObserver.cs
Normal file
45
src/GameLogic/IBucketMapObserver.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// <copyright file="IBucketMapObserver.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of an object which is observing other objects on its current map.
|
||||
/// </summary>
|
||||
public interface IBucketMapObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the maximum distance of which buckets in range the observer is interested in.
|
||||
/// </summary>
|
||||
int InfoRange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the observing buckets.
|
||||
/// </summary>
|
||||
IList<Bucket<ILocateable>> ObservingBuckets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This method is called, when another locateable is moving into the observing zones.
|
||||
/// </summary>
|
||||
/// <param name="item">The item which got added.</param>
|
||||
ValueTask LocateableAddedAsync(ILocateable item);
|
||||
|
||||
/// <summary>
|
||||
/// This method is called, when another locateable is moving out of the observing zones.
|
||||
/// </summary>
|
||||
/// <param name="item">The item which got removed.</param>
|
||||
ValueTask LocateableRemovedAsync(ILocateable item);
|
||||
|
||||
/// <summary>
|
||||
/// This method is called, when this object is moving to another zone, and old objects are getting out of range.
|
||||
/// </summary>
|
||||
/// <param name="oldObjects">The objects which are out of range.</param>
|
||||
ValueTask LocateablesOutOfScopeAsync(IEnumerable<ILocateable> oldObjects);
|
||||
|
||||
/// <summary>
|
||||
/// This method is called, when this object is moving to another zone, and new objects are getting into range.
|
||||
/// </summary>
|
||||
/// <param name="newObjects">The objects which are getting into range.</param>
|
||||
ValueTask NewLocateablesInScopeAsync(IEnumerable<ILocateable> newObjects);
|
||||
}
|
||||
64
src/GameLogic/IConfigurationChangeMediator.cs
Normal file
64
src/GameLogic/IConfigurationChangeMediator.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
// <copyright file="IConfigurationChangeMediator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// A mediator which notifies about configuration changes for registered instances.
|
||||
/// </summary>
|
||||
public interface IConfigurationChangeMediator
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers for changes of a configuration object in addition with an actual game logic object.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfig">The type of the configuration.</typeparam>
|
||||
/// <typeparam name="T">The type of the game logic object which might be modified by the changes.</typeparam>
|
||||
/// <param name="config">The configuration object in which the caller is interested in.</param>
|
||||
/// <param name="obj">The game logic object which will be provided in the <paramref name="onChange"/> and <paramref name="onDelete"/> callbacks.</param>
|
||||
/// <param name="onChange">The on-change callback.</param>
|
||||
/// <param name="onDelete">The on-delete callback.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> which is used to dispose the registration.</returns>
|
||||
IDisposable RegisterObject<TConfig, T>(TConfig config, T obj, Func<Action, TConfig, T, ValueTask>? onChange = null, Func<TConfig, T, ValueTask>? onDelete = null)
|
||||
where T : class
|
||||
where TConfig : class;
|
||||
|
||||
/// <summary>
|
||||
/// Registers for created configuration objects of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfig">The type of the configuration.</typeparam>
|
||||
/// <typeparam name="T">The type of the game logic object which might be modified by the changes.</typeparam>
|
||||
/// <param name="obj">The game logic object which will be provided in the <paramref name="onNewConfig"/> callback.</param>
|
||||
/// <param name="onNewConfig">The on-new-configuration callback.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> which is used to dispose the registration.</returns>
|
||||
IDisposable RegisterForNew<TConfig, T>(T obj, Func<TConfig, T, ValueTask> onNewConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The listener interface for a mediator which notifies about configuration changes for registered instances.
|
||||
/// </summary>
|
||||
public interface IConfigurationChangeMediatorListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the changed configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the changed configuration object.</param>
|
||||
/// <param name="id">The identifier of the changed configuration object.</param>
|
||||
/// <param name="configuration">The changed configuration object.</param>
|
||||
ValueTask HandleConfigurationChangedAsync(Type type, Guid id, object configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Handles the added configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the added configuration object.</param>
|
||||
/// <param name="id">The identifier of the added configuration object.</param>
|
||||
/// <param name="configuration">The added configuration object.</param>
|
||||
ValueTask HandleConfigurationAddedAsync(Type type, Guid id, object configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Handles the removed configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the removed configuration object.</param>
|
||||
/// <param name="id">The identifier of the removed configuration object.</param>
|
||||
ValueTask HandleConfigurationRemovedAsync(Type type, Guid id);
|
||||
}
|
||||
12
src/GameLogic/IDisabledByDefault.cs
Normal file
12
src/GameLogic/IDisabledByDefault.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="IDisabledByDefault.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface to indicate that this should be disabled by default.
|
||||
/// </summary>
|
||||
public interface IDisabledByDefault
|
||||
{
|
||||
}
|
||||
36
src/GameLogic/IDropGenerator.cs
Normal file
36
src/GameLogic/IDropGenerator.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// <copyright file="IDropGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// The interface for a drop generator.
|
||||
/// </summary>
|
||||
public interface IDropGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the item drops which are generated when a monster got killed by a player.
|
||||
/// </summary>
|
||||
/// <param name="monster">The monster which got killed.</param>
|
||||
/// <param name="gainedExperience">The experience which the player gained form the kill (relevant for the money drop).</param>
|
||||
/// <param name="player">The player who killed the monster.</param>
|
||||
/// <returns>
|
||||
/// The item drops and money which are generated when a monster got killed by a player.
|
||||
/// </returns>
|
||||
ValueTask<(IEnumerable<Item> Items, uint? Money)> GenerateItemDropsAsync(MonsterDefinition monster, int gainedExperience, Player player);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an item based on a <see cref="DropItemGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="group">The <see cref="DropItemGroup"/> which defines which item should be generated.</param>
|
||||
/// <returns>The generated item or <see langword="null"/>.</returns>
|
||||
Item? GenerateItemDrop(DropItemGroup group);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an item based on a <see cref="DropItemGroup"/>s.
|
||||
/// </summary>
|
||||
/// <param name="groups">The <see cref="DropItemGroup"/>s which define which item should be generated.</param>
|
||||
/// <returns>The generated item, money and drop effect of the selected group.</returns>
|
||||
(Item? Item, uint? Money, ItemDropEffect DropEffect) GenerateItemDrop(IEnumerable<DropItemGroup> groups);
|
||||
}
|
||||
26
src/GameLogic/IEventStateProvider.cs
Normal file
26
src/GameLogic/IEventStateProvider.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="IEventStateProvider.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a flag, if the event is running.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It has effect on re-spawning monsters, when the <see cref="MonsterSpawnArea.SpawnTrigger"/> is <see cref="SpawnTrigger.AutomaticDuringEvent"/>.
|
||||
/// </remarks>
|
||||
public interface IEventStateProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the event is currently running.
|
||||
/// </summary>
|
||||
bool IsEventRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines, if a spawn wave is currently active.
|
||||
/// </summary>
|
||||
/// <param name="waveNumber">The number of the wave.</param>
|
||||
/// <returns><see langword="true"/>, when the spawn wave is active; Otherwise, <see langword="false"/>.</returns>
|
||||
bool IsSpawnWaveActive(byte waveNumber);
|
||||
}
|
||||
20
src/GameLogic/IFeaturePlugIn.cs
Normal file
20
src/GameLogic/IFeaturePlugIn.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="IFeaturePlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A plugin interface for features.
|
||||
/// A feature plugin can be used to implicitly group dependent plugins.
|
||||
/// For example, you can check if a feature plugin is active by calling <see cref="PlugInManager.IsPlugInActive(System.Type)" />.
|
||||
/// Additionally, feature plugins can be used as a common configuration sink by implementing <see cref="ISupportCustomConfiguration{TCustomConfig}"/>.
|
||||
/// </summary>
|
||||
[Guid("D786314A-4168-4FCF-93F8-A350AD0E752E")]
|
||||
[PlugInPoint("Feature Plugins", "Feature plugins can group other plugins and provide a configuration.")]
|
||||
public interface IFeaturePlugIn
|
||||
{
|
||||
}
|
||||
217
src/GameLogic/IGameContext.cs
Normal file
217
src/GameLogic/IGameContext.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
// <copyright file="IGameContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.GameLogic.MiniGames;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The context of the game.
|
||||
/// </summary>
|
||||
public interface IGameContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when a game map got created.
|
||||
/// </summary>
|
||||
event EventHandler<GameMap>? GameMapCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a game map got removed.
|
||||
/// </summary>
|
||||
event EventHandler<GameMap>? GameMapRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the global experience rate.
|
||||
/// </summary>
|
||||
float ExperienceRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the global master experience rate.
|
||||
/// </summary>
|
||||
float MasterExperienceRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether PVP is enabled.
|
||||
/// </summary>
|
||||
bool PvpEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository provider. Used to retrieve data, e.g. from a database.
|
||||
/// </summary>
|
||||
IPersistenceContextProvider PersistenceContextProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item power up factory.
|
||||
/// </summary>
|
||||
IItemPowerUpFactory ItemPowerUpFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
GameConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the experience table. Index is the player level, value the needed experience to reach that level.
|
||||
/// </summary>
|
||||
long[] ExperienceTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the master experience table. Index is the player level, value the needed experience to reach that level.
|
||||
/// </summary>
|
||||
long[] MasterExperienceTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration change mediator.
|
||||
/// </summary>
|
||||
IConfigurationChangeMediator ConfigurationChangeMediator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plug in manager.
|
||||
/// </summary>
|
||||
PlugInManager PlugInManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the feature plug ins.
|
||||
/// </summary>
|
||||
FeaturePlugInContainer FeaturePlugIns { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offline player manager which tracks active offline players.
|
||||
/// </summary>
|
||||
Offline.OfflinePlayerManager OfflinePlayerManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players count of the game.
|
||||
/// </summary>
|
||||
int PlayerCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger factory.
|
||||
/// </summary>
|
||||
ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drop generator.
|
||||
/// </summary>
|
||||
IDropGenerator DropGenerator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object pool for path finders.
|
||||
/// </summary>
|
||||
IObjectPool<PathFinder> PathFinderPool { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duel room manager.
|
||||
/// </summary>
|
||||
DuelRoomManager DuelRoomManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of the active self defenses. The datetime holds the timestamp when self-defense ends.
|
||||
/// </summary>
|
||||
ConcurrentDictionary<(Player Attacker, Player Defender), DateTime> SelfDefenseState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the party manager which handles party creation and persistence.
|
||||
/// </summary>
|
||||
IPartyManager PartyManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialized maps which are hosted on this context.
|
||||
/// </summary>
|
||||
ValueTask<IEnumerable<GameMap>> GetMapsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players.
|
||||
/// </summary>
|
||||
ValueTask<IList<Player>> GetPlayersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Adds the player to the game.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
ValueTask AddPlayerAsync(Player player);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the player from the game.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
ValueTask RemovePlayerAsync(Player player);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maps which is meant to be hosted by the game.
|
||||
/// </summary>
|
||||
/// <param name="mapId">The map identifier.</param>
|
||||
/// <param name="createIfNotExists">If set to <c>true</c>, the map is created if it doesn't exist yet.</param>
|
||||
/// <returns>
|
||||
/// The hosted GameMap instance.
|
||||
/// </returns>
|
||||
ValueTask<GameMap?> GetMapAsync(ushort mapId, bool createIfNotExists = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mini game map which is meant to be hosted by the game.
|
||||
/// </summary>
|
||||
/// <param name="miniGameDefinition">The mini game definition.</param>
|
||||
/// <param name="requester">The requesting player.</param>
|
||||
/// <returns>
|
||||
/// The state of the mini game which contains the hosted GameMap instance.
|
||||
/// </returns>
|
||||
ValueTask<MiniGameContext> GetMiniGameAsync(MiniGameDefinition miniGameDefinition, Player requester);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the mini game instance from the context.
|
||||
/// </summary>
|
||||
/// <param name="miniGameContext">The context of the mini game.</param>
|
||||
ValueTask RemoveMiniGameAsync(MiniGameContext miniGameContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player object by character name.
|
||||
/// </summary>
|
||||
/// <param name="name">The character name.</param>
|
||||
/// <returns>The player object.</returns>
|
||||
Player? GetPlayerByCharacterName(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a global message to all players of the game with the specified message type.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
ValueTask SendGlobalMessageAsync(string message, MessageType messageType);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a global message to all players of the game with the specified message type.
|
||||
/// </summary>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
/// <param name="messageKey">The message resource key.</param>
|
||||
/// <param name="arguments">The parameters for the message.</param>
|
||||
ValueTask ShowGlobalLocalizedMessageAsync(MessageType messageType, string messageKey, params object?[] arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a global chat message to all players of the game with the specified message type.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="messageType">Type of the message.</param>
|
||||
ValueTask SendGlobalChatMessageAsync(string sender, string message, ChatMessageType messageType);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a golden global notification to all players of the game.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
ValueTask SendGlobalNotificationAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action for each player.
|
||||
/// </summary>
|
||||
/// <param name="action">The action which is executed.</param>
|
||||
/// <remarks>
|
||||
/// Please avoid doing actions which may lead to the connected-state of the players.
|
||||
/// </remarks>
|
||||
ValueTask ForEachPlayerAsync(Func<Player, Task> action);
|
||||
}
|
||||
110
src/GameLogic/IGameServerContext.cs
Normal file
110
src/GameLogic/IGameServerContext.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// <copyright file="IGameServerContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// The context of a game server.
|
||||
/// </summary>
|
||||
public interface IGameServerContext : IGameContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when a guild has been deleted.
|
||||
/// </summary>
|
||||
event EventHandler<GuildEventArgs>? GuildDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a guild alliance has been changed.
|
||||
/// </summary>
|
||||
event EventHandler<GuildEventArgs>? GuildChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier of the server.
|
||||
/// </summary>
|
||||
byte Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild server.
|
||||
/// </summary>
|
||||
IGuildServer GuildServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the login server.
|
||||
/// </summary>
|
||||
ILoginServer LoginServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the friend server.
|
||||
/// </summary>
|
||||
IFriendServer FriendServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message publisher.
|
||||
/// </summary>
|
||||
IEventPublisher EventPublisher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server configuration.
|
||||
/// </summary>
|
||||
GameServerConfiguration ServerConfiguration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the guild information.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild identifier.</param>
|
||||
ValueTask RefreshGuildInfoAsync(uint guildId);
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action for each player of the guild.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild id.</param>
|
||||
/// <param name="action">The action which should be executed.</param>
|
||||
ValueTask ForEachGuildPlayerAsync(uint guildId, Func<Player, Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action for each player of the alliance of the guild.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild id.</param>
|
||||
/// <param name="action">The action which should be executed.</param>
|
||||
ValueTask ForEachAlliancePlayerAsync(uint guildId, Func<Player, Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a guild member to the game, e.g. after a player entered a guild.
|
||||
/// </summary>
|
||||
/// <param name="guildMember">The guild member.</param>
|
||||
ValueTask RegisterGuildMemberAsync(Player guildMember);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a guild member from the game, e.g. after a player left the game or the guild.
|
||||
/// </summary>
|
||||
/// <param name="guildMember">The guild member.</param>
|
||||
ValueTask UnregisterGuildMemberAsync(Player guildMember);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a whole guild, usually after it has been disbanded.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The id of the guild.</param>
|
||||
ValueTask RemoveGuildAsync(uint guildId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the cached rival guild pairs when the hostility state between two guilds changes.
|
||||
/// </summary>
|
||||
/// <param name="guildIdA">The first guild identifier.</param>
|
||||
/// <param name="allianceGuildIdsA">All guild IDs in guild A's alliance.</param>
|
||||
/// <param name="guildIdB">The second guild identifier.</param>
|
||||
/// <param name="allianceGuildIdsB">All guild IDs in guild B's alliance.</param>
|
||||
/// <param name="created"><c>true</c> if the hostility was created; <c>false</c> if it was removed.</param>
|
||||
void UpdateGuildHostility(uint guildIdA, IReadOnlyList<uint> allianceGuildIdsA, uint guildIdB, IReadOnlyList<uint> allianceGuildIdsB, bool created);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two guilds are rivals (hostile to each other).
|
||||
/// This uses a local cache and does not call the guild server.
|
||||
/// </summary>
|
||||
/// <param name="guild1Id">The first guild identifier.</param>
|
||||
/// <param name="guild2Id">The second guild identifier.</param>
|
||||
/// <returns><c>true</c> if the guilds are rivals; <c>false</c> otherwise.</returns>
|
||||
bool AreGuildsRival(uint guild1Id, uint guild2Id);
|
||||
}
|
||||
16
src/GameLogic/IGameServerContextProvider.cs
Normal file
16
src/GameLogic/IGameServerContextProvider.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="IGameServerContextProvider.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an implementation which provides an <see cref="IGameServerContext"/>.
|
||||
/// </summary>
|
||||
public interface IGameServerContextProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the game server context.
|
||||
/// </summary>
|
||||
IGameServerContext Context { get; }
|
||||
}
|
||||
16
src/GameLogic/IIdentifiable.cs
Normal file
16
src/GameLogic/IIdentifiable.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="IIdentifiable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a identifiable object.
|
||||
/// </summary>
|
||||
public interface IIdentifiable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier.
|
||||
/// </summary>
|
||||
ushort Id { get; }
|
||||
}
|
||||
33
src/GameLogic/IItemPowerUpFactory.cs
Normal file
33
src/GameLogic/IItemPowerUpFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// <copyright file="IItemPowerUpFactory.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// A Factory for power ups which are provided by equipped items.
|
||||
/// Each power up has to be created individually for a specific player, because some depend on the attributes of the player.
|
||||
/// </summary>
|
||||
public interface IItemPowerUpFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the power ups of an individual item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="attributeSystem">The attribute system of the player who equipped the item.</param>
|
||||
/// <returns>The created power ups.</returns>
|
||||
IEnumerable<PowerUpWrapper> GetPowerUps(Item item, AttributeSystem attributeSystem);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set power ups, which are created for existing <see cref="ItemSetGroup"/>s in the equipped items.
|
||||
/// </summary>
|
||||
/// <param name="equippedItems">The equipped items.</param>
|
||||
/// <param name="attributeSystem">The attribute system of the player who equipped the items.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
/// <returns>The created set power ups.</returns>
|
||||
IEnumerable<PowerUpWrapper> GetSetPowerUps(IEnumerable<Item> equippedItems, AttributeSystem attributeSystem, GameConfiguration gameConfiguration);
|
||||
}
|
||||
43
src/GameLogic/ILocateable.cs
Normal file
43
src/GameLogic/ILocateable.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// <copyright file="ILocateable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which has a location on a map.
|
||||
/// </summary>
|
||||
public interface ILocateable : IIdentifiable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current map on which the object currently is.
|
||||
/// </summary>
|
||||
GameMap? CurrentMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the coordinates on the map.
|
||||
/// </summary>
|
||||
Point Position { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for objects which have bucket information.
|
||||
/// </summary>
|
||||
public interface IHasBucketInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current bucket where this instance currently moves at.
|
||||
/// This is helpful for other objects to determine if the observation should
|
||||
/// be continued or not.
|
||||
/// </summary>
|
||||
Bucket<ILocateable>? NewBucket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bucket where this instance currently moves away.
|
||||
/// This is helpful for other objects to determine if the observation should
|
||||
/// be continued or not.
|
||||
/// </summary>
|
||||
Bucket<ILocateable>? OldBucket { get; set; }
|
||||
}
|
||||
29
src/GameLogic/ILoggerOwner.cs
Normal file
29
src/GameLogic/ILoggerOwner.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="ILoggerOwner.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which has a <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
public interface ILoggerOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the logger of this instance.
|
||||
/// </summary>
|
||||
ILogger Logger { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which has a <see cref="ILogger" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the implementing class.</typeparam>
|
||||
public interface ILoggerOwner<out T> : ILoggerOwner
|
||||
where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the logger of this instance.
|
||||
/// </summary>
|
||||
new ILogger<T> Logger { get; }
|
||||
}
|
||||
59
src/GameLogic/IMapInitializer.cs
Normal file
59
src/GameLogic/IMapInitializer.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// <copyright file="IMapInitializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
|
||||
/// <summary>
|
||||
/// An interface for a map initializer which is responsible to create new instances of <see cref="GameMap"/>s
|
||||
/// and it's initialization.
|
||||
/// </summary>
|
||||
public interface IMapInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new game map instance of the specified game map number.
|
||||
/// </summary>
|
||||
/// <param name="mapNumber">The map number.</param>
|
||||
/// <returns>The new game map instance.</returns>
|
||||
GameMap? CreateGameMap(ushort mapNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new game map instance with the specified definition.
|
||||
/// </summary>
|
||||
/// <param name="mapDefinition">The map definition.</param>
|
||||
/// <returns>The new game map instance.</returns>
|
||||
GameMap CreateGameMap(GameMapDefinition mapDefinition);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the state of the previously created game map (e.g. by creating NPC instances).
|
||||
/// </summary>
|
||||
/// <param name="createdMap">The created map.</param>
|
||||
ValueTask InitializeStateAsync(GameMap createdMap);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the spawn on the map.
|
||||
/// </summary>
|
||||
/// <param name="spawnIndex">The spawn index.</param>
|
||||
/// <param name="gameMap">The game map on which the spawn should be initialized.</param>
|
||||
/// <param name="spawnArea">The spawn area.</param>
|
||||
/// <param name="eventStateProvider">The event state provider.</param>
|
||||
/// <param name="dropGenerator">The drop generator.</param>
|
||||
ValueTask<NonPlayerCharacter?> InitializeSpawnAsync(int spawnIndex, GameMap gameMap, MonsterSpawnArea spawnArea, IEventStateProvider? eventStateProvider = null, IDropGenerator? dropGenerator = null);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the event NPCs of the previously created game map.
|
||||
/// </summary>
|
||||
/// <param name="createdMap">The created map.</param>
|
||||
/// <param name="eventStateProvider">The event state provider.</param>
|
||||
ValueTask InitializeNpcsOnEventStartAsync(GameMap createdMap, IEventStateProvider eventStateProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the event NPCs of the previously created game map after the spawn waves started.
|
||||
/// </summary>
|
||||
/// <param name="createdMap">The created map.</param>
|
||||
/// <param name="eventStateProvider">The event state provider.</param>
|
||||
/// <param name="waveNumber">The number of the started spawn wave.</param>
|
||||
ValueTask InitializeNpcsOnWaveStartAsync(GameMap createdMap, IEventStateProvider eventStateProvider, byte waveNumber);
|
||||
}
|
||||
19
src/GameLogic/IMovable.cs
Normal file
19
src/GameLogic/IMovable.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// <copyright file="IMovable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which supports to be moved on a map.
|
||||
/// </summary>
|
||||
public interface IMovable
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves the object to the specified target coordinates.
|
||||
/// </summary>
|
||||
/// <param name="target">The target coordinates.</param>
|
||||
ValueTask MoveAsync(Point target);
|
||||
}
|
||||
52
src/GameLogic/INpcIntelligence.cs
Normal file
52
src/GameLogic/INpcIntelligence.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// <copyright file="INpcIntelligence.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of a non-player-character artificial intelligence.
|
||||
/// </summary>
|
||||
public interface INpcIntelligence
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the monster which this AI is controlling.
|
||||
/// </summary>
|
||||
NonPlayerCharacter Npc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance can walk on safezone.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance can walk on safezone; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool CanWalkOnSafezone { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a hit from an attacker.
|
||||
/// </summary>
|
||||
/// <param name="attacker">The attacker.</param>
|
||||
void RegisterHit(IAttacker attacker);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the actions.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the actions.
|
||||
/// </summary>
|
||||
void Pause();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the monster can walk on the specified target.
|
||||
/// </summary>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance can walk on the specified target; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool CanWalkOn(Point target);
|
||||
}
|
||||
30
src/GameLogic/IObjectPool.cs
Normal file
30
src/GameLogic/IObjectPool.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// <copyright file="IObjectPool.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object pool which provides an async get method which allows
|
||||
/// to postpone the get until the next object is available.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type which should be pooled.</typeparam>
|
||||
public interface IObjectPool<T> : IDisposable
|
||||
where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an object from the pool if one is available, otherwise creates one
|
||||
/// or waits until one is available.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A <typeparamref name="T" />.</returns>
|
||||
ValueTask<T> GetAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Return an object to the pool.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to add to the pool.</param>
|
||||
void Return(T obj);
|
||||
}
|
||||
35
src/GameLogic/IObservable.cs
Normal file
35
src/GameLogic/IObservable.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// <copyright file="IObservable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an observable object.
|
||||
/// </summary>
|
||||
public interface IObservable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the observers.
|
||||
/// </summary>
|
||||
ISet<IWorldObserver> Observers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock for <see cref="Observers"/>.
|
||||
/// </summary>
|
||||
AsyncReaderWriterLock ObserverLock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the observer.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer.</param>
|
||||
ValueTask AddObserverAsync(IWorldObserver observer);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the observer.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer.</param>
|
||||
ValueTask RemoveObserverAsync(IWorldObserver observer);
|
||||
}
|
||||
39
src/GameLogic/IPartyManager.cs
Normal file
39
src/GameLogic/IPartyManager.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// <copyright file="IPartyManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Manages party creation and tracks party membership for member reconnection.
|
||||
/// </summary>
|
||||
public interface IPartyManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new party with the configured maximum party size.
|
||||
/// </summary>
|
||||
/// <returns>The newly created party.</returns>
|
||||
Party CreateParty();
|
||||
|
||||
/// <summary>
|
||||
/// Called when a party member reconnects. Restores the live player into their previous party,
|
||||
/// replacing the <see cref="OfflinePartyMember"/> snapshot that was created on disconnect.
|
||||
/// </summary>
|
||||
/// <param name="member">The reconnected member.</param>
|
||||
ValueTask OnMemberReconnectedAsync(IPartyMember member);
|
||||
|
||||
/// <summary>
|
||||
/// Registers that a character belongs to a party. Called by <see cref="Party"/> internally
|
||||
/// when members are added, replaced, or removed.
|
||||
/// </summary>
|
||||
/// <param name="characterName">The character name.</param>
|
||||
/// <param name="party">The party.</param>
|
||||
internal void TrackMembership(string characterName, Party party);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the party tracking for a character. Called by <see cref="Party"/> internally
|
||||
/// when members leave or are replaced.
|
||||
/// </summary>
|
||||
/// <param name="characterName">The character name.</param>
|
||||
internal void UntrackMembership(string characterName);
|
||||
}
|
||||
41
src/GameLogic/IPartyMember.cs
Normal file
41
src/GameLogic/IPartyMember.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="IPartyMember.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// The interface for a party member.
|
||||
/// </summary>
|
||||
public interface IPartyMember : IWorldObserver, IObservable, IIdentifiable, ILocateable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current party of the player.
|
||||
/// </summary>
|
||||
Party? Party { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last party requester.
|
||||
/// </summary>
|
||||
IPartyMember? LastPartyRequester { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum health.
|
||||
/// </summary>
|
||||
uint MaximumHealth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current health.
|
||||
/// </summary>
|
||||
uint CurrentHealth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the member is currently connected to the game.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
}
|
||||
16
src/GameLogic/IPlayerSurrogate.cs
Normal file
16
src/GameLogic/IPlayerSurrogate.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="IPlayerSurrogate.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Surrogate of a player.
|
||||
/// </summary>
|
||||
public interface IPlayerSurrogate
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the owner of this instance.
|
||||
/// </summary>
|
||||
Player Owner { get; }
|
||||
}
|
||||
69
src/GameLogic/IRandomizer.cs
Normal file
69
src/GameLogic/IRandomizer.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// <copyright file="IRandomizer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Description of IRandomizer.
|
||||
/// </summary>
|
||||
public interface IRandomizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the next random boolean value.
|
||||
/// </summary>
|
||||
/// <returns>The next random boolean value.</returns>
|
||||
bool NextRandomBool();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next random boolean value, with a chance of <paramref name="percent"/> of being true.
|
||||
/// </summary>
|
||||
/// <param name="percent">The percent of the chance of being true.</param>
|
||||
/// <returns>The next random boolean value.</returns>
|
||||
bool NextRandomBool(int percent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next random boolean value, with a <paramref name="chance"/> of <paramref name="basis"/> of being true.
|
||||
/// </summary>
|
||||
/// <param name="chance">The chance of <paramref name="basis"/> of being true.</param>
|
||||
/// <param name="basis">The basis.</param>
|
||||
/// <returns>The next random boolean value.</returns>
|
||||
bool NextRandomBool(int chance, int basis);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next random boolean value, with the <paramref name="chance"/> of being true.
|
||||
/// </summary>
|
||||
/// <param name="chance">The chance of being true.</param>
|
||||
/// <returns>The next random boolean value.</returns>
|
||||
bool NextRandomBool(double chance);
|
||||
|
||||
/// <summary>
|
||||
/// The next random integer.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The next random integer between <paramref name="min"/> and <paramref name="max"/>.</returns>
|
||||
int NextInt(int min, int max);
|
||||
|
||||
/// <summary>
|
||||
/// The next random integer.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The next random integer between <paramref name="min"/> and <paramref name="max"/>.</returns>
|
||||
int NextInt(uint min, uint max);
|
||||
|
||||
/// <summary>
|
||||
/// The next random unsigned integer.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The next random integer between <paramref name="min"/> and <paramref name="max"/>.</returns>
|
||||
uint NextUInt(uint min, uint max);
|
||||
|
||||
/// <summary>
|
||||
/// The next random double between 0 and 1.
|
||||
/// </summary>
|
||||
/// <returns>The next random integer between 0 and 1.</returns>
|
||||
double NextDouble();
|
||||
}
|
||||
16
src/GameLogic/IRotatable.cs
Normal file
16
src/GameLogic/IRotatable.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="IRotatable.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a rotatable class. An instance implementing this interface can be rotated on its game map.
|
||||
/// </summary>
|
||||
public interface IRotatable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the rotation.
|
||||
/// </summary>
|
||||
Direction Rotation { get; set; }
|
||||
}
|
||||
48
src/GameLogic/ISkillList.cs
Normal file
48
src/GameLogic/ISkillList.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// <copyright file="ISkillList.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a skill list of a character.
|
||||
/// </summary>
|
||||
public interface ISkillList
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the skills.
|
||||
/// </summary>
|
||||
IEnumerable<SkillEntry> Skills { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of skills in the skill list.
|
||||
/// </summary>
|
||||
byte SkillCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the skill with the specified id.
|
||||
/// </summary>
|
||||
/// <param name="skillId">The skill identifier.</param>
|
||||
/// <returns>The skill with the specified id.</returns>
|
||||
SkillEntry? GetSkill(ushort skillId);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the learned skill.
|
||||
/// </summary>
|
||||
/// <param name="skill">The skill.</param>
|
||||
ValueTask AddLearnedSkillAsync(Skill skill);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the item skill.
|
||||
/// </summary>
|
||||
/// <param name="skillId">The skill identifier.</param>
|
||||
/// <returns>The success of removing the skill.</returns>
|
||||
ValueTask<bool> RemoveItemSkillAsync(ushort skillId);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the list contains the specified skill of the specified id.
|
||||
/// </summary>
|
||||
/// <param name="skillId">The skill identifier.</param>
|
||||
/// <returns><c>True</c>, if the skill with the specified id is contained in this list; Otherwise, <c>false</c>.</returns>
|
||||
bool ContainsSkill(ushort skillId);
|
||||
}
|
||||
237
src/GameLogic/IStorage.cs
Normal file
237
src/GameLogic/IStorage.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
// <copyright file="IStorage.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The types of item storages.
|
||||
/// </summary>
|
||||
public enum Storages
|
||||
{
|
||||
/// <summary>
|
||||
/// The inventory storage.
|
||||
/// </summary>
|
||||
Inventory,
|
||||
|
||||
/// <summary>
|
||||
/// The trade storage.
|
||||
/// </summary>
|
||||
Trade,
|
||||
|
||||
/// <summary>
|
||||
/// The vault storage.
|
||||
/// </summary>
|
||||
Vault,
|
||||
|
||||
/// <summary>
|
||||
/// The chaos machine storage.
|
||||
/// </summary>
|
||||
ChaosMachine,
|
||||
|
||||
/// <summary>
|
||||
/// The personal store storage.
|
||||
/// </summary>
|
||||
PersonalStore,
|
||||
|
||||
/// <summary>
|
||||
/// The pet trainer storage.
|
||||
/// </summary>
|
||||
PetTrainer,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the refinery of the elphis npc.
|
||||
/// </summary>
|
||||
Refinery,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the smelting dialog of the osbourne npc.
|
||||
/// </summary>
|
||||
Smelting,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the item restore dialog of the jerridon npc.
|
||||
/// </summary>
|
||||
ItemRestore,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the chaos card master dialog.
|
||||
/// </summary>
|
||||
ChaosCardMaster,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the cherry blossom spirit dialog.
|
||||
/// </summary>
|
||||
CherryBlossomSpirit,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the seed crafting dialog.
|
||||
/// </summary>
|
||||
SeedCrafting,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the seed sphere crafting dialog.
|
||||
/// </summary>
|
||||
SeedSphereCrafting,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the seed mount crafting dialog.
|
||||
/// </summary>
|
||||
SeedMountCrafting,
|
||||
|
||||
/// <summary>
|
||||
/// The storage of the seed unmount crafting dialog.
|
||||
/// </summary>
|
||||
SeedUnmountCrafting,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object which handles the storage with its logic.
|
||||
/// </summary>
|
||||
public interface IStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the underlying item storage.
|
||||
/// </summary>
|
||||
ItemStorage ItemStorage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumeration of all items.
|
||||
/// </summary>
|
||||
IEnumerable<Item> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumeration of all free item slot indexes.
|
||||
/// </summary>
|
||||
IEnumerable<byte> FreeSlots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the extensions of an inventory.
|
||||
/// </summary>
|
||||
IEnumerable<IStorage> Extensions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the item to the storage or its <see cref="Extensions"/>.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot where the items should be put in.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>True, if successful.</returns>
|
||||
ValueTask<bool> AddItemAsync(byte slot, Item item);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the item to the next free slot of the storage or its <see cref="Extensions"/>.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>True, if successful.</returns>
|
||||
ValueTask<bool> AddItemAsync(Item item);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add money to itemStorage.
|
||||
/// </summary>
|
||||
/// <param name="value">The value which should be added.</param>
|
||||
/// <returns><c>True</c>, if the money can be add to itemStorage; Otherwise, <c>false</c>.</returns>
|
||||
bool TryAddMoney(int value);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove money from itemStorage.
|
||||
/// </summary>
|
||||
/// <param name="value">The value which should be added.</param>
|
||||
/// <returns><c>True</c>, if had enought money to be remove; Otherwise, <c>false</c>.</returns>
|
||||
bool TryRemoveMoney(int value);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of a slot in which the item would fit.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The index of a slot in which the item would fit.</returns>
|
||||
byte? CheckInvSpace(Item item);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the items of another storage will fit into this storage (including extensions), and adds them if possible.
|
||||
/// </summary>
|
||||
/// <param name="anotherStorage">The other storage.</param>
|
||||
/// <returns>If it was successful.</returns>
|
||||
/// <remarks>Helpful for the trade function where all items of the trade partner has to be added to the own inventory.</remarks>
|
||||
ValueTask<bool> TryTakeAllAsync(IStorage anotherStorage);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item from the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="inventorySlot">The inventory slot.</param>
|
||||
/// <returns>The item from the specified slot.</returns>
|
||||
Item? GetItem(byte inventorySlot);
|
||||
|
||||
/// <summary>
|
||||
/// Finds items that matches the given definition.
|
||||
/// </summary>
|
||||
/// <param name="definition">The item definition to be searched.</param>
|
||||
/// <returns>The items with the same definition.</returns>
|
||||
IEnumerable<Item> FindItemsByDefinition(ItemDefinition definition);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the item from this storage.
|
||||
/// </summary>
|
||||
/// <param name="item">The item which should be removed.</param>
|
||||
ValueTask RemoveItemAsync(Item item);
|
||||
|
||||
/// <summary>
|
||||
/// Clears this storage from all of its items.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the slot belongs to this storage, or not.
|
||||
/// Extensions are not considered here, so that this function can be used to determine to which extension
|
||||
/// a slot belongs to.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the slot belongs to this storage; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool ContainsSlot(byte slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for the inventory storage, which may have equipped items.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.GameLogic.IStorage" />
|
||||
public interface IInventoryStorage : IStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the equipped items changed.
|
||||
/// </summary>
|
||||
event AsyncEventHandler<ItemEventArgs> EquippedItemsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items which are in the wearable slots.
|
||||
/// </summary>
|
||||
IEnumerable<Item> EquippedItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets equipped ammunition item.
|
||||
/// </summary>
|
||||
Item? EquippedAmmunitionItem { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The interface for a player shop storage. A shop can be opened by a player, and other players can buy the items of this shop.
|
||||
/// </summary>
|
||||
public interface IShopStorage : IStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the store is opened for other players.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if the store is opened for other players; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool StoreOpen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the store lock.
|
||||
/// </summary>
|
||||
AsyncLock StoreLock { get; }
|
||||
}
|
||||
16
src/GameLogic/ISupportIdUpdate.cs
Normal file
16
src/GameLogic/ISupportIdUpdate.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="ISupportIdUpdate.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an object which supports to set the <see cref="IIdentifiable.Id"/>.
|
||||
/// </summary>
|
||||
public interface ISupportIdUpdate : IIdentifiable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
new ushort Id { get; set; }
|
||||
}
|
||||
55
src/GameLogic/ISupportWalk.cs
Normal file
55
src/GameLogic/ISupportWalk.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
// <copyright file="ISupportWalk.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for objects which support walking.
|
||||
/// </summary>
|
||||
public interface ISupportWalk : ILocateable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance can walk on safezone.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this instance can walk on safezone; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool CanWalkOnSafezone { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is walking.
|
||||
/// </summary>
|
||||
bool IsWalking { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delay between each step. Lower delay means faster walking.
|
||||
/// </summary>
|
||||
TimeSpan StepDelay { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the walk target coordinate.
|
||||
/// </summary>
|
||||
Point WalkTarget { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the steps which are about to happen next by writing them into the given span.
|
||||
/// </summary>
|
||||
/// <param name="steps">The steps.</param>
|
||||
/// <returns>The number of written steps.</returns>
|
||||
ValueTask<int> GetStepsAsync(Memory<WalkingStep> steps);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directions of the steps which are about to happen next by writing them into the given span.
|
||||
/// </summary>
|
||||
/// <param name="directions">The directions.</param>
|
||||
/// <returns>The number of written directions.</returns>
|
||||
ValueTask<int> GetDirectionsAsync(Memory<Direction> directions);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the walking.
|
||||
/// </summary>
|
||||
ValueTask StopWalkingAsync();
|
||||
}
|
||||
88
src/GameLogic/ITrader.cs
Normal file
88
src/GameLogic/ITrader.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
// <copyright file="ITrader.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of a trader.
|
||||
/// </summary>
|
||||
public interface ITrader : IWorldObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the character name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the traders level.
|
||||
/// </summary>
|
||||
int Level { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current trading partner.
|
||||
/// </summary>
|
||||
ITrader? TradingPartner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the money which is currently in the trade.
|
||||
/// </summary>
|
||||
int TradingMoney { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the short guild identifier.
|
||||
/// </summary>
|
||||
GuildMemberStatus? GuildStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory.
|
||||
/// </summary>
|
||||
IInventoryStorage? Inventory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temporary storage, which holds the items of the trade.
|
||||
/// </summary>
|
||||
IStorage? TemporaryStorage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backup inventory.
|
||||
/// </summary>
|
||||
BackupItemStorage? BackupInventory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the available money.
|
||||
/// </summary>
|
||||
int Money { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of the player.
|
||||
/// </summary>
|
||||
StateMachine PlayerState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persistence context of the trader. It needs to be updated when a trade finishes.
|
||||
/// </summary>
|
||||
IPlayerContext PersistenceContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the game context of the trader.
|
||||
/// </summary>
|
||||
IGameContext GameContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is template player.
|
||||
/// In this case, trading is not allowed.
|
||||
/// </summary>
|
||||
bool IsTemplatePlayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves the progress of the trader.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Success of the save operation.</returns>
|
||||
ValueTask<bool> SaveProgressAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
22
src/GameLogic/IWorldObserver.cs
Normal file
22
src/GameLogic/IWorldObserver.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// <copyright file="IWorldObserver.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of an world observer.
|
||||
/// </summary>
|
||||
public interface IWorldObserver : ILoggerOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view plug ins.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The view plug ins.
|
||||
/// </value>
|
||||
ICustomPlugInContainer<IViewPlugIn> ViewPlugIns { get; }
|
||||
}
|
||||
220
src/GameLogic/InventoryStorage.cs
Normal file
220
src/GameLogic/InventoryStorage.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// <copyright file="InventoryStorage.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.DataModel;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using static MUnique.OpenMU.DataModel.InventoryConstants;
|
||||
|
||||
/// <summary>
|
||||
/// The storage of an inventory of a player, which also contains equippable slots. This class also manages the powerups which get created by equipped items.
|
||||
/// </summary>
|
||||
public class InventoryStorage : Storage, IInventoryStorage
|
||||
{
|
||||
private readonly IGameContext _gameContext;
|
||||
|
||||
private readonly Player _player;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InventoryStorage" /> class.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="context">The game context.</param>
|
||||
public InventoryStorage(Player player, IGameContext context)
|
||||
: base(
|
||||
GetInventorySize(0),
|
||||
EquippableSlotsCount,
|
||||
0,
|
||||
new ItemStorageAdapter(player.SelectedCharacter?.Inventory ?? throw Error.NotInitializedProperty(player, "SelectedCharacter.Inventory"), FirstEquippableItemSlotIndex, player.InventorySize))
|
||||
{
|
||||
this._player = player;
|
||||
this.EquippedItemsChanged += async eventArgs => await this.UpdateItemsOnChangeAsync(eventArgs.Item, eventArgs.IsEquipped).ConfigureAwait(false);
|
||||
this._gameContext = context;
|
||||
|
||||
if (player.SelectedCharacter.InventoryExtensions > 0)
|
||||
{
|
||||
var extensions = new List<Storage>(player.SelectedCharacter.InventoryExtensions);
|
||||
|
||||
var sizePerExtension = RowsOfOneExtension * RowSize;
|
||||
for (int i = 0; i < player.SelectedCharacter.InventoryExtensions; i++)
|
||||
{
|
||||
var offset = FirstExtensionItemSlotIndex + (i * sizePerExtension);
|
||||
var extension = new Storage(sizePerExtension, 0, offset, this.ItemStorage);
|
||||
extensions.Add(extension);
|
||||
}
|
||||
|
||||
this.Extensions = extensions;
|
||||
}
|
||||
|
||||
this.InitializePowerUps();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event AsyncEventHandler<ItemEventArgs>? EquippedItemsChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Item> EquippedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = FirstEquippableItemSlotIndex; i <= LastEquippableItemSlotIndex; i++)
|
||||
{
|
||||
if (this.ItemArray[i] is not null)
|
||||
{
|
||||
yield return this.ItemArray[i]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Item? EquippedAmmunitionItem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ItemArray[LeftHandSlot] is { } leftItem && (leftItem.Definition?.IsAmmunition ?? false))
|
||||
{
|
||||
return leftItem;
|
||||
}
|
||||
|
||||
if (this.ItemArray[RightHandSlot] is { } rightItem && (rightItem.Definition?.IsAmmunition ?? false))
|
||||
{
|
||||
return rightItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Additionally we need to make a temporary item persistent with the context of the player.</remarks>
|
||||
public override async ValueTask<bool> AddItemAsync(byte slot, Item item)
|
||||
{
|
||||
Item? convertedItem = null;
|
||||
if (item is TemporaryItem temporaryItem)
|
||||
{
|
||||
convertedItem = temporaryItem.MakePersistent(this._player.PersistenceContext);
|
||||
}
|
||||
|
||||
var success = await base.AddItemAsync(slot, convertedItem ?? item).ConfigureAwait(false);
|
||||
if (!success && convertedItem != null)
|
||||
{
|
||||
this._player.PersistenceContext.Detach(convertedItem);
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
var isEquippedItem = this.IsWearingSlot(slot);
|
||||
if (isEquippedItem && this.EquippedItemsChanged is { } eventHandler)
|
||||
{
|
||||
await eventHandler(new ItemEventArgs(convertedItem ?? item, true)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask RemoveItemAsync(Item item)
|
||||
{
|
||||
var isEquippedItem = this.IsWearingSlot(item.ItemSlot);
|
||||
await base.RemoveItemAsync(item).ConfigureAwait(false);
|
||||
if (isEquippedItem && this.EquippedItemsChanged is { } eventHandler)
|
||||
{
|
||||
await eventHandler(new ItemEventArgs(item, false)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsWearingSlot(int slot)
|
||||
{
|
||||
return slot >= FirstEquippableItemSlotIndex && slot <= LastEquippableItemSlotIndex;
|
||||
}
|
||||
|
||||
private async ValueTask UpdateItemsOnChangeAsync(Item item, bool isEquipped)
|
||||
{
|
||||
this._player.OnAppearanceChanged();
|
||||
|
||||
await this._player.ForEachWorldObserverAsync<IAppearanceChangedPlugIn>(
|
||||
p => p.AppearanceChangedAsync(this._player, item, isEquipped),
|
||||
false).ConfigureAwait(false); // in my tests it was not needed to send the appearance to the own players client.
|
||||
|
||||
if (this._player.Attributes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._player.Attributes.ItemPowerUps.Remove(item, out var itemPowerUps))
|
||||
{
|
||||
foreach (var powerUp in itemPowerUps)
|
||||
{
|
||||
powerUp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
this.UpdateSetPowerUps();
|
||||
|
||||
var itemAdded = this.EquippedItems.Contains(item);
|
||||
if (itemAdded)
|
||||
{
|
||||
var factory = this._gameContext.ItemPowerUpFactory;
|
||||
this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList());
|
||||
|
||||
// reset player equipped ammunition amount
|
||||
if (this.EquippedAmmunitionItem is { } ammoItem)
|
||||
{
|
||||
this._player.Attributes[Stats.AmmunitionAmount] = (float)ammoItem.Durability;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializePowerUps()
|
||||
{
|
||||
if (this._player.Attributes is null)
|
||||
{
|
||||
throw new InvalidOperationException("The player's AttributeSystem is not set yet.");
|
||||
}
|
||||
|
||||
foreach (var powerUp in this._player.Attributes.ItemPowerUps.Values.SelectMany(p => p).ToList())
|
||||
{
|
||||
powerUp.Dispose();
|
||||
}
|
||||
|
||||
var factory = this._gameContext?.ItemPowerUpFactory;
|
||||
if (factory != null)
|
||||
{
|
||||
foreach (var item in this.EquippedItems)
|
||||
{
|
||||
this._player.Attributes.ItemPowerUps.Add(item, factory.GetPowerUps(item, this._player.Attributes).ToList());
|
||||
}
|
||||
|
||||
this.UpdateSetPowerUps();
|
||||
}
|
||||
else
|
||||
{
|
||||
this._player.Logger.LogError("item power up factory not available during initialization of the inventory.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSetPowerUps()
|
||||
{
|
||||
if (this._player.Attributes is null)
|
||||
{
|
||||
throw new InvalidOperationException("The players AttributeSystem is not set yet.");
|
||||
}
|
||||
|
||||
if (this._player.Attributes.ItemSetPowerUps is not null)
|
||||
{
|
||||
foreach (var powerUp in this._player.Attributes.ItemSetPowerUps)
|
||||
{
|
||||
powerUp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var factory = this._gameContext.ItemPowerUpFactory;
|
||||
this._player.Attributes.ItemSetPowerUps = factory.GetSetPowerUps(this.EquippedItems, this._player.Attributes, this._player.GameContext.Configuration).ToList();
|
||||
}
|
||||
}
|
||||
166
src/GameLogic/ItemConstants.cs
Normal file
166
src/GameLogic/ItemConstants.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
// <copyright file="ItemConstants.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// A central place to keep item identifiers, so we can keep track of them.
|
||||
/// </summary>
|
||||
public class ItemConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier for the summon orb.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SummonOrb => new(11, 12);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the fruits.
|
||||
/// </summary>
|
||||
public static ItemIdentifier Fruits => new(15, 13);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the alcohol.
|
||||
/// </summary>
|
||||
public static ItemIdentifier Alcohol => new(9, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the apple.
|
||||
/// </summary>
|
||||
public static ItemIdentifier Apple => new(0, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the small healing potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SmallHealingPotion => new(1, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the medium healing potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier MediumHealingPotion => new(2, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the large healing potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier LargeHealingPotion => new(3, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the small mana potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SmallManaPotion => new(4, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the medium mana potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier MediumManaPotion => new(5, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the large mana potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier LargeManaPotion => new(6, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the siege potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SiegePotion => new(7, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the antidote.
|
||||
/// </summary>
|
||||
public static ItemIdentifier Antidote => new(8, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the town portal scroll.
|
||||
/// </summary>
|
||||
public static ItemIdentifier TownPortalScroll => new(10, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of chaos.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfChaos => new(15, 12);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of bless.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfBless => new(13, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of soul.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfSoul => new(14, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of life.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfLife => new(16, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of creation.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfCreation => new(22, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of guardian.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfGuardian => new(31, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the gemstone.
|
||||
/// </summary>
|
||||
public static ItemIdentifier Gemstone => new(41, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the small shield potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SmallShieldPotion => new(35, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the medium shield potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier MediumShieldPotion => new(36, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the large shield potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier LargeShieldPotion => new(37, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the small complex potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier SmallComplexPotion => new(38, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the medium complex potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier MediumComplexPotion => new(39, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the large complex potion.
|
||||
/// </summary>
|
||||
public static ItemIdentifier LargeComplexPotion => new(40, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the jewel of harmony.
|
||||
/// </summary>
|
||||
public static ItemIdentifier JewelOfHarmony => new(42, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the lower refine stone.
|
||||
/// </summary>
|
||||
public static ItemIdentifier LowerRefineStone => new(43, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the higher refine stone.
|
||||
/// </summary>
|
||||
public static ItemIdentifier HigherRefineStone => new(44, 14);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier for the wizard's ring (group 13, number 20).
|
||||
/// </summary>
|
||||
public static ItemIdentifier WizardsRing => new(20, 13);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all scrolls.
|
||||
/// </summary>
|
||||
public static ItemIdentifier AllScrolls => new(null, 15);
|
||||
}
|
||||
33
src/GameLogic/ItemEventArgs.cs
Normal file
33
src/GameLogic/ItemEventArgs.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// <copyright file="ItemEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Event args containing the involved instance of an <see cref="Item"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class ItemEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="isEquipped">Whether equipped or not.</param>
|
||||
public ItemEventArgs(Item item, bool isEquipped)
|
||||
{
|
||||
this.Item = item;
|
||||
this.IsEquipped = isEquipped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item which is involved at the event.
|
||||
/// </summary>
|
||||
public Item Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item is equipped at the event.
|
||||
/// </summary>
|
||||
public bool IsEquipped { get; }
|
||||
}
|
||||
439
src/GameLogic/ItemExtensions.cs
Normal file
439
src/GameLogic/ItemExtensions.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
// <copyright file="ItemExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Item"/>.
|
||||
/// </summary>
|
||||
public static class ItemExtensions
|
||||
{
|
||||
private const byte ShieldItemGroup = 6;
|
||||
|
||||
private static readonly byte[] AdditionalDurabilityPerLevel = { 0, 1, 2, 3, 4, 6, 8, 10, 12, 14, 17, 21, 26, 32, 39, 47 };
|
||||
|
||||
private static readonly IDictionary<AttributeDefinition, AttributeDefinition> RequirementAttributeMapping = new Dictionary<AttributeDefinition, AttributeDefinition>
|
||||
{
|
||||
{ Stats.TotalStrengthRequirementValue, Stats.TotalStrength },
|
||||
{ Stats.TotalAgilityRequirementValue, Stats.TotalAgility },
|
||||
{ Stats.TotalEnergyRequirementValue, Stats.TotalEnergy },
|
||||
{ Stats.TotalVitalityRequirementValue, Stats.TotalVitality },
|
||||
{ Stats.TotalLeadershipRequirementValue, Stats.TotalLeadership },
|
||||
};
|
||||
|
||||
private static readonly IDictionary<AttributeDefinition, AttributeDefinition> RequirementReductionAttributeMapping = new Dictionary<AttributeDefinition, AttributeDefinition>
|
||||
{
|
||||
{ Stats.TotalStrengthRequirementValue, Stats.RequiredStrengthReduction },
|
||||
{ Stats.TotalAgilityRequirementValue, Stats.RequiredAgilityReduction },
|
||||
{ Stats.TotalEnergyRequirementValue, Stats.RequiredEnergyReduction },
|
||||
{ Stats.TotalVitalityRequirementValue, Stats.RequiredVitalityReduction },
|
||||
{ Stats.TotalLeadershipRequirementValue, Stats.RequiredLeadershipReduction },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum durability of the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The maximum durability of the item.</returns>
|
||||
/// <remarks>
|
||||
/// I think this is more like the durability which can be dropped.
|
||||
/// Some items can be stacked up to 255 pieces, which increases the durability value.
|
||||
/// </remarks>
|
||||
public static byte GetMaximumDurabilityOfOnePiece(this Item item)
|
||||
{
|
||||
if (!item.IsWearable())
|
||||
{
|
||||
// Items which are not wearable don't have a "real" durability. If the item is stackable, durability means number of pieces in this case
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (item.IsTrainablePet())
|
||||
{
|
||||
return 255;
|
||||
}
|
||||
|
||||
var result = item.Definition!.Durability + AdditionalDurabilityPerLevel[item.Level];
|
||||
if (item.IsAncient())
|
||||
{
|
||||
result += 20;
|
||||
}
|
||||
else if (item.IsExcellent())
|
||||
{
|
||||
// TODO: archangel weapons, but I guess it's not a big issue if we don't, because of their already high durability
|
||||
result += 15;
|
||||
}
|
||||
else
|
||||
{
|
||||
// there are no other options which increase the durability.
|
||||
// It might be nice to add the magic values above to the ItemOptionType, as data.
|
||||
}
|
||||
|
||||
return (byte)Math.Min(byte.MaxValue, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is ancient.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is ancient; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsAncient(this Item item)
|
||||
{
|
||||
return item.ItemSetGroups.Any(itemSet => itemSet.AncientSetDiscriminator > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is excellent.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is excellent; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsExcellent(this Item item)
|
||||
{
|
||||
return item.ItemOptions.Any(link => link.ItemOption?.OptionType == ItemOptionTypes.Excellent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a "380 item", that is, if it can be upgraded with Jewel of Guardian.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is a "380 item"; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsGuardian(this Item item)
|
||||
{
|
||||
return item.Definition!.PossibleItemOptions.Any(pio => pio.PossibleOptions
|
||||
.Any(po => po.OptionType == ItemOptionTypes.GuardianOption));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this item is a defensive item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><see langword="true"/>, if the item is defensive.</returns>
|
||||
public static bool IsDefensiveItem(this Item item)
|
||||
{
|
||||
return InventoryConstants.IsDefenseItemSlot(item.ItemSlot) || item.IsShield();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a shield.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is a shield; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsShield(this Item item)
|
||||
{
|
||||
return item.Definition?.Group == ShieldItemGroup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this item is a jewelry (pendant or ring) item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is jewelry; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsJewelry(this Item item)
|
||||
{
|
||||
return item.ItemSlot >= InventoryConstants.PendantSlot && item.ItemSlot <= InventoryConstants.Ring2Slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this item is an armor item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is armor; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsArmorItem(this Item item)
|
||||
{
|
||||
return item.ItemSlot >= InventoryConstants.HelmSlot && item.ItemSlot <= InventoryConstants.BootsSlot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a is weapon which deals physical damage.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="minimumDmg">The minimum physical damage of the weapon.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance is a weapon which deals physical damage; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsPhysicalWeapon(this Item item, [NotNullWhen(true)] out float? minimumDmg)
|
||||
{
|
||||
minimumDmg = item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.MinimumPhysBaseDmgByWeapon)?.BaseValue;
|
||||
return minimumDmg is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a weapon which increases wizardry damage.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="staffRise">The staff/sword/stick's wizardry damage rise percentage.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance is a weapon which increases wizardry damage; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsWizardryWeapon(this Item item, [NotNullWhen(true)] out float? staffRise)
|
||||
{
|
||||
staffRise = item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.StaffRise)?.BaseValue;
|
||||
return staffRise is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a scepter which increases raven damage.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="scepterRise">The scepter's pet attack rise percentage.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance is a scepter which increases raven damage; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsScepter(this Item item, [NotNullWhen(true)] out float? scepterRise)
|
||||
{
|
||||
scepterRise = item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.ScepterRise)?.BaseValue;
|
||||
return scepterRise is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is a book which increases curse damage.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="bookRise">The book's curse damage rise percentage.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance is a book which increases curse damage; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsBook(this Item item, [NotNullWhen(true)] out float? bookRise)
|
||||
{
|
||||
bookRise = item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.BookRise)?.BaseValue;
|
||||
return bookRise is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random offensive item of the storage.
|
||||
/// </summary>
|
||||
/// <param name="storage">The storage.</param>
|
||||
/// <returns>A randomly selected offensive item.</returns>
|
||||
public static Item? GetRandomOffensiveItem(this IInventoryStorage storage)
|
||||
{
|
||||
var left = storage.GetItem(InventoryConstants.LeftHandSlot);
|
||||
var right = storage.GetItem(InventoryConstants.RightHandSlot);
|
||||
var pendant = storage.GetItem(InventoryConstants.PendantSlot);
|
||||
|
||||
if ((left?.Definition?.IsAmmunition ?? false)
|
||||
|| left?.Definition?.Group == ShieldItemGroup)
|
||||
{
|
||||
left = null;
|
||||
}
|
||||
|
||||
if ((right?.Definition?.IsAmmunition ?? false)
|
||||
|| right?.Definition?.Group == ShieldItemGroup)
|
||||
{
|
||||
right = null;
|
||||
}
|
||||
|
||||
var random = Rand.NextInt(3, 6);
|
||||
var result = left ?? right ?? pendant;
|
||||
if (result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (random % 3)
|
||||
{
|
||||
case 0 when left is { }:
|
||||
result = left;
|
||||
break;
|
||||
case 1 when right is { }:
|
||||
result = right;
|
||||
break;
|
||||
case 2 when pendant is { }:
|
||||
result = pendant;
|
||||
break;
|
||||
default:
|
||||
// keep first available result
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the requirement as a tuple of an attribute and the corresponding value.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="requirement">The requirement.</param>
|
||||
/// <returns>The requirement as a tuple of an attribute and the corresponding value.</returns>
|
||||
/// <remarks>
|
||||
/// Some requirements are depending on item level, drop level and item options.
|
||||
/// </remarks>
|
||||
public static (AttributeDefinition Attr, int Value) GetRequirement(this Item item, AttributeRequirement requirement)
|
||||
{
|
||||
requirement.ThrowNotInitializedProperty(requirement.Attribute is null, nameof(requirement.Attribute));
|
||||
|
||||
if (RequirementAttributeMapping.TryGetValue(requirement.Attribute, out var totalAttribute))
|
||||
{
|
||||
if (!item.IsWearable())
|
||||
{
|
||||
return (totalAttribute, requirement.MinimumValue);
|
||||
}
|
||||
|
||||
var multiplier = 3;
|
||||
if (totalAttribute == Stats.TotalEnergy)
|
||||
{
|
||||
multiplier = 4;
|
||||
|
||||
// Summoner Books are calculated differently. They are in group 5 (staffs) and are the only items in the group which can have skill.
|
||||
if (item.Definition?.Skill != null && item.Definition.Group == 5)
|
||||
{
|
||||
return (totalAttribute, item.CalculateBookEnergyRequirement(requirement.MinimumValue));
|
||||
}
|
||||
}
|
||||
|
||||
var value = item.CalculateRequirement(requirement.MinimumValue, multiplier);
|
||||
if (value > 0 && totalAttribute == Stats.TotalStrength)
|
||||
{
|
||||
var itemOption = item.ItemOptions.FirstOrDefault(o => o.ItemOption?.OptionType == ItemOptionTypes.Option);
|
||||
if (itemOption != null)
|
||||
{
|
||||
value += itemOption.Level * 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (RequirementReductionAttributeMapping.TryGetValue(requirement.Attribute, out var reductionAttribute)
|
||||
&& item.ItemOptions.FirstOrDefault(o =>
|
||||
o.ItemOption?.PowerUpDefinition?.TargetAttribute == reductionAttribute
|
||||
|| (o.ItemOption?.LevelDependentOptions.Any(l => l.PowerUpDefinition?.TargetAttribute == reductionAttribute) ?? false))
|
||||
is { } reductionOption)
|
||||
{
|
||||
var optionOfLevelPowerUp = reductionOption.ItemOption!.LevelDependentOptions
|
||||
.FirstOrDefault(o => o.Level == reductionOption.Level)?.PowerUpDefinition
|
||||
?? reductionOption.ItemOption.PowerUpDefinition;
|
||||
if (optionOfLevelPowerUp?.Boost?.ConstantValue is { } reduction)
|
||||
{
|
||||
value -= (int)reduction.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return (totalAttribute, value);
|
||||
}
|
||||
|
||||
return (requirement.Attribute, requirement.MinimumValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item data which is relevant for the visual appearance of an item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The item data which is relevant for the visual appearance of an item.</returns>
|
||||
public static ItemAppearance GetAppearance(this Item item)
|
||||
{
|
||||
var appearance = new TemporaryItemAppearance
|
||||
{
|
||||
Definition = item.Definition,
|
||||
ItemSlot = item.ItemSlot,
|
||||
Level = item.Level,
|
||||
};
|
||||
item.ItemOptions
|
||||
.Where(option => option.ItemOption?.OptionType is not null && option.ItemOption.OptionType.IsVisible)
|
||||
.Select(option => option.ItemOption!.OptionType!)
|
||||
.Distinct()
|
||||
.ForEach(appearance.VisibleOptions.Add);
|
||||
if (item.IsAncient())
|
||||
{
|
||||
// 1. The ancient option is not included in the item.ItemOptions.
|
||||
// 2. The bonus option is not always existing for ancient items. And it's not marked as visible.
|
||||
// -> we check it based on the item set group.
|
||||
appearance.VisibleOptions.Add(ItemOptionTypes.AncientOption);
|
||||
}
|
||||
|
||||
return appearance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a persistent instance of the given <see cref="ItemAppearance"/> and returns it.
|
||||
/// </summary>
|
||||
/// <param name="itemAppearance">The item appearance.</param>
|
||||
/// <param name="persistenceContext">The persistence context where the object should be added.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
/// <returns>A persistent instance of the given <see cref="ItemAppearance"/>.</returns>
|
||||
public static ItemAppearance MakePersistent(this ItemAppearance itemAppearance, IContext persistenceContext, GameConfiguration gameConfiguration)
|
||||
{
|
||||
var persistent = persistenceContext.CreateNew<ItemAppearance>();
|
||||
persistent.ItemSlot = itemAppearance.ItemSlot;
|
||||
persistent.Definition = itemAppearance.Definition;
|
||||
persistent.Level = itemAppearance.Level;
|
||||
itemAppearance.VisibleOptions.Distinct().ForEach(o => persistent.VisibleOptions.Add(gameConfiguration.ItemOptionTypes.First(iot => iot.Equals(o))));
|
||||
return persistent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the drop level.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="isAncient">A value indicating whether the item is ancient.</param>
|
||||
/// <param name="isExcellent">A value indicating whether the item is excellent.</param>
|
||||
/// <param name="itemLevel">The item level.</param>
|
||||
/// <returns>The calculated drop level.</returns>
|
||||
public static int CalculateDropLevel(this ItemDefinition item, bool isAncient, bool isExcellent, int itemLevel)
|
||||
{
|
||||
int dropLevel = item.DropLevel;
|
||||
if (isAncient)
|
||||
{
|
||||
dropLevel += 30;
|
||||
}
|
||||
else if (isExcellent)
|
||||
{
|
||||
dropLevel += 25;
|
||||
}
|
||||
else
|
||||
{
|
||||
// nothing to add
|
||||
}
|
||||
|
||||
dropLevel += 3 * itemLevel;
|
||||
return dropLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the drop level of the item in the current state.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The calculated drop level.</returns>
|
||||
public static int CalculateDropLevel(this Item item)
|
||||
{
|
||||
return item.Definition?.CalculateDropLevel(item.IsAncient(), item.IsExcellent(), item.Level) ?? 0;
|
||||
}
|
||||
|
||||
private static int CalculateRequirement(this Item item, int requirementValue, int multiplier)
|
||||
{
|
||||
if (requirementValue == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var dropLevel = item.Definition!.CalculateDropLevel(item.IsAncient(), item.IsExcellent(), item.Level);
|
||||
|
||||
return (multiplier * dropLevel * requirementValue / 100) + 20;
|
||||
}
|
||||
|
||||
private static int CalculateBookEnergyRequirement(this Item item, int energyRequirementValue)
|
||||
{
|
||||
var dropLevel = item.Definition!.CalculateDropLevel(item.IsAncient(), item.IsExcellent(), 0);
|
||||
|
||||
return (((energyRequirementValue * (dropLevel + item.Level)) * 3) / 100) + 20;
|
||||
}
|
||||
|
||||
private sealed class TemporaryItemAppearance : ItemAppearance
|
||||
{
|
||||
public override ICollection<ItemOptionType> VisibleOptions => base.VisibleOptions ??= new List<ItemOptionType>();
|
||||
}
|
||||
}
|
||||
10
src/GameLogic/ItemIdentifier.cs
Normal file
10
src/GameLogic/ItemIdentifier.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="ItemIdentifier.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// The identifier for an item, including number and group.
|
||||
/// </summary>
|
||||
public record struct ItemIdentifier(short? Number, byte Group);
|
||||
438
src/GameLogic/ItemPowerUpFactory.cs
Normal file
438
src/GameLogic/ItemPowerUpFactory.cs
Normal file
@@ -0,0 +1,438 @@
|
||||
// <copyright file="ItemPowerUpFactory.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Attributes;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of the item power up factory.
|
||||
/// </summary>
|
||||
public class ItemPowerUpFactory : IItemPowerUpFactory
|
||||
{
|
||||
private readonly ILogger<ItemPowerUpFactory> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemPowerUpFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ItemPowerUpFactory(ILogger<ItemPowerUpFactory> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<PowerUpWrapper> GetPowerUps(Item item, AttributeSystem attributeHolder)
|
||||
{
|
||||
if (item.Definition is null)
|
||||
{
|
||||
this._logger.LogWarning("Item of slot {itemSlot} ({itemId}) has no definition.", item.ItemSlot, item.GetId());
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (item.Durability <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (item.ItemSlot < InventoryConstants.FirstEquippableItemSlotIndex || item.ItemSlot > InventoryConstants.LastEquippableItemSlotIndex)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var isRightWieldWeapon = item.ItemSlot == InventoryConstants.RightHandSlot
|
||||
&& item.Definition!.BasePowerUpAttributes.Any(pu => pu.TargetAttribute == Stats.DoubleWieldWeaponCount || pu.TargetAttribute == Stats.IsOneHandedStaffEquipped);
|
||||
|
||||
AttributeDefinition? targetAttribute = null;
|
||||
foreach (var attribute in item.Definition.BasePowerUpAttributes)
|
||||
{
|
||||
if (isRightWieldWeapon)
|
||||
{
|
||||
if (attribute.TargetAttribute == Stats.StaffRise)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (attribute.TargetAttribute == Stats.MinimumPhysBaseDmgByWeapon)
|
||||
{
|
||||
targetAttribute = Stats.MinPhysBaseDmgByRightWeapon;
|
||||
}
|
||||
else if (attribute.TargetAttribute == Stats.MaximumPhysBaseDmgByWeapon)
|
||||
{
|
||||
targetAttribute = Stats.MaxPhysBaseDmgByRightWeapon;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAttribute = null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var powerUp in this.GetBasePowerUpWrappers(item, attributeHolder, attribute, targetAttribute))
|
||||
{
|
||||
yield return powerUp;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var powerUp in this.GetPowerUpsOfItemOptions(item, attributeHolder))
|
||||
{
|
||||
yield return powerUp;
|
||||
}
|
||||
|
||||
if (this.GetPetLevel(item, attributeHolder) is { } petLevel)
|
||||
{
|
||||
yield return petLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<PowerUpWrapper> GetSetPowerUps(
|
||||
IEnumerable<Item> equippedItems,
|
||||
AttributeSystem attributeHolder,
|
||||
GameConfiguration gameConfiguration)
|
||||
{
|
||||
var activeItems = equippedItems
|
||||
.Where(i => i.Durability > 0)
|
||||
.ToList();
|
||||
var itemGroups = activeItems
|
||||
.SelectMany(i => i.ItemSetGroups)
|
||||
.Select(i => i.ItemSetGroup!)
|
||||
.Distinct();
|
||||
|
||||
var result = Enumerable.Empty<PowerUpDefinition>();
|
||||
var alwaysGroups = activeItems.SelectMany(i => i.Definition!.PossibleItemSetGroups).Where(i => i.AlwaysApplies).Distinct();
|
||||
|
||||
foreach (var group in alwaysGroups.Concat(itemGroups).Distinct())
|
||||
{
|
||||
var itemsOfGroup = activeItems.Where(i =>
|
||||
((group.AlwaysApplies && i.Definition!.PossibleItemSetGroups.Contains(group))
|
||||
|| i.ItemSetGroups.Any(ios => ios.ItemSetGroup == group))
|
||||
&& (group.SetLevel == 0 || i.Level >= group.SetLevel));
|
||||
var setMustBeComplete = group.MinimumItemCount == group.Items.Count;
|
||||
if (group.SetLevel > 0 && setMustBeComplete && itemsOfGroup.All(i => i.Level > group.SetLevel))
|
||||
{
|
||||
// When all items are of higher level and the set bonus is applied when all items are there, another item set group will take care.
|
||||
// This should prevent that for example set bonus defense is applied multiple times.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group.Options is not { } options)
|
||||
{
|
||||
this._logger.LogWarning("Options of set {group} is not initialized", group);
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemCount = group.CountDistinct ? itemsOfGroup.Select(item => item.Definition).Distinct().Count() : itemsOfGroup.Count();
|
||||
var setIsComplete = itemCount == group.Items.Count;
|
||||
if (setIsComplete)
|
||||
{
|
||||
// Take all options when the set is complete
|
||||
result = result.Concat(
|
||||
options.PossibleOptions
|
||||
.Select(o => o.PowerUpDefinition ?? throw Error.NotInitializedProperty(o, nameof(o.PowerUpDefinition))));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemCount >= group.MinimumItemCount)
|
||||
{
|
||||
// Take the first n-1 options
|
||||
result = result.Concat(options.PossibleOptions.OrderBy(o => o.Number)
|
||||
.Take(itemCount - 1)
|
||||
.Select(o => o.PowerUpDefinition ?? throw Error.NotInitializedProperty(o, nameof(o.PowerUpDefinition))));
|
||||
}
|
||||
}
|
||||
|
||||
result = result.Concat(this.GetOptionCombinationBonus(activeItems, gameConfiguration));
|
||||
|
||||
return result.SelectMany(p => PowerUpWrapper.CreateByPowerUpDefinition(p, attributeHolder));
|
||||
}
|
||||
|
||||
private IEnumerable<PowerUpDefinition> GetOptionCombinationBonus(IEnumerable<Item> activeItems, GameConfiguration gameConfiguration)
|
||||
{
|
||||
if (gameConfiguration?.ItemOptionCombinationBonuses is null
|
||||
|| gameConfiguration.ItemOptionCombinationBonuses.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var activeItemOptions = activeItems.SelectMany(i => i.ItemOptions.Select(o => o.ItemOption ?? throw Error.NotInitializedProperty(o, nameof(o.ItemOption)))).ToList();
|
||||
foreach (var combinationBonus in gameConfiguration.ItemOptionCombinationBonuses.Where(c => c.Bonus is { }))
|
||||
{
|
||||
var remainingOptions = activeItemOptions.ToList<ItemOption>();
|
||||
while (this.AreRequiredOptionsFound(combinationBonus, remainingOptions))
|
||||
{
|
||||
if (combinationBonus.Bonus is not null)
|
||||
{
|
||||
yield return combinationBonus.Bonus;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("Bonus of ItemOptionCombinationBonus '{combinationBonusName}' is not initialized, id: {id}", combinationBonus.Description, combinationBonus.GetId());
|
||||
}
|
||||
|
||||
if (!combinationBonus.AppliesMultipleTimes)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool AreRequiredOptionsFound(ItemOptionCombinationBonus bonus, IList<ItemOption> itemOptions)
|
||||
{
|
||||
var allMatches = new List<ItemOption>();
|
||||
foreach (var requirement in bonus.Requirements)
|
||||
{
|
||||
var matches = itemOptions
|
||||
.Where(o => o.OptionType is not null)
|
||||
.Where(o => o.OptionType == requirement.OptionType && o.SubOptionType == requirement.SubOptionType)
|
||||
.Take(requirement.MinimumCount)
|
||||
.ToList();
|
||||
if (matches.Count < requirement.MinimumCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
allMatches.AddRange(matches);
|
||||
}
|
||||
|
||||
allMatches.ForEach(o => itemOptions.Remove(o));
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<PowerUpWrapper> GetBasePowerUpWrappers(Item item, AttributeSystem attributeHolder, ItemBasePowerUpDefinition attribute, AttributeDefinition? targetAttribute = null)
|
||||
{
|
||||
attribute.ThrowNotInitializedProperty(attribute.BaseValueElement is null, nameof(attribute.BaseValueElement));
|
||||
attribute.ThrowNotInitializedProperty(attribute.TargetAttribute is null, nameof(attribute.TargetAttribute));
|
||||
|
||||
var levelBonusElmt = (attribute.BonusPerLevelTable?.BonusPerLevel ?? Enumerable.Empty<LevelBonus>())
|
||||
.FirstOrDefault(bonus => bonus.Level == item.Level)?
|
||||
.GetAdditionalValueElement(attribute.AggregateType);
|
||||
|
||||
if (levelBonusElmt is null)
|
||||
{
|
||||
yield return new PowerUpWrapper(attribute.BaseValueElement, targetAttribute ?? attribute.TargetAttribute, attributeHolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new PowerUpWrapper(
|
||||
new CombinedElement(attribute.BaseValueElement, levelBonusElmt),
|
||||
targetAttribute ?? attribute.TargetAttribute,
|
||||
attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<PowerUpWrapper> GetPowerUpsOfItemOptions(Item item, AttributeSystem attributeHolder)
|
||||
{
|
||||
var options = item.ItemOptions;
|
||||
if (options is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var optionLink in options)
|
||||
{
|
||||
var option = optionLink.ItemOption;
|
||||
if (option is null)
|
||||
{
|
||||
this._logger.LogWarning("Item {item} (id {itemId}) has ItemOptionLink ({optionLinkId}) without option.", item, item.GetId(), optionLink.GetId());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.ItemSlot == InventoryConstants.RightHandSlot
|
||||
&& option.OptionType == ItemOptionTypes.Option
|
||||
&& option.PowerUpDefinition?.TargetAttribute == Stats.WizardryBaseDmg)
|
||||
{
|
||||
// For a RH-wielded staff (MG), its wizardry item option doesn't count (but the others do!)
|
||||
continue;
|
||||
}
|
||||
|
||||
var level = option.LevelType == LevelType.ItemLevel ? item.Level : optionLink.Level;
|
||||
|
||||
var optionOfLevel = option.LevelDependentOptions?.FirstOrDefault(l => l.Level == level);
|
||||
|
||||
// Dinorant options are an exception.
|
||||
if (optionOfLevel is null && level > 1 && item.Definition!.Skill?.Number != 49)
|
||||
{
|
||||
this._logger.LogWarning("Item {item} (id {itemId}) has IncreasableItemOption ({option}, id {optionId}) with level {level}, but no definition in LevelDependentOptions.", item, item.GetId(), option, option.GetId(), level);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (optionOfLevel?.RequiredItemLevel > item.Level)
|
||||
{
|
||||
// Some options (like harmony) although on the item can be inactive.
|
||||
continue;
|
||||
}
|
||||
|
||||
var powerUp = optionOfLevel?.PowerUpDefinition ?? option.PowerUpDefinition;
|
||||
|
||||
if (powerUp?.Boost is null)
|
||||
{
|
||||
// Some options are level dependent. If they are at level 0, they might not have any boost yet.
|
||||
continue;
|
||||
}
|
||||
|
||||
AggregateType? aggregateType = null;
|
||||
if (option.OptionType == ItemOptionTypes.Excellent)
|
||||
{
|
||||
if (item.ItemSlot == InventoryConstants.PendantSlot
|
||||
&& option.PowerUpDefinition?.TargetAttribute == Stats.PhysicalBaseDmg)
|
||||
{
|
||||
// Pendant options are not subject to double wield averaging
|
||||
aggregateType = AggregateType.AddFinal;
|
||||
}
|
||||
else if (option.PowerUpDefinition?.TargetAttribute == Stats.PhysicalBaseDmgIncrease
|
||||
&& item.Definition!.BasePowerUpAttributes.Any(pu => pu.TargetAttribute == Stats.DoubleWieldWeaponCount))
|
||||
{
|
||||
// This needs special treatment, since this option is averaged when double wielding
|
||||
aggregateType = AggregateType.AddRaw;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the normal aggregate type should be used
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var wrapper in PowerUpWrapper.CreateByPowerUpDefinition(powerUp, attributeHolder, aggregateType))
|
||||
{
|
||||
yield return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var powerUpWrapper in this.CreateExcellentAndAncientBasePowerUpWrappers(item, attributeHolder))
|
||||
{
|
||||
yield return powerUpWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this more generic and configurable?
|
||||
private IEnumerable<PowerUpWrapper> CreateExcellentAndAncientBasePowerUpWrappers(Item item, AttributeSystem attributeHolder)
|
||||
{
|
||||
var itemIsExcellent = item.IsExcellent();
|
||||
var itemIsAncient = item.IsAncient();
|
||||
|
||||
if (!itemIsAncient && !itemIsExcellent)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var baseDropLevel = item.Definition!.DropLevel;
|
||||
var ancientDropLevel = item.Definition!.CalculateDropLevel(true, false, 0);
|
||||
|
||||
if (InventoryConstants.IsDefenseItemSlot(item.ItemSlot) && !item.IsJewelry())
|
||||
{
|
||||
var baseDefense = (int)(item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.DefenseBase)?.BaseValue ?? 0);
|
||||
var additionalDefense = (baseDefense * 12 / baseDropLevel) + (baseDropLevel / 5) + 4;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalDefense, AggregateType.AddRaw), Stats.DefenseBase, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var ancientDefenseBonus = 2 + ((baseDefense + additionalDefense) * 3 / ancientDropLevel) + (ancientDropLevel / 30);
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientDefenseBonus, AggregateType.AddRaw), Stats.DefenseBase, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsShield())
|
||||
{
|
||||
var baseDefenseRate = (int)(item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.DefenseRatePvm)?.BaseValue ?? 0);
|
||||
var additionalRate = (baseDefenseRate * 25 / baseDropLevel) + 5;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalRate, AggregateType.AddRaw), Stats.DefenseRatePvm, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var baseDefense = (int)(item.Definition?.BasePowerUpAttributes.FirstOrDefault(a => a.TargetAttribute == Stats.DefenseShield)?.BaseValue ?? 0);
|
||||
var ancientDefenseBonus = 2 + ((baseDefense + item.Level) * 20 / ancientDropLevel);
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientDefenseBonus, AggregateType.AddRaw), Stats.DefenseShield, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsPhysicalWeapon(out var minPhysDmg))
|
||||
{
|
||||
var minDmgAttribute = Stats.MinimumPhysBaseDmgByWeapon;
|
||||
var maxDmgAttribute = Stats.MaximumPhysBaseDmgByWeapon;
|
||||
if (item.ItemSlot == InventoryConstants.RightHandSlot
|
||||
&& item.Definition!.BasePowerUpAttributes.Any(pu => pu.TargetAttribute == Stats.DoubleWieldWeaponCount || pu.TargetAttribute == Stats.IsOneHandedStaffEquipped))
|
||||
{
|
||||
minDmgAttribute = Stats.MinPhysBaseDmgByRightWeapon;
|
||||
maxDmgAttribute = Stats.MaxPhysBaseDmgByRightWeapon;
|
||||
}
|
||||
|
||||
var additionalDmg = ((int)minPhysDmg * 25 / baseDropLevel) + 5;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalDmg, AggregateType.AddRaw), minDmgAttribute, attributeHolder);
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalDmg, AggregateType.AddRaw), maxDmgAttribute, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var ancientBonus = 5 + (ancientDropLevel / 40);
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientBonus, AggregateType.AddRaw), minDmgAttribute, attributeHolder);
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientBonus, AggregateType.AddRaw), maxDmgAttribute, attributeHolder);
|
||||
}
|
||||
|
||||
if (itemIsExcellent
|
||||
&& item.ItemOptions.Any(io => io.ItemOption?.PowerUpDefinition?.TargetAttribute == Stats.PhysicalBaseDmgIncrease)
|
||||
&& item.Definition!.BasePowerUpAttributes.Any(pu => pu.TargetAttribute == Stats.DoubleWieldWeaponCount))
|
||||
{
|
||||
yield return new PowerUpWrapper(new SimpleElement(-1, AggregateType.AddRaw), Stats.PhysicalBaseDmgIncrease, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsWizardryWeapon(out var staffRise) && item.ItemSlot == InventoryConstants.LeftHandSlot)
|
||||
{
|
||||
var additionalRise = (((int)staffRise * 2 * 25 / baseDropLevel) + 5) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalRise, AggregateType.AddRaw), Stats.StaffRise, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var ancientRiseBonus = (2 + (ancientDropLevel / 60)) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientRiseBonus, AggregateType.AddRaw), Stats.StaffRise, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsScepter(out var scepterRise))
|
||||
{
|
||||
var additionalRise = (((int)scepterRise * 2 * 25 / baseDropLevel) + 5) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalRise, AggregateType.AddRaw), Stats.ScepterRise, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var ancientRiseBonus = (2 + (ancientDropLevel / 60)) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientRiseBonus, AggregateType.AddRaw), Stats.ScepterRise, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsBook(out var curseRise))
|
||||
{
|
||||
var additionalRise = (((int)curseRise * 2 * 25 / baseDropLevel) + 5) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(additionalRise, AggregateType.AddRaw), Stats.BookRise, attributeHolder);
|
||||
if (itemIsAncient)
|
||||
{
|
||||
var ancientRiseBonus = (2 + (ancientDropLevel / 60)) / 2;
|
||||
yield return new PowerUpWrapper(new SimpleElement(ancientRiseBonus, AggregateType.AddRaw), Stats.BookRise, attributeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemIsAncient && item.IsJewelry())
|
||||
{
|
||||
foreach (var baseAttribute in item.Definition!.BasePowerUpAttributes)
|
||||
{
|
||||
if (Stats.ElementResistanceToDamageBonus.Keys.FirstOrDefault(resistance => resistance == baseAttribute.TargetAttribute) is { } key)
|
||||
{
|
||||
yield return new PowerUpWrapper(new SimpleElement(5, AggregateType.AddRaw), Stats.ElementResistanceToDamageBonus[key], attributeHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PowerUpWrapper? GetPetLevel(Item item, AttributeSystem attributeHolder)
|
||||
{
|
||||
const byte darkHorseNumber = 4;
|
||||
|
||||
if (!item.IsTrainablePet())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PowerUpWrapper(
|
||||
new SimpleElement(item.Level, AggregateType.AddRaw),
|
||||
item.Definition?.Number == darkHorseNumber ? Stats.HorseLevel : Stats.RavenLevel,
|
||||
attributeHolder);
|
||||
}
|
||||
}
|
||||
581
src/GameLogic/ItemPriceCalculator.cs
Normal file
581
src/GameLogic/ItemPriceCalculator.cs
Normal file
@@ -0,0 +1,581 @@
|
||||
// <copyright file="ItemPriceCalculator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// This calculator calculates the item prices.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// At the moment it looks all pretty hard coded (like at the original server), so maybe in a future version we could
|
||||
/// write a more generic calculator, which can be influenced by configuration instead of hard-coded item ids.
|
||||
/// A nice approach would be to have a "rule" for each item definition, about how to calculate the price.
|
||||
/// </remarks>
|
||||
public class ItemPriceCalculator
|
||||
{
|
||||
private const short ForceWaveSkillId = 66;
|
||||
private const short ExplosionSkillId = 223;
|
||||
private const short RequiemSkillId = 224;
|
||||
private const short PollutionSkillId = 225;
|
||||
private const long MaximumPrice = 3_000_000_000;
|
||||
private const float DestroyedPetPenalty = 2.0f;
|
||||
private const float DestroyedItemPenalty = 1.4f;
|
||||
|
||||
private static readonly List<short> WorthlessSkills = [ForceWaveSkillId, ExplosionSkillId, RequiemSkillId, PollutionSkillId];
|
||||
|
||||
private static readonly Dictionary<byte, int> DropLevelIncreaseByLevel = new()
|
||||
{
|
||||
{ 5, 4 },
|
||||
{ 6, 10 },
|
||||
{ 7, 25 },
|
||||
{ 8, 45 },
|
||||
{ 9, 65 },
|
||||
{ 10, 95 },
|
||||
{ 11, 135 },
|
||||
{ 12, 185 },
|
||||
{ 13, 245 },
|
||||
{ 14, 305 },
|
||||
{ 15, 365 },
|
||||
};
|
||||
|
||||
private static readonly IDictionary<int, long> SpecialItemOldValueDictionary = new Dictionary<int, long>
|
||||
{
|
||||
{ (int)SpecialItems.Bless, 100_000 },
|
||||
{ (int)SpecialItems.Soul, 70_000 },
|
||||
{ (int)SpecialItems.Chaos, 40_000 },
|
||||
{ (int)SpecialItems.Life, 450_000 },
|
||||
{ (int)SpecialItems.Creation, 450_000 },
|
||||
};
|
||||
|
||||
private static readonly IDictionary<int, Func<Item, long>> SpecialItemDictionary = new Dictionary<int, Func<Item, long>>
|
||||
{
|
||||
{
|
||||
(int)SpecialItems.Arrow, item =>
|
||||
{
|
||||
int gold = 0;
|
||||
int baseprice = item.Level switch
|
||||
{
|
||||
1 => 1200,
|
||||
2 => 2000,
|
||||
3 => 2800,
|
||||
_ => 70,
|
||||
};
|
||||
|
||||
if (item.Durability > 0)
|
||||
{
|
||||
gold = baseprice * item.Durability() / item.Definition?.Durability ?? 1;
|
||||
}
|
||||
|
||||
return gold;
|
||||
}
|
||||
},
|
||||
{
|
||||
(int)SpecialItems.Bolt, item =>
|
||||
{
|
||||
int gold = 0;
|
||||
int baseprice = item.Level switch
|
||||
{
|
||||
1 => 1400,
|
||||
2 => 2200,
|
||||
3 => 3000,
|
||||
_ => 100,
|
||||
};
|
||||
|
||||
if (item.Durability > 0)
|
||||
{
|
||||
gold = baseprice * item.Durability() / item.Definition?.Durability ?? 1;
|
||||
}
|
||||
|
||||
return gold;
|
||||
}
|
||||
},
|
||||
{ (int)SpecialItems.Bless, _ => 9000000 },
|
||||
{ (int)SpecialItems.Soul, _ => 6000000 },
|
||||
{ (int)SpecialItems.Chaos, _ => 810000 },
|
||||
{ (int)SpecialItems.Life, _ => 45000000 },
|
||||
{ (int)SpecialItems.Creation, _ => 36000000 },
|
||||
{ (int)SpecialItems.Guardian, _ => 60000000 },
|
||||
{ (int)SpecialItems.Gemstone, _ => 18600 },
|
||||
{ (int)SpecialItems.Harmony, _ => 18600 },
|
||||
{ (int)SpecialItems.LowerRefineStone, _ => 18600 },
|
||||
{ (int)SpecialItems.HigherRefineStone, _ => 18600 },
|
||||
{ (int)SpecialItems.PackedBless, item => (item.Level + 1) * 9000000 * 10 },
|
||||
{ (int)SpecialItems.PackedSoul, item => (item.Level + 1) * 6000000 * 10 },
|
||||
{ (int)SpecialItems.PackedChaos, item => (item.Level + 1) * 810000 * 10 },
|
||||
{ (int)SpecialItems.PackedLife, item => (item.Level + 1) * 45000000 * 10 },
|
||||
{ (int)SpecialItems.PackedCreation, item => (item.Level + 1) * 36000000 * 10 },
|
||||
{ (int)SpecialItems.PackedGuardian, item => (item.Level + 1) * 60000000 * 10 },
|
||||
{ (int)SpecialItems.PackedGemstone, item => (item.Level + 1) * 18600 * 10 },
|
||||
{ (int)SpecialItems.PackedHarmony, item => (item.Level + 1) * 18600 * 10 },
|
||||
{ (int)SpecialItems.PackedLowerRefineStone, item => (item.Level + 1) * 18600 * 10 },
|
||||
{ (int)SpecialItems.PackedHigherRefineStone, item => (item.Level + 1) * 18600 * 10 },
|
||||
{ (int)SpecialItems.Fruits, _ => 33000000 },
|
||||
{ (int)SpecialItems.LochFeather, item => item.Level == 1 ? 7500000 : 180000 },
|
||||
{ (int)SpecialItems.SiegePotion, item => item.Durability() * (item.Level == 0 ? 900000 : 450000) },
|
||||
{ (int)SpecialItems.OrderGuardianLifeStone, item => item.Level == 1 ? 2400000 : 1000000 },
|
||||
{ (int)SpecialItems.ContractSummon, item => item.Level == 0 ? 1500000 : item.Level == 1 ? 1200000 : 0 },
|
||||
{ (int)SpecialItems.SplinterOfArmor, item => item.Durability() * 150 },
|
||||
{ (int)SpecialItems.BlessOfGuardian, item => item.Durability() * 300 },
|
||||
{ (int)SpecialItems.ClawOfBeast, item => item.Durability() * 3000 },
|
||||
{ (int)SpecialItems.FragmentOfHorn, _ => 30000 },
|
||||
{ (int)SpecialItems.BrokenHorn, _ => 90000 },
|
||||
{ (int)SpecialItems.HornFenrir, _ => 150000 },
|
||||
{ (int)SpecialItems.SmallSdPotion, item => item.Durability() * 2000 },
|
||||
{ (int)SpecialItems.SdPotion, item => item.Durability() * 4000 },
|
||||
{ (int)SpecialItems.LargeSdPotion, item => item.Durability() * 6000 },
|
||||
{ (int)SpecialItems.LargeHealPotion, item => item.Durability() * 1500 * (item.Level + 1) },
|
||||
{ (int)SpecialItems.LargeManaPotion, item => item.Durability() * 1500 * (item.Level + 1) },
|
||||
{ (int)SpecialItems.SmallComplexPotion, item => item.Durability() * 2500 },
|
||||
{ (int)SpecialItems.ComplexPotion, item => item.Durability() * 5000 },
|
||||
{ (int)SpecialItems.LargeComplexPotion, item => item.Durability() * 7500 },
|
||||
{
|
||||
(int)SpecialItems.Dinorant, item =>
|
||||
{
|
||||
var opts = item.ItemOptions.Where(o => o.ItemOption?.OptionType == ItemOptionTypes.Option).Count();
|
||||
return 960000 + (300000 * opts);
|
||||
}
|
||||
},
|
||||
{
|
||||
(int)SpecialItems.DevilEye, item => item.Level == 1 ? 10000 :
|
||||
item.Level == 2 ? 50000 :
|
||||
item.Level == 3 ? 100000 :
|
||||
item.Level == 4 ? 300000 :
|
||||
item.Level == 5 ? 500000 :
|
||||
item.Level == 6 ? 800000 :
|
||||
item.Level == 7 ? 1000000 : 10000
|
||||
},
|
||||
{
|
||||
(int)SpecialItems.DevilKey, item => item.Level == 1 ? 15000 :
|
||||
item.Level == 2 ? 75000 :
|
||||
item.Level == 3 ? 150000 :
|
||||
item.Level == 4 ? 450000 :
|
||||
item.Level == 5 ? 750000 :
|
||||
item.Level == 6 ? 1200000 :
|
||||
item.Level == 7 ? 1500000 : 15000
|
||||
},
|
||||
{ (int)SpecialItems.DevilInvitation, item => item.Level is 1 ? 60000 : item.Level == 2 ? 84000 : (item.Level - 1) * 60000 }, // +7 sell price on S6E3 client is 60k (same as +4). Bug?
|
||||
{ (int)SpecialItems.RemedyOfLove, _ => 900 },
|
||||
{ (int)SpecialItems.Rena, item => item.Level == 3 ? item.Durability() * 3900 : 9000 },
|
||||
{ (int)SpecialItems.Ale, _ => 750 },
|
||||
{ (int)SpecialItems.InvisibleCloak, item => item.Level == 1 ? 150000 : 600000 + ((item.Level - 1) * 60000) },
|
||||
{
|
||||
(int)SpecialItems.ScrollOfArchangel, item => item.Level == 1 ? 10000 :
|
||||
item.Level == 2 ? 50000 :
|
||||
item.Level == 3 ? 100000 :
|
||||
item.Level == 4 ? 300000 :
|
||||
item.Level == 5 ? 500000 :
|
||||
item.Level == 6 ? 800000 :
|
||||
item.Level == 7 ? 1000000 :
|
||||
item.Level == 8 ? 1200000 : 10000
|
||||
},
|
||||
{
|
||||
(int)SpecialItems.BloodBone, item => item.Level == 1 ? 10000 :
|
||||
item.Level == 2 ? 50000 :
|
||||
item.Level == 3 ? 100000 :
|
||||
item.Level == 4 ? 300000 :
|
||||
item.Level == 5 ? 500000 :
|
||||
item.Level == 6 ? 800000 :
|
||||
item.Level == 7 ? 1000000 :
|
||||
item.Level == 8 ? 1200000 : 10000
|
||||
},
|
||||
{ (int)SpecialItems.OldScroll, item => item.Level == 1 ? 500000 : (item.Level + 1) * 200000 },
|
||||
{ (int)SpecialItems.IllusionSorcererCovenant, item => item.Level == 1 ? 500000 : (item.Level + 1) * 200000 },
|
||||
{ (int)SpecialItems.ScrollOfBlood, item => item.Level == 1 ? 500000 : (item.Level + 1) * 200000 },
|
||||
{ (int)SpecialItems.FlameOfCondor, _ => 3000000 },
|
||||
{ (int)SpecialItems.FeatherOfCondor, _ => 3000000 },
|
||||
{ (int)SpecialItems.ArmorGuardman, _ => 5000 },
|
||||
{ (int)SpecialItems.WizardsRing, item => item.Level == 0 ? 30000 : 0 },
|
||||
{ (int)SpecialItems.SpiritPet, item => item.Level == 0 ? 30000000 : item.Level == 1 ? 15000000 : 0 },
|
||||
{ (int)SpecialItems.LostMap, _ => 600000 },
|
||||
{ (int)SpecialItems.SymbolKundun, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween1, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween2, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween3, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween4, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween5, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.Halloween6, item => 150 * item.Durability() },
|
||||
{ (int)SpecialItems.GemOfSecret, item => item.Level == 0 ? 60000 : 0 },
|
||||
{ (int)SpecialItems.SuspiciousScrapOfPaper, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.GaionsOrder, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.FirstSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.SecondSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.ThirdSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.FourthSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.FifthSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.SixthSecromiconFragment, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.CompleteSecromicon, item => 30000 * item.Durability() },
|
||||
{ (int)SpecialItems.ChristmasStar, _ => 200000 },
|
||||
{ (int)SpecialItems.Firecracker, _ => 200000 },
|
||||
{ (int)SpecialItems.CherryBlossomWine, item => 300 * item.Durability() },
|
||||
{ (int)SpecialItems.CherryBlossomRiceCake, item => 300 * item.Durability() },
|
||||
{ (int)SpecialItems.CherryBlossomFlowerPetal, item => 300 * item.Durability() },
|
||||
{ (int)SpecialItems.GoldenCherryBlossomBranch, item => 300 * item.Durability() },
|
||||
};
|
||||
|
||||
private enum SpecialItems
|
||||
{
|
||||
Arrow = 0xF04, // getId(4, 15),
|
||||
Bolt = 0x704, // getId(4, 7),
|
||||
Bless = 0xD0E, // getId(14,13),
|
||||
Soul = 0xE0E, // getId(14,14),
|
||||
Chaos = 0xF0C, // getId(12,15),
|
||||
Life = 0x100E, // getId(14,16),
|
||||
Creation = 0x160E, // getId(14,22),
|
||||
Guardian = 0x1F0E, // getId(14,31),
|
||||
Gemstone = 0x290E,
|
||||
Harmony = 0x2A0E,
|
||||
LowerRefineStone = 0x2B0E,
|
||||
HigherRefineStone = 0x2C0E,
|
||||
PackedBless = 0x1E0C, // getId(12,30),
|
||||
PackedSoul = 0x1F0C, // getId(12,31),
|
||||
PackedChaos = 0x8D0C,
|
||||
PackedLife = 0x880C,
|
||||
PackedCreation = 0x890C,
|
||||
PackedGuardian = 0x8A0C,
|
||||
PackedGemstone = 0x8B0C,
|
||||
PackedHarmony = 0x8C0C,
|
||||
PackedLowerRefineStone = 0x8E0C,
|
||||
PackedHigherRefineStone = 0x8F0C,
|
||||
Fruits = 0xF0D, // getId(13,15),
|
||||
LochFeather = 0xE0D, // getId(13,14),
|
||||
LargeHealPotion = 0x030E,
|
||||
LargeManaPotion = 0x060E,
|
||||
SiegePotion = 0x70E, // getId(14,7),
|
||||
OrderGuardianLifeStone = 0xB0D, // getId(13,11),
|
||||
ContractSummon = 0x70D, // getId(13,7),
|
||||
SplinterOfArmor = 0x200D, // getId(13,32),
|
||||
BlessOfGuardian = 0x210D, // getId(13,33),
|
||||
ClawOfBeast = 0x220D, // getId(13,34),
|
||||
FragmentOfHorn = 0x230D, // getId(13,35),
|
||||
BrokenHorn = 0x240D, // getId(13,36),
|
||||
HornFenrir = 0x250D, // getId(13,37),
|
||||
SmallSdPotion = 0x230E, // getId(14,35),
|
||||
SdPotion = 0x240E, // getId(14, 36),
|
||||
LargeSdPotion = 0x250E, // getId(14,37),
|
||||
SmallComplexPotion = 0x260E, // getId(14,38),
|
||||
ComplexPotion = 0x270E, // getId(14,39),
|
||||
LargeComplexPotion = 0x280E, // getId(14,40),
|
||||
Dinorant = 0x30D, // getId(13,3),
|
||||
DevilEye = 0x110E, // getId(14,17),
|
||||
DevilKey = 0x120E, // getId(14,18),
|
||||
DevilInvitation = 0x130E, // getId(14,19),
|
||||
RemedyOfLove = 0x140E, // getId(14,20),
|
||||
Rena = 0x150E, // getId(14,21),
|
||||
Ale = 0x90E, // getId(14,9),
|
||||
InvisibleCloak = 0x120D, // getId(13,18),
|
||||
ScrollOfArchangel = 0x100D, // getId(13, 16),
|
||||
BloodBone = 0x110D, // getId(13,17),
|
||||
ArmorGuardman = 0x1D0D, // getId(13,29),
|
||||
WizardsRing = 0x140D, // getId(13,20),
|
||||
SpiritPet = 0x1F0D, // getId(13,31),
|
||||
LostMap = 0x1C0E, // getId(14,28),
|
||||
SymbolKundun = 0x1D0E, // getId(14,29),
|
||||
Halloween1 = 0x2D0E, // getId(14, 45),
|
||||
Halloween2 = 0x2E0E, // getId(14, 46),
|
||||
Halloween3 = 0x2F0E, // getId(14, 47),
|
||||
Halloween4 = 0x300E, // getId(14, 48),
|
||||
Halloween5 = 0x310E, // getId(14, 49),
|
||||
Halloween6 = 0x320E, // getId(14, 50),
|
||||
GemOfSecret = 0x1A0C, // getId(12,26),
|
||||
OldScroll = 0x310D,
|
||||
IllusionSorcererCovenant = 0x320D,
|
||||
ScrollOfBlood = 0x330D,
|
||||
FlameOfCondor = 0x340D,
|
||||
FeatherOfCondor = 0x350D,
|
||||
SuspiciousScrapOfPaper = 0x650E,
|
||||
GaionsOrder = 0x660E,
|
||||
FirstSecromiconFragment = 0x670E,
|
||||
SecondSecromiconFragment = 0x680E,
|
||||
ThirdSecromiconFragment = 0x690E,
|
||||
FourthSecromiconFragment = 0x6A0E,
|
||||
FifthSecromiconFragment = 0x6B0E,
|
||||
SixthSecromiconFragment = 0x6C0E,
|
||||
CompleteSecromicon = 0x6D0E,
|
||||
ChristmasStar = 0x330E,
|
||||
Firecracker = 0x3F0E,
|
||||
CherryBlossomWine = 0x550E,
|
||||
CherryBlossomRiceCake = 0x560E,
|
||||
CherryBlossomFlowerPetal = 0x570E,
|
||||
GoldenCherryBlossomBranch = 0x5A0E,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the selling price of the item for its maximum durability.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The selling price.</returns>
|
||||
public long CalculateSellingPrice(Item item) => this.CalculateSellingPrice(item, item.GetMaximumDurabilityOfOnePiece());
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the selling price of the item, which the player gets if he is selling an item to a merchant.
|
||||
/// It's usually a third of the buying price, minus a durability factor.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="durability">The current durability of the <paramref name="item"/>.</param>
|
||||
/// <returns>The selling price.</returns>
|
||||
public long CalculateSellingPrice(Item item, byte durability)
|
||||
{
|
||||
item.ThrowNotInitializedProperty(item.Definition is null, nameof(item.Definition));
|
||||
|
||||
var sellingPrice = CalculateBuyingPrice(item) / 3;
|
||||
if (item.Definition.Group == 14 && item.Definition.Number <= 8)
|
||||
{
|
||||
// Potions + Antidote
|
||||
return sellingPrice / 10 * 10;
|
||||
}
|
||||
|
||||
if (!item.IsTrainablePet())
|
||||
{
|
||||
var maxDurability = item.GetMaximumDurabilityOfOnePiece();
|
||||
if (maxDurability > 1 && maxDurability > durability)
|
||||
{
|
||||
float multiplier = 1.0f - ((float)durability / maxDurability);
|
||||
long loss = (long)(sellingPrice * 0.6 * multiplier);
|
||||
sellingPrice -= loss;
|
||||
}
|
||||
}
|
||||
|
||||
return RoundPrice(sellingPrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the repair price of the item, which the player has to pay if he wants to repair the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="npcDiscount">If set to <c>true</c>, the item is repaired through an NPC which gives a discount.</param>
|
||||
/// <returns>The repair price.</returns>
|
||||
public long CalculateRepairPrice(Item item, bool npcDiscount)
|
||||
{
|
||||
if (item.GetMaximumDurabilityOfOnePiece() == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const long maximumBasePrice = 400_000_000;
|
||||
var isPet = item.IsTrainablePet();
|
||||
var basePrice = Math.Min(this.CalculateFinalBuyingPrice(item) / (isPet ? 1 : 3), maximumBasePrice);
|
||||
basePrice = RoundPrice(basePrice);
|
||||
|
||||
float squareRootOfBasePrice = (float)Math.Sqrt(basePrice);
|
||||
float squareRootOfSquareRoot = (float)Math.Sqrt(squareRootOfBasePrice);
|
||||
float missingDurability = 1 - ((float)item.Durability() / item.GetMaximumDurabilityOfOnePiece());
|
||||
float repairPrice = (3.0f * squareRootOfBasePrice * squareRootOfSquareRoot * missingDurability) + 1.0f;
|
||||
if (item.Durability <= 0)
|
||||
{
|
||||
if (isPet)
|
||||
{
|
||||
repairPrice *= DestroyedPetPenalty;
|
||||
}
|
||||
else
|
||||
{
|
||||
repairPrice *= DestroyedItemPenalty;
|
||||
}
|
||||
}
|
||||
|
||||
if (!npcDiscount)
|
||||
{
|
||||
repairPrice *= 2.5f;
|
||||
}
|
||||
|
||||
return RoundPrice((long)repairPrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the final buying price of the item, which the player has to pay if he wants to buy the item from a merchant.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The buying price.</returns>
|
||||
public long CalculateFinalBuyingPrice(Item item) => RoundPrice(CalculateBuyingPrice(item));
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the final "old" buying price of the item.
|
||||
/// Supposedly in earlier versions jewel reference prices were different, and those were used since for Chaos Weapon and First Wings craftings rate calculations.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>The "old" buying price.</returns>
|
||||
public long CalculateFinalOldBuyingPrice(Item item)
|
||||
{
|
||||
if (SpecialItemOldValueDictionary.TryGetValue(GetId(item.Definition!.Group, item.Definition.Number), out var oldValue))
|
||||
{
|
||||
return RoundPrice(oldValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.CalculateFinalBuyingPrice(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetId(byte group, int id)
|
||||
{
|
||||
return (id << 8) + group;
|
||||
}
|
||||
|
||||
private static long RoundPrice(long price)
|
||||
{
|
||||
var result = price;
|
||||
if (result >= 1000)
|
||||
{
|
||||
result = result / 100 * 100;
|
||||
}
|
||||
else if (result >= 100)
|
||||
{
|
||||
result = result / 10 * 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no rounding for smaller values.
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static long CalculateBuyingPrice(Item item)
|
||||
{
|
||||
item.ThrowNotInitializedProperty(item.Definition is null, nameof(item.Definition));
|
||||
|
||||
var definition = item.Definition!;
|
||||
if (definition.Value > 0 && (definition.Group == 15 || definition.Group == 12))
|
||||
{
|
||||
return definition.Value;
|
||||
}
|
||||
|
||||
if (item.IsTrainablePet())
|
||||
{
|
||||
if (item.IsDarkRaven())
|
||||
{
|
||||
return item.Level * 1_000_000;
|
||||
}
|
||||
else
|
||||
{
|
||||
return item.Level * 2_000_000;
|
||||
}
|
||||
}
|
||||
|
||||
long price = 0;
|
||||
int dropLevel = definition.DropLevel + (item.Level * 3);
|
||||
if (item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Excellent))
|
||||
{
|
||||
// increased drop level of excellent item
|
||||
dropLevel += 25;
|
||||
}
|
||||
|
||||
if (SpecialItemDictionary.TryGetValue(GetId(item.Definition.Group, item.Definition.Number), out var specialItemPriceFunction))
|
||||
{
|
||||
price = specialItemPriceFunction(item);
|
||||
}
|
||||
else if (definition.Value > 0)
|
||||
{
|
||||
price += definition.Value * definition.Value * 10 / 12;
|
||||
if (item.Definition.Group == 14 && (item.Definition.Number <= 8))
|
||||
{
|
||||
// Potions + Antidote
|
||||
if (item.Level > 0)
|
||||
{
|
||||
price *= (long)Math.Pow(2, item.Level);
|
||||
}
|
||||
|
||||
price = price / 10 * 10;
|
||||
price *= item.Durability();
|
||||
return price;
|
||||
}
|
||||
}
|
||||
else if ((item.Definition.Group == 12
|
||||
&& ((item.Definition.Number > 6 && item.Definition.Number < 36)
|
||||
|| (item.Definition.Number > 43 && item.Definition.Number != 50)))
|
||||
|| item.Definition.Group == 13
|
||||
|| item.Definition.Group == 15)
|
||||
{
|
||||
// Cape of Lord and Cape of Fighter go here
|
||||
price = (dropLevel * dropLevel * dropLevel) + 100;
|
||||
|
||||
if (item.ItemOptions.FirstOrDefault(o => o.ItemOption?.OptionType == ItemOptionTypes.Option) is { } opt
|
||||
&& opt.ItemOption?.PowerUpDefinition?.TargetAttribute == Stats.HealthRecoveryMultiplier)
|
||||
{
|
||||
// Rings, pendants, and capes. Capes with physical damage option have no extra value (possibly a source bug).
|
||||
price += price * opt.Level;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (DropLevelIncreaseByLevel.TryGetValue(item.Level, out var dropLevelIncrease))
|
||||
{
|
||||
dropLevel += dropLevelIncrease;
|
||||
}
|
||||
|
||||
// Wings
|
||||
if (item.IsWing())
|
||||
{
|
||||
// maybe we have to exclude small wings here
|
||||
price = ((dropLevel + 40) * dropLevel * dropLevel * 11) + 40000000;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = ((dropLevel + 40) * dropLevel * dropLevel / 8) + 100;
|
||||
}
|
||||
|
||||
var isOneHandedWeapon = item.Definition.Group < 6 && definition.Width < 2;
|
||||
var isShield = item.Definition.Group == 6;
|
||||
if (isOneHandedWeapon || isShield)
|
||||
{
|
||||
price = price * 80 / 100;
|
||||
}
|
||||
|
||||
if (item.HasSkill && !WorthlessSkills.Contains(definition.Skill?.Number ?? 0))
|
||||
{
|
||||
price += (long)(price * 1.5);
|
||||
}
|
||||
|
||||
// add 25% for luck
|
||||
if (item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Luck))
|
||||
{
|
||||
price += price * 25 / 100;
|
||||
}
|
||||
|
||||
var opt = item.ItemOptions.FirstOrDefault(o => o.ItemOption?.OptionType == ItemOptionTypes.Option);
|
||||
var optionLevel = opt?.Level ?? 0;
|
||||
|
||||
// Item Options (1 to 4, or 4 to 16)
|
||||
switch (optionLevel)
|
||||
{
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
price += (long)(price * 0.6);
|
||||
break;
|
||||
default:
|
||||
price += (long)(price * 0.7 * Math.Pow(2, optionLevel - 1));
|
||||
break;
|
||||
}
|
||||
|
||||
// For each wing option, add 25%
|
||||
var wingOptionCount = item.ItemOptions.Count(o => o.ItemOption?.OptionType == ItemOptionTypes.Wing);
|
||||
for (int i = 0; i < wingOptionCount; i++)
|
||||
{
|
||||
price += (long)(price * 0.25);
|
||||
}
|
||||
|
||||
// For each excellent option double the value
|
||||
var excCount = item.ItemOptions.Count(o => o.ItemOption?.OptionType == ItemOptionTypes.Excellent);
|
||||
for (int i = 0; i < excCount; i++)
|
||||
{
|
||||
price += price;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.GuardianOption))
|
||||
{
|
||||
price += price * 16 / 100;
|
||||
}
|
||||
|
||||
if (price > MaximumPrice)
|
||||
{
|
||||
price = MaximumPrice;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
}
|
||||
115
src/GameLogic/ItemStorageAdapter.cs
Normal file
115
src/GameLogic/ItemStorageAdapter.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
// <copyright file="ItemStorageAdapter.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper for another <see cref="ItemStorage"/>.
|
||||
/// Required to split one item storage into more than one storage spaces, e.g. Inventory and Personal Store which use the same ItemStorage.
|
||||
/// </summary>
|
||||
/// <seealso cref="ItemStorage"/>
|
||||
public class ItemStorageAdapter : ItemStorage
|
||||
{
|
||||
private readonly CollectionAdapter _adapter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemStorageAdapter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="actualStorage">The actual storage.</param>
|
||||
/// <param name="firstItemSlot">The first item slot.</param>
|
||||
/// <param name="itemSlotCount">The item slot count.</param>
|
||||
public ItemStorageAdapter(ItemStorage actualStorage, byte firstItemSlot, byte itemSlotCount)
|
||||
{
|
||||
this._adapter = new CollectionAdapter(actualStorage.Items, firstItemSlot, itemSlotCount);
|
||||
this.ActualStorage = actualStorage;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ICollection<Item> Items => this._adapter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual storage which is wrapped by this instance.
|
||||
/// </summary>
|
||||
public ItemStorage ActualStorage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A collection adapter which just returns items between certain item slots.
|
||||
/// </summary>
|
||||
private class CollectionAdapter : ICollection<Item>
|
||||
{
|
||||
private readonly ICollection<Item> _actualCollection;
|
||||
private readonly byte _firstItemSlot;
|
||||
private readonly byte _itemSlotCount;
|
||||
|
||||
public CollectionAdapter(ICollection<Item> actualCollection, byte firstItemSlot, byte itemSlotCount)
|
||||
{
|
||||
this._actualCollection = actualCollection;
|
||||
this._firstItemSlot = firstItemSlot;
|
||||
this._itemSlotCount = itemSlotCount;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => this._actualCollection.Count(i => this.IsSlotOfThisStorage(i.ItemSlot));
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<Item> GetEnumerator()
|
||||
{
|
||||
return this._actualCollection.Where(item => this.IsSlotOfThisStorage(item.ItemSlot)).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(Item item)
|
||||
{
|
||||
this._actualCollection.Add(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
var itemsToRemove = this.ToList();
|
||||
itemsToRemove.ForEach(item => this._actualCollection.Remove(item));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(Item item) => item is { } && this.IsSlotOfThisStorage(item.ItemSlot) && this._actualCollection.Contains(item);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(Item[] array, int arrayIndex)
|
||||
{
|
||||
var i = arrayIndex;
|
||||
foreach (var item in this)
|
||||
{
|
||||
array[i] = item;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Remove(Item item)
|
||||
{
|
||||
if (this.Contains(item))
|
||||
{
|
||||
return this._actualCollection.Remove(item);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsSlotOfThisStorage(byte itemSlot)
|
||||
{
|
||||
return itemSlot >= this._firstItemSlot && itemSlot < this._firstItemSlot + this._itemSlotCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/GameLogic/LazyExtensions.cs
Normal file
39
src/GameLogic/LazyExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// <copyright file="LazyExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="Lazy{T}"/>.
|
||||
/// </summary>
|
||||
public static class LazyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Disposes the value of the lazy instance, if it was created, asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the <see cref="IAsyncDisposable"/>.</typeparam>
|
||||
/// <param name="lazy">The lazy instance.</param>
|
||||
public static async ValueTask DisposeIfCreatedAsync<T>(this Lazy<T> lazy)
|
||||
where T : IAsyncDisposable
|
||||
{
|
||||
if (lazy.IsValueCreated)
|
||||
{
|
||||
await lazy.Value.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the value of the lazy instance, if created.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The <see cref="IDisposable"/>.</typeparam>
|
||||
/// <param name="lazy">The lazy instance.</param>
|
||||
public static void DisposeIfCreated<T>(this Lazy<T> lazy)
|
||||
where T : IDisposable
|
||||
{
|
||||
if (lazy.IsValueCreated)
|
||||
{
|
||||
lazy.Value.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/GameLogic/LimitedObjectPool.cs
Normal file
72
src/GameLogic/LimitedObjectPool.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// <copyright file="LimitedObjectPool.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IObjectPool{T}"/> which limits the amounts of created
|
||||
/// <typeparamref name="T"/> to the maximum retained objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects to pool.</typeparam>
|
||||
internal sealed class LimitedObjectPool<T> : DefaultObjectPool<T>, IObjectPool<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LimitedObjectPool{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="policy">The pooling policy to use.</param>
|
||||
/// <param name="maximumRetained">The maximum number of objects to create and retain in the pool.</param>
|
||||
public LimitedObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
|
||||
: base(policy, maximumRetained)
|
||||
{
|
||||
this._semaphore = new SemaphoreSlim(maximumRetained, maximumRetained);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LimitedObjectPool{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="policy">The pooling policy to use.</param>
|
||||
public LimitedObjectPool(IPooledObjectPolicy<T> policy)
|
||||
: base(policy, MaximumRetainedDefault)
|
||||
{
|
||||
this._semaphore = new SemaphoreSlim(MaximumRetainedDefault, MaximumRetainedDefault);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default number of maximum number of pooled objects.
|
||||
/// </summary>
|
||||
public static int MaximumRetainedDefault => Environment.ProcessorCount * 2;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<T> GetAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this._semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
return base.Get();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T Get()
|
||||
{
|
||||
this._semaphore.Wait();
|
||||
return base.Get();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Return(T obj)
|
||||
{
|
||||
base.Return(obj);
|
||||
this._semaphore.Release();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
168
src/GameLogic/LocateableExtensions.cs
Normal file
168
src/GameLogic/LocateableExtensions.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// <copyright file="LocateableExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for mu objects.
|
||||
/// </summary>
|
||||
public static class LocateableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters out inactive (non-alive) locateables.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of elements.</typeparam>
|
||||
/// <param name="locateables">The locateables.</param>
|
||||
/// <returns>
|
||||
/// All active locateables of the given enumeration.
|
||||
/// </returns>
|
||||
public static IEnumerable<T> WhereActive<T>(this IEnumerable<T> locateables)
|
||||
where T : ILocateable
|
||||
{
|
||||
return locateables.Where(l => l.IsActive());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters out invisible locateables.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of elements.</typeparam>
|
||||
/// <param name="locateables">The locateables.</param>
|
||||
/// <returns>
|
||||
/// All visible locateables of the given enumeration.
|
||||
/// </returns>
|
||||
public static IEnumerable<T> WhereNotInvisible<T>(this IEnumerable<T> locateables)
|
||||
where T : IAttackable
|
||||
{
|
||||
return locateables.Where(l => l.Attributes[Stats.IsInvisible] == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance is active (alive).
|
||||
/// </summary>
|
||||
/// <param name="locateable">The locateable.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified locateable is active; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsActive(this ILocateable locateable)
|
||||
{
|
||||
return locateable is not IAttackable attackable || (attackable.IsAlive && !attackable.IsTeleporting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the distance to another object.
|
||||
/// </summary>
|
||||
/// <param name="objectFrom">The object from which the distance is calculated.</param>
|
||||
/// <param name="objectTo">The object to which the distance is calculated.</param>
|
||||
/// <returns>The distance between this and another object.</returns>
|
||||
public static double GetDistanceTo(this ILocateable objectFrom, ILocateable objectTo)
|
||||
{
|
||||
return objectFrom.GetDistanceTo(objectTo.Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the distance to another point.
|
||||
/// </summary>
|
||||
/// <param name="objectFrom">The object from which the distance is calculated.</param>
|
||||
/// <param name="objectToPosition">The point to which the distance is calculated.</param>
|
||||
/// <returns>The distance between this and another object.</returns>
|
||||
public static double GetDistanceTo(this ILocateable objectFrom, Point objectToPosition)
|
||||
{
|
||||
return objectFrom.Position.EuclideanDistanceTo(objectToPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified coordinates are in the specified range of the object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <param name="point">The coordinates.</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, 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>
|
||||
/// <param name="locatable">The object.</param>
|
||||
/// <param name="x">The x coordinate.</param>
|
||||
/// <param name="y">The y coordinate.</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 locatable, int x, int y, int range)
|
||||
{
|
||||
int xdiff;
|
||||
int ydiff;
|
||||
var point = locatable.Position;
|
||||
if (x < point.X)
|
||||
{
|
||||
xdiff = point.X - x;
|
||||
}
|
||||
else if (x > point.X)
|
||||
{
|
||||
xdiff = x - point.X;
|
||||
}
|
||||
else
|
||||
{
|
||||
xdiff = 0;
|
||||
}
|
||||
|
||||
if (xdiff > range)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (y < point.Y)
|
||||
{
|
||||
ydiff = point.Y - y;
|
||||
}
|
||||
else if (y > point.Y)
|
||||
{
|
||||
ydiff = y - point.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
ydiff = 0;
|
||||
}
|
||||
|
||||
return ydiff <= range;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the direction to another object.
|
||||
/// </summary>
|
||||
/// <param name="objectFrom">The object from which the direction is calculated.</param>
|
||||
/// <param name="objectTo">The object to which the direction is calculated.</param>
|
||||
/// <returns>The direction between this and another object.</returns>
|
||||
/// <remarks>
|
||||
/// The returned values differ a bit, so we first have to analyze which function is correct.
|
||||
/// </remarks>
|
||||
public static Direction GetDirectionTo(this ILocateable objectFrom, ILocateable objectTo) => objectFrom.Position.GetDirectionTo(objectTo.Position);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the object is at the safezone of his current map.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <returns>True, if it is on the safezone of his current map; Otherwise, false.</returns>
|
||||
public static bool IsAtSafezone(this ILocateable obj)
|
||||
{
|
||||
var map = obj.CurrentMap;
|
||||
if (map?.Terrain?.SafezoneMap is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.Terrain.SafezoneMap[obj.Position.X, obj.Position.Y];
|
||||
}
|
||||
}
|
||||
38
src/GameLogic/LoggerExtensions.cs
Normal file
38
src/GameLogic/LoggerExtensions.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// <copyright file="LoggerExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="ILogger"/>s.
|
||||
/// </summary>
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Begins a logical operation scope.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="values">The key value pairs which describe the scope.</param>
|
||||
/// <returns>An <see cref="T:System.IDisposable" /> that ends the logical operation scope on dispose.</returns>
|
||||
public static IDisposable? BeginScope(this ILogger logger, params (string Key, object Value)[] values)
|
||||
{
|
||||
return logger.BeginScope(values.Select(pair => new KeyValuePair<string, object>(pair.Key, pair.Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a logical operation scope.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
/// <returns>An <see cref="T:System.IDisposable" /> that ends the logical operation scope on dispose.</returns>
|
||||
public static IDisposable? BeginScope(this ILogger logger, IGameContext gameContext)
|
||||
{
|
||||
if (gameContext is IGameServerContext gameServerContext)
|
||||
{
|
||||
return logger.BeginScope("GameServer {id}", gameServerContext.Id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
67
src/GameLogic/MUnique.OpenMU.GameLogic.csproj
Normal file
67
src/GameLogic/MUnique.OpenMU.GameLogic.csproj
Normal file
@@ -0,0 +1,67 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Debug\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.GameLogic.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.GameLogic.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="PlugIns\InvasionEvents\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" />
|
||||
<PackageReference Include="MathParser.org-mXparser" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
|
||||
<PackageReference Include="Nito.AsyncEx" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AttributeSystem\MUnique.OpenMU.AttributeSystem.csproj" />
|
||||
<ProjectReference Include="..\DataModel\MUnique.OpenMU.DataModel.csproj" />
|
||||
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\Pathfinding\MUnique.OpenMU.Pathfinding.csproj" />
|
||||
<ProjectReference Include="..\Persistence\MUnique.OpenMU.Persistence.csproj" />
|
||||
<ProjectReference Include="..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="PlugIns\InvasionEvents\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\PlayerMessage.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>PlayerMessage.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Properties\PlugInResources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>PlugInResources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\PlayerMessage.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>PlayerMessage.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Properties\PlugInResources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>PlugInResources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
173
src/GameLogic/MagicEffect.cs
Normal file
173
src/GameLogic/MagicEffect.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
// <copyright file="MagicEffect.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A magic effect, usually given by an applied skill or consumed item.
|
||||
/// </summary>
|
||||
public class MagicEffect : AsyncDisposable
|
||||
{
|
||||
private readonly Timer _finishTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MagicEffect"/> class.
|
||||
/// </summary>
|
||||
/// <param name="powerUp">The power up.</param>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <param name="duration">The duration.</param>
|
||||
public MagicEffect(IElement powerUp, MagicEffectDefinition definition, TimeSpan duration)
|
||||
: this(
|
||||
duration,
|
||||
definition,
|
||||
definition.PowerUpDefinitions
|
||||
.Select(def => new ElementWithTarget(powerUp, def.TargetAttribute ?? throw new InvalidOperationException($"MagicEffectDefinition {definition.GetId()} has no target attribute.")))
|
||||
.ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MagicEffect"/> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration.</param>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <param name="powerUps">The power ups.</param>
|
||||
public MagicEffect(TimeSpan duration, MagicEffectDefinition definition, params ElementWithTarget[] powerUps)
|
||||
{
|
||||
this.PowerUpElements = powerUps;
|
||||
this.Definition = definition;
|
||||
this.Duration = duration;
|
||||
this._finishTimer = new Timer(this.OnTimerTimeout, null, (int)this.Duration.TotalMilliseconds, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the effect has been timed out.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<MagicEffect>? EffectTimeOut;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier of the effect.
|
||||
/// </summary>
|
||||
public short Id => this.Definition.Number;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration of the effect.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
public float Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this.PowerUpElements.Any())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.PowerUpElements.First().Element.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the power up elements.
|
||||
/// </summary>
|
||||
public IEnumerable<ElementWithTarget> PowerUpElements { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the definition.
|
||||
/// </summary>
|
||||
public MagicEffectDefinition Definition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resets the timer.
|
||||
/// </summary>
|
||||
public void ResetTimer()
|
||||
{
|
||||
if (this.IsDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(MagicEffect));
|
||||
}
|
||||
|
||||
this._finishTimer.Change((int)this.Duration.TotalMilliseconds, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
await this._finishTimer.DisposeAsync().ConfigureAwait(false);
|
||||
await this.OnEffectTimeOutAsync().ConfigureAwait(false);
|
||||
this.EffectTimeOut = null;
|
||||
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
private async void OnTimerTimeout(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail(ex.Message, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnEffectTimeOutAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.EffectTimeOut is { } eventHandler)
|
||||
{
|
||||
await eventHandler(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!this.IsDisposed && !this.IsDisposing)
|
||||
{
|
||||
await this.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Fail(ex.Message, ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the element containing the boost value with its target attribute.
|
||||
/// </summary>
|
||||
public class ElementWithTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementWithTarget"/> class.
|
||||
/// </summary>
|
||||
/// <param name="element">The element.</param>
|
||||
/// <param name="target">The target attribute.</param>
|
||||
public ElementWithTarget(IElement element, AttributeDefinition target)
|
||||
{
|
||||
this.Element = element;
|
||||
this.Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element containing the boost value.
|
||||
/// </summary>
|
||||
public IElement Element { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target attribute.
|
||||
/// </summary>
|
||||
public AttributeDefinition Target { get; }
|
||||
}
|
||||
}
|
||||
206
src/GameLogic/MagicEffectsList.cs
Normal file
206
src/GameLogic/MagicEffectsList.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// <copyright file="MagicEffectsList.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The list of magic effects of a player instance. Automatically applies the power-ups of the effects to the player.
|
||||
/// </summary>
|
||||
public class MagicEffectsList : AsyncDisposable
|
||||
{
|
||||
private const byte InvisibleEffectStartIndex = 200;
|
||||
private readonly BitArray _contains = new(0x100);
|
||||
private readonly IAttackable _owner;
|
||||
private readonly AsyncLock _addLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MagicEffectsList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="owner">The attackable which owns this list.</param>
|
||||
public MagicEffectsList(IAttackable owner)
|
||||
{
|
||||
this._owner = owner;
|
||||
this.ActiveEffects = new SortedList<short, MagicEffect>(6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active effects.
|
||||
/// </summary>
|
||||
public IDictionary<short, MagicEffect> ActiveEffects { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active visible effect ids.
|
||||
/// </summary>
|
||||
public IList<MagicEffect> VisibleEffects => this.ActiveEffects.Values.Where(me => me.Definition.InformObservers).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Adds the effect and applies the power ups.
|
||||
/// </summary>
|
||||
/// <param name="effect">The effect.</param>
|
||||
public async ValueTask AddEffectAsync(MagicEffect effect)
|
||||
{
|
||||
bool added = false;
|
||||
using (await this._addLock.LockAsync())
|
||||
{
|
||||
if (this._contains[effect.Id])
|
||||
{
|
||||
this.UpdateEffect(effect);
|
||||
}
|
||||
else
|
||||
{
|
||||
added = true;
|
||||
this.ActiveEffects.Add(effect.Id, effect);
|
||||
this._contains[effect.Id] = true;
|
||||
foreach (var powerUp in effect.PowerUpElements)
|
||||
{
|
||||
this._owner.Attributes.AddElement(powerUp.Element, powerUp.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (added)
|
||||
{
|
||||
effect.EffectTimeOut += this.OnEffectTimeOutAsync;
|
||||
if (effect.Id < InvisibleEffectStartIndex && this._owner is IWorldObserver observer)
|
||||
{
|
||||
await observer.InvokeViewPlugInAsync<IActivateMagicEffectPlugIn>(p => p.ActivateMagicEffectAsync(effect, this._owner)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (effect.Id < InvisibleEffectStartIndex && effect.Definition.InformObservers && this._owner is IObservable observable)
|
||||
{
|
||||
await observable.ForEachWorldObserverAsync<IActivateMagicEffectPlugIn>(p => p.ActivateMagicEffectAsync(effect, this._owner), false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
effect.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all active effects.
|
||||
/// </summary>
|
||||
public async ValueTask ClearAllEffectsAsync()
|
||||
{
|
||||
while (this.ActiveEffects.Any())
|
||||
{
|
||||
await this.ActiveEffects.Values.First().DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the effects that produce a specific stat.
|
||||
/// </summary>
|
||||
/// <param name="stat">The stat produced by effect.</param>
|
||||
public async ValueTask ClearAllEffectsProducingSpecificStatAsync(AttributeDefinition stat)
|
||||
{
|
||||
var effects = this.ActiveEffects.Values.ToArray();
|
||||
|
||||
foreach (var effect in effects)
|
||||
{
|
||||
if (effect.PowerUpElements.Any(p => p.Target == stat))
|
||||
{
|
||||
await effect.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the effects after death of the player.
|
||||
/// </summary>
|
||||
public async ValueTask ClearEffectsAfterDeathAsync()
|
||||
{
|
||||
var effectsToRemove = this.ActiveEffects.Values.Where(effect => effect.Definition.StopByDeath).ToList();
|
||||
foreach (var effect in effectsToRemove)
|
||||
{
|
||||
await effect.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the currently active effect of the specified <see cref="MagicEffectDefinition.SubType"/>.
|
||||
/// </summary>
|
||||
/// <param name="subType">The <see cref="MagicEffectDefinition.SubType"/>.</param>
|
||||
/// <returns>The effect, if found.</returns>
|
||||
public async ValueTask<MagicEffect?> TryGetActiveEffectOfSubTypeAsync(byte subType)
|
||||
{
|
||||
using var l = await this._addLock.LockAsync();
|
||||
return this.ActiveEffects.Values.FirstOrDefault(e => e.Definition.SubType == subType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
await this.ClearAllEffectsAsync().ConfigureAwait(false);
|
||||
await base.DisposeAsyncCore().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask OnEffectTimeOutAsync(MagicEffect effect)
|
||||
{
|
||||
using (await this._addLock.LockAsync())
|
||||
{
|
||||
this.ActiveEffects.Remove(effect.Id);
|
||||
this._contains[effect.Id] = false;
|
||||
}
|
||||
|
||||
foreach (var powerUp in effect.PowerUpElements)
|
||||
{
|
||||
this._owner.Attributes.RemoveElement(powerUp.Element, powerUp.Target);
|
||||
}
|
||||
|
||||
if (effect.Id >= InvisibleEffectStartIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
(this._owner as IWorldObserver)?.InvokeViewPlugInAsync<IDeactivateMagicEffectPlugIn>(p => p.DeactivateMagicEffectAsync(effect, this._owner));
|
||||
if (effect.Definition.InformObservers && this._owner.IsAlive)
|
||||
{
|
||||
(this._owner as IObservable)?.ForEachWorldObserverAsync<IDeactivateMagicEffectPlugIn>(p => p.DeactivateMagicEffectAsync(effect, this._owner), false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the effect.
|
||||
/// </summary>
|
||||
/// <param name="effect">The effect.</param>
|
||||
private void UpdateEffect(MagicEffect effect)
|
||||
{
|
||||
MagicEffect magicEffect = this.ActiveEffects[effect.Id];
|
||||
if (magicEffect.Value > effect.Value)
|
||||
{
|
||||
// no de-buffing allowed
|
||||
return;
|
||||
}
|
||||
|
||||
//// GMO behaviour would be: RemoveEffect(magicEffect.Id); AddEffectAsync(effect);
|
||||
//// I change the existing Timer and Buff Value, without removing the effect itself.
|
||||
//// This doesn't only save traffic, it also looks better in game.
|
||||
magicEffect.Duration = effect.Duration;
|
||||
magicEffect.ResetTimer();
|
||||
|
||||
if (magicEffect.PowerUpElements.Select(e => e.Element)
|
||||
.SequenceEqual(effect.PowerUpElements.Select(e => e.Element)))
|
||||
{
|
||||
// if the effect power ups are the same, we can leave it like that
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var powerUp in magicEffect.PowerUpElements)
|
||||
{
|
||||
this._owner.Attributes.RemoveElement(powerUp.Element, powerUp.Target);
|
||||
}
|
||||
|
||||
magicEffect.PowerUpElements = effect.PowerUpElements;
|
||||
foreach (var powerUp in magicEffect.PowerUpElements)
|
||||
{
|
||||
this._owner.Attributes.AddElement(powerUp.Element, powerUp.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
391
src/GameLogic/MapInitializer.cs
Normal file
391
src/GameLogic/MapInitializer.cs
Normal file
@@ -0,0 +1,391 @@
|
||||
// <copyright file="MapInitializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.Views.NPC;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A basic map initializer.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.GameLogic.IMapInitializer" />
|
||||
public class MapInitializer : IMapInitializer
|
||||
{
|
||||
private readonly IDropGenerator _dropGenerator;
|
||||
private readonly IConfigurationChangeMediator? _configurationChangeMediator;
|
||||
private readonly GameConfiguration _configuration;
|
||||
private readonly ILogger<MapInitializer> _logger;
|
||||
|
||||
private readonly ConcurrentDictionary<MonsterSpawnArea, int> _spawnedMonsters = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MapInitializer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dropGenerator">The drop generator.</param>
|
||||
/// <param name="configurationChangeMediator">The configuration change mediator.</param>
|
||||
public MapInitializer(GameConfiguration configuration, ILogger<MapInitializer> logger, IDropGenerator dropGenerator, IConfigurationChangeMediator? configurationChangeMediator)
|
||||
{
|
||||
this._dropGenerator = dropGenerator;
|
||||
this._configurationChangeMediator = configurationChangeMediator;
|
||||
this._configuration = configuration;
|
||||
this._logger = logger;
|
||||
this.ChunkSize = 8;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the plug in manager.
|
||||
/// </summary>
|
||||
public PlugInManager? PlugInManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path finder pool.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The path finder pool.
|
||||
/// </value>
|
||||
public IObjectPool<PathFinder>? PathFinderPool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the chunk of created <see cref="GameMap"/>s.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The size of the chunk of created <see cref="GameMap"/>s.
|
||||
/// </value>
|
||||
protected byte ChunkSize { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameMap? CreateGameMap(ushort mapNumber)
|
||||
{
|
||||
var definition = this.GetMapDefinition(mapNumber);
|
||||
if (definition != null)
|
||||
{
|
||||
return this.InternalCreateGameMap(definition);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new game map instance with the specified definition.
|
||||
/// </summary>
|
||||
/// <param name="mapDefinition">The map definition.</param>
|
||||
/// <returns>The new game map instance.</returns>
|
||||
public GameMap CreateGameMap(GameMapDefinition mapDefinition)
|
||||
{
|
||||
return this.InternalCreateGameMap(mapDefinition);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeStateAsync(GameMap createdMap)
|
||||
{
|
||||
_ = this.PlugInManager ?? throw new InvalidOperationException("PlugInManager must be set first");
|
||||
_ = this.PathFinderPool ?? throw new InvalidOperationException("PathFinderPool must be set first");
|
||||
|
||||
this._logger.LogDebug("Start creating monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
var automaticSpawns = createdMap.Definition.MonsterSpawns
|
||||
.Where(m => m.MonsterDefinition is not null)
|
||||
.Where(m => m.SpawnTrigger is SpawnTrigger.Automatic);
|
||||
foreach (var spawnArea in automaticSpawns)
|
||||
{
|
||||
for (int i = 0; i < spawnArea.Quantity; i++)
|
||||
{
|
||||
await this.InitializeSpawnAsync(i, createdMap, spawnArea).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._spawnedMonsters.AddOrUpdate(spawnArea, spawnArea.Quantity, (_, _) => spawnArea.Quantity);
|
||||
}
|
||||
|
||||
this._configurationChangeMediator?.RegisterForNew<MonsterSpawnArea, GameMap>(createdMap, async (spawnArea, map) =>
|
||||
{
|
||||
if (!Equals(spawnArea.GameMap, map.Definition))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < spawnArea.Quantity; i++)
|
||||
{
|
||||
await this.InitializeSpawnAsync(i, map, spawnArea).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._spawnedMonsters.AddOrUpdate(spawnArea, spawnArea.Quantity, (_, _) => spawnArea.Quantity);
|
||||
});
|
||||
|
||||
this._logger.LogDebug("Finished creating monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the event NPCs of the previously created game map.
|
||||
/// </summary>
|
||||
/// <param name="createdMap">The created map.</param>
|
||||
/// <param name="eventStateProvider">The event state provider.</param>
|
||||
public async ValueTask InitializeNpcsOnEventStartAsync(GameMap createdMap, IEventStateProvider eventStateProvider)
|
||||
{
|
||||
_ = this.PlugInManager ?? throw new InvalidOperationException("PlugInManager must be set first");
|
||||
_ = this.PathFinderPool ?? throw new InvalidOperationException("PathFinderPool must be set first");
|
||||
|
||||
this._logger.LogDebug("Start creating event monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
var eventSpawns = createdMap.Definition.MonsterSpawns
|
||||
.Where(m => m.MonsterDefinition is not null)
|
||||
.Where(m => m.SpawnTrigger is SpawnTrigger.OnceAtEventStart or SpawnTrigger.AutomaticDuringEvent);
|
||||
|
||||
foreach (var spawnArea in eventSpawns)
|
||||
{
|
||||
for (int i = 0; i < spawnArea.Quantity; i++)
|
||||
{
|
||||
await this.InitializeSpawnAsync(i, createdMap, spawnArea, eventStateProvider).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
this._logger.LogDebug("Finished creating event monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeNpcsOnWaveStartAsync(GameMap createdMap, IEventStateProvider eventStateProvider, byte waveNumber)
|
||||
{
|
||||
_ = this.PlugInManager ?? throw new InvalidOperationException("PlugInManager must be set first");
|
||||
_ = this.PathFinderPool ?? throw new InvalidOperationException("PathFinderPool must be set first");
|
||||
|
||||
this._logger.LogDebug("Start creating event monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
var waveSpawns = createdMap.Definition.MonsterSpawns
|
||||
.Where(m => m.MonsterDefinition is not null)
|
||||
.Where(m => m.SpawnTrigger is SpawnTrigger.AutomaticDuringWave or SpawnTrigger.OnceAtWaveStart)
|
||||
.Where(m => m.WaveNumber == waveNumber);
|
||||
|
||||
foreach (var spawnArea in waveSpawns)
|
||||
{
|
||||
for (int i = 0; i < spawnArea.Quantity; i++)
|
||||
{
|
||||
await this.InitializeSpawnAsync(i, createdMap, spawnArea, eventStateProvider).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
this._logger.LogDebug("Finished creating event monster instances for map {createdMap}", createdMap.Definition.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<NonPlayerCharacter?> InitializeSpawnAsync(
|
||||
int spawnIndex,
|
||||
GameMap createdMap,
|
||||
MonsterSpawnArea spawnArea,
|
||||
IEventStateProvider? eventStateProvider = null,
|
||||
IDropGenerator? dropGenerator = null)
|
||||
{
|
||||
_ = this.PlugInManager ?? throw new InvalidOperationException("PlugInManager must be set first");
|
||||
_ = this.PathFinderPool ?? throw new InvalidOperationException("PathFinderPool must be set first");
|
||||
|
||||
var monsterDef = spawnArea.MonsterDefinition!;
|
||||
NonPlayerCharacter npc;
|
||||
|
||||
var intelligence = this.TryCreateConfiguredNpcIntelligence(monsterDef, createdMap);
|
||||
|
||||
if (monsterDef.ObjectKind == NpcObjectKind.Monster)
|
||||
{
|
||||
this._logger.LogDebug("Creating monster {spawn}", spawnArea);
|
||||
npc = new Monster(spawnArea, monsterDef, createdMap, dropGenerator ?? this._dropGenerator, intelligence ?? new BasicMonsterIntelligence(), this.PlugInManager, this.PathFinderPool, eventStateProvider);
|
||||
}
|
||||
else if (monsterDef.ObjectKind == NpcObjectKind.Guard)
|
||||
{
|
||||
this._logger.LogDebug("Creating guard {spawn}", spawnArea);
|
||||
npc = new Monster(spawnArea, monsterDef, createdMap, NullDropGenerator.Instance, intelligence ?? new GuardIntelligence(), this.PlugInManager, this.PathFinderPool, eventStateProvider);
|
||||
}
|
||||
else if (monsterDef.ObjectKind == NpcObjectKind.Trap)
|
||||
{
|
||||
this._logger.LogDebug("Creating trap {spawn}", spawnArea);
|
||||
npc = new Trap(spawnArea, monsterDef, createdMap, intelligence ?? new RandomAttackInRangeTrapIntelligence(createdMap));
|
||||
}
|
||||
else if (monsterDef.ObjectKind == NpcObjectKind.SoccerBall)
|
||||
{
|
||||
this._logger.LogDebug("Creating soccer ball {spawn}", spawnArea);
|
||||
npc = new SoccerBall(spawnArea, monsterDef, createdMap);
|
||||
}
|
||||
else if (monsterDef.ObjectKind == NpcObjectKind.Destructible)
|
||||
{
|
||||
this._logger.LogDebug("Creating destructible {spawn}", spawnArea);
|
||||
npc = new Destructible(spawnArea, monsterDef, createdMap, eventStateProvider, dropGenerator ?? this._dropGenerator, this.PlugInManager!);
|
||||
}
|
||||
else if (monsterDef.MerchantStore is not null)
|
||||
{
|
||||
this._logger.LogDebug("Creating merchant npc {spawn}", spawnArea);
|
||||
npc = new MerchantNpc(spawnArea, monsterDef, createdMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogDebug("Creating npc {spawn}", spawnArea);
|
||||
npc = new NonPlayerCharacter(spawnArea, monsterDef, createdMap);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
npc.SpawnIndex = spawnIndex;
|
||||
npc.Initialize();
|
||||
await createdMap.AddAsync(npc).ConfigureAwait(false);
|
||||
npc.OnSpawn();
|
||||
if (spawnArea.SpawnTrigger is SpawnTrigger.Automatic or SpawnTrigger.Wandering)
|
||||
{
|
||||
this.RegisterForConfigChanges(createdMap, spawnArea, npc);
|
||||
}
|
||||
|
||||
return npc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, $"Object {spawnArea} couldn't be initialized.", spawnArea);
|
||||
await npc.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the map definition by searching for it at the <see cref="GameConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <param name="mapNumber">The map number.</param>
|
||||
/// <returns>The game map definition.</returns>
|
||||
protected virtual GameMapDefinition? GetMapDefinition(ushort mapNumber)
|
||||
{
|
||||
return this._configuration.Maps.FirstOrDefault(m => m.Number == mapNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the game map instance with the specified definition.
|
||||
/// </summary>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <returns>
|
||||
/// The created game map instance.
|
||||
/// </returns>
|
||||
protected virtual GameMap InternalCreateGameMap(GameMapDefinition definition)
|
||||
{
|
||||
this._logger.LogDebug("Creating GameMap {0}", definition);
|
||||
return definition.BattleZone?.Type == BattleType.Soccer
|
||||
? new SoccerGameMap(definition, this._configuration.ItemDropDuration, this.ChunkSize)
|
||||
: new GameMap(definition, this._configuration.ItemDropDuration, this.ChunkSize);
|
||||
}
|
||||
|
||||
private void RegisterForConfigChanges(GameMap createdMap, MonsterSpawnArea spawnArea, NonPlayerCharacter spawnedObject)
|
||||
{
|
||||
// Apply changes of the monster definition (e.g. its attributes) instantly to the
|
||||
// already spawned instance, without having to re-spawn it. The registration is owned
|
||||
// by the NPC, so it gets disposed whenever the NPC is disposed.
|
||||
if (spawnedObject is AttackableNpcBase attackableNpc
|
||||
&& this._configurationChangeMediator?.RegisterObject(
|
||||
spawnedObject.Definition,
|
||||
attackableNpc,
|
||||
(_, _, o) =>
|
||||
{
|
||||
o.ReloadAttributes();
|
||||
return ValueTask.CompletedTask;
|
||||
}) is { } definitionRegistration)
|
||||
{
|
||||
attackableNpc.RegisterDisposable(definitionRegistration);
|
||||
}
|
||||
|
||||
this._configurationChangeMediator?.RegisterObject(
|
||||
spawnArea,
|
||||
spawnedObject,
|
||||
async (unregisterAction, area, o) =>
|
||||
{
|
||||
if (area.Quantity < o.SpawnIndex + 1)
|
||||
{
|
||||
await o.DisposeAsync().ConfigureAwait(false);
|
||||
unregisterAction();
|
||||
this._spawnedMonsters.AddOrUpdate(spawnArea, spawnArea.Quantity, (_, _) => spawnArea.Quantity);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the monster definition changed, we need to spawn a completely new NPC
|
||||
// because MonsterAttributeHolder caches stats from the definition at construction time.
|
||||
if (area.MonsterDefinition != o.Definition)
|
||||
{
|
||||
await o.DisposeAsync().ConfigureAwait(false);
|
||||
unregisterAction();
|
||||
var newNpc = await this.InitializeSpawnAsync(o.SpawnIndex, createdMap, area).ConfigureAwait(false);
|
||||
if (newNpc is not null)
|
||||
{
|
||||
this._spawnedMonsters.AddOrUpdate(spawnArea, spawnArea.Quantity, (_, _) => spawnArea.Quantity);
|
||||
this.RegisterForConfigChanges(createdMap, area, newNpc);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await createdMap.RemoveAsync(o).ConfigureAwait(false);
|
||||
o.Initialize();
|
||||
await createdMap.AddAsync(o).ConfigureAwait(false);
|
||||
o.OnSpawn();
|
||||
|
||||
if (this._spawnedMonsters.TryGetValue(area, out var previousSpawnCount))
|
||||
{
|
||||
for (int i = previousSpawnCount; i < area.Quantity; i++)
|
||||
{
|
||||
await this.InitializeSpawnAsync(i, createdMap, area).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._spawnedMonsters.AddOrUpdate(spawnArea, spawnArea.Quantity, (_, _) => spawnArea.Quantity);
|
||||
}
|
||||
},
|
||||
async (_, o) =>
|
||||
{
|
||||
await o.DisposeAsync().ConfigureAwait(false);
|
||||
this._spawnedMonsters.TryRemove(spawnArea, out var _);
|
||||
});
|
||||
|
||||
if (spawnedObject.Definition.MerchantStore is { } merchantStore)
|
||||
{
|
||||
this._configurationChangeMediator?.RegisterObject(merchantStore, spawnedObject, async (_, itemStorage, o) =>
|
||||
{
|
||||
await o.ForEachObservingAsync<Player>(
|
||||
async player =>
|
||||
{
|
||||
if (player.OpenedNpc == o)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowMerchantStoreItemListPlugIn>(
|
||||
plugin => plugin.ShowMerchantStoreItemListAsync(itemStorage.Items, StoreKind.Normal))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
},
|
||||
false).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private INpcIntelligence? TryCreateConfiguredNpcIntelligence(MonsterDefinition monsterDefinition, GameMap createdMap)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(monsterDefinition.IntelligenceTypeName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var type = Type.GetType(monsterDefinition.IntelligenceTypeName);
|
||||
if (type is null)
|
||||
{
|
||||
this._logger.LogError($"Could not find type {monsterDefinition.IntelligenceTypeName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var constructorNeedsMap = type.GetConstructors().Any(c => c.GetParameters().Any(p => p.ParameterType == typeof(GameMap)));
|
||||
if (constructorNeedsMap)
|
||||
{
|
||||
return Activator.CreateInstance(type, createdMap) as INpcIntelligence;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Activator.CreateInstance(type) as INpcIntelligence;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, $"Could not create npc intelligence for monster {monsterDefinition.Designation}, type name {monsterDefinition.IntelligenceTypeName}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
118
src/GameLogic/MasterSkillExtensions.cs
Normal file
118
src/GameLogic/MasterSkillExtensions.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
// <copyright file="MasterSkillExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using org.mariuszgromada.math.mxparser;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods regarding master skills.
|
||||
/// </summary>
|
||||
public static class MasterSkillExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, float[]> ValueResultCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the effective value of the specified skill, depending on its formula and level.
|
||||
/// </summary>
|
||||
/// <param name="skillEntry">The skill entry of the master skill.</param>
|
||||
/// <returns>The value of the specified skill, depending on its formula and level.</returns>
|
||||
public static float CalculateValue(this SkillEntry skillEntry) => skillEntry.Skill?.MasterDefinition?.CalculateValue(skillEntry.Level) ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the display value of the specified skill, depending on its formula and level.
|
||||
/// </summary>
|
||||
/// <param name="skillEntry">The skill entry of the master skill.</param>
|
||||
/// <returns>The value of the specified skill, depending on its formula and level.</returns>
|
||||
public static float CalculateDisplayValue(this SkillEntry skillEntry) => skillEntry.Skill?.MasterDefinition?.CalculateDisplayValue(skillEntry.Level) ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the next display value of the specified skill, depending on its formula and level.
|
||||
/// </summary>
|
||||
/// <param name="skillEntry">The skill entry of the master skill.</param>
|
||||
/// <returns>The value of the specified skill, depending on its formula and level.</returns>
|
||||
public static float CalculateNextDisplayValue(this SkillEntry skillEntry)
|
||||
{
|
||||
var level = Math.Min(skillEntry.Level + 1, skillEntry.Skill?.MasterDefinition?.MaximumLevel ?? 0);
|
||||
return skillEntry.Skill?.MasterDefinition.CalculateDisplayValue(level) ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base <see cref="Skill"/> of a <see cref="SkillEntry"/>.
|
||||
/// </summary>
|
||||
/// <param name="skillEntry">The <see cref="SkillEntry"/>.</param>
|
||||
/// <returns>The base <see cref="Skill"/>.</returns>
|
||||
public static Skill GetBaseSkill(this SkillEntry skillEntry)
|
||||
{
|
||||
return skillEntry.Skill!.GetBaseSkill();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base <see cref="Skill"/> of a <see cref="Skill"/>.
|
||||
/// </summary>
|
||||
/// <param name="skill">The <see cref="Skill"/>.</param>
|
||||
/// <returns>The base <see cref="Skill"/>.</returns>
|
||||
public static Skill GetBaseSkill(this Skill skill)
|
||||
{
|
||||
while (skill.MasterDefinition?.ReplacedSkill is { } replacedSkill)
|
||||
{
|
||||
skill = replacedSkill;
|
||||
}
|
||||
|
||||
return skill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base <see cref="Skill"/>s of a <see cref="Skill"/>.
|
||||
/// </summary>
|
||||
/// <param name="skill">The <see cref="Skill"/>.</param>
|
||||
/// <param name="onlyMasterSkills">If set to <c>true</c>, only master skills are returned.</param>
|
||||
/// <returns>The base <see cref="Skill"/>.</returns>
|
||||
public static IEnumerable<Skill> GetBaseSkills(this Skill skill, bool onlyMasterSkills = false)
|
||||
{
|
||||
while (skill.MasterDefinition?.ReplacedSkill is { } replacedSkill)
|
||||
{
|
||||
if (onlyMasterSkills && replacedSkill.MasterDefinition is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
else
|
||||
{
|
||||
skill = replacedSkill;
|
||||
yield return skill;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float CalculateValue(this MasterSkillDefinition? skillDefinition, int level) => skillDefinition?.ValueFormula.GetValue(level, skillDefinition.MaximumLevel) ?? 0;
|
||||
|
||||
private static float CalculateDisplayValue(this MasterSkillDefinition? skillDefinition, int level) => skillDefinition?.DisplayValueFormula.GetValue(level, skillDefinition.MaximumLevel) ?? 0;
|
||||
|
||||
private static float GetValue(this string formula, int level, int maximumLevel)
|
||||
{
|
||||
if (level <= 0 || level > maximumLevel)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (!ValueResultCache.TryGetValue(formula, out var results))
|
||||
{
|
||||
results = new float[maximumLevel];
|
||||
|
||||
var argument = new Argument("level", 0);
|
||||
var expression = new Expression(formula);
|
||||
expression.addArguments(argument);
|
||||
for (int i = 0; i < maximumLevel; i++)
|
||||
{
|
||||
argument.setArgumentValue(i + 1);
|
||||
results[i] = (float)expression.calculate();
|
||||
}
|
||||
|
||||
ValueResultCache.TryAdd(formula, results);
|
||||
}
|
||||
|
||||
return results[level - 1];
|
||||
}
|
||||
}
|
||||
27
src/GameLogic/Metrics.cs
Normal file
27
src/GameLogic/Metrics.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="Metrics.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.PlayerActions.Trade;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides information about the available metrics of this project.
|
||||
/// </summary>
|
||||
public static class Metrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all available meters of this project.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> Meters
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return GameContext.MeterName;
|
||||
yield return BaseTradeAction.MeterName;
|
||||
yield return PathFinder.MeterName;
|
||||
}
|
||||
}
|
||||
}
|
||||
375
src/GameLogic/MiniGames/BloodCastleContext.cs
Normal file
375
src/GameLogic/MiniGames/BloodCastleContext.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
// <copyright file="BloodCastleContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
using MUnique.OpenMU.GameLogic.Views.Character;
|
||||
using MUnique.OpenMU.GameLogic.Views.Inventory;
|
||||
using Nito.Disposables.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The context of a blood castle game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A blood castle event works like that:
|
||||
/// First, a player or his party (and probably another party), maximum 10 players, enter the blood castle.
|
||||
/// The game has several states:
|
||||
/// * After the event starts, first a certain amount of monsters have to be killed, so that a bridge appears on the way to the castle gate.
|
||||
/// * The castle gate has to be destroyed
|
||||
/// * Some stronger monsters wait after the castle gate. A certain amount of "Spirit Sorcerer" have to be killed.
|
||||
/// * The statue appears, which needs to be killed.
|
||||
/// * The statue drops an archangel weapon as quest item. This item has to be brought back to the archangel NPC.
|
||||
/// The game has a time limit of usually 15 or 20 minutes.
|
||||
/// After the time is up, or the quest item has been brought back, the players get some rewards:
|
||||
///
|
||||
/// Experience:
|
||||
/// 1. For each remaining second, a bonus experience is given as experience.
|
||||
/// 2. The player or party which destroyed the gate, gets extra experience. Dead party members get the half of the exp as bonus.
|
||||
/// 3. For killing the statue, the player/party gets a bonus experience.
|
||||
/// 4. For finishing the quest, the player/party gets a bonus experience.
|
||||
/// 5. To all previous exp rewards, bonuses from seals, maps etc. are applied.
|
||||
/// Money:
|
||||
/// According to the reward table - fixed values per blood castle level, depending if the player was in the winning party, or not.
|
||||
/// The winner/winners party get roughly the double money value.
|
||||
/// Score:
|
||||
/// a) If the event was won by any participant, depending on the individual success state of a player,
|
||||
/// it will get a different score rewarded. A player is categorized into these 5 states:
|
||||
/// - unfinished event
|
||||
/// - died during event
|
||||
/// - winner
|
||||
/// - member of winners party
|
||||
/// - member of winners party, but died during event.
|
||||
/// b) If the event wasn't won by any participant, players are getting a score penalty of 300.
|
||||
/// </remarks>
|
||||
public sealed class BloodCastleContext : MiniGameContext
|
||||
{
|
||||
private const short CastleGateNumber = 131;
|
||||
private const short StatueOfSaintNumber = 132;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog category for the NPC interactions.
|
||||
/// </summary>
|
||||
private const byte DialogCategoryMain = 1;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlayerGameState> _gameStates = new();
|
||||
|
||||
private IReadOnlyCollection<(string Name, int Score, int BonusExp, int BonusMoney)>? _highScoreTable;
|
||||
private TimeSpan _remainingTime;
|
||||
|
||||
private bool _gateDestroyed;
|
||||
|
||||
private Player? _winner;
|
||||
private Player? _questItemOwner;
|
||||
private Item? _questItem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BloodCastleContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of this context.</param>
|
||||
/// <param name="definition">The definition of the mini game.</param>
|
||||
/// <param name="gameContext">The game context, to which this game belongs.</param>
|
||||
/// <param name="mapInitializer">The map initializer, which is used when the event starts.</param>
|
||||
public BloodCastleContext(MiniGameMapKey key, MiniGameDefinition definition, IGameContext gameContext, IMapInitializer mapInitializer)
|
||||
: base(key, definition, gameContext, mapInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dialog numbers for different interactions with the NPC.
|
||||
/// </summary>
|
||||
private enum DialogNumber : byte
|
||||
{
|
||||
EventWinner = 0x17,
|
||||
EventNotRunning = 0x18,
|
||||
EventQuestItemMissing = 0x18,
|
||||
EventFinished = 0x2E,
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Player? Winner => this._winner;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan RemainingTime => this._remainingTime;
|
||||
|
||||
/// <summary>
|
||||
/// Player interact with Archangel.
|
||||
/// </summary>
|
||||
/// <param name="player">The player who talks to Archangel.</param>
|
||||
public async ValueTask TalkToNpcArchangelAsync(Player player)
|
||||
{
|
||||
if (this._winner is not null)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowDialogPlugIn>(p => p.ShowDialogAsync(DialogCategoryMain, (byte)DialogNumber.EventFinished)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.IsEventRunning)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowDialogPlugIn>(p => p.ShowDialogAsync(DialogCategoryMain, (byte)DialogNumber.EventNotRunning)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.TryRemoveQuestItemFromPlayerAsync(player).ConfigureAwait(false))
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowDialogPlugIn>(p => p.ShowDialogAsync(DialogCategoryMain, (byte)DialogNumber.EventQuestItemMissing)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._winner = player;
|
||||
await player.InvokeViewPlugInAsync<IShowDialogPlugIn>(p => p.ShowDialogAsync(DialogCategoryMain, (byte)DialogNumber.EventWinner)).ConfigureAwait(false);
|
||||
this.FinishEvent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask OnObjectRemovedFromMapAsync((GameMap Map, ILocateable Object) args)
|
||||
{
|
||||
if (args.Object is Player player)
|
||||
{
|
||||
if (this.IsEventRunning)
|
||||
{
|
||||
// Drop it, so that the remaining players can pick it up.
|
||||
await this.TryDropQuestItemFromPlayerAsync(player).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.TryRemoveQuestItemFromPlayerAsync(player).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this.UpdateStateAsync(BloodCastleStatus.Ended, player).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await base.OnObjectRemovedFromMapAsync(args).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")]
|
||||
protected override async void OnDestructibleDied(object? sender, DeathInformation e)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.OnDestructibleDied(sender, e);
|
||||
var destructible = sender as Destructible;
|
||||
if (destructible is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (destructible.Definition.Number == StatueOfSaintNumber)
|
||||
{
|
||||
await this.ShowGoldenMessageAsync(nameof(PlayerMessage.BloodCastleCrystalStatusDestroyed), e.KillerName).ConfigureAwait(false);
|
||||
}
|
||||
else if (destructible.Definition.Number == CastleGateNumber)
|
||||
{
|
||||
this._gateDestroyed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we don't have others, so nothing to do
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Unexpected error in OnDestructibleDied.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask OnPlayerPickedUpItemAsync((Player Picker, ILocateable DroppedItem) args)
|
||||
{
|
||||
await base.OnPlayerPickedUpItemAsync(args).ConfigureAwait(false);
|
||||
if (args.DroppedItem is DroppedItem { Item.Definition: { } definition } && definition.IsArchangelQuestItem())
|
||||
{
|
||||
this._questItemOwner = args.Picker;
|
||||
await this.ForEachPlayerAsync(player => player.ShowLocalizedGoldenMessageAsync(
|
||||
nameof(PlayerMessage.BloodCastleArchangelAquiredMessageFormat),
|
||||
args.Picker.Name,
|
||||
definition.Name.GetTranslation(player.Culture))
|
||||
.AsTask())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnGameStartAsync(ICollection<Player> players)
|
||||
{
|
||||
foreach (var player in players)
|
||||
{
|
||||
this._gameStates.TryAdd(player.Name, new PlayerGameState(player));
|
||||
}
|
||||
|
||||
_ = Task.Run(async () => await this.ShowRemainingTimeLoopAsync(this.GameEndedToken).ConfigureAwait(false), this.GameEndedToken);
|
||||
await base.OnGameStartAsync(players).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask GameEndedAsync(ICollection<Player> finishers)
|
||||
{
|
||||
await this.UpdateStateForAllAsync(BloodCastleStatus.Ended).ConfigureAwait(false);
|
||||
|
||||
var sortedFinishers = finishers
|
||||
.Select(f => this._gameStates[f.Name])
|
||||
.WhereNotNull()
|
||||
.OrderByDescending(state => state.Score)
|
||||
.ToList();
|
||||
|
||||
var scoreList = new List<(string Name, int Score, int BonusExp, int BonusMoney)>();
|
||||
int rank = 0;
|
||||
foreach (var state in sortedFinishers)
|
||||
{
|
||||
rank++;
|
||||
state.Rank = rank;
|
||||
var (bonusScore, givenMoney) = await this.GiveRewardsAndGetBonusScoreAsync(state.Player, rank).ConfigureAwait(false);
|
||||
state.AddScore(bonusScore);
|
||||
|
||||
scoreList.Add((
|
||||
state.Player.Name,
|
||||
state.Score,
|
||||
this.Definition.Rewards.FirstOrDefault(r => r.RewardType == MiniGameRewardType.Experience && (r.Rank is null || r.Rank == rank))?.RewardAmount ?? 0,
|
||||
givenMoney));
|
||||
|
||||
await this.TryRemoveQuestItemFromPlayerAsync(state.Player).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._highScoreTable = scoreList.AsReadOnly();
|
||||
|
||||
await this.SaveRankingAsync(sortedFinishers.Select(state => (state.Rank, state.Player.SelectedCharacter!, state.Score))).ConfigureAwait(false);
|
||||
await base.GameEndedAsync(finishers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ShowScoreAsync(Player player)
|
||||
{
|
||||
if (this._highScoreTable is { } table)
|
||||
{
|
||||
var isSuccessful = this._winner is not null;
|
||||
var (name, score, bonusMoney, bonusExp) = table.First(t => t.Name == player.Name);
|
||||
await player.InvokeViewPlugInAsync<IBloodCastleScoreTableViewPlugin>(p => p.ShowScoreTableAsync(isSuccessful, name, score, bonusExp, bonusMoney)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnItemDroppedOnMap(DroppedItem item)
|
||||
{
|
||||
base.OnItemDroppedOnMap(item);
|
||||
if (item.Item.Definition.IsArchangelQuestItem())
|
||||
{
|
||||
this._questItem = item.Item;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ShowRemainingTimeLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var timerInterval = TimeSpan.FromSeconds(1);
|
||||
using var timer = new PeriodicTimer(timerInterval);
|
||||
var maximumGameDuration = this.Definition.GameDuration;
|
||||
this._remainingTime = maximumGameDuration;
|
||||
|
||||
await this.UpdateStateForAllAsync(BloodCastleStatus.Started).ConfigureAwait(false);
|
||||
while (!cancellationToken.IsCancellationRequested
|
||||
&& this._remainingTime >= TimeSpan.Zero
|
||||
&& await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (this._remainingTime < maximumGameDuration && !this._gateDestroyed)
|
||||
{
|
||||
await this.UpdateStateForAllAsync(BloodCastleStatus.GateNotDestroyed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (this._remainingTime < maximumGameDuration && this._gateDestroyed)
|
||||
{
|
||||
await this.UpdateStateForAllAsync(BloodCastleStatus.GateDestroyed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._remainingTime = this._remainingTime.Subtract(timerInterval);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected exception when the game ends before running into the timeout.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Unexpected error during update blood castle status: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask UpdateStateForAllAsync(BloodCastleStatus status)
|
||||
{
|
||||
return this.ForEachPlayerAsync(player => this.UpdateStateAsync(status, player).AsTask());
|
||||
}
|
||||
|
||||
private ValueTask UpdateStateAsync(BloodCastleStatus status, Player player)
|
||||
{
|
||||
return player.InvokeViewPlugInAsync<IBloodCastleStateViewPlugin>(
|
||||
p =>
|
||||
p.UpdateStateAsync(
|
||||
status,
|
||||
this._remainingTime,
|
||||
this.NextEvent?.RequiredKills ?? 0,
|
||||
this.NextEvent?.ActualKills ?? 0,
|
||||
this._questItemOwner,
|
||||
this._questItem));
|
||||
}
|
||||
|
||||
private async ValueTask<bool> TryRemoveQuestItemFromPlayerAsync(Player player)
|
||||
{
|
||||
if (!player.TryGetQuestItem(out var item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await player.DestroyInventoryItemAsync(item).ConfigureAwait(false);
|
||||
|
||||
this._questItem = null;
|
||||
this._questItemOwner = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask TryDropQuestItemFromPlayerAsync(Player player)
|
||||
{
|
||||
if (!player.TryGetQuestItem(out var item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dropped = new DroppedItem(item, player.Position, this.Map, player);
|
||||
await this.Map.AddAsync(dropped).ConfigureAwait(false);
|
||||
await player.Inventory!.RemoveItemAsync(item).ConfigureAwait(false);
|
||||
await player.InvokeViewPlugInAsync<IItemDropResultPlugIn>(p => p.ItemDropResultAsync(item.ItemSlot, true)).ConfigureAwait(false);
|
||||
|
||||
this._questItemOwner = null;
|
||||
}
|
||||
|
||||
private sealed class PlayerGameState
|
||||
{
|
||||
private int _score;
|
||||
|
||||
public PlayerGameState(Player player)
|
||||
{
|
||||
if (player.SelectedCharacter?.CharacterClass is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The player '{player}' is in the wrong state");
|
||||
}
|
||||
|
||||
this.Player = player;
|
||||
}
|
||||
|
||||
public Player Player { get; }
|
||||
|
||||
public int Score => this._score;
|
||||
|
||||
public int Rank { get; set; }
|
||||
|
||||
public void AddScore(int value)
|
||||
{
|
||||
Interlocked.Add(ref this._score, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/GameLogic/MiniGames/BloodCastleItemExtensions.cs
Normal file
52
src/GameLogic/MiniGames/BloodCastleItemExtensions.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// <copyright file="BloodCastleItemExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Item-related extension methods for the blood castle event.
|
||||
/// </summary>
|
||||
public static class BloodCastleItemExtensions
|
||||
{
|
||||
private static readonly (short Group, short Number) ArchangelQuestItemId = (13, 19);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an item definition is that of the archangel quest item.
|
||||
/// </summary>
|
||||
/// <param name="itemDefinition">The item definition.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item definition is the archangel quest item; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsArchangelQuestItem(this ItemDefinition? itemDefinition)
|
||||
{
|
||||
return itemDefinition?.Group == ArchangelQuestItemId.Group && itemDefinition.Number == ArchangelQuestItemId.Number;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the item is the archangel quest item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified item is the archangel quest item; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsArchangelQuestItem(this Item item)
|
||||
{
|
||||
return item.Definition?.IsArchangelQuestItem() ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the quest item from the players inventory.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="item">The quest item.</param>
|
||||
/// <returns>The success.</returns>
|
||||
public static bool TryGetQuestItem(this Player player, [MaybeNullWhen(false)] out Item item)
|
||||
{
|
||||
item = player.Inventory!.Items.FirstOrDefault(i => i.IsArchangelQuestItem());
|
||||
return item is not null;
|
||||
}
|
||||
}
|
||||
663
src/GameLogic/MiniGames/ChaosCastleContext.cs
Normal file
663
src/GameLogic/MiniGames/ChaosCastleContext.cs
Normal file
@@ -0,0 +1,663 @@
|
||||
// <copyright file="ChaosCastleContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using Nito.Disposables.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The context of a chaos castle game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A chaos castle event works like that:
|
||||
/// First, one to 70 players enter the chaos castle.
|
||||
/// The map starts as a safezone.
|
||||
/// Then the event starts:
|
||||
/// * The map terrain is redefined of not being a safezone anymore.
|
||||
/// * The map is filled up to 100 objects with NPCs
|
||||
/// Then, these objects fight each other. If the player dies, the event is over for that player.
|
||||
/// If a npc dies, it doesn't re-spawn. There is a 50% chance, that a died monster explodes and moves players nearby (range 3)
|
||||
/// by <see cref="BlowOutDistance"/>, depending on the distance to the killed monster.
|
||||
/// During the game, the map is getting smaller (so-called "Trap Status") after a certain amount of objects died
|
||||
/// at the following number of remaining objects:
|
||||
/// * less than 40
|
||||
/// * less than 30
|
||||
/// * less than 20
|
||||
/// A player has won, if:
|
||||
/// 1) It's the last remaining object on the map
|
||||
/// 2) The time is up and it has the highest kill count
|
||||
/// Rewards: A jewel and/or an ancient item.
|
||||
///
|
||||
/// Different to the original chaos castle, we don't apply additional damage when players get moved.
|
||||
/// </remarks>
|
||||
public sealed class ChaosCastleContext : MiniGameContext
|
||||
{
|
||||
private const int MaxObjectsCount = 100;
|
||||
|
||||
private const int MonsterKillPoints = 2;
|
||||
private const int PlayerKillPoints = 1;
|
||||
|
||||
private static readonly (int Min, int Max)[] BlowOutDistance =
|
||||
{
|
||||
(3, 4),
|
||||
(3, 4),
|
||||
(2, 3),
|
||||
(0, 1),
|
||||
};
|
||||
|
||||
private readonly IGameContext _gameContext;
|
||||
private readonly IMapInitializer _mapInitializer;
|
||||
private readonly ConcurrentDictionary<string, PlayerGameState> _gameStates = new();
|
||||
private readonly ConcurrentDictionary<Monster, byte> _monsters = new();
|
||||
|
||||
private IReadOnlyCollection<(string Name, int Score, int BonusExp, int BonusMoney)>? _highScoreTable;
|
||||
private TimeSpan _remainingTime;
|
||||
private Player? _winner;
|
||||
private int _aliveMonstersCount;
|
||||
private ChaosCastleStatus _currentCastleStatus = ChaosCastleStatus.Running;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChaosCastleContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of this context.</param>
|
||||
/// <param name="definition">The definition of the mini game.</param>
|
||||
/// <param name="gameContext">The game context, to which this game belongs.</param>
|
||||
/// <param name="mapInitializer">The map initializer, which is used when the event starts.</param>
|
||||
public ChaosCastleContext(MiniGameMapKey key, MiniGameDefinition definition, IGameContext gameContext, IMapInitializer mapInitializer)
|
||||
: base(key, definition, gameContext, mapInitializer)
|
||||
{
|
||||
this._gameContext = gameContext;
|
||||
this._mapInitializer = mapInitializer;
|
||||
|
||||
this.Logger.LogDebug("Event {0} created, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool AllowPlayerKilling => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Player? Winner => this._winner;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan RemainingTime => this._remainingTime;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override int MinimumPlayerCount => 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsItemAllowedToEquip(Item item)
|
||||
{
|
||||
if (item.Definition is not { } definition)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (definition.Group, definition.Number)
|
||||
{
|
||||
case (13, 2): // Uniria
|
||||
case (13, 3): // Dino
|
||||
case (13, 37): // Fenrir
|
||||
return false;
|
||||
default:
|
||||
if (definition.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.TransformationSkin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return base.IsItemAllowedToEquip(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsSkillAllowed(Skill skill, Player attacker, IAttackable target)
|
||||
{
|
||||
if (!base.IsSkillAllowed(skill, attacker, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skill.SkillType == SkillType.SummonMonster)
|
||||
{
|
||||
// It's not allowed to summon a monster.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skill.SkillType == SkillType.Buff && target != attacker)
|
||||
{
|
||||
// It's only allowed to buff the own player.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask OnObjectRemovedFromMapAsync((GameMap Map, ILocateable Object) args)
|
||||
{
|
||||
if (args.Object is Player player)
|
||||
{
|
||||
await this.UpdateStateAsync(ChaosCastleStatus.Ended, player, 0).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await base.OnObjectRemovedFromMapAsync(args).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnTerrainChangingAsync(MiniGameChangeEvent changeEvent)
|
||||
{
|
||||
await base.OnTerrainChangingAsync(changeEvent).ConfigureAwait(false);
|
||||
this.Logger.LogDebug("Terrain changing by event index {0}. Event: {1}, game id {2}", changeEvent.Index, this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
switch (changeEvent.Index)
|
||||
{
|
||||
case 1:
|
||||
this._currentCastleStatus = ChaosCastleStatus.RunningShrinkingStageOne;
|
||||
break;
|
||||
case 2:
|
||||
this._currentCastleStatus = ChaosCastleStatus.RunningShrinkingStageTwo;
|
||||
break;
|
||||
case 3:
|
||||
this._currentCastleStatus = ChaosCastleStatus.RunningShrinkingStageThree;
|
||||
break;
|
||||
default:
|
||||
// no action required
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnTerrainChangedAsync(MiniGameChangeEvent changeEvent)
|
||||
{
|
||||
await base.OnTerrainChangedAsync(changeEvent).ConfigureAwait(false);
|
||||
|
||||
if (changeEvent.TerrainChanges.All(c => c.TerrainAttribute != TerrainAttributeType.NoGround))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this.CheckForFallenUsersAsync().ConfigureAwait(false);
|
||||
await this.PullMonstersAsync(changeEvent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnGameStartAsync(ICollection<Player> players)
|
||||
{
|
||||
this.Logger.LogDebug("Starting the game... Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
foreach (var player in players)
|
||||
{
|
||||
this._gameStates.TryAdd(player.Name, new PlayerGameState(player));
|
||||
}
|
||||
|
||||
await base.OnGameStartAsync(players).ConfigureAwait(false);
|
||||
|
||||
await this.SpawnMonstersAsync().ConfigureAwait(false);
|
||||
|
||||
_ = Task.Run(async () => await this.EventLoopAsync(this.GameEndedToken).ConfigureAwait(false), this.GameEndedToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask GameEndedAsync(ICollection<Player> finishers)
|
||||
{
|
||||
this.Logger.LogDebug("Game ended... Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
this._currentCastleStatus = ChaosCastleStatus.Ended;
|
||||
await this.UpdateStateForAllAsync().ConfigureAwait(false);
|
||||
|
||||
var sortedFinishers = finishers
|
||||
.Select(f => this._gameStates[f.Name])
|
||||
.WhereNotNull()
|
||||
.OrderByDescending(state => state.Score)
|
||||
.ToList();
|
||||
|
||||
var scoreList = new List<(string Name, int Score, int BonusExp, int BonusMoney)>();
|
||||
int rank = 0;
|
||||
foreach (var state in sortedFinishers)
|
||||
{
|
||||
this._winner ??= state.Player;
|
||||
rank++;
|
||||
state.Rank = rank;
|
||||
var (bonusScore, givenMoney) = await this.GiveRewardsAndGetBonusScoreAsync(state.Player, rank).ConfigureAwait(false);
|
||||
state.AddScore(bonusScore);
|
||||
|
||||
scoreList.Add((
|
||||
state.Player.Name,
|
||||
state.Score,
|
||||
this.Definition.Rewards.FirstOrDefault(r => r.RewardType == MiniGameRewardType.Experience && (r.Rank is null || r.Rank == rank))?.RewardAmount ?? 0,
|
||||
givenMoney));
|
||||
}
|
||||
|
||||
this._highScoreTable = scoreList.AsReadOnly();
|
||||
|
||||
await this.SaveRankingAsync(sortedFinishers.Select(state => (state.Rank, state.Player.SelectedCharacter!, state.Score))).ConfigureAwait(false);
|
||||
await base.GameEndedAsync(finishers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ShowScoreAsync(Player player)
|
||||
{
|
||||
if (this._highScoreTable is { } table)
|
||||
{
|
||||
var isSuccessful = this._winner is not null;
|
||||
var (name, score, bonusMoney, bonusExp) = table.First(t => t.Name == player.Name);
|
||||
await player.InvokeViewPlugInAsync<IBloodCastleScoreTableViewPlugin>(p => p.ShowScoreTableAsync(isSuccessful, name, score, bonusExp, bonusMoney)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnPlayerDied(object? sender, DeathInformation e)
|
||||
{
|
||||
base.OnPlayerDied(sender, e);
|
||||
|
||||
if (this._gameStates.TryGetValue(e.KillerName, out var playerState))
|
||||
{
|
||||
playerState.AddScore(PlayerKillPoints);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
/// <inheritdoc />
|
||||
protected override async void OnMonsterDied(object? sender, DeathInformation e)
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
{
|
||||
try
|
||||
{
|
||||
Interlocked.Decrement(ref this._aliveMonstersCount);
|
||||
base.OnMonsterDied(sender, e);
|
||||
if (sender is not Monster monster)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._monsters.Remove(monster, out _);
|
||||
|
||||
if (this._gameStates.TryGetValue(e.KillerName, out var playerState))
|
||||
{
|
||||
playerState.AddScore(MonsterKillPoints);
|
||||
}
|
||||
|
||||
// There is a 50 % chance, that a died monster explodes and moves players nearby(range 3) by one coordinate.
|
||||
if (Rand.NextRandomBool(50))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = monster.Position;
|
||||
var playersInRange = monster.CurrentMap.GetAttackablesInRange(position, 3).OfType<Player>();
|
||||
foreach (var player in playersInRange)
|
||||
{
|
||||
await this.MovePlayerByExplosionAsync(player, position).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Unexpected Error when handling a monster death.");
|
||||
}
|
||||
}
|
||||
|
||||
// see also BlowObjsFromPoint
|
||||
private async ValueTask MovePlayerByExplosionAsync(Player player, Point explosionPosition)
|
||||
{
|
||||
var playerPosition = player.Position;
|
||||
var distance = (int)playerPosition.EuclideanDistanceTo(explosionPosition);
|
||||
if (distance > 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int directionX;
|
||||
int directionY;
|
||||
if (playerPosition.X > explosionPosition.X)
|
||||
{
|
||||
directionX = 1;
|
||||
}
|
||||
else if (playerPosition.X < explosionPosition.X)
|
||||
{
|
||||
directionX = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
directionX = Rand.NextRandomBool() ? 1 : -1;
|
||||
}
|
||||
|
||||
if (playerPosition.Y > explosionPosition.Y)
|
||||
{
|
||||
directionY = 1;
|
||||
}
|
||||
else if (playerPosition.Y < explosionPosition.Y)
|
||||
{
|
||||
directionY = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
directionY = Rand.NextRandomBool() ? 1 : -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var (min, max) = BlowOutDistance[distance];
|
||||
var blowX = Rand.NextInt(min, max + 1);
|
||||
var blowY = Rand.NextInt(min, max + 1);
|
||||
if (Rand.NextRandomBool())
|
||||
{
|
||||
if (blowX >= max)
|
||||
{
|
||||
blowX = max;
|
||||
blowY = min - Rand.NextInt(0, 2);
|
||||
}
|
||||
}
|
||||
else if (blowY >= max)
|
||||
{
|
||||
blowY = max;
|
||||
blowX = min - Rand.NextInt(0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// leave the random values like they are
|
||||
}
|
||||
|
||||
blowX = Math.Max(0, blowX);
|
||||
blowY = Math.Max(0, blowY);
|
||||
|
||||
var targetX = playerPosition.X + (blowX * directionX);
|
||||
var targetY = playerPosition.Y + (blowY * directionY);
|
||||
targetX = Math.Max(0, targetX);
|
||||
targetX = Math.Min(0xFF, targetX);
|
||||
targetY = Math.Max(0, targetY);
|
||||
targetY = Math.Min(0xFF, targetY);
|
||||
|
||||
var targetPoint = new Point((byte)targetX, (byte)targetY);
|
||||
var moved = await this.SetPlayerPositionAsync(player, targetPoint).ConfigureAwait(false);
|
||||
if (moved)
|
||||
{
|
||||
this.Logger.LogDebug("Player {player} was moved to {targetPoint}.", player, targetPoint);
|
||||
await this.CheckPlayerPositionAsync(player).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> SetPlayerPositionAsync(Player player, Point point)
|
||||
{
|
||||
if (this.Map != player.CurrentMap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (player.IsTeleporting)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await player.MoveAsync(point).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask EventLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Logger.LogDebug("Starting event loop... Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
var timerInterval = TimeSpan.FromSeconds(1);
|
||||
using var timer = new PeriodicTimer(timerInterval);
|
||||
var maximumGameDuration = this.Definition.GameDuration;
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(maximumGameDuration);
|
||||
var ending = DateTime.UtcNow.Add(maximumGameDuration);
|
||||
this._remainingTime = maximumGameDuration;
|
||||
this._currentCastleStatus = ChaosCastleStatus.Started;
|
||||
await this.UpdateStateForAllAsync().ConfigureAwait(false);
|
||||
this._currentCastleStatus = ChaosCastleStatus.Running;
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await this.UpdateStateForAllAsync().ConfigureAwait(false);
|
||||
|
||||
if (this.HasGameEnded())
|
||||
{
|
||||
this.FinishEvent();
|
||||
break;
|
||||
}
|
||||
|
||||
this._remainingTime = ending.Subtract(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
this._remainingTime = TimeSpan.Zero;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
// Expected exception when the game ends before running into the timeout.
|
||||
this.Logger.LogDebug(ex, "Stopped Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Unexpected error during update chaos castle status: {0}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._remainingTime = TimeSpan.Zero;
|
||||
this._currentCastleStatus = ChaosCastleStatus.Ended;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasGameEnded()
|
||||
{
|
||||
var playerCount = this.PlayerCount;
|
||||
if (playerCount <= 0)
|
||||
{
|
||||
this.Logger.LogDebug("Game ended - all players dead. Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (playerCount == 1 && this._aliveMonstersCount == 0)
|
||||
{
|
||||
this.Logger.LogDebug("Game ended - last player remaining. Event: {0}, game id {1}", this.Definition.Name, (this._gameContext as IGameServerContext)?.Id ?? 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async ValueTask CheckForFallenUsersAsync()
|
||||
{
|
||||
// Players which are on a terrain without ground, fall down
|
||||
await this.ForEachPlayerAsync(this.CheckPlayerPositionAsync).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CheckPlayerPositionAsync(Player player)
|
||||
{
|
||||
var position = player.Position;
|
||||
var terrainIsWalkable = this.Map.Terrain.WalkMap[position.X, position.Y];
|
||||
if (terrainIsWalkable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogDebug("Player {0} is at a blocked position, it will be killed instantly.", player);
|
||||
await player.KillInstantlyAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask UpdateStateForAllAsync()
|
||||
{
|
||||
var objectCount = this._aliveMonstersCount + this.PlayerCount;
|
||||
var currentStatus = this._currentCastleStatus;
|
||||
this.Logger.LogDebug("UpdateState {0} for all players. Object Count: {1}", currentStatus, objectCount);
|
||||
await this.ForEachPlayerAsync(player => this.UpdateStateAsync(currentStatus, player, objectCount)).ConfigureAwait(false);
|
||||
if (currentStatus is ChaosCastleStatus.RunningShrinkingStageOne or ChaosCastleStatus.RunningShrinkingStageTwo or ChaosCastleStatus.RunningShrinkingStageThree)
|
||||
{
|
||||
await this.ForEachPlayerAsync(player => this.UpdateStateAsync(ChaosCastleStatus.Running, player, objectCount)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStateAsync(ChaosCastleStatus status, Player player, int objectCount)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IChaosCastleStateViewPlugin>(
|
||||
p =>
|
||||
p.UpdateStateAsync(
|
||||
status,
|
||||
this._remainingTime,
|
||||
MaxObjectsCount,
|
||||
objectCount))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SpawnMonstersAsync()
|
||||
{
|
||||
var requiredMonsters = MaxObjectsCount - this.PlayerCount;
|
||||
|
||||
this.Logger.LogDebug("{0}: Spawning {1} monsters...", this.Definition.Description, requiredMonsters);
|
||||
var spawnAreas = this.Map.Definition.MonsterSpawns.AsList();
|
||||
this.DropGenerator = new ChaosCastleDropGenerator(this._gameContext, this, requiredMonsters);
|
||||
|
||||
for (var i = 0; i < requiredMonsters && i < spawnAreas.Count; i++)
|
||||
{
|
||||
var spawnArea = spawnAreas[i];
|
||||
|
||||
if (await this._mapInitializer.InitializeSpawnAsync(i, this.Map, spawnArea, this, this.DropGenerator).ConfigureAwait(false) is Monster monster)
|
||||
{
|
||||
this._monsters.TryAdd(monster, default);
|
||||
}
|
||||
}
|
||||
|
||||
this._aliveMonstersCount = requiredMonsters;
|
||||
this.Logger.LogDebug("Monsters created.");
|
||||
}
|
||||
|
||||
private async ValueTask PullMonstersAsync(MiniGameChangeEvent changeEvent)
|
||||
{
|
||||
// Determine middle:
|
||||
var minX = changeEvent.TerrainChanges.Min(c => c.StartX);
|
||||
var maxX = changeEvent.TerrainChanges.Max(c => c.EndX);
|
||||
var minY = changeEvent.TerrainChanges.Min(c => c.StartY);
|
||||
var maxY = changeEvent.TerrainChanges.Max(x => x.EndY);
|
||||
|
||||
var middlePoint = new Point((byte)((minX + maxX) / 2), (byte)((minY + maxY) / 2));
|
||||
|
||||
var walkMap = this.Map.Terrain.WalkMap;
|
||||
|
||||
var monstersNeedPull = this._monsters.Keys
|
||||
.Where(m => !walkMap[m.Position.X, m.Position.Y])
|
||||
.Where(m => m.IsAlive);
|
||||
|
||||
bool MoveByX(byte distance, ref Point resultTarget, bool setTargetAnyway = false)
|
||||
{
|
||||
var offset = new Point(0, distance);
|
||||
var target = resultTarget;
|
||||
if (target.Y < middlePoint.Y)
|
||||
{
|
||||
target += offset;
|
||||
}
|
||||
else if (target.Y > middlePoint.Y)
|
||||
{
|
||||
target -= offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're already good to go!
|
||||
}
|
||||
|
||||
return EvaluateTarget(target, ref resultTarget, setTargetAnyway);
|
||||
}
|
||||
|
||||
bool MoveByY(byte distance, ref Point resultTarget, bool setTargetAnyway = false)
|
||||
{
|
||||
var offset = new Point(distance, 0);
|
||||
var target = resultTarget;
|
||||
if (target.X < middlePoint.X)
|
||||
{
|
||||
target += offset;
|
||||
}
|
||||
else if (target.X > middlePoint.X)
|
||||
{
|
||||
target -= offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're already good to go!
|
||||
}
|
||||
|
||||
return EvaluateTarget(target, ref resultTarget, setTargetAnyway);
|
||||
}
|
||||
|
||||
bool MoveByXandY(byte distance, ref Point resultTarget)
|
||||
{
|
||||
var target = resultTarget;
|
||||
MoveByX(distance, ref target, true);
|
||||
MoveByY(distance, ref target, true);
|
||||
return EvaluateTarget(target, ref resultTarget, false);
|
||||
}
|
||||
|
||||
bool EvaluateTarget(Point target, ref Point resultTarget, bool setTargetAnyway)
|
||||
{
|
||||
if (walkMap[target.X, target.Y])
|
||||
{
|
||||
resultTarget = target;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (setTargetAnyway)
|
||||
{
|
||||
resultTarget = target;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var monster in monstersNeedPull)
|
||||
{
|
||||
var moved = false;
|
||||
for (byte dist = 1; dist <= 5; dist++)
|
||||
{
|
||||
var target = monster.Position;
|
||||
if (MoveByX(dist, ref target)
|
||||
|| MoveByY(dist, ref target)
|
||||
|| MoveByXandY(dist, ref target))
|
||||
{
|
||||
this.Logger.LogDebug("Moving {monster} by {dist} steps to {target}", monster, dist, target);
|
||||
await monster.MoveAsync(target).ConfigureAwait(false);
|
||||
moved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!moved)
|
||||
{
|
||||
this.Logger.LogDebug("Couldn't move monster {monster} to valid coordinate.", monster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PlayerGameState
|
||||
{
|
||||
private int _score;
|
||||
|
||||
public PlayerGameState(Player player)
|
||||
{
|
||||
if (player.SelectedCharacter?.CharacterClass is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The player '{player}' is in the wrong state");
|
||||
}
|
||||
|
||||
this.Player = player;
|
||||
}
|
||||
|
||||
public Player Player { get; }
|
||||
|
||||
public int Score => this._score;
|
||||
|
||||
public int Rank { get; set; }
|
||||
|
||||
public void AddScore(int value)
|
||||
{
|
||||
Interlocked.Add(ref this._score, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/GameLogic/MiniGames/ChaosCastleDropGenerator.cs
Normal file
95
src/GameLogic/MiniGames/ChaosCastleDropGenerator.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
// <copyright file="ChaosCastleDropGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IDropGenerator"/> especially for Chaos Castle.
|
||||
/// It will drop a predefined amount of jewels during the whole event,
|
||||
/// and nothing more.
|
||||
/// </summary>
|
||||
public class ChaosCastleDropGenerator : IDropGenerator
|
||||
{
|
||||
private static readonly IImmutableList<(int Blesses, int Souls)> MonsterJewelDropsPerLevel = new List<(int Blesses, int Souls)>
|
||||
{
|
||||
new(0, 0), // Dummy
|
||||
new(0, 2), // Chaos Castle 1
|
||||
new(1, 1), // Chaos Castle 2
|
||||
new(1, 2), // Chaos Castle 3
|
||||
new(1, 2), // Chaos Castle 4
|
||||
new(2, 1), // Chaos Castle 5
|
||||
new(2, 2), // Chaos Castle 6
|
||||
new(2, 3), // Chaos Castle 7
|
||||
}.ToImmutableList();
|
||||
|
||||
private readonly Dictionary<int, ItemDefinition> _monsterDrops = new();
|
||||
private readonly IGameContext _gameContext;
|
||||
private int _killedMonsters;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChaosCastleDropGenerator"/> class.
|
||||
/// </summary>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="spawnedMonstersCount">The spawned monsters count.</param>
|
||||
public ChaosCastleDropGenerator(IGameContext gameContext, ChaosCastleContext context, int spawnedMonstersCount)
|
||||
{
|
||||
this._gameContext = gameContext;
|
||||
|
||||
// TODO: This should rather be configurable...
|
||||
var blessDefinition = this._gameContext.Configuration.Items.First(item => item is { Group: 14, Number: 13 });
|
||||
var soulDefinition = this._gameContext.Configuration.Items.First(item => item is { Group: 14, Number: 14 });
|
||||
var drops = MonsterJewelDropsPerLevel[context.Definition.GameLevel];
|
||||
AddItems(drops.Blesses, blessDefinition);
|
||||
AddItems(drops.Souls, soulDefinition);
|
||||
|
||||
void AddItems(int count, ItemDefinition definition)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
bool assigned;
|
||||
do
|
||||
{
|
||||
var randomKillCount = Rand.NextInt(1, spawnedMonstersCount + 1);
|
||||
assigned = this._monsterDrops.TryAdd(randomKillCount, definition);
|
||||
}
|
||||
while (!assigned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<(IEnumerable<Item> Items, uint? Money)> GenerateItemDropsAsync(MonsterDefinition monster, int gainedExperience, Player player)
|
||||
{
|
||||
var killCount = Interlocked.Increment(ref this._killedMonsters);
|
||||
if (this._monsterDrops.TryGetValue(killCount, out var drop))
|
||||
{
|
||||
var item = new TemporaryItem
|
||||
{
|
||||
Definition = drop,
|
||||
Durability = 1,
|
||||
};
|
||||
|
||||
return (item.GetAsEnumerable(), null);
|
||||
}
|
||||
|
||||
return (Enumerable.Empty<Item>(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Item? GenerateItemDrop(DropItemGroup group)
|
||||
{
|
||||
return this._gameContext.DropGenerator.GenerateItemDrop(group);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public (Item? Item, uint? Money, ItemDropEffect DropEffect) GenerateItemDrop(IEnumerable<DropItemGroup> groups)
|
||||
{
|
||||
return this._gameContext.DropGenerator.GenerateItemDrop(groups);
|
||||
}
|
||||
}
|
||||
41
src/GameLogic/MiniGames/ChaosCastleStatus.cs
Normal file
41
src/GameLogic/MiniGames/ChaosCastleStatus.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// <copyright file="ChaosCastleStatus.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
/// <summary>
|
||||
/// The status of a blood castle event.
|
||||
/// </summary>
|
||||
public enum ChaosCastleStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The event has just started and is running.
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running, no terrain shrinking applied yet.
|
||||
/// </summary>
|
||||
Running,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running, first terrain shrinking applied.
|
||||
/// </summary>
|
||||
RunningShrinkingStageOne,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running, second terrain shrinking applied.
|
||||
/// </summary>
|
||||
RunningShrinkingStageTwo,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running, third (final) terrain shrinking applied.
|
||||
/// </summary>
|
||||
RunningShrinkingStageThree,
|
||||
|
||||
/// <summary>
|
||||
/// The event has ended.
|
||||
/// </summary>
|
||||
Ended,
|
||||
}
|
||||
121
src/GameLogic/MiniGames/DevilSquareContext.cs
Normal file
121
src/GameLogic/MiniGames/DevilSquareContext.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// <copyright file="DevilSquareContext.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using Nito.Disposables.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The context of a devil square game.
|
||||
/// </summary>
|
||||
public sealed class DevilSquareContext : MiniGameContext
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PlayerGameState> _gameStates = new();
|
||||
|
||||
private IReadOnlyCollection<(string Name, int Score, int BonusMoney, int BonusExp)>? _highScoreTable;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DevilSquareContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of this context.</param>
|
||||
/// <param name="definition">The definition of the mini game.</param>
|
||||
/// <param name="gameContext">The game context, to which this game belongs.</param>
|
||||
/// <param name="mapInitializer">The map initializer, which is used when the event starts.</param>
|
||||
public DevilSquareContext(MiniGameMapKey key, MiniGameDefinition definition, IGameContext gameContext, IMapInitializer mapInitializer)
|
||||
: base(key, definition, gameContext, mapInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMonsterDied(object? sender, DeathInformation e)
|
||||
{
|
||||
base.OnMonsterDied(sender, e);
|
||||
|
||||
if (this._gameStates.TryGetValue(e.KillerName, out var state))
|
||||
{
|
||||
state.AddScore(this.Definition.GameLevel);
|
||||
|
||||
// todo add money? -> in the original servers, in DS drops no money!
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnGameStartAsync(ICollection<Player> players)
|
||||
{
|
||||
foreach (var player in players)
|
||||
{
|
||||
this._gameStates.TryAdd(player.Name, new PlayerGameState(player));
|
||||
}
|
||||
|
||||
await base.OnGameStartAsync(players).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask GameEndedAsync(ICollection<Player> finishers)
|
||||
{
|
||||
var sortedFinishers = finishers
|
||||
.Select(f => this._gameStates[f.Name])
|
||||
.WhereNotNull()
|
||||
.OrderBy(state => state.Score)
|
||||
.ToList();
|
||||
|
||||
var scoreList = new List<(string Name, int Score, int BonusMoney, int BonusExp)>();
|
||||
int rank = 0;
|
||||
foreach (var state in sortedFinishers)
|
||||
{
|
||||
rank++;
|
||||
state.Rank = rank;
|
||||
var (bonusScore, givenMoney) = await this.GiveRewardsAndGetBonusScoreAsync(state.Player, rank).ConfigureAwait(false);
|
||||
state.AddScore(bonusScore);
|
||||
scoreList.Add((
|
||||
state.Player.Name,
|
||||
state.Score,
|
||||
givenMoney,
|
||||
this.Definition.Rewards.FirstOrDefault(r => r.RewardType == MiniGameRewardType.Experience && (r.Rank is null || r.Rank == rank))?.RewardAmount ?? 0));
|
||||
}
|
||||
|
||||
this._highScoreTable = scoreList.AsReadOnly();
|
||||
|
||||
await this.SaveRankingAsync(sortedFinishers.Select(state => (state.Rank, state.Player.SelectedCharacter!, state.Score))).ConfigureAwait(false);
|
||||
await base.GameEndedAsync(finishers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ShowScoreAsync(Player player)
|
||||
{
|
||||
if (this._highScoreTable is { } table
|
||||
&& this._gameStates.TryGetValue(player.Name, out var state))
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IMiniGameScoreTableViewPlugin>(p => p.ShowScoreTableAsync((byte)state.Rank, table)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PlayerGameState
|
||||
{
|
||||
private int _score;
|
||||
|
||||
public PlayerGameState(Player player)
|
||||
{
|
||||
if (player.SelectedCharacter?.CharacterClass is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The player '{player}' is in the wrong state");
|
||||
}
|
||||
|
||||
this.Player = player;
|
||||
}
|
||||
|
||||
public Player Player { get; }
|
||||
|
||||
public int Score => this._score;
|
||||
|
||||
public int Rank { get; set; }
|
||||
|
||||
public void AddScore(int value)
|
||||
{
|
||||
Interlocked.Add(ref this._score, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/GameLogic/MiniGames/IBloodCastleScoreTableViewPlugin.cs
Normal file
23
src/GameLogic/MiniGames/IBloodCastleScoreTableViewPlugin.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// <copyright file="IBloodCastleScoreTableViewPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of a view whose implementation informs about the score table of a blood castle event.
|
||||
/// </summary>
|
||||
public interface IBloodCastleScoreTableViewPlugin : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the score table to the player.
|
||||
/// </summary>
|
||||
/// <param name="success">The success.</param>
|
||||
/// <param name="playerName">The player name.</param>
|
||||
/// <param name="totalScore">The total score.</param>
|
||||
/// <param name="bonusExp">The bonus experience.</param>
|
||||
/// <param name="bonusMoney">The bonus money.</param>
|
||||
ValueTask ShowScoreTableAsync(bool success, string playerName, int totalScore, int bonusExp, int bonusMoney);
|
||||
}
|
||||
50
src/GameLogic/MiniGames/IBloodCastleStateViewPlugin.cs
Normal file
50
src/GameLogic/MiniGames/IBloodCastleStateViewPlugin.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// <copyright file="IBloodCastleStateViewPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The status of a blood castle event.
|
||||
/// </summary>
|
||||
public enum BloodCastleStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The event has just started and is running.
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running, but the gate is not destroyed.
|
||||
/// </summary>
|
||||
GateNotDestroyed,
|
||||
|
||||
/// <summary>
|
||||
/// The event is running and the gate is destroyed.
|
||||
/// </summary>
|
||||
GateDestroyed,
|
||||
|
||||
/// <summary>
|
||||
/// The event has ended.
|
||||
/// </summary>
|
||||
Ended,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface of a view whose implementation informs about the status of a blood castle event.
|
||||
/// </summary>
|
||||
public interface IBloodCastleStateViewPlugin : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Update the state of the blood castle event.
|
||||
/// </summary>
|
||||
/// <param name="status">The status of the blood castle event.</param>
|
||||
/// <param name="remainingTime">The remaining time of the blood castle event.</param>
|
||||
/// <param name="maxMonster">Maximum number of monsters to kill.</param>
|
||||
/// <param name="curMonster">Current number of monsters killed.</param>
|
||||
/// <param name="questItemOwner">The player which picked up the quest item.</param>
|
||||
/// <param name="questItem">The quest item which was dropped by the statue.</param>
|
||||
ValueTask UpdateStateAsync(BloodCastleStatus status, TimeSpan remainingTime, int maxMonster, int curMonster, IIdentifiable? questItemOwner, Item? questItem);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// <copyright file="IChangeTerrainAttributesViewPlugin.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for view plugins which update the terrain attributes.
|
||||
/// </summary>
|
||||
public interface IChangeTerrainAttributesViewPlugin : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the terrain attributes.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The type of terrain attribute.</param>
|
||||
/// <param name="setAttribute">Specifies, if the attribute should be set (true), or removed (false).</param>
|
||||
/// <param name="areas">The areas of terrain.</param>
|
||||
ValueTask ChangeAttributesAsync(TerrainAttributeType attribute, bool setAttribute, IReadOnlyCollection<(byte StartX, byte StartY, byte EndX, byte EndY)> areas);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user