diff --git a/.gitignore b/.gitignore index a82bfd7..49af156 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Auto-generated directories bin/ obj/ +.vs/ \ No newline at end of file diff --git a/Content/PlanteraOverrides.cs b/Content/PlanteraOverrides.cs index 18d651b..f022f7f 100644 --- a/Content/PlanteraOverrides.cs +++ b/Content/PlanteraOverrides.cs @@ -5,6 +5,7 @@ using Terraria.ID; using Terraria.ModLoader; using Terraria.GameContent.ItemDropRules; using Continuity.Content.Items; +using continuity.Utilities; namespace Continuity.Content { @@ -47,7 +48,10 @@ namespace Continuity.Content itemLoot.RemoveWhere(rule => rule is OneFromRulesRule); if (ModLoader.HasMod("CalamityMod")) { - itemLoot.RemoveWhere(rule => rule is CalamityMod.DropHelper.AllOptionsAtOnceWithPityDropRule); + // NOTE(midnadimple): This line causes build issue in tModloader v2024.9.3.0, i dont think + // this is how you're meant to access other mods' drop rules. + itemLoot.RemoveWhere(rule => rule is CalamDropHelper.AllOptionsAtOnceWithPityDropRule); + if (ModLoader.TryGetMod("CalamityMod", out Mod calamityMod) && calamityMod.TryFind("BlossomFlux", out ModItem BlossomFlux)) { itemLoot.RemoveWhere(rule => rule is CommonDrop drop && drop.itemId == BlossomFlux.Type); diff --git a/Utilities/CalamDropHelper.cs b/Utilities/CalamDropHelper.cs new file mode 100644 index 0000000..7d88c04 --- /dev/null +++ b/Utilities/CalamDropHelper.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; +using Terraria.GameContent.ItemDropRules; +using Terraria.Utilities; +using Terraria; + +// Repurposed from https://github.com/CalamityTeam/CalamityModPublic/blob/1.4.4/Utilities/DropHelper.cs +namespace continuity.Utilities +{ + internal class CalamDropHelper + { + public struct Fraction + { + internal readonly int numerator; + internal readonly int denominator; + + public Fraction(int n, int d) + { + numerator = n < 0 ? 0 : n; + denominator = d <= 0 ? 1 : d; + } + + public static implicit operator float(Fraction f) => f.numerator / (float)f.denominator; + } + + + public struct WeightedItemStack + { + public const float DefaultWeight = 1f; + public const float MinisiculeWeight = 1E-6f; + + internal int itemID; + internal float weight; + internal int minQuantity; + internal int maxQuantity; + + internal WeightedItemStack(int id, float w) + { + itemID = id; + weight = w; + minQuantity = 1; + maxQuantity = 1; + } + + internal WeightedItemStack(int id, float w, int quantity) + { + itemID = id; + weight = w; + minQuantity = quantity; + maxQuantity = quantity; + } + + internal WeightedItemStack(int id, float w, int min, int max) + { + itemID = id; + weight = w; + minQuantity = min; + maxQuantity = max; + } + + internal int ChooseQuantity(UnifiedRandom rng) => rng.Next(minQuantity, maxQuantity + 1); + + // Allow for implicitly casting integer item IDs into weighted item stacks. + // Stack size is assumed to be 1. Weight is assumed to be default. + public static implicit operator WeightedItemStack(int id) + { + return new WeightedItemStack(id, DefaultWeight, 1); + } + } + + public class AllOptionsAtOnceWithPityDropRule : IItemDropRule + { + public WeightedItemStack[] stacks; + public Fraction dropRate; + public bool usesLuck; + public List ChainedRules { get; set; } + + public AllOptionsAtOnceWithPityDropRule(Fraction dropRate, bool luck, params WeightedItemStack[] stacks) + { + this.dropRate = dropRate; + this.stacks = stacks; + usesLuck = luck; + ChainedRules = new List(); + } + + public AllOptionsAtOnceWithPityDropRule(Fraction dropRate, bool luck, params int[] itemIDs) + { + this.dropRate = dropRate; + stacks = new WeightedItemStack[itemIDs.Length]; + for (int i = 0; i < stacks.Length; ++i) + stacks[i] = itemIDs[i]; // implicit conversion operator + usesLuck = luck; + ChainedRules = new List(); + } + + public bool CanDrop(DropAttemptInfo info) => true; + + public ItemDropAttemptResult TryDroppingItem(DropAttemptInfo info) + { + bool droppedAnything = false; + + // Roll for each drop individually. + foreach (WeightedItemStack stack in stacks) + { + bool rngRoll = usesLuck ? info.player.RollLuck(dropRate.denominator) < dropRate.numerator : info.rng.NextFloat() < dropRate; + droppedAnything |= rngRoll; + if (rngRoll) + CommonCode.DropItem(info, stack.itemID, stack.ChooseQuantity(info.rng)); + } + + // If everything fails to drop, force drop one item from the set. + if (!droppedAnything) + { + WeightedItemStack stack = info.rng.NextFromList(stacks); + CommonCode.DropItem(info, stack.itemID, stack.ChooseQuantity(info.rng)); + } + + // Calamity style drops cannot fail. You will always get at least one item. + ItemDropAttemptResult result = default; + result.State = ItemDropAttemptResultState.Success; + return result; + } + + public void ReportDroprates(List drops, DropRateInfoChainFeed ratesInfo) + { + int numDrops = stacks.Length; + float rawDropRate = dropRate; + // Combinatorics: + // OPTION 1: [The item drops = Raw Drop Rate] + // + + // OPTION 2: [ALL items fail to drop = (1-x)^n] * [This item is chosen as pity = 1/n] + float dropRateWithPityRoll = rawDropRate + (float)(Math.Pow(1f - rawDropRate, numDrops) * (1f / numDrops)); + float dropRateAdjustedForParent = dropRateWithPityRoll * ratesInfo.parentDroprateChance; + + // Report the drop rate of each individual item. This calculation includes the fact that each individual item can be guaranteed as pity. + foreach (WeightedItemStack stack in stacks) + drops.Add(new DropRateInfo(stack.itemID, stack.minQuantity, stack.maxQuantity, dropRateAdjustedForParent, ratesInfo.conditions)); + + Chains.ReportDroprates(ChainedRules, rawDropRate, drops, ratesInfo); + } + } + } +}