// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // 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; /// /// Extension methods for . /// 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 RequirementAttributeMapping = new Dictionary { { Stats.TotalStrengthRequirementValue, Stats.TotalStrength }, { Stats.TotalAgilityRequirementValue, Stats.TotalAgility }, { Stats.TotalEnergyRequirementValue, Stats.TotalEnergy }, { Stats.TotalVitalityRequirementValue, Stats.TotalVitality }, { Stats.TotalLeadershipRequirementValue, Stats.TotalLeadership }, }; private static readonly IDictionary RequirementReductionAttributeMapping = new Dictionary { { Stats.TotalStrengthRequirementValue, Stats.RequiredStrengthReduction }, { Stats.TotalAgilityRequirementValue, Stats.RequiredAgilityReduction }, { Stats.TotalEnergyRequirementValue, Stats.RequiredEnergyReduction }, { Stats.TotalVitalityRequirementValue, Stats.RequiredVitalityReduction }, { Stats.TotalLeadershipRequirementValue, Stats.RequiredLeadershipReduction }, }; /// /// Gets the maximum durability of the item. /// /// The item. /// The maximum durability of the item. /// /// 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. /// 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); } /// /// Determines whether this instance is ancient. /// /// The item. /// /// true if the specified item is ancient; otherwise, false. /// public static bool IsAncient(this Item item) { return item.ItemSetGroups.Any(itemSet => itemSet.AncientSetDiscriminator > 0); } /// /// Determines whether this instance is excellent. /// /// The item. /// /// true if the specified item is excellent; otherwise, false. /// public static bool IsExcellent(this Item item) { return item.ItemOptions.Any(link => link.ItemOption?.OptionType == ItemOptionTypes.Excellent); } /// /// Determines whether this instance is a "380 item", that is, if it can be upgraded with Jewel of Guardian. /// /// The item. /// /// true if the specified item is a "380 item"; otherwise, false. /// public static bool IsGuardian(this Item item) { return item.Definition!.PossibleItemOptions.Any(pio => pio.PossibleOptions .Any(po => po.OptionType == ItemOptionTypes.GuardianOption)); } /// /// Determines whether this item is a defensive item. /// /// The item. /// , if the item is defensive. public static bool IsDefensiveItem(this Item item) { return InventoryConstants.IsDefenseItemSlot(item.ItemSlot) || item.IsShield(); } /// /// Determines whether this instance is a shield. /// /// The item. /// /// true if the specified item is a shield; otherwise, false. /// public static bool IsShield(this Item item) { return item.Definition?.Group == ShieldItemGroup; } /// /// Determines whether equipping an item of this definition into the given hand slot would conflict /// with what the other hand already holds: a two-handed item needs the other hand free (ammunition /// aside), and nothing but ammunition fits next to an already equipped two-handed item. /// /// The definition of the item which would be equipped. /// The inventory to check the other hand in. /// The hand slot the item would be equipped into. /// true if the other hand blocks this item; otherwise, false. public static bool ConflictsWithEquippedHands(this ItemDefinition itemDefinition, IStorage inventory, byte toSlot) { if (itemDefinition.ItemSlot is null) { return false; } static bool IsOneHandedOrShield(ItemDefinition definition) => (definition.ItemSlot!.ItemSlots.Contains(InventoryConstants.RightHandSlot) && definition.ItemSlot.ItemSlots.Contains(InventoryConstants.LeftHandSlot)) || definition.Group == ShieldItemGroup; var rightHandItemDefinition = inventory.GetItem(InventoryConstants.RightHandSlot)?.Definition; return (toSlot == InventoryConstants.LeftHandSlot && itemDefinition.Width >= 2 && rightHandItemDefinition is not null && !rightHandItemDefinition.IsAmmunition) || (toSlot == InventoryConstants.RightHandSlot && IsOneHandedOrShield(itemDefinition) && inventory.GetItem(InventoryConstants.LeftHandSlot)?.Definition?.Width >= 2); } /// /// Determines whether this item is a jewelry (pendant or ring) item. /// /// The item. /// /// true if the specified item is jewelry; otherwise, false. /// public static bool IsJewelry(this Item item) { return item.ItemSlot >= InventoryConstants.PendantSlot && item.ItemSlot <= InventoryConstants.Ring2Slot; } /// /// Determines whether this item is an armor item. /// /// The item. /// /// true if the specified item is armor; otherwise, false. /// public static bool IsArmorItem(this Item item) { return item.ItemSlot >= InventoryConstants.HelmSlot && item.ItemSlot <= InventoryConstants.BootsSlot; } /// /// Determines whether this instance is a is weapon which deals physical damage. /// /// The item. /// The minimum physical damage of the weapon. /// /// true if this instance is a weapon which deals physical damage; otherwise, false. /// 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; } /// /// Determines whether this instance is a weapon which increases wizardry damage. /// /// The item. /// The staff/sword/stick's wizardry damage rise percentage. /// /// true if this instance is a weapon which increases wizardry damage; otherwise, false. /// 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; } /// /// Determines whether this instance is a scepter which increases raven damage. /// /// The item. /// The scepter's pet attack rise percentage. /// /// true if this instance is a scepter which increases raven damage; otherwise, false. /// 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; } /// /// Determines whether this instance is a book which increases curse damage. /// /// The item. /// The book's curse damage rise percentage. /// /// true if this instance is a book which increases curse damage; otherwise, false. /// 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; } /// /// Returns a random offensive item of the storage. /// /// The storage. /// A randomly selected offensive item. 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; } /// /// Gets the requirement as a tuple of an attribute and the corresponding value. /// /// The item. /// The requirement. /// The requirement as a tuple of an attribute and the corresponding value. /// /// Some requirements are depending on item level, drop level and item options. /// 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); } /// /// Gets the item data which is relevant for the visual appearance of an item. /// /// The item. /// The item data which is relevant for the visual appearance of an item. 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; } /// /// Creates a persistent instance of the given and returns it. /// /// The item appearance. /// The persistence context where the object should be added. /// The game configuration. /// A persistent instance of the given . public static ItemAppearance MakePersistent(this ItemAppearance itemAppearance, IContext persistenceContext, GameConfiguration gameConfiguration) { var persistent = persistenceContext.CreateNew(); 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; } /// /// Calculates the drop level. /// /// The item. /// A value indicating whether the item is ancient. /// A value indicating whether the item is excellent. /// The item level. /// The calculated drop level. 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; } /// /// Calculates the drop level of the item in the current state. /// /// The item. /// The calculated drop level. 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 VisibleOptions => base.VisibleOptions ??= new List(); } }