├── .gitignore ├── CHANGELOG.md ├── ItemStats.sln ├── ItemStats.sln.DotSettings.user ├── ItemStats ├── ItemStats.csproj ├── ItemStats.csproj.DotSettings ├── Properties │ └── AssemblyInfo.cs ├── packages.config └── src │ ├── ContextProvider.cs │ ├── Hooks.cs │ ├── ItemStatDefinitions.cs │ ├── ItemStatProvider.cs │ ├── ItemStatsMod.cs │ ├── Stat │ ├── IStat.cs │ ├── ItemStat.cs │ ├── ItemStatDef.cs │ └── StatContext.cs │ ├── StatCalculation │ ├── DefaultStatCalculationStrategy.cs │ └── IStatCalculationStrategy.cs │ ├── StatModification │ ├── AbstractStatModifier.cs │ ├── IStatModifier.cs │ ├── Modifiers │ │ ├── HealingIncreaseModifier.cs │ │ └── LuckModifier.cs │ └── StatModifiers.cs │ └── ValueFormatters │ ├── Colors.cs │ ├── Extensions.cs │ ├── IStatFormatter.cs │ └── ModifierFormatter.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | .idea/ 3 | bin/ 4 | obj/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes in 2.2.1 2 | 3 | * Fixed clover incorrectly calculating stats for items with >= 100% chance to proc 4 | 5 | * Fixed clover modifying the incorrect stat on Fresh Meat 6 | 7 | * Fixed incorrect percentage formatting for the new items 8 | 9 | # Changes in 2.2.0 10 | 11 | * Anniversary update support 12 | 13 | # Changes in 2.1.0 14 | 15 | * Fixed incorrect stats of Strides of Heresy 16 | 17 | * Fixed formatting issues 18 | 19 | * Luck stat modifier now shows purity 20 | 21 | * Added config option to hide detailed descriptions in item pickup popup 22 | 23 | # Changes in 2.0.0 24 | 25 | * Add custom item and stat modifier API 26 | 27 | * Support for new/updated items from the past few updates 28 | 29 | * Now shows healing bonuses from Rejuvenation Rack 30 | 31 | * Now shows item description on pickup 32 | 33 | # Changes in 1.5.0 34 | 35 | * Support for new/updated items from the Artifacts update 36 | 37 | * Added scaling gold information to Ghor's Tome 38 | 39 | * Fixed Genesis Loop recharge duration 40 | 41 | # Changes in 1.4.0 42 | 43 | * Support for new/updated items from the newest update 44 | 45 | # Changes in 1.3.1 46 | 47 | * Fixed the lunar ability replacement thingy from crashing when spectating in multiplayer 48 | 49 | * Updated the Tooth healing amount 50 | 51 | # Changes in 1.3 52 | 53 | * Support for new/updated items from the skills 2.0 update 54 | 55 | # Changes in 1.2.1 56 | 57 | * Tri-tip Dagger is now affected by clover 58 | * Halcyon Seed now has stats more accurate to the description 59 | * Missile proc chance is now affected by clover 60 | * Predatory Instincts now has proper attack speed stats 61 | * Fixed turtle hp stat 62 | * Aegis is now capped at 100% health 63 | * Kill health threshold for old guiliotine is fixed 64 | * Rej-rack no longer has proc chance lmao 65 | 66 | # Changes in 1.2.0 67 | 68 | * Support for new/updated items 69 | * Fixed a few missing/broken stats for pre-patch items 70 | 71 | # Changes in 1.1.1 72 | 73 | * Implemented frost relic 74 | * Fixed stun grenade fuckup 75 | 76 | # Changes in 1.1.0 77 | 78 | * Items that roll chance now show the clover bonus 79 | * Rusty key now shows proper loot chances for item groups 80 | * Less confusing stat text 81 | -------------------------------------------------------------------------------- /ItemStats.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ItemStats", "ItemStats\ItemStats.csproj", "{35991648-A109-4E7A-A120-08C4FA3780F8}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {35991648-A109-4E7A-A120-08C4FA3780F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {35991648-A109-4E7A-A120-08C4FA3780F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {35991648-A109-4E7A-A120-08C4FA3780F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {35991648-A109-4E7A-A120-08C4FA3780F8}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /ItemStats.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | 2 6 | <AssemblyExplorer> 7 | <Assembly Path="C:\Users\MAX\RiderProjects\ItemStats\ItemStatsMod\bin\Release\UnityEngine.dll" /> 8 | <Assembly Path="D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\Assembly-CSharp.dll" /> 9 | <Assembly Path="D:\SteamLibrary\steamapps\common\Risk of Rain 2\BepInEx\core\BepInEx.dll" /> 10 | <Assembly Path="C:\Users\MAX\RiderProjects\ItemStats\ItemStats\bin\Release\R2API.dll" /> 11 | <Assembly Path="D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\RoR2.dll" /> 12 | </AssemblyExplorer> 13 | 14 | -------------------------------------------------------------------------------- /ItemStats/ItemStats.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {35991648-A109-4E7A-A120-08C4FA3780F8} 8 | Library 9 | Properties 10 | ItemStats 11 | ItemStats 12 | v4.6.1 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\packages\Lib.Harmony.1.2.0.1\lib\net45\0Harmony.dll 37 | True 38 | 39 | 40 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\BepInEx\core\BepInEx.dll 41 | 42 | 43 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\com.unity.multiplayer-hlapi.Runtime.dll 44 | 45 | 46 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\BepInEx\plugins\MMHOOK\MMHOOK_RoR2.dll 47 | 48 | 49 | bin\Release\R2API.dll 50 | 51 | 52 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\Rewired_Core.dll 53 | 54 | 55 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\RoR2.dll 56 | 57 | 58 | 59 | 60 | 61 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\UnityEngine.dll 62 | 63 | 64 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\UnityEngine.CoreModule.dll 65 | 66 | 67 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\UnityEngine.Networking.dll 68 | 69 | 70 | D:\SteamLibrary\steamapps\common\Risk of Rain 2\Risk of Rain 2_Data\Managed\UnityEngine.UI.dll 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 108 | -------------------------------------------------------------------------------- /ItemStats/ItemStats.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | CSharp80 -------------------------------------------------------------------------------- /ItemStats/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("ItemStats")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("ItemStats")] 12 | [assembly: AssemblyCopyright("Copyright ontrigger 2021")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("35991648-A109-4E7A-A120-08C4FA3780F8")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("2.2.1.0")] 35 | [assembly: AssemblyFileVersion("2.2.1.0")] -------------------------------------------------------------------------------- /ItemStats/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /ItemStats/src/ContextProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using ItemStats.ValueFormatters; 4 | using RoR2; 5 | 6 | namespace ItemStats 7 | { 8 | public static class ContextProvider 9 | { 10 | public static Dictionary GetPlayerIdToItemCountMap(ItemIndex index) 11 | { 12 | return LocalUserManager.readOnlyLocalUsersList 13 | .ToDictionary(user => user.id, user => user.cachedBody.CountItems(index)); 14 | } 15 | 16 | public static IEnumerable GetPlayerBodiesExcept(int userId) 17 | { 18 | return LocalUserManager.readOnlyLocalUsersList 19 | .Where(user => user.id != userId) 20 | .Select(user => user.cachedBody); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /ItemStats/src/Hooks.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using R2API.Utils; 3 | using RoR2; 4 | using RoR2.UI; 5 | using GenericNotification = On.RoR2.UI.GenericNotification; 6 | using HUD = On.RoR2.UI.HUD; 7 | using ScoreboardStrip = On.RoR2.UI.ScoreboardStrip; 8 | 9 | namespace ItemStats 10 | { 11 | internal static class Hooks 12 | { 13 | // TODO: prevent memleak 14 | private static readonly Dictionary DisplayToMasterRef = 15 | new Dictionary(); 16 | 17 | private static readonly Dictionary IconToMasterRef = 18 | new Dictionary(); 19 | 20 | public static void Init() 21 | { 22 | HUD.Update += HudUpdateHook; 23 | 24 | ScoreboardStrip.SetMaster += ScoreboardSetMasterHook; 25 | 26 | GenericNotification.SetItem += SetNotificationItemHook; 27 | 28 | On.RoR2.UI.ItemInventoryDisplay.AllocateIcons += ItemDisplayAllocateIconsHook; 29 | 30 | On.RoR2.UI.ItemIcon.SetItemIndex += ItemIconSetItemIndexHook; 31 | } 32 | 33 | private static void ItemIconSetItemIndexHook(On.RoR2.UI.ItemIcon.orig_SetItemIndex orig, ItemIcon self, 34 | ItemIndex newIndex, int newCount) 35 | { 36 | orig(self, newIndex, newCount); 37 | 38 | var itemDef = ItemCatalog.GetItemDef(newIndex); 39 | if (self.tooltipProvider != null && itemDef != null) 40 | { 41 | var itemDescription = Language.GetString(itemDef.descriptionToken); 42 | 43 | IconToMasterRef.TryGetValue(self, out var master); 44 | 45 | // TODO: use a pool to reduce StatContext allocations 46 | itemDescription += ItemStatsMod.GetStatsForItem(newIndex, newCount, new StatContext(master)); 47 | 48 | self.tooltipProvider.overrideBodyText = itemDescription; 49 | } 50 | } 51 | 52 | private static void ItemDisplayAllocateIconsHook(On.RoR2.UI.ItemInventoryDisplay.orig_AllocateIcons orig, 53 | ItemInventoryDisplay self, int count) 54 | { 55 | orig(self, count); 56 | 57 | var icons = self.GetFieldValue>("itemIcons"); 58 | 59 | DisplayToMasterRef.TryGetValue(self, out var masterRef); 60 | 61 | // naive, but not worth improving as it is not called every frame 62 | icons.ForEach(i => IconToMasterRef[i] = masterRef); 63 | } 64 | 65 | private static void ScoreboardSetMasterHook(ScoreboardStrip.orig_SetMaster orig, RoR2.UI.ScoreboardStrip self, 66 | CharacterMaster master) 67 | { 68 | orig(self, master); 69 | 70 | if (master) DisplayToMasterRef[self.itemInventoryDisplay] = master; 71 | } 72 | 73 | private static void HudUpdateHook(HUD.orig_Update orig, RoR2.UI.HUD self) 74 | { 75 | orig(self); 76 | 77 | if (self.itemInventoryDisplay && self.targetMaster) 78 | DisplayToMasterRef[self.itemInventoryDisplay] = self.targetMaster; 79 | } 80 | 81 | private static void SetNotificationItemHook(GenericNotification.orig_SetItem orig, 82 | RoR2.UI.GenericNotification self, 83 | ItemDef itemDef) 84 | { 85 | orig(self, itemDef); 86 | 87 | if (ItemStatsMod.DetailedPickupDescriptions.Value) 88 | { 89 | self.descriptionText.token = itemDef.descriptionToken; 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /ItemStats/src/ItemStatDefinitions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ItemStats.Stat; 3 | using ItemStats.ValueFormatters; 4 | using RoR2; 5 | using UnityEngine; 6 | namespace ItemStats 7 | { 8 | internal partial class ItemStatProvider 9 | { 10 | static ItemStatProvider() 11 | { 12 | CustomItemDefs = new Dictionary(); 13 | 14 | ItemDefs = new Dictionary(); 15 | } 16 | 17 | public static void Init() 18 | { 19 | ItemDefs[ItemCatalog.FindItemIndex("Bear")] = new ItemStatDef 20 | { 21 | Stats = new List 22 | { 23 | new ItemStat( 24 | (itemCount, ctx) => 1f - 1f / (0.15f * itemCount + 1f), 25 | (value, ctx) => $"Block Chance: {value.FormatPercentage()}" 26 | ) 27 | } 28 | }; 29 | ItemDefs[ItemCatalog.FindItemIndex("Hoof")] = new ItemStatDef 30 | { 31 | Stats = new List 32 | { 33 | new ItemStat( 34 | (itemCount, ctx) => itemCount * 0.14f, 35 | (value, ctx) => $"Movement Speed Increase: {value.FormatPercentage()}" 36 | ) 37 | } 38 | }; 39 | ItemDefs[ItemCatalog.FindItemIndex("Syringe")] = new ItemStatDef 40 | { 41 | Stats = new List 42 | { 43 | new ItemStat( 44 | (itemCount, ctx) => itemCount * 0.15f, 45 | (value, ctx) => $"Attack Speed Increase: {value.FormatPercentage()}" 46 | ) 47 | } 48 | }; 49 | ItemDefs[ItemCatalog.FindItemIndex("Mushroom")] = new ItemStatDef 50 | { 51 | Stats = new List 52 | { 53 | new ItemStat( 54 | (itemCount, ctx) => 0.0225f + 0.0225f * itemCount, 55 | (value, ctx) => $"Healing Per Second: {value.FormatPercentage(maxValue: 1f)}" 56 | ), 57 | new ItemStat( 58 | (itemCount, ctx) => 1.5f + 1.5f * itemCount, 59 | (value, ctx) => $"Area Increase: {value.FormatInt("m")}" 60 | ) 61 | } 62 | }; 63 | ItemDefs[ItemCatalog.FindItemIndex("CritGlasses")] = new ItemStatDef 64 | { 65 | Stats = new List 66 | { 67 | new ItemStat( 68 | (itemCount, ctx) => itemCount * 0.1f, 69 | (value, ctx) => $"Additional Crit Chance: {value.FormatPercentage(maxValue: 1f)}" 70 | ) 71 | } 72 | }; 73 | ItemDefs[ItemCatalog.FindItemIndex("Feather")] = new ItemStatDef 74 | { 75 | Stats = new List 76 | { 77 | new ItemStat( 78 | (itemCount, ctx) => itemCount, 79 | (value, ctx) => $"Total Additional Jumps: {value.FormatInt()}" 80 | ) 81 | } 82 | }; 83 | ItemDefs[ItemCatalog.FindItemIndex("Seed")] = new ItemStatDef 84 | { 85 | Stats = new List 86 | { 87 | new ItemStat( 88 | (itemCount, ctx) => itemCount, 89 | (value, ctx) => $"Total Heal: {value.FormatInt("HP")}" 90 | ) 91 | } 92 | }; 93 | ItemDefs[ItemCatalog.FindItemIndex("GhostOnKill")] = new ItemStatDef 94 | { 95 | Stats = new List 96 | { 97 | new ItemStat( 98 | (itemCount, ctx) => itemCount * 30f, 99 | (value, ctx) => $"Ghost Duration: {value.FormatInt("s")}" 100 | ), 101 | new ItemStat( 102 | (itemCount, ctx) => 0.07f, 103 | (value, ctx) => $"Proc Chance: {value.FormatPercentage()}" 104 | // StatModifiers.Luck 105 | ) 106 | } 107 | }; 108 | ItemDefs[ItemCatalog.FindItemIndex("Knurl")] = new ItemStatDef 109 | { 110 | Stats = new List 111 | { 112 | new ItemStat( 113 | (itemCount, ctx) => itemCount * 40f, 114 | (value, ctx) => $"Bonus Health: {value.FormatInt("HP")}" 115 | ), 116 | new ItemStat( 117 | (itemCount, ctx) => itemCount * 1.6f, 118 | (value, ctx) => $"Additional Regeneration: {value.FormatInt("HP/s")}" 119 | ) 120 | } 121 | }; 122 | ItemDefs[ItemCatalog.FindItemIndex("Clover")] = new ItemStatDef 123 | { 124 | Stats = new List 125 | { 126 | new ItemStat( 127 | (itemCount, ctx) => itemCount, 128 | (value, ctx) => $"Additional Rerolls: {value.FormatInt()}" 129 | ) 130 | } 131 | }; 132 | ItemDefs[ItemCatalog.FindItemIndex("Medkit")] = new ItemStatDef 133 | { 134 | Stats = new List 135 | { 136 | new ItemStat( 137 | (itemCount, ctx) => 138 | 20f + 0.05f * itemCount * (ctx.Master != null ? ctx.Master.GetBody().maxHealth : 1), 139 | (value, ctx) => 140 | { 141 | var statValue = ctx.Master != null 142 | ? $"{value.FormatInt("HP")}" 143 | : $"{value.FormatPercentage()}"; 144 | return "Health Healed: " + statValue; 145 | }) 146 | } 147 | }; 148 | ItemDefs[ItemCatalog.FindItemIndex("Crowbar")] = new ItemStatDef 149 | { 150 | Stats = new List 151 | { 152 | new ItemStat( 153 | (itemCount, ctx) => 0.75f * itemCount, 154 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 155 | ) 156 | } 157 | }; 158 | ItemDefs[ItemCatalog.FindItemIndex("Tooth")] = new ItemStatDef 159 | { 160 | Stats = new List 161 | { 162 | new ItemStat( 163 | (itemCount, ctx) => 0.02f * itemCount, 164 | (value, ctx) => $"Heal Amount: {value.FormatPercentage()}" 165 | ) 166 | } 167 | }; 168 | ItemDefs[ItemCatalog.FindItemIndex("Talisman")] = new ItemStatDef 169 | { 170 | Stats = new List 171 | { 172 | new ItemStat( 173 | (itemCount, ctx) => 2f + itemCount * 2f, 174 | (value, ctx) => $"Cooldown Reduction: {value.FormatInt("s")}" 175 | ) 176 | } 177 | }; 178 | ItemDefs[ItemCatalog.FindItemIndex("Bandolier")] = new ItemStatDef 179 | { 180 | Stats = new List 181 | { 182 | new ItemStat( 183 | (itemCount, ctx) => 1f - 1f / Mathf.Pow(itemCount + 1, 0.33f), 184 | (value, ctx) => $"Drop Chance: {value.FormatPercentage()}" 185 | ) 186 | } 187 | }; 188 | ItemDefs[ItemCatalog.FindItemIndex("IceRing")] = new ItemStatDef 189 | { 190 | Stats = new List 191 | { 192 | new ItemStat( 193 | (itemCount, ctx) => 2.5f * itemCount, 194 | (value, ctx) => $"Ice Blast Damage: {value.FormatPercentage()}" 195 | ), 196 | new ItemStat( 197 | (itemCount, ctx) => 3f * itemCount, 198 | (value, ctx) => $"Ice Debuff Duration: {value.FormatInt("s")}" 199 | ) 200 | } 201 | }; 202 | ItemDefs[ItemCatalog.FindItemIndex("FireRing")] = new ItemStatDef 203 | { 204 | Stats = new List 205 | { 206 | new ItemStat( 207 | (itemCount, ctx) => 3f * itemCount, 208 | (value, ctx) => $"Fire Tornado Damage: {value.FormatPercentage()}" 209 | ) 210 | } 211 | }; 212 | ItemDefs[ItemCatalog.FindItemIndex("WarCryOnMultiKill")] = new ItemStatDef 213 | { 214 | Stats = new List 215 | { 216 | new ItemStat( 217 | (itemCount, ctx) => 2f + 4f * itemCount, 218 | (value, ctx) => $"Frenzy Duration: {value.FormatInt("s")}" 219 | ) 220 | } 221 | }; 222 | ItemDefs[ItemCatalog.FindItemIndex("SprintOutOfCombat")] = new ItemStatDef 223 | { 224 | Stats = new List 225 | { 226 | new ItemStat( 227 | (itemCount, ctx) => itemCount * 0.3f, 228 | (value, ctx) => $"Speed Increase: {value.FormatPercentage()}" 229 | ) 230 | } 231 | }; 232 | ItemDefs[ItemCatalog.FindItemIndex("StunChanceOnHit")] = new ItemStatDef 233 | { 234 | Stats = new List 235 | { 236 | new ItemStat( 237 | (itemCount, ctx) => 1f - 1f / (0.05f * itemCount + 1f), 238 | (value, ctx) => $"Stun Chance Increase: {value.FormatPercentage(maxValue: 1f)}" 239 | // StatModifiers.Luck 240 | ) 241 | } 242 | }; 243 | ItemDefs[ItemCatalog.FindItemIndex("WarCryOnCombat")] = new ItemStatDef 244 | { 245 | Stats = new List 246 | { 247 | new ItemStat( 248 | (itemCount, ctx) => 2f + 4f * itemCount, 249 | (value, ctx) => $"Frenzy Duration: {value.FormatInt()}" 250 | ) 251 | } 252 | }; 253 | ItemDefs[ItemCatalog.FindItemIndex("SecondarySkillMagazine")] = new ItemStatDef 254 | { 255 | Stats = new List 256 | { 257 | new ItemStat( 258 | (itemCount, ctx) => itemCount, 259 | (value, ctx) => $"Bonus Stock: {value.FormatInt()}" 260 | ) 261 | } 262 | }; 263 | ItemDefs[ItemCatalog.FindItemIndex("UtilitySkillMagazine")] = new ItemStatDef 264 | { 265 | Stats = new List 266 | { 267 | new ItemStat( 268 | (itemCount, ctx) => itemCount * 2f, 269 | (value, ctx) => $"Bonus Charges: {value.FormatInt()}" 270 | ) 271 | } 272 | }; 273 | ItemDefs[ItemCatalog.FindItemIndex("AutoCastEquipment")] = new ItemStatDef 274 | { 275 | Stats = new List 276 | { 277 | new ItemStat( 278 | (itemCount, ctx) => 1f - (0.5f * Mathf.Pow(0.85f, itemCount - 1)), 279 | (value, ctx) => $"Cooldown Decrease: {value.FormatPercentage()}" 280 | ) 281 | } 282 | }; 283 | ItemDefs[ItemCatalog.FindItemIndex("KillEliteFrenzy")] = new ItemStatDef 284 | { 285 | Stats = new List 286 | { 287 | new ItemStat( 288 | (itemCount, ctx) => itemCount * 4f, 289 | (value, ctx) => $"Frenzy Duration: {value.FormatInt("s")}" 290 | ) 291 | } 292 | }; 293 | ItemDefs[ItemCatalog.FindItemIndex("BossDamageBonus")] = new ItemStatDef 294 | { 295 | Stats = new List 296 | { 297 | new ItemStat( 298 | (itemCount, ctx) => 0.2f + 0.2f * (itemCount - 1), 299 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 300 | ) 301 | } 302 | }; 303 | ItemDefs[ItemCatalog.FindItemIndex("ExplodeOnDeath")] = new ItemStatDef 304 | { 305 | Stats = new List 306 | { 307 | new ItemStat( 308 | (itemCount, ctx) => 12f + 2.4f * (itemCount - 1f), 309 | (value, ctx) => $"Radius Increase: {value.FormatInt("m")}" 310 | ), 311 | new ItemStat( 312 | (itemCount, ctx) => 3.5f * (1f + (itemCount - 1) * 0.8f), 313 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 314 | ) 315 | } 316 | }; 317 | ItemDefs[ItemCatalog.FindItemIndex("HealWhileSafe")] = new ItemStatDef 318 | { 319 | Stats = new List 320 | { 321 | new ItemStat( 322 | (itemCount, ctx) => 3f * itemCount, 323 | (value, ctx) => $"Bonus Health Regen: {value.FormatInt("HP/s")}" 324 | ) 325 | } 326 | }; 327 | ItemDefs[ItemCatalog.FindItemIndex("IgniteOnKill")] = new ItemStatDef 328 | { 329 | Stats = new List 330 | { 331 | new ItemStat( 332 | (itemCount, ctx) => 8f + 4f * itemCount, 333 | (value, ctx) => $"Radius Increase: {value.FormatInt("m")}" 334 | ), 335 | new ItemStat( 336 | (itemCount, ctx) => 1.5f + 1.5f * itemCount, 337 | (value, ctx) => $"Duration Increase: {value.FormatPercentage()}" 338 | ) 339 | } 340 | }; 341 | ItemDefs[ItemCatalog.FindItemIndex("WardOnLevel")] = new ItemStatDef 342 | { 343 | Stats = new List 344 | { 345 | new ItemStat( 346 | (itemCount, ctx) => 8f + 8f * itemCount, 347 | (value, ctx) => $"Radius Increase: {value.FormatInt("m")}" 348 | ) 349 | } 350 | }; 351 | ItemDefs[ItemCatalog.FindItemIndex("NovaOnHeal")] = new ItemStatDef 352 | { 353 | Stats = new List 354 | { 355 | new ItemStat( 356 | (itemCount, ctx) => itemCount, 357 | (value, ctx) => $"Soul Energy: {value.FormatPercentage()}" 358 | ) 359 | } 360 | }; 361 | ItemDefs[ItemCatalog.FindItemIndex("HealOnCrit")] = new ItemStatDef 362 | { 363 | Stats = new List 364 | { 365 | new ItemStat( 366 | (itemCount, ctx) => 4f + itemCount * 4f, 367 | (value, ctx) => $"Health per Crit: {value.FormatInt("HP")}" 368 | ) 369 | } 370 | }; 371 | ItemDefs[ItemCatalog.FindItemIndex("BleedOnHit")] = new ItemStatDef 372 | { 373 | Stats = new List 374 | { 375 | new ItemStat( 376 | (itemCount, ctx) => 0.1f * itemCount, 377 | (value, ctx) => $"Bleed Chance Increase: {value.FormatPercentage(maxValue: 1f)}" 378 | // StatModifiers.Luck 379 | ) 380 | } 381 | }; 382 | ItemDefs[ItemCatalog.FindItemIndex("SlowOnHit")] = new ItemStatDef 383 | { 384 | Stats = new List 385 | { 386 | new ItemStat( 387 | (itemCount, ctx) => 2 * itemCount, 388 | (value, ctx) => $"Slow Duration: {value.FormatInt("s")}" 389 | ) 390 | } 391 | }; 392 | ItemDefs[ItemCatalog.FindItemIndex("EquipmentMagazine")] = new ItemStatDef 393 | { 394 | Stats = new List 395 | { 396 | new ItemStat( 397 | (itemCount, ctx) => itemCount, 398 | (value, ctx) => $"Bonus Charges: {value.FormatInt()}" 399 | ), 400 | new ItemStat( 401 | (itemCount, ctx) => 1 - Mathf.Pow(0.85f, itemCount), 402 | (value, ctx) => $"Cooldown Decrease: {value.FormatPercentage()}" 403 | ) 404 | } 405 | }; 406 | ItemDefs[ItemCatalog.FindItemIndex("GoldOnHit")] = new ItemStatDef 407 | { 408 | Stats = new List 409 | { 410 | new ItemStat( 411 | //TODO: make run a modifier 412 | (itemCount, ctx) => itemCount * 2f * Run.instance.difficultyCoefficient, 413 | (value, ctx) => $"Gold per Hit(*): {value.FormatInt()}" 414 | ), 415 | new ItemStat( 416 | (itemCount, ctx) => 0.3f, 417 | (value, ctx) => $"Proc Chance: {value.FormatPercentage()}" 418 | // modifiers: StatModifiers.Luck 419 | ) 420 | } 421 | }; 422 | ItemDefs[ItemCatalog.FindItemIndex("IncreaseHealing")] = new ItemStatDef 423 | { 424 | Stats = new List 425 | { 426 | new ItemStat( 427 | (itemCount, ctx) => itemCount, 428 | (value, ctx) => $"Healing Increase: {value.FormatPercentage()}" 429 | ) 430 | } 431 | }; 432 | ItemDefs[ItemCatalog.FindItemIndex("PersonalShield")] = new ItemStatDef 433 | { 434 | Stats = new List 435 | { 436 | new ItemStat( 437 | (itemCount, ctx) => 0.08f * itemCount, 438 | (value, ctx) => $"Shield Health Increase: {value.FormatPercentage()}" 439 | ) 440 | } 441 | }; 442 | ItemDefs[ItemCatalog.FindItemIndex("ChainLightning")] = new ItemStatDef 443 | { 444 | Stats = new List 445 | { 446 | new ItemStat( 447 | (itemCount, ctx) => itemCount * 2f, 448 | (value, ctx) => $"Total Bounces: {value.FormatInt()}" 449 | ), 450 | new ItemStat( 451 | (itemCount, ctx) => 20f + 2f * itemCount, 452 | (value, ctx) => $"Bounce Range: {value.FormatInt("m")}" 453 | ), 454 | new ItemStat( 455 | (itemCount, ctx) => 0.25f, 456 | (value, ctx) => $"Proc Chance: {value.FormatPercentage()}" 457 | // StatModifiers.Luck 458 | ) 459 | } 460 | }; 461 | ItemDefs[ItemCatalog.FindItemIndex("TreasureCache")] = new ItemStatDef 462 | { 463 | Stats = new List 464 | { 465 | new ItemStat( 466 | (itemCount, ctx) => itemCount, 467 | (value, ctx) => $"Unlockable Caches: {value}" 468 | // StatModifiers.TreasureCache 469 | ), 470 | } 471 | }; 472 | ItemDefs[ItemCatalog.FindItemIndex("BounceNearby")] = new ItemStatDef 473 | { 474 | Stats = new List 475 | { 476 | new ItemStat( 477 | (itemCount, ctx) => 1f - 100f / (100f + 20f * itemCount), 478 | (value, ctx) => $"Hook Chance: {value.FormatPercentage()}" 479 | // modifiers: StatModifiers.Luck 480 | ), 481 | new ItemStat( 482 | (itemCount, ctx) => 5f + itemCount * 5f, 483 | (value, ctx) => $"Max Enemies Hooked: {value.FormatInt()}" 484 | ) 485 | } 486 | }; 487 | ItemDefs[ItemCatalog.FindItemIndex("SprintBonus")] = new ItemStatDef 488 | { 489 | Stats = new List 490 | { 491 | new ItemStat( 492 | (itemCount, ctx) => 0.25f * itemCount, 493 | (value, ctx) => $"Speed Increase: {value.FormatPercentage()}" 494 | ) 495 | } 496 | }; 497 | ItemDefs[ItemCatalog.FindItemIndex("SprintArmor")] = new ItemStatDef 498 | { 499 | Stats = new List 500 | { 501 | new ItemStat( 502 | (itemCount, ctx) => 30f * itemCount, 503 | (value, ctx) => $"Sprint Bonus Armor: {value.FormatInt()}" 504 | ) 505 | } 506 | }; 507 | ItemDefs[ItemCatalog.FindItemIndex("ShockNearby")] = new ItemStatDef 508 | { 509 | Stats = new List 510 | { 511 | new ItemStat( 512 | (itemCount, ctx) => 2f * itemCount, 513 | (value, ctx) => $"Total Bounces: {value.FormatInt()}" 514 | ) 515 | } 516 | }; 517 | ItemDefs[ItemCatalog.FindItemIndex("BeetleGland")] = new ItemStatDef 518 | { 519 | Stats = new List 520 | { 521 | new ItemStat( 522 | (itemCount, ctx) => itemCount, 523 | (value, ctx) => $"Total Guards: {value.FormatInt()}" 524 | ) 525 | } 526 | }; 527 | ItemDefs[ItemCatalog.FindItemIndex("ShieldOnly")] = new ItemStatDef 528 | { 529 | Stats = new List 530 | { 531 | new ItemStat( 532 | (itemCount, ctx) => 0.5f + (itemCount - 1) * 0.25f, 533 | (value, ctx) => $"Max Health Increase: {value.FormatPercentage()}" 534 | ) 535 | } 536 | }; 537 | ItemDefs[ItemCatalog.FindItemIndex("StickyBomb")] = new ItemStatDef 538 | { 539 | Stats = new List 540 | { 541 | new ItemStat( 542 | (itemCount, ctx) => 0.05f * itemCount, 543 | (value, ctx) => $"Proc Chance Increase: {value.FormatPercentage(maxValue: 1f)}" 544 | // StatModifiers.Luck 545 | ) 546 | } 547 | }; 548 | ItemDefs[ItemCatalog.FindItemIndex("RepeatHeal")] = new ItemStatDef 549 | { 550 | Stats = new List 551 | { 552 | //TODO: need to get masters maxhealth to get actual heal amount 553 | new ItemStat( 554 | (itemCount, ctx) => 0.1f / itemCount, 555 | (value, ctx) => $"Health Fraction/s: {value.FormatPercentage()}" 556 | ), 557 | new ItemStat( 558 | (itemCount, ctx) => itemCount, 559 | (value, ctx) => $"Healing per Heal Increase: {value.FormatPercentage()}" 560 | ) 561 | } 562 | }; 563 | ItemDefs[ItemCatalog.FindItemIndex("HeadHunter")] = new ItemStatDef 564 | { 565 | Stats = new List 566 | { 567 | new ItemStat( 568 | (itemCount, ctx) => 3f + 5f * itemCount, 569 | (value, ctx) => $"Empowerment Duration: {value.FormatInt("s")}" 570 | ) 571 | } 572 | }; 573 | ItemDefs[ItemCatalog.FindItemIndex("ExtraLife")] = new ItemStatDef 574 | { 575 | Stats = new List 576 | { 577 | new ItemStat( 578 | (itemCount, ctx) => itemCount, 579 | (value, ctx) => $"Extra Lives: {value.FormatInt()}" 580 | ) 581 | } 582 | }; 583 | ItemDefs[ItemCatalog.FindItemIndex("AlienHead")] = new ItemStatDef 584 | { 585 | Stats = new List 586 | { 587 | new ItemStat( 588 | (itemCount, ctx) => 1 - Mathf.Pow(0.75f, itemCount), 589 | (value, ctx) => $"Cooldown Reduction: {value.FormatPercentage(2)}" 590 | ) 591 | } 592 | }; 593 | ItemDefs[ItemCatalog.FindItemIndex("Firework")] = new ItemStatDef 594 | { 595 | Stats = new List 596 | { 597 | new ItemStat( 598 | (itemCount, ctx) => 4 + itemCount * 4, 599 | (value, ctx) => $"Firework Count: {value.FormatInt()}" 600 | ) 601 | } 602 | }; 603 | ItemDefs[ItemCatalog.FindItemIndex("Missile")] = new ItemStatDef 604 | { 605 | Stats = new List 606 | { 607 | new ItemStat( 608 | (itemCount, ctx) => 3 * itemCount, 609 | (value, ctx) => $"Missile Total Damage: {value.FormatPercentage()}" 610 | ), 611 | new ItemStat( 612 | (itemCount, ctx) => 0.1f, 613 | (value, ctx) => $"Proc Chance: {value.FormatPercentage()}" 614 | // StatModifiers.Luck 615 | ) 616 | } 617 | }; 618 | ItemDefs[ItemCatalog.FindItemIndex("Infusion")] = new ItemStatDef 619 | { 620 | Stats = new List 621 | { 622 | new ItemStat( 623 | (itemCount, ctx) => 100 * itemCount, 624 | (value, ctx) => $"Max Additional Health: {value.FormatInt("HP")}" 625 | ), 626 | new ItemStat( 627 | (itemCount, ctx) => itemCount, 628 | (value, ctx) => $"Health Gained Per Kill: {value.FormatInt("HP")}" 629 | ) 630 | } 631 | }; 632 | ItemDefs[ItemCatalog.FindItemIndex("AttackSpeedOnCrit")] = new ItemStatDef 633 | { 634 | Stats = new List 635 | { 636 | new ItemStat( 637 | (itemCount, ctx) => 0.12f + 0.24f * itemCount, 638 | (value, ctx) => $"Max Attack Speed: {value.FormatPercentage()}" 639 | ), 640 | new ItemStat( 641 | (itemCount, ctx) => 0.05f, 642 | (value, ctx) => $"Crit Chance Bonus: {value.FormatPercentage()}" 643 | ) 644 | } 645 | }; 646 | ItemDefs[ItemCatalog.FindItemIndex("Icicle")] = new ItemStatDef 647 | { 648 | Stats = new List 649 | { 650 | new ItemStat( 651 | (itemCount, ctx) => 3f + 3f * itemCount, 652 | (value, ctx) => $"Radius: {value.FormatInt("m")}" 653 | ) 654 | } 655 | }; 656 | ItemDefs[ItemCatalog.FindItemIndex("Behemoth")] = new ItemStatDef 657 | { 658 | Stats = new List 659 | { 660 | new ItemStat( 661 | (itemCount, ctx) => 1.5f + 2.5f * itemCount, 662 | (value, ctx) => $"Explosion Radius: {value.FormatInt("m", 1)}" 663 | ) 664 | } 665 | }; 666 | ItemDefs[ItemCatalog.FindItemIndex("BarrierOnKill")] = new ItemStatDef 667 | { 668 | Stats = new List 669 | { 670 | new ItemStat( 671 | (itemCount, ctx) => 15f * itemCount, 672 | (value, ctx) => $"Barrier Health: {value.FormatInt("HP")}" 673 | ) 674 | } 675 | }; 676 | ItemDefs[ItemCatalog.FindItemIndex("BarrierOnOverHeal")] = new ItemStatDef 677 | { 678 | Stats = new List 679 | { 680 | new ItemStat( 681 | (itemCount, ctx) => 0.5f * itemCount, 682 | (value, ctx) => $"Barrier From Overheal: {value.FormatPercentage()}" 683 | ) 684 | } 685 | }; 686 | ItemDefs[ItemCatalog.FindItemIndex("ExecuteLowHealthElite")] = new ItemStatDef 687 | { 688 | Stats = new List 689 | { 690 | new ItemStat( 691 | (itemCount, ctx) => 1 - 1 / (1 + 0.13f * itemCount), 692 | (value, ctx) => $"Kill Health Threshold: {value.FormatPercentage()}" 693 | ) 694 | } 695 | }; 696 | ItemDefs[ItemCatalog.FindItemIndex("EnergizedOnEquipmentUse")] = new ItemStatDef 697 | { 698 | Stats = new List 699 | { 700 | new ItemStat( 701 | (itemCount, ctx) => 8f + 4f * (itemCount - 1), 702 | (value, ctx) => $"Attack Speed Duration: {value.FormatInt("s")}" 703 | ) 704 | } 705 | }; 706 | ItemDefs[ItemCatalog.FindItemIndex("TitanGoldDuringTP")] = new ItemStatDef 707 | { 708 | Stats = new List 709 | { 710 | new ItemStat( 711 | (itemCount, ctx) => itemCount, 712 | (value, ctx) => $"Health Boost: {value.FormatPercentage()}" 713 | ), 714 | new ItemStat( 715 | (itemCount, ctx) => 0.5f + 0.5f * itemCount, 716 | (value, ctx) => $"Damage Boost: {value.FormatPercentage()}" 717 | ) 718 | } 719 | }; 720 | ItemDefs[ItemCatalog.FindItemIndex("SprintWisp")] = new ItemStatDef 721 | { 722 | Stats = new List 723 | { 724 | new ItemStat( 725 | (itemCount, ctx) => 3f * itemCount, 726 | (value, ctx) => $"Damage Boost: {value.FormatPercentage()}" 727 | ) 728 | } 729 | }; 730 | ItemDefs[ItemCatalog.FindItemIndex("Dagger")] = new ItemStatDef 731 | { 732 | Stats = new List 733 | { 734 | new ItemStat( 735 | (itemCount, ctx) => 1.5f * itemCount, 736 | (value, ctx) => $"Dagger Damage: {value.FormatPercentage()}" 737 | ) 738 | } 739 | }; 740 | ItemDefs[ItemCatalog.FindItemIndex("LunarUtilityReplacement")] = new ItemStatDef 741 | { 742 | Stats = new List 743 | { 744 | new ItemStat( 745 | (itemCount, ctx) => 3f * itemCount, 746 | (value, ctx) => $"Skill Duration: {value.FormatInt("s")}" 747 | ), 748 | new ItemStat( 749 | (itemCount, ctx) => 750 | { 751 | var healthFraction = ctx.Master != null ? ctx.Master.GetBody().maxHealth : 1; 752 | 753 | // heal 1.3% of max HP per 5 times/s across 3 * itemCount seconds 754 | return healthFraction * 0.013f * 5 * (3f * itemCount); 755 | }, 756 | (value, ctx) => 757 | { 758 | var statValue = ctx.Master != null 759 | ? $"{value.FormatInt("HP")}" 760 | : $"{value.FormatPercentage()}"; 761 | return "Health Healed: " + statValue; 762 | }) 763 | } 764 | }; 765 | ItemDefs[ItemCatalog.FindItemIndex("NearbyDamageBonus")] = new ItemStatDef 766 | { 767 | Stats = new List 768 | { 769 | new ItemStat( 770 | (itemCount, ctx) => 0.2f * itemCount, 771 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 772 | ) 773 | } 774 | }; 775 | ItemDefs[ItemCatalog.FindItemIndex("TPHealingNova")] = new ItemStatDef 776 | { 777 | Stats = new List 778 | { 779 | new ItemStat( 780 | (itemCount, ctx) => itemCount, 781 | (value, ctx) => $"Max Occurrences: {value.FormatInt()}" 782 | // StatModifiers.TpHealingNova 783 | ) 784 | } 785 | }; 786 | ItemDefs[ItemCatalog.FindItemIndex("ArmorReductionOnHit")] = new ItemStatDef 787 | { 788 | Stats = new List 789 | { 790 | new ItemStat( 791 | (itemCount, ctx) => 8f * itemCount, 792 | (value, ctx) => $"Duration: {value.FormatInt("s")}" 793 | ) 794 | } 795 | }; 796 | ItemDefs[ItemCatalog.FindItemIndex("Thorns")] = new ItemStatDef 797 | { 798 | Stats = new List 799 | { 800 | new ItemStat( 801 | (itemCount, ctx) => 5 + 2 * (itemCount - 1), 802 | (value, ctx) => $"Max Targets: {value.FormatInt()}" 803 | ), 804 | new ItemStat( 805 | (itemCount, ctx) => 25f + 10f * (itemCount - 1), 806 | (value, ctx) => $"Radius: {value.FormatInt("m")}" 807 | ) 808 | } 809 | }; 810 | ItemDefs[ItemCatalog.FindItemIndex("FlatHealth")] = new ItemStatDef 811 | { 812 | Stats = new List 813 | { 814 | new ItemStat( 815 | (itemCount, ctx) => 25 * itemCount, 816 | (value, ctx) => $"Health Increase: {value.FormatInt()}" 817 | ) 818 | } 819 | }; 820 | ItemDefs[ItemCatalog.FindItemIndex("Pearl")] = new ItemStatDef 821 | { 822 | Stats = new List 823 | { 824 | new ItemStat( 825 | (itemCount, ctx) => 0.1f * itemCount, 826 | (value, ctx) => $"Health Increase: {value.FormatPercentage()}" 827 | ) 828 | } 829 | }; 830 | ItemDefs[ItemCatalog.FindItemIndex("ShinyPearl")] = new ItemStatDef 831 | { 832 | Stats = new List 833 | { 834 | new ItemStat( 835 | (itemCount, ctx) => 0.1f * itemCount, 836 | (value, ctx) => $"Stat Increase: {value.FormatPercentage()}" 837 | ) 838 | } 839 | }; 840 | ItemDefs[ItemCatalog.FindItemIndex("BonusGoldPackOnKill")] = new ItemStatDef 841 | { 842 | Stats = new List 843 | { 844 | new ItemStat( 845 | (itemCount, ctx) => Run.instance.GetDifficultyScaledCost(25), 846 | (value, ctx) => $"per Drop: {value.FormatInt("$")}" 847 | ), 848 | new ItemStat( 849 | (itemCount, ctx) => 0.04f * itemCount, 850 | (value, ctx) => $"Drop Chance: {value.FormatPercentage()}" 851 | // StatModifiers.Luck 852 | ) 853 | } 854 | }; 855 | ItemDefs[ItemCatalog.FindItemIndex("LunarPrimaryReplacement")] = new ItemStatDef 856 | { 857 | Stats = new List 858 | { 859 | new ItemStat( 860 | (itemCount, ctx) => 12f * itemCount, 861 | (value, ctx) => $"Max Charges: {value.FormatInt()}" 862 | ), 863 | new ItemStat( 864 | (itemCount, ctx) => 2f * itemCount, 865 | (value, ctx) => $"Recharge Delay: {value.FormatInt("s")}" 866 | ) 867 | } 868 | }; 869 | ItemDefs[ItemCatalog.FindItemIndex("LaserTurbine")] = new ItemStatDef 870 | { 871 | Stats = new List 872 | { 873 | new ItemStat( 874 | (itemCount, ctx) => 3f * itemCount, 875 | (value, ctx) => $"Pierce Damage: {value.FormatPercentage()}" 876 | ), 877 | new ItemStat( 878 | (itemCount, ctx) => 10f * itemCount, 879 | (value, ctx) => $"Explosion Damage: {value.FormatPercentage()}" 880 | ), 881 | new ItemStat( 882 | (itemCount, ctx) => 3f * itemCount, 883 | (value, ctx) => $"On Return Damage: {value.FormatPercentage()}" 884 | ) 885 | } 886 | }; 887 | ItemDefs[ItemCatalog.FindItemIndex("NovaOnLowHealth")] = new ItemStatDef 888 | { 889 | Stats = new List 890 | { 891 | new ItemStat( 892 | (itemCount, ctx) => 30f / itemCount, 893 | (value, ctx) => $"Recharge Delay: {value.FormatInt("s")}" 894 | ) 895 | } 896 | }; 897 | ItemDefs[ItemCatalog.FindItemIndex("ArmorPlate")] = new ItemStatDef 898 | { 899 | Stats = new List 900 | { 901 | new ItemStat( 902 | (itemCount, ctx) => itemCount * 5, 903 | (value, ctx) => $"Reduced damage: {value.FormatInt()}" 904 | ) 905 | } 906 | }; 907 | ItemDefs[ItemCatalog.FindItemIndex("Squid")] = new ItemStatDef 908 | { 909 | Stats = new List 910 | { 911 | new ItemStat( 912 | (itemCount, ctx) => itemCount, 913 | (value, ctx) => $"Attack Speed: {value.FormatPercentage(0)}" 914 | ) 915 | } 916 | }; 917 | ItemDefs[ItemCatalog.FindItemIndex("DeathMark")] = new ItemStatDef 918 | { 919 | Stats = new List 920 | { 921 | new ItemStat( 922 | (itemCount, ctx) => itemCount * 0.5f, 923 | (value, ctx) => $"Increased Damage: {value.FormatPercentage()}" 924 | ) 925 | } 926 | }; 927 | ItemDefs[ItemCatalog.FindItemIndex("Plant")] = new ItemStatDef 928 | { 929 | Stats = new List 930 | { 931 | new ItemStat( 932 | (itemCount, ctx) => 3 + 1.5f * (itemCount - 1), 933 | (value, ctx) => $"Radius: {value.FormatInt("m", 1)}" 934 | ) 935 | } 936 | }; 937 | ItemDefs[ItemCatalog.FindItemIndex("FocusConvergence")] = new ItemStatDef 938 | { 939 | Stats = new List 940 | { 941 | new ItemStat( 942 | (itemCount, ctx) => 90f / (1f + 0.3f * Mathf.Min(itemCount, 3f)), 943 | (value, ctx) => $"Minimum Charge Time: {value.FormatInt("s")}" 944 | ), 945 | new ItemStat( 946 | (itemCount, ctx) => 1 / (2 * Mathf.Min(itemCount, 3f)), 947 | (value, ctx) => $"Zone Size: {value.FormatPercentage(2)}" 948 | ) 949 | } 950 | }; 951 | ItemDefs[ItemCatalog.FindItemIndex("DeathMark")] = new ItemStatDef 952 | { 953 | Stats = new List 954 | { 955 | new ItemStat( 956 | (itemCount, ctx) => 7f * itemCount, 957 | (value, ctx) => $"Debuff Duration: {value.FormatInt("s")}" 958 | ) 959 | } 960 | }; 961 | ItemDefs[ItemCatalog.FindItemIndex("Plant")] = new ItemStatDef 962 | { 963 | Stats = new List 964 | { 965 | new ItemStat( 966 | (itemCount, ctx) => 5f * itemCount, 967 | (value, ctx) => $"Healing Radius: {value.FormatInt("s")}" 968 | ) 969 | } 970 | }; 971 | ItemDefs[ItemCatalog.FindItemIndex("Squid")] = new ItemStatDef 972 | { 973 | Stats = new List 974 | { 975 | new ItemStat( 976 | (itemCount, ctx) => itemCount, 977 | (value, ctx) => $"Attack Speed: {value.FormatPercentage()}" 978 | ) 979 | } 980 | }; 981 | ItemDefs[ItemCatalog.FindItemIndex("CaptainDefenseMatrix")] = new ItemStatDef 982 | { 983 | Stats = new List 984 | { 985 | new ItemStat( 986 | (itemCount, ctx) => itemCount, 987 | (value, ctx) => $"Projectile Count: {value.FormatInt(" projectile(s)")}" 988 | ) 989 | } 990 | }; 991 | ItemDefs[ItemCatalog.FindItemIndex("CutHp")] = new ItemStatDef 992 | { 993 | Stats = new List 994 | { 995 | new ItemStat( 996 | (itemCount, ctx) => 1f / (itemCount + 1), 997 | (value, ctx) => $"Health Reduction: {value.FormatPercentage()}" 998 | ) 999 | } 1000 | }; 1001 | ItemDefs[ItemCatalog.FindItemIndex("Phasing")] = new ItemStatDef 1002 | { 1003 | Stats = new List 1004 | { 1005 | new ItemStat( 1006 | (itemCount, ctx) => 30 * Mathf.Pow(0.5f, itemCount - 1), 1007 | (value, ctx) => $"Cooldown: {value.FormatInt("s")}" 1008 | ) 1009 | } 1010 | }; 1011 | ItemDefs[ItemCatalog.FindItemIndex("FallBoots")] = new ItemStatDef 1012 | { 1013 | Stats = new List 1014 | { 1015 | new ItemStat( 1016 | (itemCount, ctx) => 10f * Mathf.Pow(0.5f, itemCount - 1), 1017 | (value, ctx) => $"Recharge Time: {value.FormatInt("s", 2)}" 1018 | ) 1019 | } 1020 | }; 1021 | ItemDefs[ItemCatalog.FindItemIndex("JumpBoost")] = new ItemStatDef 1022 | { 1023 | Stats = new List 1024 | { 1025 | new ItemStat( 1026 | (itemCount, ctx) => 10f * itemCount, 1027 | (value, ctx) => $"Boost Length: {value.FormatInt("m")}" 1028 | ) 1029 | } 1030 | }; 1031 | ItemDefs[ItemCatalog.FindItemIndex("LunarDagger")] = new ItemStatDef 1032 | { 1033 | Stats = new List 1034 | { 1035 | new ItemStat( 1036 | (itemCount, ctx) => Mathf.Pow(2f, itemCount), 1037 | (value, ctx) => $"Base Damage Increase: {value.FormatPercentage()}" 1038 | ), 1039 | new ItemStat( 1040 | (itemCount, ctx) => 1f / Mathf.Pow(2f, itemCount), 1041 | (value, ctx) => $"Max Health Reduction: {value.FormatPercentage()}" 1042 | ) 1043 | } 1044 | }; 1045 | ItemDefs[ItemCatalog.FindItemIndex("Incubator")] = new ItemStatDef 1046 | { 1047 | Stats = new List 1048 | { 1049 | new ItemStat( 1050 | (itemCount, ctx) => (7f + 1f * itemCount) / 100f, 1051 | (value, ctx) => $"Summon Chance: {value.FormatPercentage()}" 1052 | // StatModifiers.Luck 1053 | ), 1054 | new ItemStat( 1055 | (itemCount, ctx) => itemCount, 1056 | (value, ctx) => $"Base Health: {value.FormatPercentage()}" 1057 | ) 1058 | } 1059 | }; 1060 | ItemDefs[ItemCatalog.FindItemIndex("SiphonOnLowHealth")] = new ItemStatDef 1061 | { 1062 | Stats = new List 1063 | { 1064 | new ItemStat( 1065 | (itemCount, ctx) => itemCount, 1066 | (value, ctx) => $"Additional Enemies: {value.FormatInt()}" 1067 | ) 1068 | } 1069 | }; 1070 | ItemDefs[ItemCatalog.FindItemIndex("FireballsOnHit")] = new ItemStatDef 1071 | { 1072 | Stats = new List 1073 | { 1074 | new ItemStat( 1075 | (itemCount, ctx) => 3 * itemCount, 1076 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 1077 | ), 1078 | new ItemStat( 1079 | (itemCount, ctx) => 0.10f, 1080 | (value, ctx) => $"Proc Chance: {value.FormatPercentage()}" 1081 | ) 1082 | } 1083 | }; 1084 | ItemDefs[ItemCatalog.FindItemIndex("BleedOnHitAndExplode")] = new ItemStatDef 1085 | { 1086 | Stats = new List 1087 | { 1088 | new ItemStat( 1089 | (itemCount, ctx) => 4 * itemCount, 1090 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 1091 | ), 1092 | new ItemStat( 1093 | (itemCount, ctx) => 0.15f * itemCount, 1094 | (value, ctx) => $"Explosion Damage Increase: {value.FormatPercentage()}" 1095 | ) 1096 | } 1097 | }; 1098 | ItemDefs[ItemCatalog.FindItemIndex("MonstersOnShrineUse")] = new ItemStatDef 1099 | { 1100 | Stats = new List 1101 | { 1102 | new ItemStat( 1103 | (itemCount, ctx) => 0.4f * itemCount, 1104 | (value, ctx) => $"Enemy Difficulty Increase: {value.FormatPercentage()}" 1105 | ), 1106 | } 1107 | }; 1108 | ItemDefs[ItemCatalog.FindItemIndex("RandomDamageZone")] = new ItemStatDef 1109 | { 1110 | Stats = new List 1111 | { 1112 | new ItemStat( 1113 | (itemCount, ctx) => 16f * Mathf.Pow(1.5f, itemCount - 1), 1114 | (value, ctx) => $"Radius Increase: {value.FormatInt("m")}" 1115 | ), 1116 | } 1117 | }; 1118 | ItemDefs[ItemCatalog.FindItemIndex("LunarBadLuck")] = new ItemStatDef 1119 | { 1120 | Stats = new List 1121 | { 1122 | new ItemStat( 1123 | (itemCount, ctx) => itemCount, 1124 | (value, ctx) => $"Cooldown Reduction: {value.FormatInt("s")}" 1125 | ), 1126 | } 1127 | }; 1128 | // Charged Perforator 1129 | ItemDefs[ItemCatalog.FindItemIndex("LightningStrikeOnHit")] = new ItemStatDef 1130 | { 1131 | Stats = new List 1132 | { 1133 | new ItemStat( 1134 | (itemCount, ctx) => 5f * itemCount, 1135 | (value, ctx) => $"Damage: {value.FormatPercentage()}" 1136 | ), 1137 | } 1138 | }; 1139 | // Empathy Cores 1140 | ItemDefs[ItemCatalog.FindItemIndex("RoboBallBuddy")] = new ItemStatDef 1141 | { 1142 | Stats = new List 1143 | { 1144 | new ItemStat( 1145 | (itemCount, ctx) => 1f * itemCount, 1146 | (value, ctx) => $"Damage per Ally: {value.FormatPercentage()}" 1147 | ), 1148 | } 1149 | }; 1150 | // Planula 1151 | ItemDefs[ItemCatalog.FindItemIndex("ParentEgg")] = new ItemStatDef 1152 | { 1153 | Stats = new List 1154 | { 1155 | new ItemStat( 1156 | (itemCount, ctx) => 15 * itemCount, 1157 | (value, ctx) => $"Heal Amount: {value.FormatInt(" HP")}" 1158 | ), 1159 | } 1160 | }; 1161 | // Essence of Heresy 1162 | ItemDefs[ItemCatalog.FindItemIndex("LunarSpecialReplacement")] = new ItemStatDef 1163 | { 1164 | Stats = new List 1165 | { 1166 | new ItemStat( 1167 | (itemCount, ctx) => 10 * itemCount, 1168 | (value, ctx) => $"Effect Duration: {value.FormatInt("s")}" 1169 | ), 1170 | new ItemStat( 1171 | (itemCount, ctx) => 8 * itemCount, 1172 | (value, ctx) => $"Cooldown: {value.FormatInt("s")}" 1173 | ), 1174 | } 1175 | }; 1176 | // Hooks of Heresy 1177 | ItemDefs[ItemCatalog.FindItemIndex("LunarSecondaryReplacement")] = new ItemStatDef 1178 | { 1179 | Stats = new List 1180 | { 1181 | new ItemStat( 1182 | (itemCount, ctx) => 3 * itemCount, 1183 | (value, ctx) => $"Root Duration: {value.FormatInt("s")}" 1184 | ), 1185 | new ItemStat( 1186 | (itemCount, ctx) => 5 * itemCount, 1187 | (value, ctx) => $"Cooldown: {value.FormatInt("s")}" 1188 | ), 1189 | } 1190 | }; 1191 | // Hunter's Harpoon 1192 | ItemDefs[ItemCatalog.FindItemIndex("MoveSpeedOnKill")] = new ItemStatDef 1193 | { 1194 | Stats = new List 1195 | { 1196 | new ItemStat( 1197 | (itemCount, ctx) => 1f + (itemCount - 1f) * 0.5f, 1198 | (value, ctx) => $"Total Duration: {value.FormatInt("s", 1)}" 1199 | ), 1200 | } 1201 | }; 1202 | // Symbiotic Scorpion 1203 | ItemDefs[ItemCatalog.FindItemIndex("PermanentDebuffOnHit")] = new ItemStatDef 1204 | { 1205 | Stats = new List 1206 | { 1207 | new ItemStat( 1208 | (itemCount, ctx) => 1f + (itemCount - 1f) * 0.5f, 1209 | (value, ctx) => $"Armor Reduction: {value.FormatInt()}" 1210 | ), 1211 | } 1212 | }; 1213 | // Mocha 1214 | ItemDefs[ItemCatalog.FindItemIndex("AttackSpeedAndMoveSpeed")] = new ItemStatDef 1215 | { 1216 | Stats = new List 1217 | { 1218 | new ItemStat( 1219 | (itemCount, ctx) => itemCount * 0.075f, 1220 | (value, ctx) => $"Attack Speed Increase: {value.FormatPercentage()}" 1221 | ), 1222 | new ItemStat( 1223 | (itemCount, ctx) => itemCount * 0.07f, 1224 | (value, ctx) => $"Move Speed Increase {value.FormatPercentage()}" 1225 | ), 1226 | } 1227 | }; 1228 | // Laser Scope 1229 | ItemDefs[ItemCatalog.FindItemIndex("CritDamage")] = new ItemStatDef 1230 | { 1231 | Stats = new List 1232 | { 1233 | new ItemStat( 1234 | (itemCount, ctx) => itemCount, 1235 | (value, ctx) => $"Additional Damage {value.FormatPercentage()}" 1236 | ), 1237 | } 1238 | }; 1239 | // Safer Spaces 1240 | ItemDefs[ItemCatalog.FindItemIndex("BearVoid")] = new ItemStatDef 1241 | { 1242 | Stats = new List 1243 | { 1244 | new ItemStat( 1245 | (itemCount, ctx) => Mathf.CeilToInt(15f * Mathf.Pow(0.9f, itemCount)), 1246 | (value, ctx) => $"Recharge Duration {value.FormatInt("s", 1)}" 1247 | ), 1248 | }, 1249 | }; 1250 | // Weeping Fungus 1251 | ItemDefs[ItemCatalog.FindItemIndex("MushroomVoid")] = new ItemStatDef 1252 | { 1253 | Stats = new List 1254 | { 1255 | new ItemStat( 1256 | (itemCount, ctx) => 0.01f * itemCount, 1257 | (value, ctx) => $"Heal Amount: {value.FormatPercentage()}" 1258 | ), 1259 | }, 1260 | AdditionalText = "Description is incorrect".SetColor("red") 1261 | }; 1262 | // Benthic Bloom 1263 | ItemDefs[ItemCatalog.FindItemIndex("CloverVoid")] = new ItemStatDef 1264 | { 1265 | Stats = new List 1266 | { 1267 | new ItemStat( 1268 | (itemCount, ctx) => 3f * itemCount, 1269 | (value, ctx) => $"Items Upgraded: {value.FormatInt()}" 1270 | ), 1271 | }, 1272 | }; 1273 | // Ignition Tank 1274 | ItemDefs[ItemCatalog.FindItemIndex("StrengthenBurn")] = new ItemStatDef 1275 | { 1276 | Stats = new List 1277 | { 1278 | new ItemStat( 1279 | (itemCount, ctx) => 3f * itemCount, 1280 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 1281 | ), 1282 | }, 1283 | }; 1284 | // Needletick 1285 | ItemDefs[ItemCatalog.FindItemIndex("BleedOnHitVoid")] = new ItemStatDef 1286 | { 1287 | Stats = new List 1288 | { 1289 | new ItemStat( 1290 | (itemCount, ctx) => 0.1f * itemCount, 1291 | (value, ctx) => $"Collapse Chance: {value.FormatPercentage()}" 1292 | ), 1293 | }, 1294 | }; 1295 | // Lost Seer's Lenses 1296 | ItemDefs[ItemCatalog.FindItemIndex("CritGlassesVoid")] = new ItemStatDef 1297 | { 1298 | Stats = new List 1299 | { 1300 | new ItemStat( 1301 | (itemCount, ctx) => 0.05f * itemCount, 1302 | (value, ctx) => $"Instakill Chance: {value.FormatPercentage()}" 1303 | ), 1304 | }, 1305 | }; 1306 | // Tentabauble 1307 | ItemDefs[ItemCatalog.FindItemIndex("SlowOnHitVoid")] = new ItemStatDef 1308 | { 1309 | Stats = new List 1310 | { 1311 | new ItemStat( 1312 | (itemCount, ctx) => itemCount * 0.05f, 1313 | (value, ctx) => $"Root Chance: {value.FormatPercentage()}" 1314 | ), 1315 | new ItemStat( 1316 | (itemCount, ctx) => itemCount, 1317 | (value, ctx) => $"Root Duration: {value.FormatInt("s")}" 1318 | ), 1319 | }, 1320 | }; 1321 | // Plasma Shrimp 1322 | ItemDefs[ItemCatalog.FindItemIndex("Plasma Shrimp")] = new ItemStatDef 1323 | { 1324 | Stats = new List 1325 | { 1326 | new ItemStat( 1327 | (itemCount, ctx) => itemCount * 0.4f, 1328 | (value, ctx) => $"Total Damage: {value.FormatPercentage()}" 1329 | ), 1330 | new ItemStat( 1331 | (itemCount, ctx) => 1, 1332 | (value, ctx) => $"Missile Count: {value.FormatInt()}" 1333 | ), 1334 | }, 1335 | }; 1336 | // Polylute 1337 | ItemDefs[ItemCatalog.FindItemIndex("ChainLightningVoid")] = new ItemStatDef 1338 | { 1339 | Stats = new List 1340 | { 1341 | new ItemStat( 1342 | (itemCount, ctx) => itemCount * 3f, 1343 | (value, ctx) => $"Total Strikes: {value.FormatInt()}" 1344 | ), 1345 | }, 1346 | }; 1347 | // Lysate Cell 1348 | ItemDefs[ItemCatalog.FindItemIndex("EquipmentMagazineVoid")] = new ItemStatDef 1349 | { 1350 | Stats = new List 1351 | { 1352 | new ItemStat( 1353 | (itemCount, ctx) => itemCount, 1354 | (value, ctx) => $"Bonus Charges: {value.FormatInt()}" 1355 | ), 1356 | }, 1357 | }; 1358 | // Voidsent Flame 1359 | ItemDefs[ItemCatalog.FindItemIndex("ExplodeOnDeathVoid")] = new ItemStatDef 1360 | { 1361 | Stats = new List 1362 | { 1363 | new ItemStat( 1364 | (itemCount, ctx) => 3.5f * (1f + (itemCount - 1) * 0.8f), 1365 | (value, ctx) => $"Base Damage: {value.FormatPercentage()}" 1366 | ), 1367 | new ItemStat( 1368 | (itemCount, ctx) => 12f + 2.4f * (itemCount - 1f), 1369 | (value, ctx) => $"Radius: {value.FormatInt("m")}" 1370 | ), 1371 | }, 1372 | }; 1373 | // Delicate Watch 1374 | ItemDefs[ItemCatalog.FindItemIndex("FragileDamageBonus")] = new ItemStatDef 1375 | { 1376 | Stats = new List 1377 | { 1378 | new ItemStat( 1379 | (itemCount, ctx) => itemCount * 0.2f, 1380 | (value, ctx) => $"Damage Increase: {value.FormatPercentage()}" 1381 | ), 1382 | }, 1383 | }; 1384 | } 1385 | } 1386 | } 1387 | -------------------------------------------------------------------------------- /ItemStats/src/ItemStatProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RoR2; 3 | 4 | namespace ItemStats 5 | { 6 | internal static partial class ItemStatProvider 7 | { 8 | private static readonly Dictionary ItemDefs; 9 | 10 | private static readonly Dictionary CustomItemDefs; 11 | 12 | public static string ProvideStatsForItem(ItemIndex index, int count, StatContext context) 13 | { 14 | var itemStatDef = GetItemStatDef(index); 15 | 16 | return itemStatDef != null ? itemStatDef.ProcessItem(index, count, context) : ""; 17 | } 18 | 19 | public static void AddCustomItemDef(ItemIndex idx, ItemStatDef customDef) 20 | { 21 | CustomItemDefs[idx] = customDef; 22 | } 23 | 24 | public static ItemStatDef GetItemStatDef(ItemIndex index) 25 | { 26 | if (!ItemDefs.TryGetValue(index, out var itemStatDef)) CustomItemDefs.TryGetValue(index, out itemStatDef); 27 | 28 | return itemStatDef; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ItemStats/src/ItemStatsMod.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BepInEx; 3 | using BepInEx.Configuration; 4 | using BepInEx.Logging; 5 | using ItemStats.StatModification; 6 | using R2API.Utils; 7 | using RoR2; 8 | 9 | namespace ItemStats 10 | { 11 | [BepInDependency("com.bepis.r2api")] 12 | [BepInPlugin("dev.ontrigger.itemstats", "ItemStats", "2.2.1")] 13 | [NetworkCompatibility(CompatibilityLevel.NoNeedForSync, VersionStrictness.DifferentModVersionsAreOk)] 14 | public class ItemStatsMod : BaseUnityPlugin 15 | { 16 | internal new static ManualLogSource Logger { get; private set; } 17 | 18 | public static ConfigEntry DetailedPickupDescriptions; 19 | 20 | private ItemStatsMod() 21 | { 22 | Logger = base.Logger; 23 | } 24 | 25 | public void Awake() 26 | { 27 | InitConfig(); 28 | 29 | ItemCatalog.availability.CallWhenAvailable(() => 30 | { 31 | ItemStatProvider.Init(); 32 | StatModifiers.Init(); 33 | Hooks.Init(); 34 | }); 35 | } 36 | 37 | private void InitConfig() 38 | { 39 | DetailedPickupDescriptions = Config.Bind( 40 | "Settings", 41 | "DetailedPickupDescriptions", 42 | true, 43 | "Toggle displaying full item descriptions in the pickup popup" 44 | ); 45 | } 46 | 47 | public static void AddCustomItemStatDef(ItemIndex index, ItemStatDef customDef) 48 | { 49 | ItemStatProvider.AddCustomItemDef(index, customDef); 50 | } 51 | 52 | public static ItemStatDef GetItemStatDef(ItemIndex index) 53 | { 54 | return ItemStatProvider.GetItemStatDef(index); 55 | } 56 | 57 | public static string GetStatsForItem(ItemIndex index, int count, StatContext context) 58 | { 59 | return ItemStatProvider.ProvideStatsForItem(index, count, context); 60 | } 61 | 62 | public static void AddStatModifier(AbstractStatModifier modifier) 63 | { 64 | StatModifiers.AddStatModifier(modifier); 65 | } 66 | 67 | public static List GetModifiersForItemIndex(ItemIndex index) 68 | { 69 | return StatModifiers.GetModifiersForItemIndex(index); 70 | } 71 | 72 | public static List GetModifiersForItemDef(ItemStatDef itemStatDef) 73 | { 74 | return StatModifiers.GetModifiersForItemDef(itemStatDef); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /ItemStats/src/Stat/IStat.cs: -------------------------------------------------------------------------------- 1 | namespace ItemStats.Stat 2 | { 3 | public interface IStat 4 | { 5 | float? GetInitialStat(float count, StatContext context); 6 | 7 | string Format(float statValue, StatContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /ItemStats/src/Stat/ItemStat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ItemStats.Stat 4 | { 5 | public class ItemStat : IStat 6 | { 7 | public Func Formula { get; } 8 | public Func Formatter { get; } 9 | 10 | public ItemStat(Func formula, Func formatter) 11 | { 12 | Formula = formula; 13 | Formatter = formatter; 14 | } 15 | 16 | public float? GetInitialStat(float count, StatContext context) 17 | { 18 | try 19 | { 20 | return Formula(count, context); 21 | } 22 | catch (NullReferenceException e) 23 | { 24 | ItemStatsMod.Logger.LogError("Caught " + e); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | public string Format(float statValue, StatContext context) 31 | { 32 | return Formatter(statValue, context); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /ItemStats/src/Stat/ItemStatDef.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ItemStats.Stat; 3 | using ItemStats.StatCalculation; 4 | using ItemStats.StatModification; 5 | using RoR2; 6 | 7 | namespace ItemStats 8 | { 9 | public class ItemStatDef 10 | { 11 | public IStatCalculationStrategy StatCalculationStrategy = new DefaultStatCalculationStrategy(); 12 | 13 | public List Stats; 14 | 15 | // additional text that only appears on stat tooltip and not the logbook 16 | public string AdditionalText; 17 | 18 | public string ProcessItem(ItemIndex index, int count, StatContext context) 19 | { 20 | return StatCalculationStrategy.ProcessItem(this, index, count, context); 21 | } 22 | } 23 | 24 | public static class ItemStatDefExtensions 25 | { 26 | public static List GetStatModifiers(this ItemStatDef statDef) 27 | { 28 | return StatModifiers.GetModifiersForItemDef(statDef); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ItemStats/src/Stat/StatContext.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using RoR2; 3 | 4 | namespace ItemStats 5 | { 6 | public class StatContext 7 | { 8 | public StatContext([CanBeNull] CharacterMaster master) 9 | { 10 | Master = master; 11 | Inventory = master != null ? master.inventory : null; 12 | } 13 | 14 | [CanBeNull] public CharacterMaster Master { get; } 15 | [CanBeNull] public Inventory Inventory { get; } 16 | } 17 | 18 | public static class StatContextExtensions 19 | { 20 | public static int CountItems(this StatContext ctx, ItemIndex idx) 21 | { 22 | return ctx.Inventory != null ? ctx.Inventory.GetItemCount(idx) : 0; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /ItemStats/src/StatCalculation/DefaultStatCalculationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using RoR2; 4 | 5 | namespace ItemStats.StatCalculation 6 | { 7 | public class DefaultStatCalculationStrategy : IStatCalculationStrategy 8 | { 9 | public string ProcessItem(ItemStatDef statDef, ItemIndex itemIndex, int count, StatContext context) 10 | { 11 | var fullStatText = new StringBuilder(); 12 | fullStatText.AppendLine(); 13 | 14 | if (statDef.AdditionalText != null) 15 | { 16 | fullStatText.AppendLine(); 17 | fullStatText.Append(statDef.AdditionalText); 18 | fullStatText.AppendLine(); 19 | } 20 | 21 | var statList = statDef.Stats; 22 | var modifierList = statDef.GetStatModifiers(); 23 | 24 | for (var statIndex = 0; statIndex < statList.Count; statIndex++) 25 | { 26 | var stat = statList[statIndex]; 27 | var m = stat.GetInitialStat(count, context); 28 | if (!m.HasValue) continue; 29 | var originalValue = m.Value; 30 | 31 | var lastLine = statIndex == statList.Count - 1; 32 | 33 | var modifiedValueSum = 0f; 34 | var formattedContributions = new StringBuilder(); 35 | 36 | foreach (var statModifier in modifierList) 37 | { 38 | if (!statModifier.AffectsItem(itemIndex, statIndex)) continue; 39 | 40 | m = statModifier.ModifyValue(originalValue, itemIndex, statIndex, context); 41 | 42 | var modifierContribution = (float) m - originalValue; 43 | 44 | // skip modifiers that contrib less that 1% to the final value 45 | if (!ContributionSignificant(modifierContribution)) continue; 46 | 47 | formattedContributions.AppendLine(); 48 | formattedContributions.Append(" "); 49 | formattedContributions.Append( 50 | statModifier.Format(modifierContribution, itemIndex, statIndex, context) 51 | ); 52 | 53 | modifiedValueSum += modifierContribution; 54 | } 55 | 56 | var finalFormattedValue = stat.Format(originalValue + modifiedValueSum, context); 57 | 58 | // explicitly align left on the last line to fix the stack counter alignment 59 | var lastLineAlignment = lastLine ? "" : ""; 60 | 61 | fullStatText.AppendLine(); 62 | fullStatText.Append(lastLineAlignment + finalFormattedValue); 63 | fullStatText.Append(formattedContributions); 64 | } 65 | 66 | 67 | return fullStatText.Append($"
({count} stacks)").ToString(); 68 | } 69 | 70 | private static bool ContributionSignificant(float contrib) 71 | { 72 | return Math.Round(Math.Abs(contrib), 4) > 0; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /ItemStats/src/StatCalculation/IStatCalculationStrategy.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | 3 | namespace ItemStats.StatCalculation 4 | { 5 | public interface IStatCalculationStrategy 6 | { 7 | string ProcessItem(ItemStatDef statDef, ItemIndex itemIndex, int count, StatContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /ItemStats/src/StatModification/AbstractStatModifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using RoR2; 5 | 6 | namespace ItemStats.StatModification 7 | { 8 | public abstract class AbstractStatModifier : IStatModifier 9 | { 10 | protected abstract Func ModifyValueFunc { get; } 11 | 12 | protected virtual Func ModifyItemCountFunc => 13 | (itemCount, itemIndex, itemStatIndex, context) => itemCount; 14 | 15 | protected abstract Func FormatFunc { get; } 16 | 17 | public abstract Dictionary> AffectedItems { get; } 18 | 19 | public float ModifyValue(float result, ItemIndex itemIndex, int statIndex, StatContext context) 20 | { 21 | return ModifyValueFunc(result, itemIndex, statIndex, context); 22 | } 23 | 24 | public int ModifyItemCount(int count, ItemIndex itemIndex, int statIndex, StatContext context) 25 | { 26 | return ModifyItemCountFunc(count, itemIndex, statIndex, context); 27 | } 28 | 29 | public string Format(float result, ItemIndex itemIndex, int statIndex, StatContext context) 30 | { 31 | return FormatFunc(result, itemIndex, statIndex, context); 32 | } 33 | 34 | public bool AffectsItem(ItemIndex itemIndex, int statIndex) 35 | { 36 | if (AffectedItems.TryGetValue(itemIndex, out var affectedStats)) 37 | { 38 | return affectedStats.Contains(statIndex); 39 | } 40 | 41 | ; 42 | 43 | return false; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ItemStats/src/StatModification/IStatModifier.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | 3 | namespace ItemStats.StatModification 4 | { 5 | public interface IStatModifier 6 | { 7 | float ModifyValue(float result, ItemIndex itemIndex, int statIndex, StatContext context); 8 | 9 | int ModifyItemCount(int count, ItemIndex itemIndex, int statIndex, StatContext context); 10 | 11 | string Format(float result, ItemIndex itemIndex, int statIndex, StatContext context); 12 | 13 | bool AffectsItem(ItemIndex itemIndex, int statIndex); 14 | } 15 | } -------------------------------------------------------------------------------- /ItemStats/src/StatModification/Modifiers/HealingIncreaseModifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ItemStats.ValueFormatters; 4 | using RoR2; 5 | 6 | namespace ItemStats.StatModification 7 | { 8 | public class HealingIncreaseModifier : AbstractStatModifier 9 | { 10 | protected override Func ModifyValueFunc => 11 | (result, itemIndex, itemStatIndex, context) => 12 | result * (1 + context.CountItems(ItemCatalog.FindItemIndex("IncreaseHealing"))); 13 | 14 | protected override Func FormatFunc => 15 | (result, itemIndex, itemStatIndex, ctx) => 16 | { 17 | string formattedResult; 18 | if (itemIndex == ItemCatalog.FindItemIndex("Mushroom") || itemIndex == ItemCatalog.FindItemIndex("Tooth")) 19 | { 20 | formattedResult = result.FormatPercentage(signed: true, color: Colors.ModifierColor); 21 | } 22 | else 23 | { 24 | formattedResult = result.FormatInt(signed: true, color: Colors.ModifierColor, postfix: "HP"); 25 | } 26 | 27 | return $"{formattedResult} from Rejuvenation Rack"; 28 | }; 29 | 30 | public override Dictionary> AffectedItems => 31 | new Dictionary> 32 | { 33 | [ItemCatalog.FindItemIndex("Mushroom")] = new[] {0}, 34 | [ItemCatalog.FindItemIndex("HealWhileSafe")] = new[] {0}, 35 | [ItemCatalog.FindItemIndex("Medkit")] = new[] {0}, 36 | [ItemCatalog.FindItemIndex("Tooth")] = new[] {0}, 37 | [ItemCatalog.FindItemIndex("HealOnCrit")] = new[] {0}, 38 | [ItemCatalog.FindItemIndex("Seed")] = new[] {0}, 39 | [ItemCatalog.FindItemIndex("Knurl")] = new[] {1} 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /ItemStats/src/StatModification/Modifiers/LuckModifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using ItemStats.ValueFormatters; 5 | using RoR2; 6 | using UnityEngine; 7 | 8 | namespace ItemStats.StatModification 9 | { 10 | public class LuckModifier : AbstractStatModifier 11 | { 12 | protected override Func ModifyValueFunc => 13 | (result, itemIndex, itemStatIndex, context) => 14 | { 15 | // if chance is already >= 100% then return same value so 16 | // that there are no contribution stats 17 | if (result >= 1) 18 | { 19 | return result; 20 | } 21 | 22 | var cloverCount = context.CountItems(ItemCatalog.FindItemIndex("Clover")); 23 | var purityCount = context.CountItems(ItemCatalog.FindItemIndex("LunarBadLuck")); 24 | 25 | var luck = cloverCount - purityCount; 26 | if (luck > 0) 27 | { 28 | return 1 - Mathf.Pow(1 - result, 1 + luck); 29 | } 30 | 31 | return (float) Math.Round(Math.Pow(result, 1 + Math.Abs(luck)), 4); 32 | }; 33 | 34 | protected override Func FormatFunc => 35 | (result, itemIndex, itemStatIndex, ctx) => 36 | { 37 | // TODO: pass the original value to be able to properly show clover and purity contribution 38 | var itemCount = ctx.CountItems(itemIndex); 39 | if (itemCount <= 0) 40 | { 41 | return $"{result.FormatPercentage(signed: true, color: Colors.ModifierColor)} from luck"; 42 | } 43 | 44 | var itemStatDef = ItemStatsMod.GetItemStatDef(itemIndex); 45 | var itemStat = itemStatDef.Stats[itemStatIndex]; 46 | 47 | // ReSharper disable once PossibleInvalidOperationException 48 | var originalValue = Mathf.Clamp01(itemStat.GetInitialStat(itemCount, ctx).Value); 49 | 50 | var cloverCount = ctx.CountItems(ItemCatalog.FindItemIndex("Clover")); 51 | var purityCount = ctx.CountItems(ItemCatalog.FindItemIndex("LunarBadLuck")); 52 | 53 | var cloverContribution = 1 - Mathf.Pow(1 - originalValue, 1 + cloverCount) - originalValue; 54 | 55 | var purityContribution = 56 | (float) Math.Round(Mathf.Pow(originalValue, 1 + purityCount), 3) - originalValue; 57 | 58 | var stringBuilder = new StringBuilder(); 59 | 60 | if (cloverCount > 0) 61 | { 62 | stringBuilder 63 | .Append(cloverContribution.FormatPercentage(signed: true, color: Colors.ModifierColor)) 64 | .Append(" from Clover"); 65 | 66 | if (purityCount > 0) stringBuilder.AppendLine().Append(" "); 67 | } 68 | 69 | if (purityCount > 0) 70 | { 71 | stringBuilder 72 | .Append(purityContribution.FormatPercentage(signed: true, color: Colors.ModifierColor)) 73 | .Append(" from Purity"); 74 | } 75 | 76 | return stringBuilder.ToString(); 77 | }; 78 | 79 | public override Dictionary> AffectedItems => 80 | new Dictionary> 81 | { 82 | [ItemCatalog.FindItemIndex("GhostOnKill")] = new[] {1}, 83 | [ItemCatalog.FindItemIndex("StunChanceOnHit")] = new[] {0}, 84 | [ItemCatalog.FindItemIndex("BleedOnHit")] = new[] {0}, 85 | [ItemCatalog.FindItemIndex("GoldOnHit")] = new[] {1}, 86 | [ItemCatalog.FindItemIndex("ChainLightning")] = new[] {2}, 87 | [ItemCatalog.FindItemIndex("BounceNearby")] = new[] {0}, 88 | [ItemCatalog.FindItemIndex("StickyBomb")] = new[] {0}, 89 | [ItemCatalog.FindItemIndex("Missile")] = new[] {1}, 90 | [ItemCatalog.FindItemIndex("BonusGoldPackOnKill")] = new[] {1}, 91 | [ItemCatalog.FindItemIndex("Incubator")] = new[] {0}, 92 | [ItemCatalog.FindItemIndex("FireballsOnHit")] = new[] {1} 93 | }; 94 | } 95 | } -------------------------------------------------------------------------------- /ItemStats/src/StatModification/StatModifiers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using RoR2; 3 | 4 | namespace ItemStats.StatModification 5 | { 6 | internal static class StatModifiers 7 | { 8 | private static readonly Dictionary> ModifierDefs; 9 | 10 | static StatModifiers() 11 | { 12 | ModifierDefs = new Dictionary>(); 13 | } 14 | 15 | public static void Init() 16 | { 17 | AddStatModifier(new LuckModifier()); 18 | AddStatModifier(new HealingIncreaseModifier()); 19 | } 20 | 21 | public static void AddStatModifier(AbstractStatModifier modifier) 22 | { 23 | foreach (var itemIndex in modifier.AffectedItems.Keys) 24 | { 25 | var itemStatDef = ItemStatProvider.GetItemStatDef(itemIndex); 26 | if (itemStatDef == null) 27 | { 28 | throw new KeyNotFoundException($"Affected ItemStatDef with ItemIndex ${itemIndex} not found"); 29 | } 30 | 31 | if (ModifierDefs.TryGetValue(itemStatDef, out var existingEntry)) 32 | { 33 | if (!existingEntry.Contains(modifier)) 34 | { 35 | existingEntry.Add(modifier); 36 | } 37 | } 38 | else 39 | { 40 | ModifierDefs[itemStatDef] = new List {modifier}; 41 | } 42 | } 43 | } 44 | 45 | public static List GetModifiersForItemIndex(ItemIndex itemIndex) 46 | { 47 | var itemStatDef = ItemStatProvider.GetItemStatDef(itemIndex); 48 | if (itemStatDef == null) 49 | { 50 | throw new KeyNotFoundException($"ItemStatDef with ItemIndex ${itemIndex} not found"); 51 | } 52 | 53 | return GetModifiersForItemDef(itemStatDef); 54 | } 55 | 56 | public static List GetModifiersForItemDef(ItemStatDef itemStatDef) 57 | { 58 | ModifierDefs.TryGetValue(itemStatDef, out var existingEntry); 59 | return existingEntry ?? new List(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /ItemStats/src/ValueFormatters/Colors.cs: -------------------------------------------------------------------------------- 1 | namespace ItemStats.ValueFormatters 2 | { 3 | public static class Colors 4 | { 5 | public const string Green = "\"green\""; 6 | public const string Blue = "\"blue\""; 7 | public const string Black = "\"black\""; 8 | public const string Orange = "\"orange\""; 9 | public const string Purple = "\"purple\""; 10 | public const string Red = "\"red\""; 11 | public const string White = "\"white\""; 12 | public const string Yellow = "\"yellow\""; 13 | public const string ModifierColor = "#FFB6C1"; 14 | } 15 | } -------------------------------------------------------------------------------- /ItemStats/src/ValueFormatters/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RoR2; 3 | using UnityEngine; 4 | 5 | namespace ItemStats.ValueFormatters 6 | { 7 | public static class Extensions 8 | { 9 | public static string WrapIn(this string str, string start, string end = "") 10 | { 11 | return String.Concat(start, str, end); 12 | } 13 | 14 | public static string SetColor(this string str, string color) 15 | { 16 | return str.WrapIn($"", ""); 17 | } 18 | 19 | public static int CountItems(this CharacterBody body, ItemIndex index) 20 | { 21 | return body != null ? body.inventory.GetItemCount(index) : 0; 22 | } 23 | 24 | public static string FormatInt( 25 | this float value, string postfix = "", 26 | int decimals = 0, bool signed = false, 27 | string color = Colors.Green) 28 | { 29 | if (!signed) value = Mathf.Abs(value); 30 | var sign = signed && value > 0 ? "+" : ""; 31 | 32 | return $"{sign}{Math.Round(value, decimals)}{postfix}".SetColor(color); 33 | } 34 | 35 | public static string FormatPercentage( 36 | this float value, int decimalPlaces = 1, 37 | float scale = 100f, float maxValue = float.MaxValue, 38 | bool signed = false, string color = Colors.Green) 39 | { 40 | // color light blue 41 | var maxStackMessage = value >= maxValue ? "(Max Stack)".SetColor("#ADD8E6") : ""; 42 | value = Mathf.Min(value, maxValue); 43 | if (!signed) value = Mathf.Abs(value); 44 | 45 | // amount of ### 46 | var trailFormatStr = new string('#', decimalPlaces); 47 | var valueStr = Math.Round(value * scale, decimalPlaces).ToString($"0.{trailFormatStr}"); 48 | valueStr += "%"; 49 | 50 | var sign = signed && value > 0 ? "+" : ""; 51 | 52 | return $"{sign}{valueStr}".SetColor(color) + $" {maxStackMessage}"; 53 | } 54 | 55 | public static string FormatModifier(this float value, string statText = "", string color = "#FFB6C1") 56 | { 57 | var sign = value >= 0 ? "+" : "-"; 58 | //TODO: turn color into a separate decorator and get rid of this 59 | var trailFormatStr = new string('#', 1); 60 | var valueStr = Math.Round(value * 100f, 1).ToString($"0.{trailFormatStr}"); 61 | valueStr += "%"; 62 | 63 | return " " + $"{sign}{valueStr} ".SetColor(color); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /ItemStats/src/ValueFormatters/IStatFormatter.cs: -------------------------------------------------------------------------------- 1 | namespace ItemStats.ValueFormatters 2 | { 3 | public interface IStatFormatter 4 | { 5 | string Format(float value); 6 | } 7 | } -------------------------------------------------------------------------------- /ItemStats/src/ValueFormatters/ModifierFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ItemStats.ValueFormatters 4 | { 5 | public class ModifierFormatter : IStatFormatter 6 | { 7 | private readonly string _color; 8 | private readonly string _statText; 9 | 10 | public ModifierFormatter(string statText = "", string color = "#FFB6C1") 11 | { 12 | _statText = statText; 13 | _color = color; 14 | } 15 | 16 | public string Format(float value) 17 | { 18 | var sign = value >= 0 ? "+" : "-"; 19 | //TODO: turn color into a separate decorator and get rid of this 20 | var trailFormatStr = new string('#', 1); 21 | var valueStr = Math.Round(value * 100f, 1).ToString($"0.{trailFormatStr}"); 22 | valueStr += "%"; 23 | 24 | return " " + $"{sign}{valueStr} ".SetColor(_color) + _statText; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ontrigger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ItemStats 2.2.1 2 | Provides current stack bonuses for an item in the tooltip 3 | 4 | ![demo](https://i.nyah.moe/VvsO.png) 5 | 6 | # Installation 7 | First thing you'll need is the BepInExPack. If you don't have it, get it [here](https://thunderstore.io/package/bbepis/BepInExPack/) and follow the installation instructions. 8 | 9 | Afterwards, get the zip file from releases and extract the ItemStats folder in BepInEx/plugins. That's it! 10 | 11 | # Custom Item API 12 | In order to use API, add a Bepin SoftDependency to your BaseUnityPlugin annotations. 13 | Make sure to check if ItemStats is loaded before trying to use it 14 | 15 | Then, create an ItemStatDef for your item (see ItemStatDefinitions.cs for examples). 16 | 17 | Use the R2API ItemApi Submodule to add your item and get its ItemIndex. 18 | 19 | After that, simply call `ItemStatsMod.AddCustomItemStatDef(myItemIndex, myItemStatDef)` 20 | 21 | To create an item stat modifier, extend from `AbstractStatModifier`. Check ItemStats.StatModification.Modifiers for examples 22 | Add an instance of the class with `ItemStatsMod.AddStatModifier(new MyStatModifier())` 23 | 24 | # Building 25 | 26 | You will first need the following libraries: 27 | 28 | * Assembly-CSharp (duh) 29 | * UnityEngine 30 | * UnityEngine.CoreModule 31 | * BepInEx 32 | * MMHOOK_Assembly-CSharp 33 | * R2API 34 | 35 | Open the solution, link the libraries in the Lib folder, DL the required NuGet package and compile. 36 | 37 | # Contributors 38 | 39 | * kylewill0725 40 | * orare 41 | * XuaTheGrate 42 | --------------------------------------------------------------------------------