// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // 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; /// /// The implementation of the item power up factory. /// public class ItemPowerUpFactory : IItemPowerUpFactory { private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The logger. public ItemPowerUpFactory(ILogger logger) { this._logger = logger; } /// public IEnumerable 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; } } /// public IEnumerable GetSetPowerUps( IEnumerable 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(); 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 GetOptionCombinationBonus(IEnumerable 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(); 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 itemOptions) { var allMatches = new List(); 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 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()) .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 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 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); } }