Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MagicSupremacyReebalance v1.1.1
MagicSupremacyReebalance.dll
Decompiled 2 days agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("MagicSupremacyReebalance")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.1.0")] [assembly: AssemblyInformationalVersion("1.1.1")] [assembly: AssemblyProduct("MagicSupremacyReebalance")] [assembly: AssemblyTitle("MagicSupremacyReebalance")] [assembly: AssemblyVersion("1.1.1.0")] namespace DragonMotion.MagicSupremacyReebalance; internal static class BalanceApplier { private const string MushroomItem = "StaffMushroomcallerMushroomDO"; private const string TotemakuItem = "StaffTotemcallerTotemDO"; private const string TotemicBoomerangItem = "StaffTotemcallerBoomerangDO"; private const string GraveblightItem = "StaffDeathcallerBlasterDO"; private const string FrostspikeItem = "StaffFrostcallerShotgunDO"; private const string IncineratorItem = "StaffFirecallerBlasterDO"; private const string EmberOrbitItem = "StaffFirecallerRingDO"; private const string PrimeMonolithItem = "StaffStonecallerBurstDO"; private const string CandleItem = "StaffLightcallerHealDO"; private const string LuminarisItem = "StaffLightcallerSphereDO"; private const string WindcallerItem = "StaffWindcallerBuffDO"; private const string BloodRequiemItem = "StaffBloodcallerShotgunDO"; private const string SanguineConcordItem = "StaffBloodcallerHealDO"; private const string TempestEyeItem = "StaffStormcallerSphereDO"; private const string ThundersurgeItem = "StaffStormcallerShockerDO"; private const string MushroomTrapAoe = "aoe_mushroomcaller_staff_trap_DO"; private const string GraveblightChildAoe = "aoe_deathcaller_staff_shotgun_DO"; private const string FrostRainAoe1 = "aoe_frostcaller_staff_shotgun_rain_1_DO"; private const string FrostRainAoe2 = "aoe_frostcaller_staff_shotgun_rain_2_DO"; private const string EmberOrbitAoe = "aoe_firecaller_staff_ring_orbit_DO"; private const string WindShieldAoe = "aoe_windcaller_staff_shield_DO"; private const string BloodRequiemSecondaryAoe = "aoe_bloodcaller_staff_debuff_DO"; private const string TempestTrapAoe = "aoe_stormcaller_staff_sphere_trap_DO"; private const string ThundersurgeOrbitAoe = "aoe_stormcaller_staff_shocker_orbit_DO"; private const string TotemBuffStatus = "SE_TotemBuff_DO"; private const string CandleSigilStatus = "SE_LightcallerSigil_DO"; private const string CandleHealStatus = "SE_LightcallerHeal_DO"; private const string WindShieldStatus = "SE_WindcallerShield_DO"; private const string WindPetalStatus = "SE_WindcallerPetal_DO"; private const string BloodBuffStatus = "SE_BloodBuff_DO"; private static readonly HashSet<string> Logged = new HashSet<string>(); private static readonly HashSet<string> Warned = new HashSet<string>(); internal static void ApplyItemsAndStatusEffects(ObjectDB objectDb, BalanceConfig config, ManualLogSource log) { EpicLootCompatibility.RefreshMagicWeaponRegistry(objectDb, log); if (config.Totemaku.Enabled.Value && TryGetStatus<SE_Stats>(objectDb, "SE_TotemBuff_DO", log, out SE_Stats status)) { status.m_healthPerTick = config.Totemaku.HealPerTickAtQuality1.Value; LogOnce(log, "StaffTotemcallerTotemDO", "quality-scaled support healing"); } if (config.TotemicBoomerang.Enabled.Value && TryGetShared(objectDb, "StaffTotemcallerBoomerangDO", log, out var shared)) { shared.m_damages.m_slash = config.TotemicBoomerang.BaseSlash.Value; shared.m_damages.m_spirit = config.TotemicBoomerang.BaseSpirit.Value; shared.m_damagesPerLevel.m_slash = config.TotemicBoomerang.SlashPerQuality.Value; shared.m_damagesPerLevel.m_spirit = config.TotemicBoomerang.SpiritPerQuality.Value; LogOnce(log, "StaffTotemcallerBoomerangDO", "item damage and quality progression"); } if (config.Graveblight.Enabled.Value && TryGetShared(objectDb, "StaffDeathcallerBlasterDO", log, out var shared2)) { if (shared2.m_secondaryAttack == null) { WarnOnce(log, "StaffDeathcallerBlasterDO:secondary", "Secondary Attack data was not found."); } else { shared2.m_secondaryAttack.m_projectiles = config.Graveblight.SecondaryProjectiles.Value; shared2.m_secondaryAttack.m_damageMultiplier = config.Graveblight.SecondaryItemMultiplier.Value; LogOnce(log, "StaffDeathcallerBlasterDO", "secondary pellet count and item multiplier"); } } if (config.Frostspike.Enabled.Value && TryGetShared(objectDb, "StaffFrostcallerShotgunDO", log, out var shared3)) { shared3.m_damages.m_frost = config.Frostspike.BaseFrostPerProjectile.Value; shared3.m_damagesPerLevel.m_frost = config.Frostspike.FrostPerQualityPerProjectile.Value; if (shared3.m_attack == null) { WarnOnce(log, "StaffFrostcallerShotgunDO:primary", "Primary Attack data was not found."); } else { shared3.m_attack.m_projectiles = config.Frostspike.PrimaryProjectiles.Value; shared3.m_attack.m_attackEitr = config.Frostspike.PrimaryEitr.Value; LogOnce(log, "StaffFrostcallerShotgunDO", "proc surface, per-pellet damage, quality progression and eitr cost"); } } if (config.Incinerator.Enabled.Value && TryGetShared(objectDb, "StaffFirecallerBlasterDO", log, out var shared4)) { shared4.m_damages.m_pierce = config.Incinerator.BasePierce.Value; shared4.m_damages.m_fire = config.Incinerator.BaseFire.Value; shared4.m_damagesPerLevel.m_pierce = config.Incinerator.PiercePerQuality.Value; shared4.m_damagesPerLevel.m_fire = config.Incinerator.FirePerQuality.Value; if (shared4.m_secondaryAttack == null) { WarnOnce(log, "StaffFirecallerBlasterDO:secondary", "Secondary Attack data was not found."); } else { shared4.m_secondaryAttack.m_damageMultiplier = config.Incinerator.SecondaryItemMultiplier.Value; shared4.m_secondaryAttack.m_burstInterval = config.Incinerator.SecondaryBurstInterval.Value; shared4.m_secondaryAttack.m_attackEitr = config.Incinerator.SecondaryEitr.Value; } LogOnce(log, "StaffFirecallerBlasterDO", "item damage plus secondary damage and burst cadence"); } if (config.EmberOrbit.Enabled.Value && TryGetShared(objectDb, "StaffFirecallerRingDO", log, out var shared5)) { shared5.m_damages.m_slash = config.EmberOrbit.BaseSlash.Value; shared5.m_damages.m_fire = config.EmberOrbit.BaseFire.Value; shared5.m_damages.m_chop = config.EmberOrbit.BaseChop.Value; shared5.m_damagesPerLevel.m_slash = config.EmberOrbit.SlashPerQuality.Value; shared5.m_damagesPerLevel.m_fire = config.EmberOrbit.FirePerQuality.Value; shared5.m_damagesPerLevel.m_chop = config.EmberOrbit.ChopPerQuality.Value; LogOnce(log, "StaffFirecallerRingDO", "item damage and quality progression"); } if (config.PrimeMonolith.Enabled.Value && TryGetShared(objectDb, "StaffStonecallerBurstDO", log, out var shared6)) { shared6.m_damages.m_blunt = config.PrimeMonolith.BaseBlunt.Value; shared6.m_damagesPerLevel.m_blunt = config.PrimeMonolith.BluntPerQuality.Value; if (shared6.m_attack == null) { WarnOnce(log, "StaffStonecallerBurstDO:primary", "Primary Attack data was not found."); } else { shared6.m_attack.m_burstInterval = config.PrimeMonolith.PrimaryBurstInterval.Value; shared6.m_attack.m_attackEitr = config.PrimeMonolith.PrimaryEitr.Value; } LogOnce(log, "StaffStonecallerBurstDO", "primary damage, quality progression and burst cadence"); } if (config.Candle.Enabled.Value) { if (TryGetStatus<SE_Stats>(objectDb, "SE_LightcallerSigil_DO", log, out SE_Stats status2)) { status2.m_healthUpFront = config.Candle.PrimaryHealAtQuality1.Value; } if (TryGetStatus<SE_Stats>(objectDb, "SE_LightcallerHeal_DO", log, out SE_Stats status3)) { status3.m_healthUpFront = config.Candle.SecondaryUpFrontHeal.Value; status3.m_healthOverTime = config.Candle.SecondaryHealOverTimeAtQuality1.Value; } LogOnce(log, "StaffLightcallerHealDO", "quality-scaled primary and secondary healing"); } if (config.Luminaris.Enabled.Value && TryGetShared(objectDb, "StaffLightcallerSphereDO", log, out var shared7)) { shared7.m_damages.m_blunt = config.Luminaris.BaseBlunt.Value; shared7.m_damages.m_spirit = config.Luminaris.BaseSpirit.Value; shared7.m_damagesPerLevel.m_blunt = config.Luminaris.BluntPerQuality.Value; shared7.m_damagesPerLevel.m_spirit = config.Luminaris.SpiritPerQuality.Value; LogOnce(log, "StaffLightcallerSphereDO", "item damage and quality progression"); } if (config.Windcaller.Enabled.Value) { if (TryGetStatus<SE_Shield>(objectDb, "SE_WindcallerShield_DO", log, out SE_Shield status4)) { status4.m_absorbDamage = config.Windcaller.ShieldBaseAbsorb.Value; status4.m_absorbDamagePerSkillLevel = config.Windcaller.ShieldAbsorbPerSkillLevel.Value; status4.m_ttlPerItemLevel = config.Windcaller.ShieldLifetimePerQuality.Value; } if (TryGetStatus<SE_Stats>(objectDb, "SE_WindcallerPetal_DO", log, out SE_Stats status5)) { ((StatusEffect)status5).m_ttl = config.Windcaller.PetalDurationAtQuality1.Value; } LogOnce(log, "StaffWindcallerBuffDO", "shield scaling and quality-scaled secondary duration"); } if (config.BloodRequiem.Enabled.Value && TryGetShared(objectDb, "StaffBloodcallerShotgunDO", log, out var shared8)) { shared8.m_damages.m_damage = config.BloodRequiem.BaseGenericPerProjectile.Value; shared8.m_damagesPerLevel.m_damage = config.BloodRequiem.GenericPerQualityPerProjectile.Value; if (shared8.m_attack == null) { WarnOnce(log, "StaffBloodcallerShotgunDO:primary", "Primary Attack data was not found."); } else { shared8.m_attack.m_projectiles = config.BloodRequiem.PrimaryProjectiles.Value; } LogOnce(log, "StaffBloodcallerShotgunDO", "primary damage, quality progression and proc surface"); } if (config.SanguineConcord.Enabled.Value && TryGetStatus<SE_Stats>(objectDb, "SE_BloodBuff_DO", log, out SE_Stats status6)) { status6.m_healthRegenMultiplier = config.SanguineConcord.SecondaryHealthRegenMultiplier.Value; LogOnce(log, "StaffBloodcallerHealDO", "secondary health regeneration multiplier"); } if (config.TempestEye.Enabled.Value && TryGetShared(objectDb, "StaffStormcallerSphereDO", log, out var shared9)) { shared9.m_damages.m_pierce = config.TempestEye.BasePierce.Value; shared9.m_damages.m_lightning = config.TempestEye.BaseLightning.Value; shared9.m_damagesPerLevel.m_pierce = config.TempestEye.PiercePerQuality.Value; shared9.m_damagesPerLevel.m_lightning = config.TempestEye.LightningPerQuality.Value; LogOnce(log, "StaffStormcallerSphereDO", "item damage and quality progression"); } if (config.Thundersurge.Enabled.Value && TryGetShared(objectDb, "StaffStormcallerShockerDO", log, out var shared10)) { shared10.m_damages.m_lightning = config.Thundersurge.BaseLightning.Value; shared10.m_damagesPerLevel.m_lightning = config.Thundersurge.LightningPerQuality.Value; if (shared10.m_attack == null) { WarnOnce(log, "StaffStormcallerShockerDO:primary", "Primary Attack data was not found."); } else { shared10.m_attack.m_burstInterval = config.Thundersurge.PrimaryBurstInterval.Value; shared10.m_attack.m_attackEitr = config.Thundersurge.PrimaryEitr.Value; } LogOnce(log, "StaffStormcallerShockerDO", "primary damage, quality progression and burst cadence"); } } internal static void ApplyScenePrefabs(ZNetScene scene, BalanceConfig config, ManualLogSource log) { //IL_019c: Unknown result type (might be due to invalid IL or missing references) if (config.Mushroom.Enabled.Value && TryGetAoe(scene, "aoe_mushroomcaller_staff_trap_DO", log, out var aoe)) { aoe.m_damage.m_damage = config.Mushroom.TrapGenericDamage.Value; LogOnce(log, "StaffMushroomcallerMushroomDO", "one-hit trap up-front damage"); } if (config.Graveblight.Enabled.Value && TryGetAoe(scene, "aoe_deathcaller_staff_shotgun_DO", log, out var aoe2)) { aoe2.m_damage.m_poison = config.Graveblight.ChildAoePoison.Value; aoe2.m_damagePerLevel.m_blunt = config.Graveblight.ChildAoeBluntPerQuality.Value; aoe2.m_damagePerLevel.m_poison = config.Graveblight.ChildAoePoisonPerQuality.Value; } if (config.Frostspike.Enabled.Value) { PatchAoeLifetime(scene, "aoe_frostcaller_staff_shotgun_rain_1_DO", config.Frostspike.RainAoeLifetime.Value, log); PatchAoeLifetime(scene, "aoe_frostcaller_staff_shotgun_rain_2_DO", config.Frostspike.RainAoeLifetime.Value, log); } if (config.EmberOrbit.Enabled.Value && TryGetAoe(scene, "aoe_firecaller_staff_ring_orbit_DO", log, out var aoe3)) { aoe3.m_damage.m_slash = config.EmberOrbit.OrbitSlash.Value; aoe3.m_damage.m_fire = config.EmberOrbit.OrbitFire.Value; } if (config.Windcaller.Enabled.Value && TryGetAoe(scene, "aoe_windcaller_staff_shield_DO", log, out var aoe4)) { aoe4.m_skill = (SkillType)(config.Windcaller.ScaleShieldWithElementalMagic.Value ? 9 : 0); } if (config.BloodRequiem.Enabled.Value && TryGetAoe(scene, "aoe_bloodcaller_staff_debuff_DO", log, out var aoe5)) { aoe5.m_damage.m_damage = config.BloodRequiem.SecondaryAoeGeneric.Value; aoe5.m_damagePerLevel.m_damage = config.BloodRequiem.SecondaryAoeGenericPerQuality.Value; aoe5.m_ttl = config.BloodRequiem.SecondaryAoeLifetime.Value; } if (config.TempestEye.Enabled.Value && TryGetAoe(scene, "aoe_stormcaller_staff_sphere_trap_DO", log, out var aoe6)) { aoe6.m_damage.m_lightning = config.TempestEye.TrapBaseLightning.Value; aoe6.m_damagePerLevel.m_lightning = config.TempestEye.TrapLightningPerQuality.Value; } if (config.Thundersurge.Enabled.Value && TryGetAoe(scene, "aoe_stormcaller_staff_shocker_orbit_DO", log, out var aoe7)) { aoe7.m_damage.m_lightning = config.Thundersurge.OrbitLightning.Value; } } private static void PatchAoeLifetime(ZNetScene scene, string prefabName, float lifetime, ManualLogSource log) { if (TryGetAoe(scene, prefabName, log, out var aoe)) { aoe.m_ttl = lifetime; } } private static bool TryGetShared(ObjectDB objectDb, string prefabName, ManualLogSource log, out SharedData shared) { shared = null; GameObject itemPrefab = objectDb.GetItemPrefab(prefabName); ItemDrop val = (((Object)(object)itemPrefab == (Object)null) ? null : itemPrefab.GetComponent<ItemDrop>()); if (val?.m_itemData?.m_shared == null) { WarnOnce(log, prefabName, "ItemDrop.SharedData was not found in ObjectDB."); return false; } shared = val.m_itemData.m_shared; return true; } private static bool TryGetStatus<T>(ObjectDB objectDb, string prefabName, ManualLogSource log, out T status) where T : StatusEffect { StatusEffect statusEffect = objectDb.GetStatusEffect(StringExtensionMethods.GetStableHashCode(prefabName)); status = (T)(object)((statusEffect is T) ? statusEffect : null); if ((Object)(object)status != (Object)null) { return true; } WarnOnce(log, prefabName, typeof(T).Name + " status was not found in ObjectDB."); return false; } private static bool TryGetAoe(ZNetScene scene, string prefabName, ManualLogSource log, out Aoe aoe) { aoe = null; GameObject prefab = scene.GetPrefab(prefabName); if ((Object)(object)prefab != (Object)null) { aoe = prefab.GetComponentInChildren<Aoe>(true); } if ((Object)(object)aoe != (Object)null) { return true; } WarnOnce(log, prefabName, "Aoe component was not found in ZNetScene."); return false; } private static void LogOnce(ManualLogSource log, string target, string description) { if (Logged.Add(target)) { log.LogInfo((object)("Patched " + target + ": " + description + ".")); } } private static void WarnOnce(ManualLogSource log, string target, string description) { if (Warned.Add(target)) { log.LogWarning((object)("Could not patch " + target + ": " + description + " Magic Supremacy may have changed its prefab chain.")); } } } internal sealed class BalanceConfig { internal sealed class MushroomSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> TrapGenericDamage { get; } internal MushroomSettings(ConfigFile config) { Enabled = BindEnabled(config, "10 - T1 Black Forest - Mushroom Branch"); TrapGenericDamage = BindFloat(config, "10 - T1 Black Forest - Mushroom Branch", "Trap generic damage", 20f, 0f, 500f, "Up-front generic damage of the one-hit mushroom trap. Original: 10. Poison and spirit remain 40/10 plus 3/1 per quality."); } } internal sealed class TotemakuSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> HealPerTickAtQuality1 { get; } internal ConfigEntry<float> HealPerTickPerQuality { get; } internal TotemakuSettings(ConfigFile config) { Enabled = BindEnabled(config, "11 - T1 Black Forest - Totemaku"); HealPerTickAtQuality1 = BindFloat(config, "11 - T1 Black Forest - Totemaku", "Healing per tick at quality 1", 3f, 0f, 100f, "Healing from SE_TotemBuff_DO every 2 seconds at quality 1. Original: 3 at every quality."); HealPerTickPerQuality = BindFloat(config, "11 - T1 Black Forest - Totemaku", "Healing per tick per quality", 0.5f, 0f, 25f, "Healing added every 2 seconds for each quality above 1. Default Q1-Q4: 3/3.5/4/4.5."); } } internal sealed class TotemicBoomerangSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BaseSlash { get; } internal ConfigEntry<float> BaseSpirit { get; } internal ConfigEntry<float> SlashPerQuality { get; } internal ConfigEntry<float> SpiritPerQuality { get; } internal TotemicBoomerangSettings(ConfigFile config) { Enabled = BindEnabled(config, "12 - T1 Black Forest - Totemic Boomerang"); BaseSlash = BindFloat(config, "12 - T1 Black Forest - Totemic Boomerang", "Base slash", 30f, 0f, 500f, "Slash per throw at quality 1. Original: 32."); BaseSpirit = BindFloat(config, "12 - T1 Black Forest - Totemic Boomerang", "Base spirit", 5f, 0f, 500f, "Spirit per throw at quality 1. Original: 5."); SlashPerQuality = BindFloat(config, "12 - T1 Black Forest - Totemic Boomerang", "Slash per quality", 3f, 0f, 100f, "Slash gained per quality above 1. Original: 4."); SpiritPerQuality = BindFloat(config, "12 - T1 Black Forest - Totemic Boomerang", "Spirit per quality", 1f, 0f, 100f, "Spirit gained per quality above 1. Original: 1."); } } internal sealed class GraveblightSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<int> SecondaryProjectiles { get; } internal ConfigEntry<float> SecondaryItemMultiplier { get; } internal ConfigEntry<float> ChildAoePoison { get; } internal ConfigEntry<float> ChildAoeBluntPerQuality { get; } internal ConfigEntry<float> ChildAoePoisonPerQuality { get; } internal GraveblightSettings(ConfigFile config) { Enabled = BindEnabled(config, "20 - T2 Swamp - Graveblight"); SecondaryProjectiles = BindInt(config, "20 - T2 Swamp - Graveblight", "Secondary projectile count", 3, 1, 20, "Pellets in the secondary volley. Original: 5. Each pellet has a separate hit/proc surface and child poison AoE."); SecondaryItemMultiplier = BindFloat(config, "20 - T2 Swamp - Graveblight", "Secondary item damage multiplier", 1f / 3f, 0f, 10f, "Item-damage multiplier per secondary pellet. Original: 0.2. Three default pellets preserve about one item payload if all connect."); ChildAoePoison = BindFloat(config, "20 - T2 Swamp - Graveblight", "Child AoE base poison", 60f, 0f, 1000f, "Poison of each child AoE at quality 1. Original: 200. The applied debuff makes poison VeryWeak."); ChildAoeBluntPerQuality = BindFloat(config, "20 - T2 Swamp - Graveblight", "Child AoE blunt per quality", 1f, 0f, 100f, "Blunt added to the child AoE for every quality above 1. Original: 1."); ChildAoePoisonPerQuality = BindFloat(config, "20 - T2 Swamp - Graveblight", "Child AoE poison per quality", 3f, 0f, 100f, "Poison added to the child AoE for every quality above 1. Original: 3."); } } internal sealed class FrostspikeSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<int> PrimaryProjectiles { get; } internal ConfigEntry<float> BaseFrostPerProjectile { get; } internal ConfigEntry<float> FrostPerQualityPerProjectile { get; } internal ConfigEntry<float> PrimaryEitr { get; } internal ConfigEntry<float> RainAoeLifetime { get; } internal FrostspikeSettings(ConfigFile config) { Enabled = BindEnabled(config, "30 - T3 Mountains - Frostspike"); PrimaryProjectiles = BindInt(config, "30 - T3 Mountains - Frostspike", "Primary projectile count", 2, 1, 20, "Pellets in the primary volley. Original: 5; version 1.0.0: 4. Two pellets reduce independent Epic Loot and on-hit events while preserving the version-1.0 direct volley total."); BaseFrostPerProjectile = BindFloat(config, "30 - T3 Mountains - Frostspike", "Base frost per projectile", 60f, 0f, 1000f, "Frost per pellet at quality 1. Original: 30. With two pellets the direct Q1 total is 120, unchanged from version 1.0.0."); FrostPerQualityPerProjectile = BindFloat(config, "30 - T3 Mountains - Frostspike", "Frost per quality per projectile", 6f, 0f, 100f, "Frost gained per pellet per quality above 1. Original: 3. With two pellets the direct Q4 total is 156, unchanged from version 1.0.0."); PrimaryEitr = BindFloat(config, "30 - T3 Mountains - Frostspike", "Primary eitr cost", 25f, 0f, 500f, "Primary cost before skill reduction. Original Magic Supremacy value: 20."); RainAoeLifetime = BindFloat(config, "30 - T3 Mountains - Frostspike", "Rain crystal damage lifetime", 8f, 0.02f, 120f, "Lifetime of each rain crystal damage AoE. Original: 60; version 1.0.0: 6. Hit interval remains 1.2 seconds."); } } internal sealed class IncineratorSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BasePierce { get; } internal ConfigEntry<float> BaseFire { get; } internal ConfigEntry<float> PiercePerQuality { get; } internal ConfigEntry<float> FirePerQuality { get; } internal ConfigEntry<float> SecondaryItemMultiplier { get; } internal ConfigEntry<float> SecondaryBurstInterval { get; } internal ConfigEntry<float> SecondaryEitr { get; } internal IncineratorSettings(ConfigFile config) { Enabled = BindEnabled(config, "40 - T4 Plains - Incinerator"); BasePierce = BindFloat(config, "40 - T4 Plains - Incinerator", "Base pierce", 70f, 0f, 1000f, "Item pierce at quality 1. Original: 80."); BaseFire = BindFloat(config, "40 - T4 Plains - Incinerator", "Base fire", 105f, 0f, 1000f, "Item fire at quality 1. Original: 120."); PiercePerQuality = BindFloat(config, "40 - T4 Plains - Incinerator", "Pierce per quality", 1f, 0f, 100f, "Pierce gained per quality above 1. Original: 1."); FirePerQuality = BindFloat(config, "40 - T4 Plains - Incinerator", "Fire per quality", 2f, 0f, 100f, "Fire gained per quality above 1. Original: 2."); SecondaryItemMultiplier = BindFloat(config, "40 - T4 Plains - Incinerator", "Secondary item damage multiplier", 0.2f, 0f, 10f, "Item-damage multiplier per rapid-fire burst. Original: 0.2; version 1.0.0: 0.15. With the default interval this is 0.50 item payload per second, versus 0.75 in version 1.0.0 and 1.00 originally."); SecondaryBurstInterval = BindFloat(config, "40 - T4 Plains - Incinerator", "Secondary burst interval", 0.4f, 0.02f, 5f, "Seconds between held secondary bursts. Original and version 1.0.0: 0.20. The default gives 2.5 independent hit/enchant events per second instead of 5."); SecondaryEitr = BindFloat(config, "40 - T4 Plains - Incinerator", "Secondary eitr per burst", 4f, 0f, 500f, "Cost of each held secondary burst before skill reduction. Original and version 1.0.0: 3. The default raw cost rate is 10 eitr per second instead of 15."); } } internal sealed class EmberOrbitSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BaseSlash { get; } internal ConfigEntry<float> BaseFire { get; } internal ConfigEntry<float> BaseChop { get; } internal ConfigEntry<float> SlashPerQuality { get; } internal ConfigEntry<float> FirePerQuality { get; } internal ConfigEntry<float> ChopPerQuality { get; } internal ConfigEntry<float> OrbitSlash { get; } internal ConfigEntry<float> OrbitFire { get; } internal EmberOrbitSettings(ConfigFile config) { Enabled = BindEnabled(config, "41 - T4 Plains - Orbit of Embers"); BaseSlash = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Base slash", 28f, 0f, 1000f, "Slash per throw at quality 1. Original: 40."); BaseFire = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Base fire", 42f, 0f, 1000f, "Fire per throw at quality 1. Original: 60."); BaseChop = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Base chop", 10f, 0f, 1000f, "Chop per throw at quality 1. Original: 60. Chop is real Character damage and bypasses the ordinary armor pool; it is not tree-only utility."); SlashPerQuality = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Slash per quality", 2f, 0f, 100f, "Slash gained per quality above 1. Original: 2."); FirePerQuality = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Fire per quality", 3f, 0f, 100f, "Fire gained per quality above 1. Original: 3."); ChopPerQuality = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Chop per quality", 1f, 0f, 100f, "Chop gained per quality above 1. Original: 3."); OrbitSlash = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Orbit slash per tick", 6f, 0f, 500f, "Fixed orbit slash every 0.75 seconds. Original: 18."); OrbitFire = BindFloat(config, "41 - T4 Plains - Orbit of Embers", "Orbit fire per tick", 6f, 0f, 500f, "Fixed orbit fire every 0.75 seconds. Original: 18."); } } internal sealed class PrimeMonolithSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BaseBlunt { get; } internal ConfigEntry<float> BluntPerQuality { get; } internal ConfigEntry<float> PrimaryBurstInterval { get; } internal ConfigEntry<float> PrimaryEitr { get; } internal PrimeMonolithSettings(ConfigFile config) { Enabled = BindEnabled(config, "42 - T4 Plains - Prime Monolith"); BaseBlunt = BindFloat(config, "42 - T4 Plains - Prime Monolith", "Base blunt", 35f, 0f, 500f, "Damage of each primary burst at quality 1. Original: 23; version 1.0.0: 18. The slower default cadence keeps sustained damage below the verified Plains two-handed median."); BluntPerQuality = BindFloat(config, "42 - T4 Plains - Prime Monolith", "Blunt per quality", 2f, 0f, 100f, "Blunt gained per quality above 1. Original: 2."); PrimaryBurstInterval = BindFloat(config, "42 - T4 Plains - Prime Monolith", "Primary burst interval", 0.4f, 0.02f, 5f, "Seconds between held primary bursts. Original and version 1.0.0: 0.15. The lower event rate limits on-hit and Epic Loot proc frequency."); PrimaryEitr = BindFloat(config, "42 - T4 Plains - Prime Monolith", "Primary eitr per burst", 6f, 0f, 500f, "Cost of each held primary burst before skill reduction. Original: 4. At the default cadence this is 15 raw eitr per second instead of the original 26.67."); } } internal sealed class CandleSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> PrimaryHealAtQuality1 { get; } internal ConfigEntry<float> PrimaryHealPerQuality { get; } internal ConfigEntry<float> SecondaryUpFrontHeal { get; } internal ConfigEntry<float> SecondaryHealOverTimeAtQuality1 { get; } internal ConfigEntry<float> SecondaryHealOverTimePerQuality { get; } internal CandleSettings(ConfigFile config) { Enabled = BindEnabled(config, "52 - T5 Mistlands - Candle of Vital Light"); PrimaryHealAtQuality1 = BindFloat(config, "52 - T5 Mistlands - Candle of Vital Light", "Primary heal at quality 1", 35f, 0f, 1000f, "Up-front healing from each one-hit sigil at quality 1. Live source value: 35; the original tooltip incorrectly says 40."); PrimaryHealPerQuality = BindFloat(config, "52 - T5 Mistlands - Candle of Vital Light", "Primary heal per quality", 5f, 0f, 100f, "Healing added to each sigil per quality above 1. Default Q1-Q4: 35/40/45/50."); SecondaryUpFrontHeal = BindFloat(config, "52 - T5 Mistlands - Candle of Vital Light", "Secondary up-front heal", 20f, 0f, 1000f, "Immediate group heal on a fresh application or refresh. Original: 20."); SecondaryHealOverTimeAtQuality1 = BindFloat(config, "52 - T5 Mistlands - Candle of Vital Light", "Secondary heal over time at quality 1", 100f, 0f, 5000f, "Total heal-over-time budget at quality 1. Original: 100 over 10 seconds."); SecondaryHealOverTimePerQuality = BindFloat(config, "52 - T5 Mistlands - Candle of Vital Light", "Secondary heal over time per quality", 10f, 0f, 500f, "Heal-over-time budget added per quality above 1. Combined default fresh-application total including 20 up-front: 120/130/140/150 over the original 10 seconds."); } } internal sealed class LuminarisSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BaseBlunt { get; } internal ConfigEntry<float> BaseSpirit { get; } internal ConfigEntry<float> BluntPerQuality { get; } internal ConfigEntry<float> SpiritPerQuality { get; } internal LuminarisSettings(ConfigFile config) { Enabled = BindEnabled(config, "50 - T5 Mistlands - Luminaris"); BaseBlunt = BindFloat(config, "50 - T5 Mistlands - Luminaris", "Base blunt", 50f, 0f, 1000f, "Item blunt at quality 1. Original: 100; version 1.0.0: 55."); BaseSpirit = BindFloat(config, "50 - T5 Mistlands - Luminaris", "Base spirit", 75f, 0f, 1000f, "Item spirit at quality 1. Original: 150; version 1.0.0: 83."); BluntPerQuality = BindFloat(config, "50 - T5 Mistlands - Luminaris", "Blunt per quality", 1f, 0f, 100f, "Blunt gained per quality above 1. Original: 2."); SpiritPerQuality = BindFloat(config, "50 - T5 Mistlands - Luminaris", "Spirit per quality", 4f, 0f, 100f, "Spirit gained per quality above 1. Original: 4; version 1.0.0: 3."); } } internal sealed class WindcallerSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<bool> ScaleShieldWithElementalMagic { get; } internal ConfigEntry<float> ShieldBaseAbsorb { get; } internal ConfigEntry<float> ShieldAbsorbPerSkillLevel { get; } internal ConfigEntry<int> ShieldLifetimePerQuality { get; } internal ConfigEntry<float> PetalDurationAtQuality1 { get; } internal ConfigEntry<float> PetalDurationPerQuality { get; } internal WindcallerSettings(ConfigFile config) { Enabled = BindEnabled(config, "51 - T5 Mistlands - Springbloom Parasol"); ScaleShieldWithElementalMagic = config.Bind<bool>("51 - T5 Mistlands - Springbloom Parasol", "Scale shield with Elemental Magic", true, "Set the source AoE skill to ElementalMagic so absorb-per-skill receives the real skill instead of 0. Original source AoE skill: None."); ShieldBaseAbsorb = BindFloat(config, "51 - T5 Mistlands - Springbloom Parasol", "Shield base absorb", 70f, 0f, 5000f, "Base shield absorption. Original: 70."); ShieldAbsorbPerSkillLevel = BindFloat(config, "51 - T5 Mistlands - Springbloom Parasol", "Shield absorb per skill level", 3f, 0f, 100f, "Absorption per Elemental Magic level. Original serialized value: 3, but unreachable because source AoE skill was None."); ShieldLifetimePerQuality = BindInt(config, "51 - T5 Mistlands - Springbloom Parasol", "Shield lifetime seconds per quality", 30, 1, 600, "Seconds of shield duration per item quality. Original: 60, producing 240 seconds at Q4; default produces 120 seconds at Q4."); PetalDurationAtQuality1 = BindFloat(config, "51 - T5 Mistlands - Springbloom Parasol", "Secondary buff duration at quality 1", 120f, 1f, 1200f, "Duration of the mobility/carry buff at quality 1. Original: 180 seconds at every quality."); PetalDurationPerQuality = BindFloat(config, "51 - T5 Mistlands - Springbloom Parasol", "Secondary buff duration per quality", 20f, 0f, 300f, "Seconds added per quality above 1. Default Q1-Q4: 120/140/160/180."); } } internal sealed class BloodRequiemSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<int> PrimaryProjectiles { get; } internal ConfigEntry<float> BaseGenericPerProjectile { get; } internal ConfigEntry<float> GenericPerQualityPerProjectile { get; } internal ConfigEntry<float> SecondaryAoeGeneric { get; } internal ConfigEntry<float> SecondaryAoeGenericPerQuality { get; } internal ConfigEntry<float> SecondaryAoeLifetime { get; } internal BloodRequiemSettings(ConfigFile config) { Enabled = BindEnabled(config, "60 - T6 Ashlands - Blood Requiem"); PrimaryProjectiles = BindInt(config, "60 - T6 Ashlands - Blood Requiem", "Primary projectile count", 3, 1, 40, "Pellets per volley. Original: 12; version 1.0.0: 8. Three pellets reduce independent on-hit, Chain Lightning and flat-lifesteal events."); BaseGenericPerProjectile = BindFloat(config, "60 - T6 Ashlands - Blood Requiem", "Base generic per projectile", 60f, 0f, 1000f, "Generic damage per pellet at quality 1. Original: 17; version 1.0.0: 25.5."); GenericPerQualityPerProjectile = BindFloat(config, "60 - T6 Ashlands - Blood Requiem", "Generic per quality per projectile", 7f, 0f, 100f, "Generic damage per pellet per quality above 1. Original: 3; version 1.0.0: 4.5. Default volley totals: 180/201/222/243. Generic damage bypasses the ordinary armor pool."); SecondaryAoeGeneric = BindFloat(config, "60 - T6 Ashlands - Blood Requiem", "Secondary AoE base generic", 18f, 0f, 1000f, "Generic damage of every secondary field check at quality 1. Original: 30."); SecondaryAoeGenericPerQuality = BindFloat(config, "60 - T6 Ashlands - Blood Requiem", "Secondary AoE generic per quality", 1f, 0f, 100f, "Generic damage gained by every field check per quality above 1. Original: 2."); SecondaryAoeLifetime = BindFloat(config, "60 - T6 Ashlands - Blood Requiem", "Secondary AoE lifetime", 8f, 0.02f, 120f, "Lifetime of the radius-19 secondary field. Original: 60 seconds; interval remains 1 second. Cost: 100 health and 4 Inspiration before skill scaling."); } } internal sealed class SanguineConcordSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> SecondaryHealthRegenMultiplier { get; } internal SanguineConcordSettings(ConfigFile config) { Enabled = BindEnabled(config, "63 - T6 Ashlands - Sanguine Concord"); SecondaryHealthRegenMultiplier = BindFloat(config, "63 - T6 Ashlands - Sanguine Concord", "Secondary health regeneration multiplier", 1.3f, 0f, 10f, "Health regeneration multiplier of the 180-second secondary buff. Original: 1.15. The same buff already grants 50 maximum health through Magic Supremacy code."); } } internal sealed class TempestEyeSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BasePierce { get; } internal ConfigEntry<float> BaseLightning { get; } internal ConfigEntry<float> PiercePerQuality { get; } internal ConfigEntry<float> LightningPerQuality { get; } internal ConfigEntry<float> TrapBaseLightning { get; } internal ConfigEntry<float> TrapLightningPerQuality { get; } internal TempestEyeSettings(ConfigFile config) { Enabled = BindEnabled(config, "61 - T6 Ashlands - Tempest Eye"); BasePierce = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Base pierce", 75f, 0f, 1000f, "Item pierce at quality 1. Original: 120; version 1.0.0: 84."); BaseLightning = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Base lightning", 75f, 0f, 1000f, "Item lightning at quality 1. Original: 120; version 1.0.0: 84."); PiercePerQuality = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Pierce per quality", 0f, 0f, 100f, "Pierce gained per quality above 1. Original: 0."); LightningPerQuality = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Lightning per quality", 4f, 0f, 100f, "Lightning gained per quality above 1. Original: 6."); TrapBaseLightning = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Trap base lightning", 135f, 0f, 1000f, "One-hit secondary trap lightning at quality 1. Original: 150 at every quality."); TrapLightningPerQuality = BindFloat(config, "61 - T6 Ashlands - Tempest Eye", "Trap lightning per quality", 5f, 0f, 100f, "Trap lightning gained per quality above 1. Original: 0; default reaches 150 at quality 4."); } } internal sealed class ThundersurgeSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<float> BaseLightning { get; } internal ConfigEntry<float> LightningPerQuality { get; } internal ConfigEntry<float> PrimaryBurstInterval { get; } internal ConfigEntry<float> PrimaryEitr { get; } internal ConfigEntry<float> OrbitLightning { get; } internal ThundersurgeSettings(ConfigFile config) { Enabled = BindEnabled(config, "62 - T6 Ashlands - Thundersurge"); BaseLightning = BindFloat(config, "62 - T6 Ashlands - Thundersurge", "Base lightning", 43f, 0f, 1000f, "Damage of each primary burst at quality 1. Original: 35; version 1.0.0: 33. The slower cadence keeps sustained damage below the verified Ashlands two-handed median."); LightningPerQuality = BindFloat(config, "62 - T6 Ashlands - Thundersurge", "Lightning per quality", 2f, 0f, 100f, "Lightning gained per quality above 1. Original: 2."); PrimaryBurstInterval = BindFloat(config, "62 - T6 Ashlands - Thundersurge", "Primary burst interval", 0.4f, 0.02f, 5f, "Seconds between held primary bursts. Original and version 1.0.0: 0.20. The lower event rate limits on-hit and Epic Loot proc frequency."); PrimaryEitr = BindFloat(config, "62 - T6 Ashlands - Thundersurge", "Primary eitr per burst", 5.5f, 0f, 500f, "Cost of each held primary burst before skill reduction. Original: 4. At the default cadence this is 13.75 raw eitr per second, close to the original 20 while accounting for the lower proc surface."); OrbitLightning = BindFloat(config, "62 - T6 Ashlands - Thundersurge", "Orbit lightning per tick", 10f, 0f, 1000f, "Fixed orbit damage every 0.5 seconds for 120 seconds. Original: 20; the orbit does not inherit item quality."); } } internal sealed class InspirationSettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<bool> TotemActivates { get; } internal ConfigEntry<float> TotemGainPercent { get; } internal ConfigEntry<int> TotemMaxBonus { get; } internal ConfigEntry<bool> WindActivates { get; } internal ConfigEntry<float> WindGainPercent { get; } internal ConfigEntry<int> WindMaxBonus { get; } internal ConfigEntry<string> BloodSourceStatusEffect { get; } internal ConfigEntry<bool> BloodActivates { get; } internal ConfigEntry<float> BloodGainPercent { get; } internal ConfigEntry<int> BloodMaxBonus { get; } internal InspirationSettings(ConfigFile config) { Enabled = config.Bind<bool>("90 - Inspiration source fixes", "Enabled", true, "Make Totemcaller, Windcaller and Bloodcaller able to charge enough Inspiration for their own most expensive attacks."); TotemActivates = config.Bind<bool>("90 - Inspiration source fixes", "Totem source activates", true, "Original Totemcaller Utility value: false."); TotemGainPercent = BindFloat(config, "90 - Inspiration source fixes", "Totem source gain percent", 60f, -99f, 10000f, "Totemcaller gain-speed bonus. Original: 10 percent. Default gives 75 seconds per point in isolation."); TotemMaxBonus = BindInt(config, "90 - Inspiration source fixes", "Totem source max bonus", 1, 0, 7, "Added maximum points. Original: 0; base max is 1 and Boomerang costs 2."); WindActivates = config.Bind<bool>("90 - Inspiration source fixes", "Wind source activates", true, "Original Windcaller Utility value: false."); WindGainPercent = BindFloat(config, "90 - Inspiration source fixes", "Wind source gain percent", 60f, -99f, 10000f, "Windcaller gain-speed bonus. Original: 10 percent. Default gives 75 seconds per point in isolation."); WindMaxBonus = BindInt(config, "90 - Inspiration source fixes", "Wind source max bonus", 2, 0, 7, "Added maximum points. Original: 1; base max is 1 and Parasol secondary costs 3."); BloodSourceStatusEffect = config.Bind<string>("90 - Inspiration source fixes", "Blood source status effect", "SE_BloodcallerTrinketOff_DO", "Exact equip StatusEffect referenced by ArmorBloodcallerTrinketDO in Magic Supremacy 3.0.6."); BloodActivates = config.Bind<bool>("90 - Inspiration source fixes", "Blood source activates", true, "Bloodcaller has no source in the original Inspiration source list."); BloodGainPercent = BindFloat(config, "90 - Inspiration source fixes", "Blood source gain percent", 100f, -99f, 10000f, "Bloodcaller gain-speed bonus. Default matches the same-tier Stormcaller source."); BloodMaxBonus = BindInt(config, "90 - Inspiration source fixes", "Blood source max bonus", 3, 0, 7, "Added maximum points. Base max is 1 and Blood Requiem secondary costs 4."); } } internal sealed class EpicLootCompatibilitySettings { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<bool> UseCurrentAttackProjectileCount { get; } internal ConfigEntry<bool> UseCurrentAttackAccuracy { get; } internal ConfigEntry<float> DoubleShotAccuracyMultiplierFallback { get; } internal ConfigEntry<float> TripleShotAccuracyMultiplierFallback { get; } internal EpicLootCompatibilitySettings(ConfigFile config) { Enabled = config.Bind<bool>("95 - Epic Loot multishot compatibility", "Enabled", true, "Correct Epic Loot DoubleMagicShot/TripleBowShot only for the 16 Magic Supremacy weapons."); UseCurrentAttackProjectileCount = config.Bind<bool>("95 - Epic Loot multishot compatibility", "Use current attack projectile count", true, "Multiply the active primary or secondary attack's own projectile count, instead of always using the weapon primary count."); UseCurrentAttackAccuracy = config.Bind<bool>("95 - Epic Loot multishot compatibility", "Use current attack accuracy", true, "Apply the multishot accuracy multiplier to the active attack's own accuracy, instead of always using primary accuracy."); DoubleShotAccuracyMultiplierFallback = BindFloat(config, "95 - Epic Loot multishot compatibility", "Double shot accuracy multiplier fallback", 1.2f, 0f, 10f, "Used only when Epic Loot's multiplier cannot be derived because primary accuracy is zero. Active balanced Epic Loot value: 1.20."); TripleShotAccuracyMultiplierFallback = BindFloat(config, "95 - Epic Loot multishot compatibility", "Triple shot accuracy multiplier fallback", 1.25f, 0f, 10f, "Used only when Epic Loot's multiplier cannot be derived because primary accuracy is zero. Active balanced Epic Loot value: 1.25."); } } private const int CurrentSchemaVersion = 3; internal ConfigEntry<int> SchemaVersion { get; } internal ConfigEntry<bool> Enabled { get; } internal MushroomSettings Mushroom { get; } internal TotemakuSettings Totemaku { get; } internal TotemicBoomerangSettings TotemicBoomerang { get; } internal GraveblightSettings Graveblight { get; } internal FrostspikeSettings Frostspike { get; } internal IncineratorSettings Incinerator { get; } internal EmberOrbitSettings EmberOrbit { get; } internal PrimeMonolithSettings PrimeMonolith { get; } internal CandleSettings Candle { get; } internal LuminarisSettings Luminaris { get; } internal WindcallerSettings Windcaller { get; } internal BloodRequiemSettings BloodRequiem { get; } internal SanguineConcordSettings SanguineConcord { get; } internal TempestEyeSettings TempestEye { get; } internal ThundersurgeSettings Thundersurge { get; } internal InspirationSettings Inspiration { get; } internal EpicLootCompatibilitySettings EpicLootCompatibility { get; } internal BalanceConfig(ConfigFile config) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown SchemaVersion = config.Bind<int>("00 - General", "Config schema version", 0, new ConfigDescription("Internal migration marker. Do not edit manually.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3), Array.Empty<object>())); Enabled = config.Bind<bool>("00 - General", "Enabled", true, "Enable the complete Magic Supremacy rebalance. Restart Valheim after editing this file."); Mushroom = new MushroomSettings(config); Totemaku = new TotemakuSettings(config); TotemicBoomerang = new TotemicBoomerangSettings(config); Graveblight = new GraveblightSettings(config); Frostspike = new FrostspikeSettings(config); Incinerator = new IncineratorSettings(config); EmberOrbit = new EmberOrbitSettings(config); PrimeMonolith = new PrimeMonolithSettings(config); Candle = new CandleSettings(config); Luminaris = new LuminarisSettings(config); Windcaller = new WindcallerSettings(config); BloodRequiem = new BloodRequiemSettings(config); SanguineConcord = new SanguineConcordSettings(config); TempestEye = new TempestEyeSettings(config); Thundersurge = new ThundersurgeSettings(config); Inspiration = new InspirationSettings(config); EpicLootCompatibility = new EpicLootCompatibilitySettings(config); MigrateLegacyDefaults(config); } private void MigrateLegacyDefaults(ConfigFile config) { int value = SchemaVersion.Value; if (value < 3) { if (value == 0) { Migrate(Frostspike.PrimaryProjectiles, 4, 2); Migrate(Frostspike.RainAoeLifetime, 6f, 8f); Migrate(Incinerator.SecondaryItemMultiplier, 0.15f, 0.2f); Migrate(PrimeMonolith.BaseBlunt, 18f, 35f); Migrate(Luminaris.BaseBlunt, 55f, 50f); Migrate(Luminaris.BaseSpirit, 83f, 75f); Migrate(Luminaris.SpiritPerQuality, 3f, 4f); Migrate(BloodRequiem.PrimaryProjectiles, 8, 3); Migrate(BloodRequiem.BaseGenericPerProjectile, 25.5f, 60f); Migrate(BloodRequiem.GenericPerQualityPerProjectile, 4.5f, 7f); Migrate(TempestEye.BasePierce, 84f, 75f); Migrate(TempestEye.BaseLightning, 84f, 75f); Migrate(Thundersurge.BaseLightning, 33f, 43f); Migrate(Inspiration.TotemGainPercent, 10f, 60f); Migrate(Inspiration.WindGainPercent, 10f, 60f); } else { Migrate(Frostspike.PrimaryProjectiles, 3, 2); Migrate(Frostspike.BaseFrostPerProjectile, 40f, 60f); Migrate(Frostspike.FrostPerQualityPerProjectile, 4f, 6f); Migrate(Incinerator.SecondaryItemMultiplier, 0.15f, 0.2f); Migrate(Incinerator.SecondaryBurstInterval, 0.3f, 0.4f); Migrate(PrimeMonolith.BaseBlunt, 20f, 35f); Migrate(PrimeMonolith.PrimaryBurstInterval, 0.25f, 0.4f); Migrate(BloodRequiem.PrimaryProjectiles, 4, 3); Migrate(BloodRequiem.BaseGenericPerProjectile, 50f, 60f); Migrate(Thundersurge.BaseLightning, 31f, 43f); Migrate(Thundersurge.PrimaryBurstInterval, 0.3f, 0.4f); } SchemaVersion.Value = 3; config.Save(); } } private static void Migrate(ConfigEntry<int> entry, int oldDefault, int newDefault) { if (entry.Value == oldDefault) { entry.Value = newDefault; } } private static void Migrate(ConfigEntry<float> entry, float oldDefault, float newDefault) { if (Math.Abs(entry.Value - oldDefault) <= 0.0001f) { entry.Value = newDefault; } } private static ConfigEntry<bool> BindEnabled(ConfigFile config, string section) { return config.Bind<bool>(section, "Enabled", true, "Enable this weapon-specific rebalance block."); } private static ConfigEntry<float> BindFloat(ConfigFile config, string section, string key, float defaultValue, float minimum, float maximum, string description) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown return config.Bind<float>(section, key, defaultValue, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(minimum, maximum), Array.Empty<object>())); } private static ConfigEntry<int> BindInt(ConfigFile config, string section, string key, int defaultValue, int minimum, int maximum, string description) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown return config.Bind<int>(section, key, defaultValue, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<int>(minimum, maximum), Array.Empty<object>())); } } internal static class EpicLootCompatibility { private sealed class BurstState { internal int Projectiles { get; } internal float ProjectileAccuracy { get; } internal BurstState(int projectiles, float projectileAccuracy) { Projectiles = projectiles; ProjectileAccuracy = projectileAccuracy; } } internal static readonly string[] MagicWeaponPrefabNames = new string[16] { "StaffMushroomcallerMushroomDO", "StaffTotemcallerTotemDO", "StaffTotemcallerBoomerangDO", "StaffDeathcallerBlasterDO", "BowFrostcallerIceDO", "StaffFrostcallerShotgunDO", "StaffFirecallerBlasterDO", "StaffFirecallerRingDO", "StaffStonecallerBurstDO", "StaffLightcallerHealDO", "StaffLightcallerSphereDO", "StaffWindcallerBuffDO", "StaffBloodcallerShotgunDO", "StaffBloodcallerHealDO", "StaffStormcallerSphereDO", "StaffStormcallerShockerDO" }; private const string MultiShotTypeName = "EpicLoot.MagicItemEffects.MultiShot"; private static readonly HashSet<SharedData> MagicWeaponSharedData = new HashSet<SharedData>(); private static readonly Dictionary<Attack, BurstState> BurstStates = new Dictionary<Attack, BurstState>(); private static FieldInfo _shotProjectilesField; private static bool _installed; private static bool _loggedRegistry; private static bool _loggedCorrection; internal static void Install(Harmony harmony) { //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Expected O, but got Unknown //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Expected O, but got Unknown //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown if (_installed) { return; } MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (!((Object)(object)instance == (Object)null) && instance.Balance.Enabled.Value && instance.Balance.EpicLootCompatibility.Enabled.Value) { Type type = AccessTools.TypeByName("EpicLoot.MagicItemEffects.MultiShot"); _shotProjectilesField = ((type == null) ? null : AccessTools.Field(type, "ShotProjectiles")); MethodInfo methodInfo = AccessTools.Method(typeof(Attack), "FireProjectileBurst", (Type[])null, (Type[])null); if (_shotProjectilesField == null || methodInfo == null) { instance.Log.LogWarning((object)"Epic Loot MultiShot compatibility was not installed because its exact FireProjectileBurst state could not be found."); return; } HarmonyMethod val = new HarmonyMethod(typeof(EpicLootCompatibility), "CapturePrefix", (Type[])null); val.priority = 800; val.before = new string[1] { "randyknapp.mods.epicloot" }; HarmonyMethod val2 = val; val = new HarmonyMethod(typeof(EpicLootCompatibility), "CorrectPrefix", (Type[])null); val.priority = 0; val.after = new string[1] { "randyknapp.mods.epicloot" }; HarmonyMethod val3 = val; val = new HarmonyMethod(typeof(EpicLootCompatibility), "RestoreFinalizer", (Type[])null); val.priority = 0; val.after = new string[1] { "randyknapp.mods.epicloot" }; HarmonyMethod val4 = val; harmony.Patch((MethodBase)methodInfo, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo, val3, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, val4, (HarmonyMethod)null); _installed = true; instance.Log.LogInfo((object)"Installed scoped Epic Loot MultiShot correction for Magic Supremacy weapons."); } } internal static void RefreshMagicWeaponRegistry(ObjectDB objectDb, ManualLogSource log) { MagicWeaponSharedData.Clear(); int num = 0; string[] magicWeaponPrefabNames = MagicWeaponPrefabNames; foreach (string text in magicWeaponPrefabNames) { GameObject itemPrefab = objectDb.GetItemPrefab(text); SharedData val = (((Object)(object)itemPrefab == (Object)null) ? null : itemPrefab.GetComponent<ItemDrop>())?.m_itemData?.m_shared; if (val != null) { MagicWeaponSharedData.Add(val); num++; } } if (!_loggedRegistry) { if (num == MagicWeaponPrefabNames.Length) { log.LogInfo((object)$"Registered all {num} Magic Supremacy weapons for scoped Epic Loot compatibility."); } else { log.LogWarning((object)$"Registered {num}/{MagicWeaponPrefabNames.Length} Magic Supremacy weapons for Epic Loot compatibility; missing prefabs will be left untouched."); } _loggedRegistry = true; } } internal static void Reset() { BurstStates.Clear(); MagicWeaponSharedData.Clear(); _shotProjectilesField = null; _installed = false; _loggedRegistry = false; _loggedCorrection = false; } private static void CapturePrefix(Attack __instance) { if (TryGetSettings(__instance, out var _)) { BurstStates[__instance] = new BurstState(__instance.m_projectiles, __instance.m_projectileAccuracy); _shotProjectilesField.SetValue(null, 0); } } private static void CorrectPrefix(Attack __instance) { if (!BurstStates.TryGetValue(__instance, out var value) || !TryGetSettings(__instance, out var settings)) { return; } int num = (int)_shotProjectilesField.GetValue(null); if (num != 2 && num != 3) { return; } if (settings.UseCurrentAttackProjectileCount.Value) { __instance.m_projectiles = value.Projectiles * num; } if (settings.UseCurrentAttackAccuracy.Value) { float accuracyMultiplier = GetAccuracyMultiplier(__instance, settings, num); __instance.m_projectileAccuracy = value.ProjectileAccuracy * accuracyMultiplier; } if (!_loggedCorrection) { MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (instance != null) { instance.Log.LogInfo((object)"Corrected an Epic Loot multishot burst from the active Attack's own projectile count/accuracy; state will be restored after this burst."); } _loggedCorrection = true; } } private static Exception RestoreFinalizer(Attack __instance, Exception __exception) { if (!BurstStates.TryGetValue(__instance, out var value)) { return __exception; } __instance.m_projectiles = value.Projectiles; __instance.m_projectileAccuracy = value.ProjectileAccuracy; BurstStates.Remove(__instance); return __exception; } private static float GetAccuracyMultiplier(Attack attack, BalanceConfig.EpicLootCompatibilitySettings settings, int projectileMultiplier) { Attack val = attack.GetWeapon()?.m_shared?.m_attack; if (val != null && Mathf.Abs(val.m_projectileAccuracy) > 0.0001f) { return attack.m_projectileAccuracy / val.m_projectileAccuracy; } if (projectileMultiplier != 3) { return settings.DoubleShotAccuracyMultiplierFallback.Value; } return settings.TripleShotAccuracyMultiplierFallback.Value; } private static bool TryGetSettings(Attack attack, out BalanceConfig.EpicLootCompatibilitySettings settings) { settings = null; MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; ItemData val = ((attack != null) ? attack.GetWeapon() : null); if ((Object)(object)instance == (Object)null || val?.m_shared == null || !instance.Balance.Enabled.Value || !instance.Balance.EpicLootCompatibility.Enabled.Value || !MagicWeaponSharedData.Contains(val.m_shared)) { return false; } settings = instance.Balance.EpicLootCompatibility; return true; } } internal static class InspirationBridge { private const string InspirationSystemTypeName = "Magic_Supremacy.InspirationSystem"; private const string InspirationConfigTypeName = "Magic_Supremacy.InspirationSystemConfigs"; private static bool _installed; private static bool _loggedApplication; internal static void Reset() { _installed = false; _loggedApplication = false; } internal static void Install(Harmony harmony) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Expected O, but got Unknown MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (_installed || (Object)(object)instance == (Object)null || !instance.Balance.Enabled.Value || !instance.Balance.Inspiration.Enabled.Value) { return; } Type type = AccessTools.TypeByName("Magic_Supremacy.InspirationSystem"); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "SetDefinitions", (Type[])null, (Type[])null)); if (methodInfo == null) { instance.Log.LogWarning((object)"Could not find Magic Supremacy InspirationSystem.SetDefinitions; standalone Inspiration source fixes were not installed."); return; } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(InspirationBridge), "SetDefinitionsPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _installed = true; Type type2 = AccessTools.TypeByName("Magic_Supremacy.InspirationSystemConfigs"); MethodInfo methodInfo2 = ((type2 == null) ? null : AccessTools.Method(type2, "RebuildDefinitionsFromConfig", (Type[])null, (Type[])null)); if (methodInfo2 == null) { instance.Log.LogWarning((object)"Inspiration hook was installed, but the current Magic Supremacy definitions could not be rebuilt immediately. A later Magic Supremacy config rebuild will apply it."); return; } try { methodInfo2.Invoke(null, null); } catch (Exception ex) { instance.Log.LogWarning((object)("Could not rebuild Magic Supremacy Inspiration definitions: " + ex.GetBaseException().Message)); } } private static void SetDefinitionsPrefix(object[] __args) { MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; BalanceConfig.InspirationSettings inspirationSettings = instance?.Balance?.Inspiration; if ((Object)(object)instance == (Object)null || inspirationSettings == null || !instance.Balance.Enabled.Value || !inspirationSettings.Enabled.Value || __args == null || __args.Length == 0) { return; } if (!(__args[0] is IList sources)) { instance.Log.LogWarning((object)"Magic Supremacy supplied an unsupported Inspiration source collection; source fixes were skipped."); return; } bool flag; bool flag2; bool flag3; try { flag = UpsertSource(sources, "SE_TotemcallerBelt_DO", "Totemcaller Utility", inspirationSettings.TotemActivates.Value, inspirationSettings.TotemGainPercent.Value, inspirationSettings.TotemMaxBonus.Value, addIfMissing: false); flag2 = UpsertSource(sources, "SE_WindcallerBelt_DO", "Windcaller Utility", inspirationSettings.WindActivates.Value, inspirationSettings.WindGainPercent.Value, inspirationSettings.WindMaxBonus.Value, addIfMissing: false); flag3 = UpsertSource(sources, inspirationSettings.BloodSourceStatusEffect.Value, "Bloodcaller Utility", inspirationSettings.BloodActivates.Value, inspirationSettings.BloodGainPercent.Value, inspirationSettings.BloodMaxBonus.Value, addIfMissing: true); } catch (Exception ex) { instance.Log.LogWarning((object)("Could not apply Inspiration source fixes safely: " + ex.GetBaseException().Message)); return; } if (!_loggedApplication) { instance.Log.LogInfo((object)$"Inspiration sources applied: Totemcaller={flag}, Windcaller={flag2}, Bloodcaller={flag3}."); _loggedApplication = true; } } private static bool UpsertSource(IList sources, string sourceName, string displayName, bool activates, float gainPercent, int maxBonus, bool addIfMissing) { if (string.IsNullOrWhiteSpace(sourceName)) { return false; } object obj = null; for (int i = 0; i < sources.Count; i++) { object obj2 = sources[i]; if (obj2 != null && string.Equals(ReadString(obj2, "SourceName"), sourceName, StringComparison.Ordinal)) { obj = obj2; break; } } if (obj == null) { if (!addIfMissing) { return false; } Type type = AccessTools.TypeByName("Magic_Supremacy.InspirationSystem+InspirationSourceDef"); if (type == null) { return false; } FieldInfo fieldInfo = AccessTools.Field(type, "SourceType"); if (fieldInfo == null) { return false; } obj = Activator.CreateInstance(type); if (!SetField(obj, "DisplayName", displayName) || !SetField(obj, "Name", displayName) || !SetField(obj, "SourceName", sourceName)) { return false; } object value = Enum.Parse(fieldInfo.FieldType, "StatusEffect"); fieldInfo.SetValue(obj, value); } if (!SetField(obj, "ActivatesInspiration", activates) || !SetField(obj, "InspirationGainPercentBonus", gainPercent) || !SetField(obj, "MaxInspirationBonus", maxBonus)) { return false; } if (!sources.Contains(obj)) { sources.Add(obj); } return true; } private static string ReadString(object instance, string fieldName) { return AccessTools.Field(instance.GetType(), fieldName)?.GetValue(instance) as string; } private static bool SetField(object instance, string fieldName, object value) { FieldInfo fieldInfo = AccessTools.Field(instance.GetType(), fieldName); if (fieldInfo == null) { return false; } fieldInfo.SetValue(instance, value); return true; } } [BepInPlugin("dragonmotion.magicsupremacyreebalance", "MagicSupremacyReebalance", "1.1.1")] [BepInDependency("Dreanegade.Magic_Supremacy", "3.0.6")] [BepInDependency("randyknapp.mods.epicloot", "0.12.15")] [BepInIncompatibility("dragonmotion.magicsupremacyfrostrebalance")] public sealed class MagicSupremacyReebalancePlugin : BaseUnityPlugin { [HarmonyPatch(typeof(ObjectDB), "Awake")] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "Dreanegade.Magic_Supremacy", "org.bepinex.helpers.ItemManager" })] private static class ObjectDbAwakePatch { private static void Postfix(ObjectDB __instance) { ApplyObjectDb(__instance); } } [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "Dreanegade.Magic_Supremacy", "org.bepinex.helpers.ItemManager" })] private static class ObjectDbCopyOtherDbPatch { private static void Postfix(ObjectDB __instance) { MagicSupremacyReebalancePlugin instance = Instance; if ((Object)(object)instance != (Object)null && (Object)(object)__instance != (Object)null && instance.Balance.Enabled.Value) { MagicSupremacyStatusBridge.RestoreAfterCopy(__instance, ((BaseUnityPlugin)instance).Logger); } ApplyObjectDb(__instance); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "Dreanegade.Magic_Supremacy", "org.bepinex.helpers.ItemManager" })] private static class ZNetSceneAwakePatch { private static void Postfix(ZNetScene __instance) { ApplyZNetScene(__instance); } } public const string PluginGuid = "dragonmotion.magicsupremacyreebalance"; public const string PluginName = "MagicSupremacyReebalance"; public const string PluginVersion = "1.1.1"; internal const string MagicSupremacyGuid = "Dreanegade.Magic_Supremacy"; internal const string EpicLootGuid = "randyknapp.mods.epicloot"; internal const string ItemManagerHarmonyId = "org.bepinex.helpers.ItemManager"; internal const string OldFrostRebalanceGuid = "dragonmotion.magicsupremacyfrostrebalance"; private static readonly Harmony Harmony = new Harmony("dragonmotion.magicsupremacyreebalance"); internal static MagicSupremacyReebalancePlugin Instance { get; private set; } internal BalanceConfig Balance { get; private set; } internal ManualLogSource Log => ((BaseUnityPlugin)this).Logger; private void Awake() { Instance = this; Balance = new BalanceConfig(((BaseUnityPlugin)this).Config); MagicSupremacyStatusBridge.Install(((BaseUnityPlugin)this).Logger); Harmony.PatchAll(typeof(MagicSupremacyReebalancePlugin).Assembly); EpicLootCompatibility.Install(Harmony); InspirationBridge.Install(Harmony); ((BaseUnityPlugin)this).Logger.LogInfo((object)"MagicSupremacyReebalance 1.1.1 loaded. Rebalance values are applied during ObjectDB/ZNetScene initialization; no update-loop patches are used."); } private void OnDestroy() { Harmony.UnpatchSelf(); MagicSupremacyStatusBridge.Reset(); EpicLootCompatibility.Reset(); InspirationBridge.Reset(); Instance = null; } internal static void ApplyObjectDb(ObjectDB objectDb) { MagicSupremacyReebalancePlugin instance = Instance; if (!((Object)(object)instance == (Object)null) && !((Object)(object)objectDb == (Object)null) && instance.Balance.Enabled.Value) { BalanceApplier.ApplyItemsAndStatusEffects(objectDb, instance.Balance, ((BaseUnityPlugin)instance).Logger); } } internal static void ApplyZNetScene(ZNetScene scene) { MagicSupremacyReebalancePlugin instance = Instance; if (!((Object)(object)instance == (Object)null) && !((Object)(object)scene == (Object)null) && instance.Balance.Enabled.Value) { BalanceApplier.ApplyScenePrefabs(scene, instance.Balance, ((BaseUnityPlugin)instance).Logger); } } } internal static class MagicSupremacyStatusBridge { private const string RegisterTypeName = "Magic_Supremacy.StatusEffectRegister"; private const string RegisterMethodName = "ObjectDB_Awake_Postfix"; private static Action<ObjectDB> _registerStatusEffects; private static bool _loggedRestore; private static bool _warnedUnavailable; private static bool _warnedFailure; internal static void Install(ManualLogSource log) { Type type = AccessTools.TypeByName("Magic_Supremacy.StatusEffectRegister"); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "ObjectDB_Awake_Postfix", new Type[1] { typeof(ObjectDB) }, (Type[])null)); if (methodInfo == null || !methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { WarnUnavailable(log); return; } try { _registerStatusEffects = (Action<ObjectDB>)Delegate.CreateDelegate(typeof(Action<ObjectDB>), methodInfo); } catch (Exception ex) { WarnUnavailable(log, ex.GetBaseException().Message); } } internal static void RestoreAfterCopy(ObjectDB objectDb, ManualLogSource log) { if ((Object)(object)objectDb == (Object)null) { return; } if (_registerStatusEffects == null) { WarnUnavailable(log); return; } try { _registerStatusEffects(objectDb); if (!_loggedRestore) { log.LogInfo((object)"Restored Magic Supremacy status-effect registration after ObjectDB.CopyOtherDB replaced the status list."); _loggedRestore = true; } } catch (Exception ex) { if (!_warnedFailure) { log.LogWarning((object)("Magic Supremacy status effects could not be restored after ObjectDB.CopyOtherDB: " + ex.GetBaseException().Message)); _warnedFailure = true; } } } internal static void Reset() { _registerStatusEffects = null; _loggedRestore = false; _warnedUnavailable = false; _warnedFailure = false; } private static void WarnUnavailable(ManualLogSource log, string detail = null) { if (!_warnedUnavailable) { string text = (string.IsNullOrWhiteSpace(detail) ? string.Empty : (" (" + detail + ")")); log.LogWarning((object)("Could not find the exact Magic Supremacy status registration method; CopyOtherDB recovery is unavailable" + text + ".")); _warnedUnavailable = true; } } } internal static class SupportScaling { [HarmonyPatch(typeof(Character), "RPC_Damage")] private static class CharacterRpcDamagePatch { private static void Prefix(Character __instance, HitData hit, out DamagePatchState __state) { __state = null; MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if ((Object)(object)instance == (Object)null || !instance.Balance.Enabled.Value || hit == null || !IsManagedHash(hit.m_statusEffectHash)) { return; } __state = new DamagePatchState(_pendingApplication); _pendingApplication = new PendingApplication(hit.m_statusEffectHash, hit.m_itemLevel); SEMan sEMan = __instance.GetSEMan(); if (!((Object)(object)((sEMan != null) ? sEMan.GetStatusEffect(hit.m_statusEffectHash) : null) != (Object)null)) { ObjectDB instance2 = ObjectDB.instance; StatusEffect obj = ((instance2 != null) ? instance2.GetStatusEffect(hit.m_statusEffectHash) : null); SE_Stats val = (SE_Stats)(object)((obj is SE_Stats) ? obj : null); if (val != null) { __state.TemplateRestore = new TemplateRestoreState(val); ApplyQualityValues((StatusEffect)(object)val, hit.m_itemLevel, instance.Balance); } } } private static Exception Finalizer(Exception __exception, DamagePatchState __state) { if (__state == null) { return __exception; } __state.TemplateRestore?.Restore(); _pendingApplication = __state.PreviousPending; return __exception; } } [HarmonyPatch(typeof(SE_Stats), "ResetTime")] private static class StatsResetTimePatch { [HarmonyPriority(800)] private static void Prefix(StatusEffect __instance) { PendingApplication pendingApplication = _pendingApplication; MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (pendingApplication != null && !((Object)(object)instance == (Object)null) && instance.Balance.Enabled.Value && !((Object)(object)__instance == (Object)null) && __instance.NameHash() == pendingApplication.StatusHash) { ApplyQualityValues(__instance, pendingApplication.ItemLevel, instance.Balance, includeCandleHot: false); } } } [HarmonyPatch(typeof(StatusEffect), "SetLevel")] private static class StatusEffectSetLevelPatch { [HarmonyPriority(0)] private static void Postfix(StatusEffect __instance, int itemLevel) { MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (!((Object)(object)instance == (Object)null) && instance.Balance.Enabled.Value) { SE_Stats val = (SE_Stats)(object)((__instance is SE_Stats) ? __instance : null); bool includeCandleHot = val == null || (Object)(object)((StatusEffect)val).m_character == (Object)null; ApplyQualityValues(__instance, itemLevel, instance.Balance, includeCandleHot); } } } [HarmonyPatch(typeof(SE_Stats), "GetTooltipString")] private static class StatsTooltipCleanupPatch { [HarmonyPriority(0)] private static Exception Finalizer(SE_Stats __instance, Exception __exception) { MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (!((Object)(object)instance == (Object)null) && instance.Balance.Enabled.Value && !((Object)(object)__instance == (Object)null)) { ObjectDB instance2 = ObjectDB.instance; if ((object)((instance2 != null) ? instance2.GetStatusEffect(((StatusEffect)__instance).NameHash()) : null) == __instance) { ApplyQualityValues((StatusEffect)(object)__instance, 1, instance.Balance); return __exception; } } return __exception; } } [HarmonyPatch(typeof(ItemData), "GetTooltip", new Type[] { typeof(ItemData), typeof(int), typeof(bool), typeof(float), typeof(int) })] [HarmonyPriority(0)] [HarmonyAfter(new string[] { "Dreanegade.Magic_Supremacy" })] private static class CandleTooltipPatch { private const string OriginalHealMarkup = "<color=orange>40</color>"; private static void Postfix(ItemData item, int qualityLevel, ref string __result) { MagicSupremacyReebalancePlugin instance = MagicSupremacyReebalancePlugin.Instance; if (!((Object)(object)instance == (Object)null) && instance.Balance.Enabled.Value && instance.Balance.Candle.Enabled.Value && item?.m_shared != null && IsCandle(item) && !string.IsNullOrEmpty(__result)) { int num = __result.LastIndexOf("<color=orange>40</color>", StringComparison.Ordinal); if (num >= 0) { string value = "<color=orange>" + QualityValue(instance.Balance.Candle.PrimaryHealAtQuality1.Value, instance.Balance.Candle.PrimaryHealPerQuality.Value, qualityLevel).ToString("0.##", CultureInfo.InvariantCulture) + "</color>"; __result = __result.Remove(num, "<color=orange>40</color>".Length).Insert(num, value); } } } } private sealed class TemplateRestoreState { private readonly SE_Stats _status; private readonly float _ttl; private readonly float _healthPerTick; private readonly float _healthUpFront; private readonly float _healthOverTime; private readonly float _healthOverTimeDuration; internal TemplateRestoreState(SE_Stats status) { _status = status; _ttl = ((StatusEffect)status).m_ttl; _healthPerTick = status.m_healthPerTick; _healthUpFront = status.m_healthUpFront; _healthOverTime = status.m_healthOverTime; _healthOverTimeDuration = status.m_healthOverTimeDuration; } internal void Restore() { ((StatusEffect)_status).m_ttl = _ttl; _status.m_healthPerTick = _healthPerTick; _status.m_healthUpFront = _healthUpFront; _status.m_healthOverTime = _healthOverTime; _status.m_healthOverTimeDuration = _healthOverTimeDuration; } } private sealed class DamagePatchState { internal PendingApplication PreviousPending { get; } internal TemplateRestoreState TemplateRestore { get; set; } internal DamagePatchState(PendingApplication previousPending) { PreviousPending = previousPending; } } private sealed class PendingApplication { internal int StatusHash { get; } internal int ItemLevel { get; } internal PendingApplication(int statusHash, int itemLevel) { StatusHash = statusHash; ItemLevel = itemLevel; } } private const string TotemBuffStatus = "SE_TotemBuff_DO"; private const string CandleSigilStatus = "SE_LightcallerSigil_DO"; private const string CandleHealStatus = "SE_LightcallerHeal_DO"; private const string WindPetalStatus = "SE_WindcallerPetal_DO"; private const string CandleItem = "StaffLightcallerHealDO"; private const string CandleSharedName = "$item_staff_lightcaller_heal_DO"; private static readonly int TotemBuffHash = StringExtensionMethods.GetStableHashCode("SE_TotemBuff_DO"); private static readonly int CandleSigilHash = StringExtensionMethods.GetStableHashCode("SE_LightcallerSigil_DO"); private static readonly int CandleHealHash = StringExtensionMethods.GetStableHashCode("SE_LightcallerHeal_DO"); private static readonly int WindPetalHash = StringExtensionMethods.GetStableHashCode("SE_WindcallerPetal_DO"); [ThreadStatic] private static PendingApplication _pendingApplication; private static void ApplyQualityValues(StatusEffect status, int itemLevel, BalanceConfig config, bool includeCandleHot = true) { SE_Stats val = (SE_Stats)(object)((status is SE_Stats) ? status : null); if (val == null) { return; } string name = ((Object)status).name; if (string.Equals(name, "SE_TotemBuff_DO", StringComparison.Ordinal) && config.Totemaku.Enabled.Value) { val.m_healthPerTick = QualityValue(config.Totemaku.HealPerTickAtQuality1.Value, config.Totemaku.HealPerTickPerQuality.Value, itemLevel); } else if (string.Equals(name, "SE_LightcallerSigil_DO", StringComparison.Ordinal) && config.Candle.Enabled.Value) { val.m_healthUpFront = QualityValue(config.Candle.PrimaryHealAtQuality1.Value, config.Candle.PrimaryHealPerQuality.Value, itemLevel); } else if (string.Equals(name, "SE_LightcallerHeal_DO", StringComparison.Ordinal) && config.Candle.Enabled.Value) { val.m_healthUpFront = config.Candle.SecondaryUpFrontHeal.Value; if (includeCandleHot) { val.m_healthOverTime = QualityValue(config.Candle.SecondaryHealOverTimeAtQuality1.Value, config.Candle.SecondaryHealOverTimePerQuality.Value, itemLevel); } } else if (string.Equals(name, "SE_WindcallerPetal_DO", StringComparison.Ordinal) && config.Windcaller.Enabled.Value) { ((StatusEffect)val).m_ttl = QualityValue(config.Windcaller.PetalDurationAtQuality1.Value, config.Windcaller.PetalDurationPerQuality.Value, itemLevel); } } private static bool IsManagedHash(int hash) { if (hash != TotemBuffHash && hash != CandleSigilHash && hash != CandleHealHash) { return hash == WindPetalHash; } return true; } private static bool IsCandle(ItemData item) { if (!string.Equals(((Object)(object)item.m_dropPrefab == (Object)null) ? null : ((Object)item.m_dropPrefab).name, "StaffLightcallerHealDO", StringComparison.Ordinal)) { return string.Equals(item.m_shared.m_name, "$item_staff_lightcaller_heal_DO", StringComparison.Ordinal); } return true; } private static float QualityValue(float baseValue, float perQuality, int itemLevel) { return baseValue + perQuality * (float)(Mathf.Max(1, itemLevel) - 1); } }