diff --git a/src/GameLogic/DefaultDropGenerator.cs b/src/GameLogic/DefaultDropGenerator.cs index 6ee087062..a59711a68 100644 --- a/src/GameLogic/DefaultDropGenerator.cs +++ b/src/GameLogic/DefaultDropGenerator.cs @@ -18,14 +18,19 @@ public class DefaultDropGenerator : IDropGenerator /// public static readonly int BaseMoneyDrop = 7; - private static readonly int DropLevelMaxGap = 12; + private const int DropLevelMaxGap = 12; + private const int JewelOfChaosMaxMonsterLevel = 66; + private const int JewelOfChaosGroup = 12; + private const int JewelOfChaosNumber = 15; + private const int SkillDropChancePercent = 50; private readonly IRandomizer _randomizer; /// - /// A re-useable list of drop item groups. + /// A re-usable list of drop item groups. /// - private readonly List _dropGroups = new(64); + private readonly List _chanceDropGroups = new(64); + private readonly List _guaranteedDropGroups = new(16); private readonly AsyncLock _lock = new(); @@ -52,7 +57,7 @@ public DefaultDropGenerator(GameConfiguration config, IRandomizer randomizer) this._droppableItems = config.Items.Where(i => i.DropsFromMonsters).ToList(); this._ancientItems = this._droppableItems.Where( i => i.PossibleItemSetGroups.Any( - o => o.Options?.PossibleOptions.Any( + g => g.Options?.PossibleOptions.Any( o => object.Equals(o.OptionType, ItemOptionTypes.AncientOption)) ?? false)) .ToList(); } @@ -68,41 +73,37 @@ public DefaultDropGenerator(GameConfiguration config, IRandomizer randomizer) } using var l = await this._lock.LockAsync(); - this._dropGroups.Clear(); - if (monster.DropItemGroups.MaxBy(g => g.Chance) is { Chance: >= 1.0 } alwaysDrops) - { - this._dropGroups.Add(alwaysDrops); - } - else if (monster.ObjectKind == NpcObjectKind.Destructible) + this._guaranteedDropGroups.Clear(); + this._chanceDropGroups.Clear(); + + if (monster.ObjectKind == NpcObjectKind.Destructible) { - this._dropGroups.AddRange(monster.DropItemGroups ?? []); + this.PartitionDropGroups(monster.DropItemGroups ?? []); } else { - this._dropGroups.AddRange(monster.DropItemGroups ?? []); - this._dropGroups.AddRange(character.DropItemGroups ?? []); - this._dropGroups.AddRange(map.DropItemGroups ?? []); - this._dropGroups.AddRange(await GetQuestItemGroupsAsync(player).ConfigureAwait(false) ?? []); - - this._dropGroups.RemoveAll(g => !IsGroupRelevant(monster, g)); - this._dropGroups.Sort((x, y) => x.Chance.CompareTo(y.Chance)); + this.PartitionDropGroups(monster.DropItemGroups ?? []); + this.PartitionDropGroups(character.DropItemGroups ?? [], monster); + this.PartitionDropGroups(map.DropItemGroups ?? [], monster); + this.PartitionDropGroups(await GetQuestItemGroupsAsync(player).ConfigureAwait(false) ?? [], monster); } - var totalChance = this._dropGroups.Sum(g => g.Chance); uint money = 0; IList? droppedItems = null; - for (int i = 0; i < monster.NumberOfMaximumItemDrops; i++) + var remainingDrops = monster.NumberOfMaximumItemDrops; + + // Guaranteed groups, no random selection needed. + foreach (var group in this._guaranteedDropGroups) { - var group = this.SelectRandomGroup(this._dropGroups, totalChance); - if (group is null) + if (remainingDrops <= 0) { - continue; + break; } var item = this.GenerateItemDropOrMoney(monster, group, gainedExperience, out var droppedMoney); if (item is not null) { - droppedItems ??= new List(1); + droppedItems ??= new List(monster.NumberOfMaximumItemDrops); droppedItems.Add(item); } @@ -110,9 +111,43 @@ public DefaultDropGenerator(GameConfiguration config, IRandomizer randomizer) { money += droppedMoney.Value; } + + remainingDrops--; + } + + // Chance based groups with random selection + if (remainingDrops > 0 && this._chanceDropGroups.Count > 0) + { + double totalChance = 0; + foreach (var group in this._chanceDropGroups) + { + totalChance += group.Chance; + } + + for (int i = 0; i < remainingDrops; i++) + { + var group = this.SelectRandomGroup(this._chanceDropGroups, totalChance); + if (group is null) + { + continue; + } + + var item = this.GenerateItemDropOrMoney(monster, group, gainedExperience, out var droppedMoney); + if (item is not null) + { + droppedItems ??= new List(monster.NumberOfMaximumItemDrops); + droppedItems.Add(item); + } + + if (droppedMoney is not null) + { + money += droppedMoney.Value; + } + } } - this._dropGroups.Clear(); + this._guaranteedDropGroups.Clear(); + this._chanceDropGroups.Clear(); return (droppedItems ?? Enumerable.Empty(), money > 0 ? money : null); } @@ -185,7 +220,7 @@ protected void ApplyRandomOptions(Item item) if (item.CanHaveSkill()) { - item.HasSkill = this._randomizer.NextRandomBool(50); + item.HasSkill = this._randomizer.NextRandomBool(SkillDropChancePercent); } } @@ -280,6 +315,26 @@ private static bool IsGroupRelevant(MonsterDefinition monsterDefinition, DropIte return true; } + private void PartitionDropGroups(IEnumerable groups, MonsterDefinition? monster = null) + { + foreach (var group in groups) + { + if (monster is not null && !IsGroupRelevant(monster, group)) + { + continue; + } + + if (group.Chance >= 1.0) + { + this._guaranteedDropGroups.Add(group); + } + else + { + this._chanceDropGroups.Add(group); + } + } + } + private Item? GenerateItemDrop(DropItemGroup selectedGroup, ICollection possibleItems) { var item = selectedGroup.ItemType switch @@ -333,9 +388,13 @@ private void ApplyOption(Item item, ItemOptionDefinition option) var itemOptionLink = new ItemOptionLink { ItemOption = newOption, - Level = newOption?.LevelDependentOptions.Select(ldo => ldo.Level) + Level = newOption.LevelDependentOptions + .Select(ldo => ldo.Level) .Concat(newOption.LevelDependentOptions.Count > 0 ? [1] : []) // For base def/dmg opts level 1 is not an ItemOptionOfLevel entry - .Distinct().Where(l => l <= this._maxItemOptionLevelDrop).SelectRandom() ?? 0, + .Distinct() + .Where(l => l <= this._maxItemOptionLevelDrop) + .DefaultIfEmpty(0) + .SelectRandom(), }; item.ItemOptions.Add(itemOptionLink); } @@ -361,7 +420,9 @@ private void ApplyOption(Item item, ItemOptionDefinition option) private void ApplyRandomAncientOption(Item item) { - var ancientSet = item.Definition?.PossibleItemSetGroups.Where(g => g!.Options?.PossibleOptions.Any(o => object.Equals(o.OptionType, ItemOptionTypes.AncientOption)) ?? false).SelectRandom(this._randomizer); + var ancientSet = item.Definition?.PossibleItemSetGroups + .Where(g => g!.Options?.PossibleOptions.Any(o => object.Equals(o.OptionType, ItemOptionTypes.AncientOption)) ?? false) + .SelectRandom(this._randomizer); if (ancientSet is null) { return; @@ -383,12 +444,14 @@ private void AddRandomExcOptions(Item item) var possibleItemOptions = item.Definition!.PossibleItemOptions; var excellentOptions = possibleItemOptions.FirstOrDefault( o => o.PossibleOptions.Any(p => object.Equals(p.OptionType, ItemOptionTypes.Excellent))); + if (excellentOptions is null) { return; } var existingOptionCount = item.ItemOptions.Count(o => object.Equals(o.ItemOption?.OptionType, ItemOptionTypes.Excellent)); + for (int i = existingOptionCount; i < excellentOptions.MaximumOptionsPerItem; i++) { if (i == 0) @@ -444,10 +507,10 @@ private void AddRandomExcOptions(Item item) { filteredPossibleItems = [.. selectedGroup.PossibleItems.Where(it => it.DropLevel <= monsterLevel)]; - if (monsterLevel > 66) + if (monsterLevel > JewelOfChaosMaxMonsterLevel) { // Jewel of Chaos doesn't drop after a certain monster level - filteredPossibleItems.RemoveAll(it => it.Group == 12 && it.Number == 15); + filteredPossibleItems.RemoveAll(it => it.Group == JewelOfChaosGroup && it.Number == JewelOfChaosNumber); } } else