Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 94 additions & 31 deletions src/GameLogic/DefaultDropGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ public class DefaultDropGenerator : IDropGenerator
/// </summary>
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;

/// <summary>
/// A re-useable list of drop item groups.
/// A re-usable list of drop item groups.
/// </summary>
private readonly List<DropItemGroup> _dropGroups = new(64);
private readonly List<DropItemGroup> _chanceDropGroups = new(64);
private readonly List<DropItemGroup> _guaranteedDropGroups = new(16);

private readonly AsyncLock _lock = new();

Expand All @@ -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();
}
Expand All @@ -68,51 +73,81 @@ 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 ?? []);
Comment thread
eduardosmaniotto marked this conversation as resolved.
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<Item>? 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<Item>(1);
droppedItems ??= new List<Item>(monster.NumberOfMaximumItemDrops);
droppedItems.Add(item);
}

if (droppedMoney is not null)
{
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++)
Comment thread
eduardosmaniotto marked this conversation as resolved.
{
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<Item>(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<Item>(), money > 0 ? money : null);
}

Expand Down Expand Up @@ -185,7 +220,7 @@ protected void ApplyRandomOptions(Item item)

if (item.CanHaveSkill())
{
item.HasSkill = this._randomizer.NextRandomBool(50);
item.HasSkill = this._randomizer.NextRandomBool(SkillDropChancePercent);
}
}

Expand Down Expand Up @@ -280,6 +315,26 @@ private static bool IsGroupRelevant(MonsterDefinition monsterDefinition, DropIte
return true;
}

private void PartitionDropGroups(IEnumerable<DropItemGroup> 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<ItemDefinition> possibleItems)
{
var item = selectedGroup.ItemType switch
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down