Files
AdamuSw/src/GameLogic/ItemPowerUpFactory.cs
2026-07-14 19:00:35 +03:00

438 lines
19 KiB
C#

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