//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic.Attributes;
using System.Collections.Concurrent;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.NPC;
///
/// The attribute system for monsters, which is considering monster definitions.
///
public class MonsterAttributeHolder : IAttributeSystem
{
private static readonly IDictionary> StatMapping =
new Dictionary>
{
{ Stats.CurrentHealth, m => m.Health },
{ Stats.DefensePvm, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) * (1 + ((m as Monster)?.SummonedBy?.Attributes?[Stats.SummonedMonsterDefenseIncrease] ?? 0)) },
{ Stats.DefensePvp, m => m.Attributes.GetValueOfAttribute(Stats.DefenseBase) * (1 + ((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> SetterMapping =
new Dictionary>
{
{ Stats.CurrentHealth, (m, v) => m.Health = (int)v },
};
private static readonly ConcurrentDictionary> MonsterStatAttributesCache = new();
private readonly AttackableNpcBase _monster;
private readonly object _attributesLock = new();
private IDictionary _statAttributes;
///
/// Attribute dictionary of a monster instance.
/// Most monster instances don't have additional attributes, so we just instantiate one if needed.
///
private IDictionary? _attributes;
///
/// Initializes a new instance of the class.
///
/// The monster.
public MonsterAttributeHolder(AttackableNpcBase monster)
{
this._monster = monster;
this._statAttributes = GetStatAttributeOfMonster(monster.Definition);
}
///
public float this[AttributeDefinition attributeDefinition]
{
get => this.GetValueOfAttribute(attributeDefinition);
set
{
if (SetterMapping.TryGetValue(attributeDefinition, out var setAction))
{
setAction(this._monster, value);
}
}
}
///
public float GetValueOfAttribute(AttributeDefinition attributeDefinition)
{
IDictionary? 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;
}
///
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);
}
///
public void RemoveElement(IElement element, AttributeDefinition targetAttribute)
{
IDictionary? 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;
}
}
}
///
public void AddAttributeRelationship(AttributeRelationship combination, IAttributeSystem sourceAttributeHolder, AggregateType aggregateType)
{
throw new NotImplementedException();
}
///
public IElement GetOrCreateAttribute(AttributeDefinition attributeDefinition)
{
throw new NotImplementedException();
}
///
/// Reloads the stat attributes from the (possibly changed) ,
/// so that configuration changes take effect on the running game server.
///
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 GetStatAttributeOfMonster(MonsterDefinition monsterDefinition)
{
return MonsterStatAttributesCache.GetOrAdd(monsterDefinition, BuildStatAttributes);
}
private static IDictionary BuildStatAttributes(MonsterDefinition monsterDefinition)
{
return monsterDefinition.Attributes.ToDictionary(
m => m.AttributeDefinition ?? throw Error.NotInitializedProperty(m, nameof(m.AttributeDefinition)),
m => m.Value);
}
private IDictionary GetAttributeDictionary()
{
lock (this._attributesLock)
{
var attributes = this._attributes;
if (attributes is null)
{
attributes = new Dictionary();
this._attributes = attributes;
}
return attributes;
}
}
}