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