// // 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; } } }