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 ValheimWeaponClasses v1.0.0
ValheimWeaponClasses.dll
Decompiled 11 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn; using Jotunn.Configs; using Jotunn.Entities; using Jotunn.GUI; using Jotunn.Managers; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; using ValheimWeaponClasses; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("ValheimWeaponClasses")] [assembly: AssemblyDescription("Class Obelisk build piece for Valheim")] [assembly: AssemblyProduct("ValheimWeaponClasses")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: ComVisible(false)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] public static class ArcherPerkManager { private static Dictionary<long, float> arrowSlingerBuffs = new Dictionary<long, float>(); private static Dictionary<string, float> originalDrawDurations = new Dictionary<string, float>(); public const float ARROW_SLINGER_DURATION = 10f; private const string Intro = "Expert marksmen with unparalleled bow and crossbow mastery."; private const string Outro = "Perfect for players who enjoy ranged combat and precision shooting."; public static readonly List<PerkInfo> Perks = new List<PerkInfo> { new PerkInfo { RequiredLevel = 10, Name = "Steady Draw", Description = "-15% stamina drain while drawing" }, new PerkInfo { RequiredLevel = 20, Name = "Arrow Slinger", Description = "Arrows give a buff on hit that reduces draw time by 50% for 10 seconds" }, new PerkInfo { RequiredLevel = 30, Name = "Wind Reader", Description = "+15% damage beyond 25m; -25% stamina while aiming" }, new PerkInfo { RequiredLevel = 40, Name = "Magic Shot", Description = "50% chance to not consume an arrow on attack" }, new PerkInfo { RequiredLevel = 50, Name = "Adrenaline Rush", Description = "Consecutive hits return 5% stamina" } }; public static bool HasArcherPerk(Player player, int requiredLevel) { if ((Object)(object)player == (Object)null) { return false; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null || !playerData.IsClassActive(PlayerClass.Archer)) { return false; } return playerData.GetClassLevel(PlayerClass.Archer) >= requiredLevel; } public static string GetClassDescription(Player player) { int currentLevel = PlayerClassManager.GetPlayerData(player)?.GetClassLevel(PlayerClass.Archer) ?? 0; return PerkDescriptionBuilder.Build("Expert marksmen with unparalleled bow and crossbow mastery.", Perks, "Perfect for players who enjoy ranged combat and precision shooting.", currentLevel); } public static float ApplyLv10_SteadyDrawStamina(Player player, float staminaCost) { if (!HasArcherPerk(player, 10)) { return staminaCost; } if (((Character)player).IsDrawingBow()) { return staminaCost * 0.85f; } return staminaCost; } public static void ApplyArrowSlingerDrawSpeed(Player player, ItemData weapon) { if (HasArcherPerk(player, 20) && weapon?.m_shared?.m_attack != null) { string name = weapon.m_shared.m_name; Attack attack = weapon.m_shared.m_attack; if (!originalDrawDurations.ContainsKey(name)) { originalDrawDurations[name] = attack.m_drawDurationMin; } long playerID = player.GetPlayerID(); if (arrowSlingerBuffs.ContainsKey(playerID) && Time.time < arrowSlingerBuffs[playerID]) { attack.m_drawDurationMin = originalDrawDurations[name] * 0.5f; } else { attack.m_drawDurationMin = originalDrawDurations[name]; } } } public static void RestoreDrawDuration(ItemData weapon) { if (weapon?.m_shared?.m_attack != null) { string name = weapon.m_shared.m_name; if (originalDrawDurations.ContainsKey(name)) { weapon.m_shared.m_attack.m_drawDurationMin = originalDrawDurations[name]; } } } public static void TriggerArrowSlingerBuff(Player archer) { if (HasArcherPerk(archer, 20)) { long playerID = archer.GetPlayerID(); float num = Time.time + 10f; if (!arrowSlingerBuffs.ContainsKey(playerID) || arrowSlingerBuffs[playerID] < num) { arrowSlingerBuffs[playerID] = num; AddArrowSlingerStatusEffect(archer); ((Character)archer).Message((MessageType)1, "Arrow Slinger: 50% faster draw for 10s!", 0, (Sprite)null); } } } public static void AddArrowSlingerStatusEffect(Player player) { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) try { SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null) { RemoveArrowSlingerStatusEffect(player); ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon(); Sprite val = ((currentWeapon != null) ? currentWeapon.GetIcon() : null); if ((Object)(object)val == (Object)null) { val = GetDefaultBowIcon(); } SE_Stats val2 = ScriptableObject.CreateInstance<SE_Stats>(); ((Object)val2).name = "SE_ArrowSlinger"; ((StatusEffect)val2).m_name = "Arrow Slinger"; ((StatusEffect)val2).m_tooltip = "Draw speed increased by 50%"; ((StatusEffect)val2).m_icon = val; ((StatusEffect)val2).m_ttl = 10f; ((StatusEffect)val2).m_startMessage = ""; ((StatusEffect)val2).m_startMessageType = (MessageType)2; ((StatusEffect)val2).m_stopMessage = ""; ((StatusEffect)val2).m_stopMessageType = (MessageType)2; sEMan.AddStatusEffect((StatusEffect)(object)val2, true, 0, 0f); } } catch (Exception ex) { Logger.LogError((object)("Error adding Arrow Slinger status effect: " + ex.Message)); } } private static void RemoveArrowSlingerStatusEffect(Player player) { try { SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_ArrowSlinger"), true); } } catch (Exception ex) { Logger.LogError((object)("Error removing Arrow Slinger status effect: " + ex.Message)); } } private static Sprite GetDefaultBowIcon() { try { string[] array = new string[5] { "Bow", "BowFineWood", "BowHuntsman", "BowDraugrFang", "CrossbowArbalest" }; foreach (string text in array) { ObjectDB instance = ObjectDB.instance; GameObject val = ((instance != null) ? instance.GetItemPrefab(text) : null); if ((Object)(object)val != (Object)null) { ItemDrop component = val.GetComponent<ItemDrop>(); object obj; if (component == null) { obj = null; } else { ItemData itemData = component.m_itemData; obj = ((itemData != null) ? itemData.GetIcon() : null); } if ((Object)obj != (Object)null) { return component.m_itemData.GetIcon(); } } } return null; } catch { return null; } } public static bool HasActiveArrowSlingerBuff(long playerID) { if (arrowSlingerBuffs.ContainsKey(playerID)) { return Time.time < arrowSlingerBuffs[playerID]; } return false; } public static float ApplyLv30_WindReaderDamage(Player archer, Vector3 shotOrigin, Vector3 hitPoint, float baseDamage) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (!HasArcherPerk(archer, 30)) { return baseDamage; } float num = Vector3.Distance(shotOrigin, hitPoint); if (num > 25f) { float num2 = baseDamage * 0.15f; if (Random.Range(0f, 1f) < 0.2f) { ((Character)archer).Message((MessageType)1, $"Long Shot! +15% damage ({num:F0}m)", 0, (Sprite)null); } return baseDamage + num2; } return baseDamage; } public static float ApplyLv30_WindReaderStamina(Player archer, float staminaDrain) { if (!HasArcherPerk(archer, 30)) { return staminaDrain; } if (((Character)archer).IsDrawingBow()) { return staminaDrain * 0.75f; } return staminaDrain; } public static bool ShouldConsumeArrow(Player archer) { if (!HasArcherPerk(archer, 40)) { return true; } if (Random.Range(0f, 1f) < 0.5f) { ((Character)archer).Message((MessageType)1, "Magic Shot! Arrow not consumed", 0, (Sprite)null); return false; } return true; } public static void TriggerAdrenalineRush(Player archer) { if (HasArcherPerk(archer, 50)) { float num = ((Character)archer).GetMaxStamina() * 0.05f; ((Character)archer).AddStamina(num); if (Random.Range(0f, 1f) < 0.3f) { ((Character)archer).Message((MessageType)1, $"Adrenaline Rush! +{num:F0} stamina", 0, (Sprite)null); } } } public static void UpdateBuffs() { float currentTime = Time.time; foreach (long playerID in (from kvp in arrowSlingerBuffs where kvp.Value < currentTime select kvp.Key).ToList()) { arrowSlingerBuffs.Remove(playerID); Player val = ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((Player p) => p.GetPlayerID() == playerID)); if ((Object)(object)val != (Object)null) { RemoveArrowSlingerStatusEffect(val); } } } } [HarmonyPatch] public static class ArcherPerkPatches { private static float lastCleanupTime; [HarmonyPatch(typeof(Player), "UpdateAttackBowDraw")] [HarmonyPrefix] public static void Player_UpdateAttackBowDraw_Prefix(Player __instance, ItemData weapon, float dt) { try { if (!((Object)(object)__instance == (Object)null) && weapon != null && ClassCombatManager.IsBowWeapon(weapon)) { ArcherPerkManager.ApplyArrowSlingerDrawSpeed(__instance, weapon); } } catch (Exception ex) { Logger.LogError((object)("Error in Player_UpdateAttackBowDraw_Prefix (Archer): " + ex.Message)); } } [HarmonyPatch(typeof(Projectile), "OnHit")] [HarmonyPrefix] public static void Projectile_OnHit_Prefix(Projectile __instance, Collider collider, Vector3 hitPoint) { try { if ((Object)(object)__instance == (Object)null || (Object)(object)collider == (Object)null) { return; } Character component = ((Component)collider).GetComponent<Character>(); if ((Object)(object)component == (Object)null || component is Player) { return; } Player val = FindProjectileOwner(__instance); if ((Object)(object)val == (Object)null) { return; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(val); if (playerData != null && playerData.IsClassActive(PlayerClass.Archer)) { ItemData currentWeapon = ((Humanoid)val).GetCurrentWeapon(); if (ClassCombatManager.IsBowWeapon(currentWeapon)) { ArcherPerkManager.RestoreDrawDuration(currentWeapon); } ArcherPerkManager.TriggerArrowSlingerBuff(val); ArcherPerkManager.TriggerAdrenalineRush(val); } } catch (Exception ex) { Logger.LogError((object)("Error in Projectile_OnHit_Prefix (Archer): " + ex.Message)); } } [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] public static void Character_Damage_Prefix_ArcherPerks(Character __instance, ref HitData hit) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) try { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val == null || (Object)(object)__instance == (Object)null || __instance is Player || !IsProjectileDamage(hit) || !ClassCombatManager.IsBowWeapon(((Humanoid)val).GetCurrentWeapon())) { return; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(val); if (playerData != null && playerData.IsClassActive(PlayerClass.Archer)) { float totalDamage = hit.GetTotalDamage(); Vector3 position = ((Component)val).transform.position; Vector3 point = hit.m_point; float num = ArcherPerkManager.ApplyLv30_WindReaderDamage(val, position, point, totalDamage); if (num > totalDamage) { float num2 = num / totalDamage; hit.m_damage.m_damage *= num2; hit.m_damage.m_pierce *= num2; hit.m_damage.m_slash *= num2; } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Prefix_ArcherPerks: " + ex.Message)); } } [HarmonyPatch(typeof(Player), "UseStamina")] [HarmonyPrefix] public static void Player_UseStamina_Prefix(Player __instance, ref float v) { try { if (!((Object)(object)__instance == (Object)null)) { v = ArcherPerkManager.ApplyLv10_SteadyDrawStamina(__instance, v); v = ArcherPerkManager.ApplyLv30_WindReaderStamina(__instance, v); } } catch (Exception ex) { Logger.LogError((object)("Error in Player_UseStamina_Prefix (Archer): " + ex.Message)); } } [HarmonyPatch(typeof(Game), "Update")] [HarmonyPostfix] public static void Game_Update_Postfix() { try { if (Time.time - lastCleanupTime >= 1f) { lastCleanupTime = Time.time; ArcherPerkManager.UpdateBuffs(); } } catch (Exception ex) { Logger.LogError((object)("Error in Game_Update_Postfix (Archer cleanup): " + ex.Message)); } } private static Player FindProjectileOwner(Projectile projectile) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) try { ZNetView component = ((Component)projectile).GetComponent<ZNetView>(); if ((Object)(object)component != (Object)null && component.IsValid()) { long ownerID = component.GetZDO().GetLong("owner", 0L); if (ownerID != 0L) { return ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((Player p) => p.GetPlayerID() == ownerID)); } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && Vector3.Distance(((Component)localPlayer).transform.position, ((Component)projectile).transform.position) < 100f) { return localPlayer; } return null; } catch { return null; } } private static bool IsProjectileDamage(HitData hit) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 if ((int)hit.m_skill != 8 && (int)hit.m_skill != 14) { if (hit.m_damage.m_pierce > 0f && hit.m_damage.m_blunt == 0f) { return hit.m_damage.m_slash == 0f; } return false; } return true; } [HarmonyPatch(typeof(Inventory), "RemoveItem", new Type[] { typeof(ItemData), typeof(int) })] [HarmonyPrefix] public static void Inventory_RemoveItem_MagicShot_PreFix(Inventory __instance, ItemData item, int amount) { try { if (item == null || !IsArrowItem(item)) { return; } Player val = FindPlayerWithInventory(__instance); if (!((Object)(object)val == (Object)null) && ClassCombatManager.IsBowWeapon(((Humanoid)val).GetCurrentWeapon()) && ArcherPerkManager.HasArcherPerk(val, 40) && Random.Range(0f, 1f) < 0.5f) { List<ItemData> list = (from i in __instance.GetAllItems() where i.m_shared.m_name == item.m_shared.m_name orderby i.m_stack select i).ToList(); if (list.Count > 0) { ItemData obj = list.First(); obj.m_stack += amount; ((Character)val).Message((MessageType)1, "Magic Shot! Arrow not consumed", 0, (Sprite)null); } } } catch (Exception ex) { Logger.LogError((object)("Error in Inventory_RemoveItem_MagicShot_PreFix: " + ex.Message)); } } private static bool IsArrowItem(ItemData item) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 if (item?.m_shared == null) { return false; } if ((int)item.m_shared.m_itemType != 9 && !item.m_shared.m_name.ToLower().Contains("arrow")) { return item.m_shared.m_name.ToLower().Contains("bolt"); } return true; } private static Player FindPlayerWithInventory(Inventory inventory) { return ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((Player p) => ((Humanoid)p).GetInventory() == inventory)); } } [HarmonyPatch] public static class AssassinPerkManager { public class PoisonData { public int stacks; public float endTime; public float lastTickTime; public float damagePerTick; public float totalDamage; public Player source; public float originalSpeed; public float originalRunSpeed; } public static Dictionary<Character, PoisonData> poisonTracking = new Dictionary<Character, PoisonData>(); private static Dictionary<Player, float> assassinationBuffs = new Dictionary<Player, float>(); public const int MAX_POISON_STACKS = 3; public const float POISON_DURATION = 10f; public const float ASSASSINATION_SPEED_DURATION = 5f; public const float ASSASSINATION_SPEED_BONUS = 2f; public const float ENVENOMOUS_SLOW_PER_STACK = 0.2f; public const float TWIST_KNIFE_DAMAGE_BONUS = 0.25f; public const float TWIST_KNIFE_DAMAGE_REDUCTION = 0.15f; public const float ASSASSIN_BACKSTAB_BONUS = 0.3f; public const float ASSASSIN_CUT_THROAT_BONUS = 0.15f; public const float ASSASSIN_STEALTH_BONUS = 1f; public static bool DAMAGE_MODS_ON = true; private const string Intro = "Stealthy fighters who strike from the shadows with deadly precision."; private const string Outro = "Great for players who prefer tactical, stealthy gameplay and damage over time."; public static readonly List<PerkInfo> Perks = new List<PerkInfo> { new PerkInfo { RequiredLevel = 10, Name = "Cutthroat", Description = "+15% knife damage; +30% backstab multiplier" }, new PerkInfo { RequiredLevel = 20, Name = "Venom Coating", Description = "Knife hits apply stacking Poison (up to 3 stacks) based on skill level" }, new PerkInfo { RequiredLevel = 30, Name = "Envenomous", Description = "Poisons apply 15% movement speed slow per stack" }, new PerkInfo { RequiredLevel = 40, Name = "Assassination", Description = "First knife hit from stealth deals +100% damage" }, new PerkInfo { RequiredLevel = 50, Name = "Twist the Knife", Description = "+25% damage to poisoned targets; poisoned enemies deal -10% damage" } }; public static bool HasAssassinPerk(Player player, int requiredLevel) { if ((Object)(object)player == (Object)null) { return false; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null || !playerData.IsClassActive(PlayerClass.Assassin)) { return false; } return playerData.GetClassLevel(PlayerClass.Assassin) >= requiredLevel; } public static string GetClassDescription(Player player) { int currentLevel = PlayerClassManager.GetPlayerData(player)?.GetClassLevel(PlayerClass.Assassin) ?? 0; return PerkDescriptionBuilder.Build("Stealthy fighters who strike from the shadows with deadly precision.", Perks, "Great for players who prefer tactical, stealthy gameplay and damage over time.", currentLevel); } public static void ApplyLv20_VenomCoating(Player player, Character target, HitData hit) { if (HasAssassinPerk(player, 20) && !((Object)(object)target == (Object)null) && !target.IsDead()) { float skillLevel = ((Character)player).GetSkillLevel((SkillType)2); float num = skillLevel / 10f; if (!poisonTracking.ContainsKey(target)) { poisonTracking[target] = new PoisonData(); } PoisonData poisonData = poisonTracking[target]; if (poisonData.stacks < 3) { poisonData.stacks++; AddPoisonStatusEffect(target, poisonData.stacks); ((Character)player).Message((MessageType)1, $"Venom Applied! {poisonData.stacks}/{3} stacks", 0, (Sprite)null); } poisonData.endTime = Time.time + 10f; poisonData.damagePerTick = num * (float)poisonData.stacks; poisonData.source = player; poisonData.totalDamage = skillLevel * (float)poisonData.stacks; target.GetSEMan().GetStatusEffect(StringExtensionMethods.GetStableHashCode("Poison")); float num2 = 1f - (float)poisonData.stacks * 0.2f; poisonData.originalSpeed = target.m_speed; target.m_speed *= num2; poisonData.originalRunSpeed = target.m_runSpeed; target.m_runSpeed *= num2; } } private static void AddPoisonStatusEffect(Character target, int stacks) { try { SEMan sEMan = target.GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_AssassinPoison"), true); SE_Stats val = ScriptableObject.CreateInstance<SE_Stats>(); ((Object)val).name = "SE_AssassinPoison"; ((StatusEffect)val).m_name = $"Venom ({stacks} stacks)"; ((StatusEffect)val).m_tooltip = $"Taking poison damage over time. {stacks}/{3} stacks"; Sprite poisonIcon = GetPoisonIcon(); if ((Object)(object)poisonIcon != (Object)null) { ((StatusEffect)val).m_icon = poisonIcon; } ((StatusEffect)val).m_ttl = 10f; sEMan.AddStatusEffect((StatusEffect)(object)val, true, 0, 0f); } } catch (Exception ex) { Logger.LogError((object)("Error adding poison status effect: " + ex.Message)); } } private static Sprite GetPoisonIcon() { try { ObjectDB instance = ObjectDB.instance; StatusEffect val = ((instance != null) ? instance.GetStatusEffect(StringExtensionMethods.GetStableHashCode("Poison")) : null); if ((Object)(object)val != (Object)null) { return val.m_icon; } return null; } catch { return null; } } public static float ApplyLv30_EnvenomousMovementSlow(Character target, float baseSpeed) { if ((Object)(object)target == (Object)null || !poisonTracking.ContainsKey(target)) { return baseSpeed; } PoisonData poisonData = poisonTracking[target]; if ((Object)(object)poisonData.source == (Object)null || !HasAssassinPerk(poisonData.source, 30)) { return baseSpeed; } float num = 1f - (float)poisonData.stacks * 0.2f; num = Mathf.Max(num, 0.25f); return baseSpeed * num; } private static void ApplyAssassinationSpeedBuff(Character c, Player player) { player.GetPlayerID(); float value = Time.time + 5f; assassinationBuffs[player] = value; AnimationSpeedManager.Set((Character)(object)player, "Assassin_Knife_AS", 2f); AddAssassinationStatusEffect(player); } private static void AddAssassinationStatusEffect(Player player) { try { SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_AssassinationSpeed"), true); SE_Stats val = ScriptableObject.CreateInstance<SE_Stats>(); ((Object)val).name = "SE_AssassinationSpeed"; ((StatusEffect)val).m_name = "Assassination Speed"; ((StatusEffect)val).m_tooltip = "+25% attack speed"; ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon(); ((StatusEffect)val).m_icon = ((currentWeapon != null) ? currentWeapon.GetIcon() : null); ((StatusEffect)val).m_ttl = 5f; sEMan.AddStatusEffect((StatusEffect)(object)val, true, 0, 0f); } } catch (Exception ex) { Logger.LogError((object)("Error adding assassination status effect: " + ex.Message)); } } public static void ProcessPoisonDamage() { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) float time = Time.time; List<Character> list = new List<Character>(); foreach (KeyValuePair<Character, PoisonData> item in poisonTracking) { Character key = item.Key; PoisonData value = item.Value; if ((Object)(object)key == (Object)null || key.IsDead() || time > value.endTime) { ClearSpeedDebuff(key, value.originalSpeed, value.originalRunSpeed); list.Add(key); } else if (time - value.lastTickTime >= 1f) { value.lastTickTime = time; HitData val = new HitData(); val.m_damage.m_poison = value.damagePerTick; Player source = value.source; val.m_attacker = ((source != null) ? ((Character)source).GetZDOID() : ZDOID.None); val.m_point = ((Component)key).transform.position; key.Damage(val); } } foreach (Character item2 in list) { if ((Object)(object)item2 != (Object)null) { SEMan sEMan = item2.GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_AssassinPoison"), true); } } poisonTracking.Remove(item2); } } public static void ClearSpeedDebuff(Character target, float originalSpeed, float originalRunSpeed) { if (!target.IsDead()) { target.m_speed = originalSpeed; target.m_runSpeed = originalRunSpeed; } } public static bool IsStealthHit(Player attacker, Character target) { if ((Object)(object)attacker == (Object)null || (Object)(object)target == (Object)null) { return false; } BaseAI baseAI = target.GetBaseAI(); if ((Object)(object)baseAI == (Object)null) { return false; } if (baseAI.IsAlerted()) { return false; } if (baseAI.HaveTarget()) { return false; } return true; } public static bool IsBackstab(Vector3 hitPoint, Character target) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)target == (Object)null) { return false; } Vector3 val = hitPoint - ((Component)target).transform.position; Vector3 normalized = ((Vector3)(ref val)).normalized; return Vector3.Angle(((Component)target).transform.forward, normalized) > 120f; } public static HitData ModDamage(HitData hit, float mod) { if (hit == null) { return hit; } hit.m_damage.m_damage *= mod; hit.m_damage.m_slash *= mod; hit.m_damage.m_pierce *= mod; hit.m_damage.m_blunt *= mod; hit.m_damage.m_fire *= mod; hit.m_damage.m_frost *= mod; hit.m_damage.m_spirit *= mod; hit.m_damage.m_poison *= mod; return hit; } public static void CleanupBuffs() { float currentTime = Time.time; foreach (Player item in (from kvp in assassinationBuffs where kvp.Value < currentTime select kvp.Key).ToList()) { assassinationBuffs.Remove(item); if ((Object)(object)item != (Object)null) { SEMan sEMan = ((Character)item).GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_AssassinationSpeed"), true); } AnimationSpeedManager.Clear((Character)(object)item, "Assassin_Knife_AS"); } } } [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] public static void Character_Damage_Assassin_Prefix(Character __instance, ref HitData hit) { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) try { PlayerClassData playerData = PlayerClassManager.GetPlayerData(Player.m_localPlayer); if (!playerData.IsClassActive("Assassin") || hit.m_damage.m_poison > 0f) { return; } Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val == null || (Object)(object)__instance == (Object)null || __instance is Player) { Character attacker2 = hit.GetAttacker(); if (poisonTracking.ContainsKey(attacker2)) { float mod = 0.15f; hit = ModDamage(hit, mod); } } else if (ClassCombatManager.IsKnifeWeapon(((Humanoid)val).GetCurrentWeapon()) && playerData != null && playerData.IsClassActive(PlayerClass.Assassin)) { float num = 0f; if (HasAssassinPerk(val, 10)) { num += 0.15f; } if (IsBackstab(hit.m_point, __instance) && HasAssassinPerk(val, 10)) { num += 0.3f; } if (IsStealthHit(val, __instance) && HasAssassinPerk(val, 40)) { num += 1f; ApplyAssassinationSpeedBuff(__instance, val); } if (HasAssassinPerk(val, 50)) { num += 0.25f; } _ = hit.m_damage; _ = hit.m_damage; if (DAMAGE_MODS_ON) { hit = ModDamage(hit, num + 1f); } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Assassin_Prefix: " + ex.Message)); } } [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPostfix] public static void Character_Damage_Assassin_Postfix(Character __instance, HitData hit) { try { if (hit.m_damage.m_poison > 0f) { return; } Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val != null && !((Object)(object)__instance == (Object)null) && !(__instance is Player) && !(hit.GetTotalDamage() <= 0f) && ClassCombatManager.IsKnifeWeapon(((Humanoid)val).GetCurrentWeapon()) && (hit.m_damage.m_blunt != 0f || hit.m_damage.m_slash != 0f || hit.m_damage.m_pierce != 0f)) { PlayerClassData playerData = PlayerClassManager.GetPlayerData(val); if (playerData != null && playerData.IsClassActive(PlayerClass.Assassin)) { ApplyLv20_VenomCoating(val, __instance, hit); } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Assassin_Postfix: " + ex.Message)); } } [HarmonyPatch(typeof(Game), "Update")] [HarmonyPostfix] public static void Game_Update_Assassin_Postfix() { try { ProcessPoisonDamage(); if (Time.time % 1f < Time.deltaTime) { CleanupBuffs(); } } catch (Exception ex) { Logger.LogError((object)("Error in Game_Update_Assassin_Postfix: " + ex.Message)); } } } public static class ClassCombatManager { public static bool IsSwordWeapon(ItemData weapon) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Invalid comparison between Unknown and I4 if ((weapon == null || (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() != 3) && (weapon == null || (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() != 14)) { return false; } string text = weapon.m_shared.m_name?.ToLower() ?? ""; if (!text.Contains("sword") && !text.Contains("blade") && !text.Contains("saber")) { return text.Contains("katana"); } return true; } public static bool IsBowWeapon(ItemData weapon) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 if (weapon == null) { return false; } return (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() == 4; } public static bool IsBluntWeapon(ItemData weapon) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Invalid comparison between Unknown and I4 if ((weapon == null || (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() != 3) && (weapon == null || (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() != 14)) { return false; } string text = weapon.m_shared.m_name?.ToLower() ?? ""; if (!text.Contains("mace") && !text.Contains("hammer") && !text.Contains("club")) { return text.Contains("sledge"); } return true; } public static bool IsKnifeWeapon(ItemData weapon) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 if (weapon == null || (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() != 3) { return false; } string text = weapon.m_shared.m_name?.ToLower() ?? ""; if (!text.Contains("knife") && !text.Contains("dagger") && !text.Contains("seax")) { return text.Contains("razor"); } return true; } public static bool IsSpearWeapon(ItemData weapon) { string name = weapon.m_shared.m_name; if (name == null || !name.ToLower().Contains("spear")) { string name2 = weapon.m_shared.m_name; if (name2 == null || !name2.ToLower().Contains("atgeir")) { return weapon.m_shared.m_name?.ToLower().Contains("halberd") ?? false; } } return true; } public static bool IsUnarmedAttack(ItemData weapon) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (weapon != null && (int)weapon.m_shared.m_itemType != 0) { string name = weapon.m_shared.m_name; if (name == null || !name.ToLower().Contains("claws")) { return weapon.m_shared.m_name == "Unarmed"; } } return true; } public static bool IsMagicWeapon(ItemData weapon) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Invalid comparison between Unknown and I4 if (weapon != null && (int)(weapon.m_shared?.m_itemType).GetValueOrDefault() == 14) { string name = weapon.m_shared.m_name; if (name == null || !name.ToLower().Contains("staff")) { return (Object)(object)weapon.m_shared.m_attackStatusEffect != (Object)null; } return true; } return false; } public static float GetClassDamageMultiplier(Player player, ItemData weapon) { if ((Object)(object)player == (Object)null) { return 1f; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null || playerData.activeClasses.Count == 0) { return 1f; } float num = 1f; foreach (string activeClass in playerData.activeClasses) { float classSpecificDamageMultiplier = GetClassSpecificDamageMultiplier(activeClass, playerData, weapon); if (classSpecificDamageMultiplier > 1f) { num += classSpecificDamageMultiplier - 1f; } } return num; } private static float GetClassSpecificDamageMultiplier(string className, PlayerClassData playerData, ItemData weapon) { int classLevel = playerData.GetClassLevel(className); return className switch { "Sword Master" => GetSwordMasterDamageBonus(classLevel, weapon), "Archer" => GetArcherDamageBonus(classLevel, weapon), "Crusher" => GetCrusherDamageBonus(classLevel, weapon), "Assassin" => GetAssassinDamageBonus(classLevel, weapon), "Brawler" => GetBrawlerDamageBonus(classLevel, weapon), "Wizard" => GetWizardDamageBonus(classLevel, weapon), "Lancer" => GetLancerDamageBonus(classLevel, weapon), "Bulwark" => GetBulwarkDamageBonus(classLevel, weapon), _ => 1f, }; } private static float GetSwordMasterDamageBonus(int level, ItemData weapon) { if (!IsSwordWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.1f; } return 1f + num; } private static float GetArcherDamageBonus(int level, ItemData weapon) { if (!IsBowWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 50) { num += 0.2f; } return 1f + num; } private static float GetCrusherDamageBonus(int level, ItemData weapon) { if (!IsBluntWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.12f; } return 1f + num; } private static float GetAssassinDamageBonus(int level, ItemData weapon) { if (!IsKnifeWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.12f; } return 1f + num; } private static float GetBrawlerDamageBonus(int level, ItemData weapon) { if (!IsUnarmedAttack(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.15f; } return 1f + num; } private static float GetWizardDamageBonus(int level, ItemData weapon) { if (!IsMagicWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.08f; } return 1f + num; } private static float GetLancerDamageBonus(int level, ItemData weapon) { if (!IsSpearWeapon(weapon)) { return 1f; } float num = 0f; num += (float)level * 0.005f; if (level >= 10) { num += 0.1f; } return 1f + num; } private static float GetBulwarkDamageBonus(int level, ItemData weapon) { return 1f; } public static void ShowDamageBonusMessage(Player player, float multiplier, string weaponType) { if (multiplier > 1f && (Object)(object)player == (Object)(object)Player.m_localPlayer) { float num = (multiplier - 1f) * 100f; if (Random.Range(0f, 1f) < 0.1f) { ((Character)player).Message((MessageType)1, $"{weaponType} Mastery: +{num:F0}% damage!", 0, (Sprite)null); } } } public static float GetClassSpecificDamageMultiplierPublic(string className, PlayerClassData playerData, ItemData weapon) { int classLevel = playerData.GetClassLevel(className); return className switch { "Sword Master" => GetSwordMasterDamageBonus(classLevel, weapon), "Archer" => GetArcherDamageBonus(classLevel, weapon), "Crusher" => GetCrusherDamageBonus(classLevel, weapon), "Assassin" => GetAssassinDamageBonus(classLevel, weapon), "Brawler" => GetBrawlerDamageBonus(classLevel, weapon), "Wizard" => GetWizardDamageBonus(classLevel, weapon), "Lancer" => GetLancerDamageBonus(classLevel, weapon), "Bulwark" => GetBulwarkDamageBonus(classLevel, weapon), _ => 1f, }; } } [HarmonyPatch] public static class CombatPatches { [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] public static void Character_Damage_Prefix(Character __instance, ref HitData hit) { try { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val != null) { ItemData currentWeapon = ((Humanoid)val).GetCurrentWeapon(); float classDamageMultiplier = ClassCombatManager.GetClassDamageMultiplier(val, currentWeapon); if (classDamageMultiplier > 1f) { hit.m_damage.m_slash *= classDamageMultiplier; hit.m_damage.m_pierce *= classDamageMultiplier; hit.m_damage.m_blunt *= classDamageMultiplier; hit.m_damage.m_chop *= classDamageMultiplier; string weaponTypeName = GetWeaponTypeName(currentWeapon); ClassCombatManager.ShowDamageBonusMessage(val, classDamageMultiplier, weaponTypeName); } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Prefix: " + ex.Message)); } } private static string GetWeaponTypeName(ItemData weapon) { if (ClassCombatManager.IsSwordWeapon(weapon)) { return "Sword"; } if (ClassCombatManager.IsBowWeapon(weapon)) { return "Bow"; } if (ClassCombatManager.IsBluntWeapon(weapon)) { return "Blunt"; } if (ClassCombatManager.IsKnifeWeapon(weapon)) { return "Knife"; } if (ClassCombatManager.IsSpearWeapon(weapon)) { return "Spear"; } if (ClassCombatManager.IsUnarmedAttack(weapon)) { return "Unarmed"; } if (ClassCombatManager.IsMagicWeapon(weapon)) { return "Magic"; } return "Weapon"; } } [HarmonyPatch] public static class ProjectilePatches { private class ProjectileInfo { public Player owner; public ItemData weapon; public float createdTime; public string projectileName; } private static Dictionary<int, ProjectileInfo> trackedProjectiles = new Dictionary<int, ProjectileInfo>(); [HarmonyPatch(typeof(Projectile), "Setup")] [HarmonyPostfix] public static void Projectile_Setup_Postfix(Projectile __instance, Character owner, Vector3 velocity, float hitNoise, HitData hitData, ItemData item) { try { Player val = (Player)(object)((owner is Player) ? owner : null); if (val != null && (Object)(object)__instance != (Object)null) { ItemData weaponFromProjectileContext = GetWeaponFromProjectileContext(val, __instance, item); ProjectileInfo value = new ProjectileInfo { owner = val, weapon = weaponFromProjectileContext, createdTime = Time.time, projectileName = ((Object)__instance).name }; trackedProjectiles[((Object)__instance).GetInstanceID()] = value; Debug.Log((object)("Registered projectile " + ((Object)__instance).name + " for " + val.GetPlayerName() + " with weapon type " + (weaponFromProjectileContext?.m_shared?.m_name ?? "unknown"))); } } catch (Exception ex) { Logger.LogError((object)("Error in Projectile_Setup_Postfix: " + ex.Message)); } } [HarmonyPatch(typeof(Projectile), "Awake")] [HarmonyPostfix] public static void Projectile_Awake_Postfix(Projectile __instance) { try { if ((Object)(object)__instance != (Object)null && !trackedProjectiles.ContainsKey(((Object)__instance).GetInstanceID())) { ProjectileInfo value = new ProjectileInfo { owner = null, weapon = null, createdTime = Time.time, projectileName = ((Object)__instance).name }; trackedProjectiles[((Object)__instance).GetInstanceID()] = value; } } catch (Exception ex) { Logger.LogError((object)("Error in Projectile_Awake_Postfix: " + ex.Message)); } } private static ItemData GetWeaponFromProjectileContext(Player player, Projectile projectile, ItemData item) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Expected O, but got Unknown //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown if (item != null) { return item; } if ((Object)(object)player == (Object)null || (Object)(object)projectile == (Object)null) { return null; } string text = ((Object)projectile).name.ToLower(); if (text.Contains("arrow")) { return ((IEnumerable<ItemData>)((Humanoid)player).GetInventory().GetAllItems()).FirstOrDefault((Func<ItemData, bool>)((ItemData i) => (int)i.m_shared.m_itemType == 4)); } if (text.Contains("spear") || text.Contains("harpoon")) { return new ItemData { m_shared = new SharedData(), m_shared = { m_itemType = (ItemType)14 }, m_shared = { m_name = "spear" } }; } return ((Humanoid)player).GetCurrentWeapon(); } private static Player ResolveProjectileOwner(Projectile projectile) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) try { ZNetView component = ((Component)projectile).GetComponent<ZNetView>(); if ((Object)(object)component != (Object)null && component.IsValid()) { long ownerID = component.GetZDO().GetLong("owner", 0L); if (ownerID != 0L) { return ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((Player p) => p.GetPlayerID() == ownerID)); } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && Vector3.Distance(((Component)localPlayer).transform.position, ((Component)projectile).transform.position) < 50f) { return localPlayer; } return null; } catch { return null; } } [HarmonyPatch(typeof(Game), "Update")] [HarmonyPostfix] public static void Game_Update_Postfix() { try { if (Time.frameCount % 300 != 0) { return; } List<int> list = new List<int>(); float time = Time.time; foreach (KeyValuePair<int, ProjectileInfo> trackedProjectile in trackedProjectiles) { if (time - trackedProjectile.Value.createdTime > 30f) { list.Add(trackedProjectile.Key); } } foreach (int item in list) { trackedProjectiles.Remove(item); } } catch (Exception ex) { Logger.LogError((object)("Error in Game_Update_Postfix (projectile cleanup): " + ex.Message)); } } } public static class ClassDamageBonusManager { public static float DamageBonusPerLevel = 0.005f; public static float MaxDamageBonus = 0.25f; public static float CalculateDamageBonus(Player attacker, ItemData weapon) { if ((Object)(object)attacker == (Object)null || weapon == null) { return 1f; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(attacker); if (playerData == null || playerData.activeClasses.Count == 0) { return 1f; } float num = 1f; bool flag = false; foreach (string activeClass in playerData.activeClasses) { if (IsWeaponAppropriateForClass(weapon, activeClass)) { float num2 = (float)playerData.GetClassLevel(activeClass) * DamageBonusPerLevel; num2 = Mathf.Min(num2, MaxDamageBonus); num += num2; flag = true; } } return num; } private static bool IsWeaponAppropriateForClass(ItemData weapon, string className) { return className switch { "Sword Master" => ClassCombatManager.IsSwordWeapon(weapon), "Archer" => ClassCombatManager.IsBowWeapon(weapon), "Crusher" => ClassCombatManager.IsBluntWeapon(weapon), "Assassin" => ClassCombatManager.IsKnifeWeapon(weapon), "Brawler" => ClassCombatManager.IsUnarmedAttack(weapon), "Wizard" => ClassCombatManager.IsMagicWeapon(weapon), "Lancer" => ClassCombatManager.IsSpearWeapon(weapon), "Bulwark" => false, _ => false, }; } public static string GetDamageBonusInfo(Player player, ItemData weapon) { if ((Object)(object)player == (Object)null || weapon == null) { return "No weapon equipped"; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null || playerData.activeClasses.Count == 0) { return "No active classes"; } StringBuilder stringBuilder = new StringBuilder(); float num = 0f; bool flag = false; foreach (string activeClass in playerData.activeClasses) { if (IsWeaponAppropriateForClass(weapon, activeClass)) { int classLevel = playerData.GetClassLevel(activeClass); float num2 = (float)classLevel * DamageBonusPerLevel; num2 = Mathf.Min(num2, MaxDamageBonus); stringBuilder.AppendLine($"{activeClass} Lv.{classLevel}: +{num2 * 100f:F1}%"); num += num2; flag = true; } } if (!flag) { return "No active classes match " + weapon.m_shared.m_name; } stringBuilder.AppendLine($"Total Bonus: +{num * 100f:F1}%"); stringBuilder.AppendLine($"Damage Multiplier: {1f + num:F3}x"); return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } } [HarmonyPatch] public static class DamageBonusPatches { [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] public static void Character_Damage_Prefix(Character __instance, ref HitData hit) { try { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val == null || __instance is Player) { return; } ItemData currentWeapon = ((Humanoid)val).GetCurrentWeapon(); if (currentWeapon != null) { float num = ClassDamageBonusManager.CalculateDamageBonus(val, currentWeapon); if (num > 1f) { hit.m_damage.m_damage *= num; hit.m_damage.m_blunt *= num; hit.m_damage.m_slash *= num; hit.m_damage.m_pierce *= num; hit.m_damage.m_chop *= num; hit.m_damage.m_pickaxe *= num; hit.m_damage.m_fire *= num; hit.m_damage.m_frost *= num; hit.m_damage.m_lightning *= num; hit.m_damage.m_poison *= num; hit.m_damage.m_spirit *= num; } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Prefix (Damage Bonus): " + ex.Message)); } } } [BepInPlugin("com.bunzboi.valheimweaponclass", "ValheimWeaponClasses", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] internal class ClassObeliskMod : BaseUnityPlugin { public const string PluginGUID = "com.bunzboi.valheimweaponclass"; public const string PluginName = "ValheimWeaponClasses"; public const string PluginVersion = "1.0.0"; private GameObject TestPanel; private static ClassObeliskMod _instance; private Harmony _harmony; private ConfigEntry<bool> _enableMod; public static ClassObeliskMod Instance => _instance; private void Awake() { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown _instance = this; _enableMod = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableMod", true, "Enable the class obelisk mod"); if (_enableMod.Value) { _harmony = new Harmony("com.bunzboi.valheimweaponclass"); _harmony.PatchAll(); PrefabManager.OnVanillaPrefabsAvailable += AddClassObelisk; ((BaseUnityPlugin)this).Logger.LogInfo((object)"ValheimWeaponClasses loaded successfully!"); } } private void AddClassObelisk() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown try { GameObject val = CreateClassObeliskPrefab(); if ((Object)(object)val != (Object)null) { PieceConfig val2 = new PieceConfig(); val2.Name = "$class_obelisk"; val2.PieceTable = "Hammer"; val2.Category = "Crafting"; val2.AddRequirement("Wood", 2, true); CustomPiece val3 = new CustomPiece(val, true, val2); PieceManager.Instance.AddPiece(val3); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Class Obelisk piece added successfully!"); PrefabManager.OnVanillaPrefabsAvailable -= AddClassObelisk; } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Error adding Class Obelisk: " + ex.Message)); } } private GameObject CreateClassObeliskPrefab() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) try { GameObject prefab = PrefabManager.Instance.GetPrefab("piece_workbench"); if ((Object)(object)prefab == (Object)null) { ((BaseUnityPlugin)this).Logger.LogError((object)"Could not find workbench prefab!"); return null; } GameObject obj = PrefabManager.Instance.CreateClonedPrefab("ClassObelisk", prefab); new PieceConfig { Name = "$class_obelisk", PieceTable = "Hammer", Category = "Crafting" }.AddRequirement("Wood", 2, true); CraftingStation component = obj.GetComponent<CraftingStation>(); if ((Object)(object)component != (Object)null) { Object.DestroyImmediate((Object)(object)component); } obj.AddComponent<ClassObeliskInteract>(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Class Obelisk prefab created successfully!"); return obj; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Error creating Class Obelisk prefab: " + ex.Message)); return null; } } } public class ClassObeliskInteract : MonoBehaviour, Hoverable, Interactable { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static UnityAction <>9__20_0; internal void <CreateEnhancedCloseButton>b__20_0() { CloseClassSelectionGUI(); } } public static GameObject classSelectionPanel; public static Text descriptionText; public static ScrollRect descriptionScrollRect; public static GameObject selectClassButton; public static string selectedClassName = ""; public static readonly string[] ClassNames = new string[8] { "Sword Master", "Archer", "Crusher", "Assassin", "Brawler", "Wizard", "Lancer", "Bulwark" }; private static readonly Dictionary<string, Func<Player, string>> DynamicClassDescriptionProviders = new Dictionary<string, Func<Player, string>> { { "Sword Master", SwordMasterPerkManager.GetClassDescription }, { "Archer", ArcherPerkManager.GetClassDescription }, { "Crusher", CrusherPerkManager.GetClassDescription }, { "Assassin", AssassinPerkManager.GetClassDescription }, { "Brawler", BrawlerPerkManager.GetClassDescription }, { "Wizard", WizardPerkManager.GetClassDescription }, { "Lancer", LancerPerkManager.GetClassDescription }, { "Bulwark", BulwarkPerkManager.GetClassDescription } }; private void Start() { } private void Update() { if ((Object)(object)classSelectionPanel != (Object)null && classSelectionPanel.activeSelf && ZInput.instance != null && (ZInput.GetButtonDown("Escape") || ZInput.GetButtonDown("JoyMenu"))) { CloseClassSelectionGUI(); } } public string GetHoverText() { return "Class Obelisk\n[<color=yellow>E</color>] Select your combat class"; } public string GetHoverName() { return "Class Obelisk"; } public bool Interact(Humanoid user, bool hold, bool alt) { if (hold || alt) { return false; } Player val = (Player)(object)((user is Player) ? user : null); if ((Object)(object)val == (Object)null) { return false; } OpenClassSelectionGUI(val); return true; } public bool UseItem(Humanoid user, ItemData item) { return false; } private void OpenClassSelectionGUI(Player player) { CloseClassSelectionGUI(); CreateEnhancedClassSelectionGUI(player); } private void CreateEnhancedClassSelectionGUI(Player player) { try { PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null) { Debug.LogError((object)"Could not get player data for GUI creation"); return; } CreateWoodenGUIPanel(); CreateTwoColumnClassButtons(player); CreateDescriptionWindow(player); CreateSelectClassButton(player); CreateEnhancedCloseButton(); string text = ((playerData.activeClasses.Count > 0) ? string.Join(", ", playerData.activeClasses) : "None"); UpdateDescriptionText("Current Active Classes: " + text + "\n\nClick on a class above to see its description and benefits."); selectedClassName = ""; UpdateSelectButton(); } catch (Exception ex) { Debug.LogError((object)("Error creating enhanced GUI: " + ex.Message)); } } private void CreateWoodenGUIPanel() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)classSelectionPanel)) { if (GUIManager.Instance == null) { Logger.LogError((object)"GUIManager instance is null"); return; } if (!Object.op_Implicit((Object)(object)GUIManager.CustomGUIFront)) { Logger.LogError((object)"GUIManager CustomGUI is null"); return; } classSelectionPanel = GUIManager.Instance.CreateWoodpanel(GUIManager.CustomGUIFront.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 0f), 1450f, 900f, false); classSelectionPanel.SetActive(false); classSelectionPanel.AddComponent<DragWindowCntrl>(); GUIManager.Instance.CreateText("Choose a class!", classSelectionPanel.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -50f), GUIManager.Instance.AveriaSerifBold, 30, GUIManager.Instance.ValheimOrange, true, Color.black, 400f, 50f, false).GetComponent<Text>().alignment = (TextAnchor)4; } bool flag = !classSelectionPanel.activeSelf; classSelectionPanel.SetActive(flag); GUIManager.BlockInput(flag); } private void CreateTwoColumnClassButtons(Player player) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("ButtonContainer"); val.transform.SetParent(classSelectionPanel.transform, false); RectTransform obj = val.AddComponent<RectTransform>(); obj.anchorMin = new Vector2(0.05f, 0.65f); obj.anchorMax = new Vector2(0.95f, 0.85f); obj.offsetMin = Vector2.zero; obj.offsetMax = Vector2.zero; float num = 180f; float num2 = 50f; float num3 = 25f; float num4 = 15f; float num5 = (0f - (num * 4f + num3 * 3f)) / 2f + num / 2f; float num6 = 25f; for (int i = 0; i < ClassNames.Length; i++) { int num7 = i % 4; int num8 = i / 4; float x = num5 + (float)num7 * (num + num3); float y = num6 - (float)num8 * (num2 + num4); CreateEnhancedClassButton(val, ClassNames[i], player, x, y, num, num2); } } private void CreateEnhancedClassButton(GameObject parent, string className, Player player, float x, float y, float width, float height) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown int classLevel = PlayerClassManager.GetPlayerData(player).GetClassLevel(className); string text = $"{className} ({classLevel})"; GameObject obj = GUIManager.Instance.CreateButton(text, parent.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(x, y), (float)(int)width, (float)(int)height); ((UnityEvent)obj.GetComponent<Button>().onClick).AddListener((UnityAction)delegate { OnClassButtonClicked(className, player); }); obj.AddComponent<ButtonHoverEffect>(); } private void CreateDescriptionWindow(Player player) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01c2: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("DescriptionContainer"); val.transform.SetParent(classSelectionPanel.transform, false); RectTransform obj = val.AddComponent<RectTransform>(); obj.anchorMin = new Vector2(0.15f, 0.2f); obj.anchorMax = new Vector2(0.85f, 0.6f); obj.offsetMin = Vector2.zero; obj.offsetMax = Vector2.zero; ((Graphic)val.AddComponent<Image>()).color = new Color(0.1f, 0.1f, 0.1f, 0.8f); GameObject val2 = new GameObject("Viewport"); val2.transform.SetParent(val.transform, false); RectTransform val3 = val2.AddComponent<RectTransform>(); val3.anchorMin = Vector2.zero; val3.anchorMax = Vector2.one; val3.offsetMin = new Vector2(15f, 15f); val3.offsetMax = new Vector2(-15f, -15f); val2.AddComponent<RectMask2D>(); GameObject obj2 = GUIManager.Instance.CreateText("Select a class to view its description...", val2.transform, Vector2.zero, Vector2.one, Vector2.zero, GUIManager.Instance.AveriaSerif, 18, new Color(0.9f, 0.9f, 0.9f, 1f), false, Color.black, 0f, 0f, false); descriptionText = obj2.GetComponent<Text>(); descriptionText.alignment = (TextAnchor)0; descriptionText.horizontalOverflow = (HorizontalWrapMode)0; descriptionText.verticalOverflow = (VerticalWrapMode)1; RectTransform component = obj2.GetComponent<RectTransform>(); component.anchorMin = new Vector2(0f, 1f); component.anchorMax = new Vector2(1f, 1f); component.pivot = new Vector2(0.5f, 1f); component.anchoredPosition = Vector2.zero; component.sizeDelta = Vector2.zero; ContentSizeFitter obj3 = obj2.AddComponent<ContentSizeFitter>(); obj3.horizontalFit = (FitMode)0; obj3.verticalFit = (FitMode)2; descriptionScrollRect = val.AddComponent<ScrollRect>(); descriptionScrollRect.viewport = val3; descriptionScrollRect.content = component; descriptionScrollRect.horizontal = false; descriptionScrollRect.vertical = true; descriptionScrollRect.movementType = (MovementType)2; descriptionScrollRect.scrollSensitivity = 20f; } private void CreateSelectClassButton(Player player) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown selectClassButton = GUIManager.Instance.CreateButton("Select Class", classSelectionPanel.transform, new Vector2(0.5f, 0.08f), new Vector2(0.5f, 0.08f), new Vector2(0f, 20f), 200f, 35f); ((UnityEvent)selectClassButton.GetComponent<Button>().onClick).AddListener((UnityAction)delegate { if (!string.IsNullOrEmpty(selectedClassName)) { OnClassSelected(selectedClassName, player); } }); UpdateSelectButton(); } private void CreateEnhancedCloseButton() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown GameObject obj = GUIManager.Instance.CreateButton("X", classSelectionPanel.transform, new Vector2(0.92f, 0.92f), new Vector2(0.98f, 0.98f), Vector2.zero, 35f, 35f); Button component = obj.GetComponent<Button>(); Image component2 = obj.GetComponent<Image>(); if ((Object)(object)component2 != (Object)null) { ((Graphic)component2).color = new Color(0.8f, 0.3f, 0.3f, 0.9f); ColorBlock colors = ((Selectable)component).colors; ((ColorBlock)(ref colors)).normalColor = new Color(0.8f, 0.3f, 0.3f, 0.9f); ((ColorBlock)(ref colors)).highlightedColor = new Color(0.9f, 0.4f, 0.4f, 1f); ((ColorBlock)(ref colors)).pressedColor = new Color(0.7f, 0.2f, 0.2f, 1f); ((Selectable)component).colors = colors; } Text componentInChildren = obj.GetComponentInChildren<Text>(); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.fontSize = 18; componentInChildren.fontStyle = (FontStyle)1; ((Graphic)componentInChildren).color = Color.white; } ButtonClickedEvent onClick = component.onClick; object obj2 = <>c.<>9__20_0; if (obj2 == null) { UnityAction val = delegate { CloseClassSelectionGUI(); }; <>c.<>9__20_0 = val; obj2 = (object)val; } ((UnityEvent)onClick).AddListener((UnityAction)obj2); } private void OnClassButtonClicked(string className, Player player) { selectedClassName = className; Func<Player, string> value; string text = (DynamicClassDescriptionProviders.TryGetValue(className, out value) ? value(player) : ("Description for " + className + " coming soon...")); PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData != null && playerData.IsClassActive(className)) { text += "\n\n\n<size=24><b>Active</b></size>"; } UpdateDescriptionText(text); UpdateSelectButton(); } private void UpdateDescriptionText(string description) { if ((Object)(object)descriptionText != (Object)null) { descriptionText.text = description; descriptionText.horizontalOverflow = (HorizontalWrapMode)0; descriptionText.verticalOverflow = (VerticalWrapMode)1; LayoutRebuilder.ForceRebuildLayoutImmediate(((Graphic)descriptionText).rectTransform); Canvas.ForceUpdateCanvases(); if ((Object)(object)descriptionScrollRect != (Object)null) { descriptionScrollRect.verticalNormalizedPosition = 1f; } } else { Debug.LogError((object)"descriptionText is null when trying to update!"); } } private void UpdateSelectButton() { if (!((Object)(object)selectClassButton != (Object)null)) { return; } Button component = selectClassButton.GetComponent<Button>(); Text componentInChildren = selectClassButton.GetComponentInChildren<Text>(); if (string.IsNullOrEmpty(selectedClassName)) { ((Selectable)component).interactable = false; if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.text = "Select a Class"; } } else { ((Selectable)component).interactable = true; if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.text = "Select " + selectedClassName; } } } private void OnClassSelected(string className, Player player) { if (string.IsNullOrEmpty(className)) { Debug.LogError((object)"Cannot select class: className is null or empty"); if (player != null) { ((Character)player).Message((MessageType)2, "Error: Invalid class selection", 0, (Sprite)null); } return; } if (PlayerClassManager.SetPlayerActiveClass(player, className)) { PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); string text = string.Join(", ", playerData.activeClasses); ((Character)player).Message((MessageType)2, "Selected " + className + " Class! Active: " + text, 0, (Sprite)null); } else { ((Character)player).Message((MessageType)2, "Failed to select " + className + " class", 0, (Sprite)null); Debug.LogError((object)("Failed to set active class " + className + " for " + player.GetPlayerName())); } CloseClassSelectionGUI(); } private static void CloseClassSelectionGUI() { if ((Object)(object)classSelectionPanel != (Object)null) { Object.Destroy((Object)(object)classSelectionPanel); classSelectionPanel = null; descriptionText = null; descriptionScrollRect = null; selectClassButton = null; selectedClassName = ""; if (GUIManager.Instance != null) { GUIManager.BlockInput(false); } } } } public class ButtonHoverEffect : MonoBehaviour, IPointerEnterHandler, IEventSystemHandler, IPointerExitHandler { private Vector3 originalScale; private bool isHovering; private void Start() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) originalScale = ((Component)this).transform.localScale; } public void OnPointerEnter(PointerEventData eventData) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (!isHovering) { isHovering = true; ((Component)this).transform.localScale = originalScale * 1.05f; } } public void OnPointerExit(PointerEventData eventData) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (isHovering) { isHovering = false; ((Component)this).transform.localScale = originalScale; } } } public static class CrusherPerkManager { public const float COLD_STEEL_FROST_MULTIPLIER = 0.2f; public const float THUNDERING_BLOWS_RADIUS = 5f; private const string Intro = "Powerful warriors who excel with heavy blunt weapons."; private const string Outro = "Best suited for players who like devastating area attacks and crowd control."; public static readonly List<PerkInfo> Perks = new List<PerkInfo> { new PerkInfo { RequiredLevel = 10, Name = "Bonebreaker", Description = "+15% blunt damage; +25% stagger power" }, new PerkInfo { RequiredLevel = 20, Name = "Cold Steel", Description = "Melee attacks imbued with frost dealing +20% weapon damage as frost" }, new PerkInfo { RequiredLevel = 30, Name = "Thundering Blows", Description = "Heavy melee attacks generate 2m shockwave of lightning damage" }, new PerkInfo { RequiredLevel = 40, Name = "Might of the Earth", Description = "-30% stamina drain on attacks from wielding heavy weapons" }, new PerkInfo { RequiredLevel = 50, Name = "Colossus", Description = "Ignore movement speed penalties from armor weight" } }; public static bool HasCrusherPerk(Player player, int requiredLevel) { if ((Object)(object)player == (Object)null) { return false; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(player); if (playerData == null || !playerData.IsClassActive(PlayerClass.Crusher)) { return false; } return playerData.GetClassLevel(PlayerClass.Crusher) >= requiredLevel; } public static string GetClassDescription(Player player) { int currentLevel = PlayerClassManager.GetPlayerData(player)?.GetClassLevel(PlayerClass.Crusher) ?? 0; return PerkDescriptionBuilder.Build("Powerful warriors who excel with heavy blunt weapons.", Perks, "Best suited for players who like devastating area attacks and crowd control.", currentLevel); } public static float ApplyLv10_BonebreakerDamage(Player player, float baseDamage) { if (!HasCrusherPerk(player, 10)) { return baseDamage; } return baseDamage * 1.15f; } public static float ApplyLv10_BonebreakerStagger(Player player, float baseStagger) { if (!HasCrusherPerk(player, 10)) { return baseStagger; } return baseStagger * 1.25f; } public static void ApplyLv20_ColdSteelFrost(Player player, ref HitData hit, float weaponDamage) { if (HasCrusherPerk(player, 20)) { float num = weaponDamage * 0.2f; hit.m_damage.m_frost += num; if (Random.Range(0f, 1f) < 0.3f) { ((Character)player).Message((MessageType)1, $"Cold Steel! +{num:F0} frost damage", 0, (Sprite)null); } } } public static void TriggerLv30_ThunderingBlows(Player player, Vector3 hitPoint, float baseDamage) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Expected O, but got Unknown //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) if (!HasCrusherPerk(player, 30)) { return; } List<Character> list = (from c in Physics.OverlapSphere(hitPoint, 5f) select ((Component)c).GetComponent<Character>() into c where (Object)(object)c != (Object)null && (Object)(object)c != (Object)(object)player && !c.IsDead() && c.IsMonsterFaction(0f) select c).ToList(); if (list.Count == 0) { return; } float lightning = baseDamage * 0.5f; foreach (Character item in list) { HitData val = new HitData(); val.m_attacker = ((Character)player).GetZDOID(); val.m_damage.m_lightning = lightning; val.m_point = ((Component)item).transform.position; Vector3 val2 = ((Component)item).transform.position - hitPoint; val.m_dir = ((Vector3)(ref val2)).normalized; val.m_skill = (SkillType)3; item.Damage(val); } ((Character)player).Message((MessageType)1, $"Thundering Blow! Hit {list.Count} enemies", 0, (Sprite)null); } private static void CreateThunderingBlowsEffect(Vector3 position) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) try { ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab("lightningAOE") : null); if ((Object)(object)val != (Object)null) { Object.Instantiate<GameObject>(val, position, Quaternion.identity); return; } ZNetScene instance2 = ZNetScene.instance; GameObject val2 = ((instance2 != null) ? instance2.GetPrefab("vfx_HitSparks") : null); if ((Object)(object)val2 != (Object)null) { Object.Instantiate<GameObject>(val2, position, Quaternion.identity); } } catch (Exception ex) { Logger.LogWarning((object)("Could not create Thundering Blows effect: " + ex.Message)); } } public static float ApplyLv40_MightOfTheEarthStamina(Player player, float staminaCost) { if (!HasCrusherPerk(player, 40)) { return staminaCost; } ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon(); if (currentWeapon != null && ClassCombatManager.IsBluntWeapon(currentWeapon)) { return staminaCost * 0.7f; } return staminaCost; } public static bool IsHeavyAttack(AttackType attackType) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 return (int)attackType == 1; } } [HarmonyPatch] public static class CrusherPerkPatches { private static bool triggerThunderingBlowsDamage; [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] public static void Character_Damage_Crusher_Prefix(Character __instance, ref HitData hit) { try { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val == null || (Object)(object)__instance == (Object)null || __instance is Player || !ClassCombatManager.IsBluntWeapon(((Humanoid)val).GetCurrentWeapon())) { return; } PlayerClassData playerData = PlayerClassManager.GetPlayerData(val); if (playerData != null && playerData.IsClassActive(PlayerClass.Crusher)) { float totalDamage = hit.GetTotalDamage(); float blunt = hit.m_damage.m_blunt; if (blunt > 0f) { hit.m_damage.m_blunt = CrusherPerkManager.ApplyLv10_BonebreakerDamage(val, blunt); } hit.m_staggerMultiplier = CrusherPerkManager.ApplyLv10_BonebreakerStagger(val, hit.m_staggerMultiplier); CrusherPerkManager.ApplyLv20_ColdSteelFrost(val, ref hit, totalDamage); } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Crusher_Prefix: " + ex.Message)); } } [HarmonyPatch(typeof(Humanoid), "StartAttack")] [HarmonyPrefix] public static void Humanoid_StartAttack_Prefix(Character target, bool secondaryAttack) { if (ClassCombatManager.IsBluntWeapon(((Humanoid)Player.m_localPlayer).GetCurrentWeapon()) && secondaryAttack) { triggerThunderingBlowsDamage = true; } } [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPostfix] public static void Character_Damage_Crusher_Postfix(Character __instance, HitData hit) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) try { Character attacker = hit.GetAttacker(); Player val = (Player)(object)((attacker is Player) ? attacker : null); if (val != null && !((Object)(object)__instance == (Object)null) && !(__instance is Player) && ClassCombatManager.IsBluntWeapon(((Humanoid)val).GetCurrentWeapon())) { PlayerClassData playerData = PlayerClassManager.GetPlayerData(val); if (playerData != null && playerData.IsClassActive(PlayerClass.Crusher) && hit.m_damage.m_blunt > 0f && triggerThunderingBlowsDamage) { CrusherPerkManager.TriggerLv30_ThunderingBlows(val, hit.m_point, hit.GetTotalDamage()); triggerThunderingBlowsDamage = false; } } } catch (Exception ex) { Logger.LogError((object)("Error in Character_Damage_Crusher_Postfix: " + ex.Message)); } } [HarmonyPatch(typeof(Player), "UseStamina")] [HarmonyPrefix] public static void Player_UseStamina_Crusher_Prefix(Player __instance, ref float v) { try { if (!((Object)(object)__instance == (Object)null)) { v = CrusherPerkManager.ApplyLv40_MightOfTheEarthStamina(__instance, v); } } catch (Exception ex) { Logger.LogError((object)("Error in Player_UseStamina_Crusher_Prefix: " + ex.Message)); } } [HarmonyPatch(typeof(Humanoid), "EquipItem")] [HarmonyPostfix] public static void Humanoid_EquipItem_Prefix(ItemData item, bool triggerEquipEffects = true) { try { if (item != null && CrusherPerkManager.HasCrusherPerk(Player.m_localPlayer, 50) && !(item.m_shared.m_movementModifier > 0f)) { item.m_shared.m_movementModifier = 0f; } } catch (Exception ex) { Logger.LogError((object)("Error in Player_GetRunSpeedFactor_Crusher_Postfix: " + ex.Message)); } } } public enum PlayerClass { SwordMaster, Archer, Crusher, Assassin, Brawler, Wizard, Lancer, Bulwark } public static class PlayerClassHelper { private static readonly Dictionary<PlayerClass, string> DisplayNames = new Dictionary<PlayerClass, string> { { PlayerClass.SwordMaster, "Sword Master" }, { PlayerClass.Archer, "Archer" }, { PlayerClass.Crusher, "Crusher" }, { PlayerClass.Assassin, "Assassin" }, { PlayerClass.Brawler, "Brawler" }, { PlayerClass.Wizard, "Wizard" }, { PlayerClass.Lancer, "Lancer" }, { PlayerClass.Bulwark, "Bulwark" } }; private static readonly Dictionary<PlayerClass, string> InternalNames = new Dictionary<PlayerClass, string> { { PlayerClass.SwordMaster, "Sword Master" }, { PlayerClass.Archer, "Archer" }, { PlayerClass.Crusher, "Crusher" }, { PlayerClass.Assassin, "Assassin" }, { PlayerClass.Brawler, "Brawler" }, { PlayerClass.Wizard, "Wizard" }, { PlayerClass.Lancer, "Lancer" }, { PlayerClass.Bulwark, "Bulwark" } }; private static readonly Dictionary<PlayerClass, string> WeaponTypes = new Dictionary<PlayerClass, string> { { PlayerClass.SwordMaster, "Swords" }, { PlayerClass.Archer, "Bows & Crossbows" }, { PlayerClass.Crusher, "Maces & Hammers" }, { PlayerClass.Assassin, "Knives" }, { PlayerClass.Brawler, "Unarmed" }, { PlayerClass.Wizard, "Staves" }, { PlayerClass.Lancer, "Spears & Polearms" }, { PlayerClass.Bulwark, "Shields" } }; public static string GetDisplayName(PlayerClass playerClass) { return DisplayNames[playerClass]; } public static string GetInternalName(PlayerClass playerClass) { return InternalNames[playerClass]; } public static string GetWeaponType(PlayerClass playerClass) { return WeaponTypes[playerClass]; } public static PlayerClass? ParseFromDisplayName(string displayName) { if (string.IsNullOrEmpty(displayName)) { return null; } foreach (KeyValuePair<PlayerClass, string> displayName2 in DisplayNames) { if (string.Equals(displayName2.Value, displayName, StringComparison.OrdinalIgnoreCase)) { return displayName2.Key; } } if (Enum.TryParse<PlayerClass>(displayName.Replace(" ", ""), ignoreCase: true, out var result)) { return result; } return null; } public static PlayerClass? ParseFromInternalName(string internalName) { if (string.IsNullOrEmpty(internalName)) { return null; } foreach (KeyValuePair<PlayerClass, string> internalName2 in InternalNames) { if (string.Equals(internalName2.Value, internalName, StringComparison.OrdinalIgnoreCase)) { return internalName2.Key; } } return null; } public static List<PlayerClass> GetAllClasses() { return Enum.GetValues(typeof(PlayerClass)).Cast<PlayerClass>().ToList(); } public static string[] GetAllDisplayNames() { return (from c in GetAllClasses() select GetDisplayName(c)).ToArray(); } public static bool IsWeaponAppropriateForClass(ItemData weapon, PlayerClass playerClass) { return playerClass switch { PlayerClass.SwordMaster => ClassCombatManager.IsSwordWeapon(weapon), PlayerClass.Archer => ClassCombatManager.IsBowWeapon(weapon), PlayerClass.Crusher => ClassCombatManager.IsBluntWeapon(weapon), PlayerClass.Assassin => ClassCombatManager.IsKnifeWeapon(weapon), PlayerClass.Brawler => ClassCombatManager.IsUnarmedAttack(weapon), PlayerClass.Wizard => ClassCombatManager.IsMagicWeapon(weapon), PlayerClass.Lancer => ClassCombatManager.IsSpearWeapon(weapon), PlayerClass.Bulwark => true, _ => false, }; } public static List<string> ToInternalNames(List<PlayerClass> classes) { return classes.Select((PlayerClass c) => GetInternalName(c)).ToList(); } public static List<PlayerClass> FromInternalNames(List<string> internalNames) { List<PlayerClass> list = new List<PlayerClass>(); foreach (string internalName in internalNames) { PlayerClass? playerClass = ParseFromInternalName(internalName); if (playerClass.HasValue) { list.Add(playerClass.Value); } } return list; } } [Serializable] public class PlayerClassData { public Dictionary<string, int> classLevels = new Dictionary<string, int>(); public Dictionary<string, float> classXP = new Dictionary<string, float>(); public List<string> activeClasses = new List<string>(); public Dictionary<string, bool> classUnlocked = new Dictionary<string, bool>(); [SerializeField] private List<string> _classNames = new List<string>(); [SerializeField] private List<int> _classLevels = new List<int>(); [SerializeField] private List<float> _classXPValues = new List<float>(); [SerializeField] private List<bool> _classUnlockedValues = new List<bool>(); public PlayerClassData() { InitializeAllClasses(); } private void InitializeAllClasses() { foreach (PlayerClass allClass in PlayerClassHelper.GetAllClasses()) { string internalName = PlayerClassHelper.GetInternalName(allClass); if (!classLevels.ContainsKey(internalName)) { classLevels[internalName] = 0; } if (!classXP.ContainsKey(internalName)) { classXP[internalName] = 0f; } if (!classUnlocked.ContainsKey(internalName)) { classUnlocked[internalName] = true; } } } public void PrepareForSerialization() { _classNames.Clear(); _classLevels.Clear(); _classXPValues.Clear(); _classUnlockedValues.Clear(); foreach (KeyValuePair<string, int> classLevel in classLevels) { _classNames.Add(classLevel.Key); _classLevels.Add(classLevel.Value); _classXPValues.Add(classXP.ContainsKey(classLevel.Key) ? classXP[classLevel.Key] : 0f); _classUnlockedValues.Add(!classUnlocked.ContainsKey(classLevel.Key) || classUnlocked[classLevel.Key]); } } public void RestoreFromSerialization() { classLevels.Clear(); classXP.Clear(); classUnlocked.Clear(); for (int i = 0; i < _classNames.Count; i++) { if (i < _classLevels.Count) { classLevels[_classNames[i]] = _classLevels[i]; } if (i < _classXPValues.Count) { classXP[_classNames[i]] = _classXPValues[i]; } if (i < _classUnlockedValues.Count) { classUnlocked[_classNames[i]] = _classUnlockedValues[i]; } } InitializeAllClasses(); } public bool CanSelectSecondClass() { return classLevels.Values.Any((int level) => level >= 50); } public int GetMaxActiveClasses() { if (!CanSelectSecondClass()) { return 1; } return 2; } public bool IsClassActive(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); return activeClasses.Contains(internalName); } public bool IsClassActive(string className) { return activeClasses.Contains(className); } public bool CanActivateClass(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!classUnlocked.ContainsKey(internalName) || !classUnlocked[internalName]) { return false; } if (IsClassActive(playerClass)) { return false; } return activeClasses.Count < GetMaxActiveClasses(); } public void SetActiveClass(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!classUnlocked.ContainsKey(internalName) || !classUnlocked[internalName]) { Debug.LogWarning((object)("Cannot set active class " + internalName + ": class not unlocked")); return; } List<string> values = new List<string>(activeClasses); activeClasses.Clear(); activeClasses.Add(internalName); Debug.Log((object)("Set active class: " + internalName + " (was: " + string.Join(", ", values) + ")")); } public void AddActiveClass(PlayerClass playerClass) { if (!CanActivateClass(playerClass)) { Debug.LogWarning((object)$"Cannot activate class {playerClass}"); return; } string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!activeClasses.Contains(internalName)) { activeClasses.Add(internalName); Debug.Log((object)("Added active class: " + internalName + ". Active classes: " + string.Join(", ", activeClasses))); } } public void RemoveActiveClass(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (activeClasses.Remove(internalName)) { Debug.Log((object)("Removed active class: " + internalName + ". Active classes: " + string.Join(", ", activeClasses))); } } public int GetClassLevel(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!classLevels.ContainsKey(internalName)) { return 0; } return classLevels[internalName]; } public int GetClassLevel(string className) { if (!classLevels.ContainsKey(className)) { return 0; } return classLevels[className]; } public float GetClassXP(PlayerClass playerClass) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!classXP.ContainsKey(internalName)) { return 0f; } return classXP[internalName]; } public float GetClassXP(string className) { if (!classXP.ContainsKey(className)) { return 0f; } return classXP[className]; } public void AddClassXP(PlayerClass playerClass, float xpAmount) { string internalName = PlayerClassHelper.GetInternalName(playerClass); if (!classXP.ContainsKey(internalName)) { classXP[internalName] = 0f; } classXP[internalName] += xpAmount; CheckForLevelUp(internalName); } public void AddClassXP(string className, float xpAmount) { if (!classXP.ContainsKey(className)) { classXP[className] = 0f; } classXP[className] += xpAmount; CheckForLevelUp(className); } private void CheckForLevelUp(string className) { int classLevel = GetClassLevel(className); if (classLevel >= 50) { return; } float num = GetClassXP(className); float totalXPForLevel = XPCurveHelper.GetTotalXPForLevel(classLevel + 1); if (num >= totalXPForLevel) { classLevels[className] = classLevel + 1; Debug.Log((object)$"Class {className} leveled up to {classLevel + 1}!"); if ((classLevel + 1) % 10 == 0) { Debug.Log((object)$"New perk unlocked for {className} at level {classLevel + 1}!"); } } } public List<PlayerClass> GetActiveClassEnums() { return PlayerClassHelper.FromInternalNames(activeClasses); } } public static class PlayerClassManager { private static Dictionary<long, PlayerClassData> playerData = new Dictionary<long, PlayerClassData>(); private static readonly Dictionary<PlayerClass, Func<Player, string>> ClassDescriptionProviders = new Dictionary<PlayerClass, Func<Player, string>> { { PlayerClass.SwordMaster, SwordMasterPerkManager.GetClassDescription }, { PlayerClass.Archer, ArcherPerkManager.GetClassDescription }, { PlayerClass.Crusher, CrusherPerkManager.GetClassDescription }, { PlayerClass.Assassin, AssassinPerkManager.GetClassDescription }, { PlayerClass.Brawler, BrawlerPerkManager.GetClassDescription }, { PlayerClass.Wizard, WizardPerkManager.GetClassDescription }, { PlayerClass.Lancer, LancerPerkManager.GetClassDescription }, { PlayerClass.Bulwark, BulwarkPerkManager.GetClassDescription } }; public static PlayerClassData GetPlayerData(Player player) { if ((Object)(object)player == (Object)null) { Debug.LogError((object)"GetPlayerData called with null player"); return null; } long playerID = player.GetPlayerID(); if (!playerData.ContainsKey(playerID)) { playerData[playerID] = new PlayerClassData(); Debug.Log((object)$"Created new class data for player {player.GetPlayerName()} (ID: {playerID})"); } return playerData[playerID]; } public static void SetPlayerDataDirectly(long playerId, PlayerClassData data) { playerData[playerId] = data; Debug.Log((object)$"[MANAGER] Directly set class data for player ID: {playerId}"); } public static bool SetPlayerActiveClass(Player player, PlayerClass playerClass) { PlayerClassData playerClassData = GetPlayerData(player); if (playerClassData == null) { Debug.LogError((object)("Could not get player data for " + (((player != null) ? player.GetPlayerName() : null) ?? "null"))); return false; } string text = string.Join(", ", playerClassData.activeClasses); playerClassData.SetActiveClass(playerClass); string text2 = string.Join(", ", playerClassData.activeClasses); Debug.Log((object)("Class change for " + player.GetPlayerName() + ": '" + text + "' -> '" + text2 + "'")); return playerClassData.IsClassActive(playerClass); } public static bool SetPlayerActiveClass(Player player, string className) { PlayerClass? playerClass = PlayerClassHelper.ParseFromInternalName(className); if (!playerClass.HasValue) { Debug.LogError((object)("Invalid class name: " + className)); return false; } return SetPlayerActiveClass(player, playerClass.Value); } public static string[] GetAllClassNames() { return PlayerClassHelper.GetAllDisplayNames(); } public static string GetWeaponTypeForClass(PlayerClass playerClass) { return PlayerClassHelper.GetWeaponType(playerClass); } public static string GetWeaponTypeForClass(string className) { PlayerClass? playerClass = PlayerClassHelper.ParseFromInternalName(className); if (!playerClass.HasValue) { return "Unknown"; } return PlayerClassHelper.GetWeaponType(playerClass.Value); } public static bool HasActiveClasses(Player player) { PlayerClassData playerClassData = GetPlayerData(player); if (playerClassData != null) { return playerClassData.activeClasses.Count > 0; } return false; } public static string GetActiveClassesString(Player player) { PlayerClassData playerClassData = GetPlayerData(player); if (playerClassData == null || playerClassData.activeClasses.Count == 0) { return "None"; } return string.Join(", ", playerClassData.activeClasses); } public static string BuildActiveClassesCompendiumText(Player player) { PlayerClassData playerClassData = GetPlayerData(player); if (playerClassData == null || playerClassData.activeClasses.Count == 0) { return "No active class selected. Visit a Class Obelisk to choose one."; } StringBuilder stringBuilder = new StringBuilder(); foreach (PlayerClass activeClassEnum in playerClassData.GetActiveClassEnums()) { int classLevel = playerClassData.GetClassLevel(activeClassEnum); stringBuilder.Append($"<color=yellow>{PlayerClassHelper.GetDisplayName(activeClassEnum)} — Level {classLevel}</color>\n"); if (classLevel >= 50) { stringBuilder.Append("<color=#00FFFF>Max Level Reached</color>\n\n"); } else { (float current, float required) xPProgress = XPCurveHelper.GetXPProgress(playerClassData.GetClassXP(activeClassEnum), classLevel); float item = xPProgress.current; float item2 = xPProgress.required; float num = ((item2 > 0f) ? (item / item2 * 100f) : 0f); stringBuilder.Append($"<color=#00FFFF>XP: {item:N0} / {item2:N0} ({num:F0}%) to next level</color>\n\n"); } stringBuilder.Append(ClassDescriptionProviders[activeClassEnum](player)); stringBuilder.Append("\n\n"); } if (playerClassData.activeClasses.Count < playerClassData.GetMaxActiveClasses()) { stringBuilder.Append("<color=#AAAAAA>A second class slot is available — visit a Class Obelisk to choose one.</color>"); } return stringBuilder.ToString(); } public static void ClearAllPlayerData() { playerData.Clear(); Debug.Log((object)"Cleared all player class data from memory"); } public static void ReloadPlayerData(Player player) { if (!((Object)(object)player == (Object)null)) { long playerID = player.GetPlayerID(); if (playerData.ContainsKey(playerID)) { playerData.Remove(playerID); } GetPlayerData(player); Debug.Log((object)("Reloaded data for player " + player.GetPlayerName())); } } public static bool ResetActiveClassProgress(Player player, string activeClassInternalName) { PlayerClassData playerClassData = GetPlayerData(player); if ((Object)(object)player == (Object)null || playerClassData == null || string.IsNullOrWhiteSpace(activeClassInternalName)) { return false; } if (!playerClassData.activeClasses.Contains(activeClassInternalName)) { return false; } if (!playerClassData.classLevels.ContainsKey(activeClassInternalName)) { playerClassData.classLevels[activeClassInternalName] = 0; } if (!playerClassData.classXP.ContainsKey(activeClassInternalName)) { playerClassData.classXP[activeClassInternalName] = 0f; } playerClassData.classLevels[activeClassInternalName] = 0; playerClassData.classXP[activeClassInternalName] = 0f; if (!playerClassData.activeClasses.Contains(activeClassInternalName)) { playerClassData.activeClasses.Add(activeClassInternalName); } return true; } } [HarmonyPatch(typeof(Player), "Save")] public static class Player_Save_Patch { private static void Postfix(Player __instance, ZPackage pkg) { try { PlayerClassData playerData = PlayerClassManager.GetPlayerData(__instance); if (playerData != null) { playerData.PrepareForSerialization(); string text = JsonUtility.ToJson((object)playerData, false); pkg.Write(text); } else { pkg.Write(""); Debug.LogWarning((object)("[PATCH:SAVE] No class data to save for " + __instance.GetPlayerName())); } } catch (Exception arg) { Debug.LogError((object)$"[PATCH:SAVE] Error saving class data: {arg}"); } } } [HarmonyPatch(typeof(Player), "Load")] public static class Player_Load_Patch { private static void Postfix(Player __instance, ZPackage pkg) { try { if (pkg.GetPos() < pkg.Size()) { string text = pkg.ReadString(); if (!string.IsNullOrEmpty(text)) { PlayerClassData playerClassData = JsonUtility.FromJson<PlayerClassData>(text); playerClassData.RestoreFromSerialization(); PlayerClassManager.SetPlayerDataDirectly(__instance.GetPlayerID(), playerClassData); } } } catch (Exception arg) { Debug.LogError((object)$"[PATCH:LOAD] Error loading class data: {arg}"); } } } public class PerkInfo { public int RequiredLevel; public string Name; public string Description; } public static class PerkDescriptionBuilder { public static string Build(string intro, List<PerkInfo> perks, string outro, int currentLevel) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(intro).Append("\n\n"); stringBuilder.Append("Passive Perks by Level:\n"); foreach (PerkInfo perk in perks) { stringBuilder.Append((currentLevel >= perk.RequiredLevel) ? $"• Lv{perk.RequiredLevel} – {perk.Name}: {perk.Description}\n" : $"• Lv{perk.RequiredLevel} – ???\n"); } stringBuilder.Append("\n").Append(outro); return stringBuilder.ToString(); } } public static class SEUtils { public static class SEReflection { private static readonly FieldInfo _fiTime = typeof(StatusEffect).GetField("m_time", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly FieldInfo _fiTTL = typeof(StatusEffect).GetField("m_ttl", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly FieldInfo _fiStacks = typeof(StatusEffect).GetField("m_stacks", BindingFlags.Instance | BindingFlags.NonPublic); public static float GetElapsedTime(StatusEffect se) { return (float)(_fiTime?.GetValue(se) ?? ((object)0f)); } public static float GetDuration(StatusEffect se) { return (float)(_fiTTL?.GetValue(se) ?? ((object)0f)); } public static int GetStacks(StatusEffect se) { return (int)(_fiStacks?.GetValue(se) ?? ((object)0)); } } public static bool HasEffect<T>(Character c) where T : StatusEff