Decompiled source of ParryStrike v0.1.0

plugins/ParryStrike/ParryStrike.dll

Decompiled a day ago
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Narukami")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright © 2026 Narukami")]
[assembly: AssemblyDescription("Parry enemy attacks with your own swing.")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("Parry Strike")]
[assembly: AssemblyTitle("Parry Strike")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Narukami/ParryStrike")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ParryStrike
{
	internal static class ParryHelper
	{
		internal static bool IsLocalPlayer(Player player)
		{
			if ((Object)(object)player != (Object)null && (Object)(object)player == (Object)(object)Player.m_localPlayer && ((Character)player).IsOwner())
			{
				return !((Character)player).IsDead();
			}
			return false;
		}

		internal static bool CanUseWeaponForParry(Player player)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Invalid comparison between Unknown and I4
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: Invalid comparison between Unknown and I4
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Invalid comparison between Unknown and I4
			if (!IsLocalPlayer(player))
			{
				return false;
			}
			if (((Character)player).IsStaggering() || ((Character)player).InDodge() || ((Character)player).IsEncumbered())
			{
				return false;
			}
			ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon();
			if (currentWeapon == null)
			{
				return false;
			}
			if (Settings.AllowBowParry.Value)
			{
				return true;
			}
			if ((int)currentWeapon.m_shared.m_itemType == 4)
			{
				return false;
			}
			SkillType skillType = currentWeapon.m_shared.m_skillType;
			if ((int)skillType != 8)
			{
				return (int)skillType != 14;
			}
			return false;
		}

		internal static bool IsFacingHit(Character defender, HitData hit)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			Vector3 dir = hit.m_dir;
			dir.y = 0f;
			if (((Vector3)(ref dir)).sqrMagnitude < 0.0001f)
			{
				return true;
			}
			return Vector3.Dot(dir, ((Component)defender).transform.forward) <= 0f;
		}

		internal static Vector3 DirectionTo(Character from, Character to)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			Vector3 val = ((Component)to).transform.position - ((Component)from).transform.position;
			val.y = 0f;
			if (!(((Vector3)(ref val)).sqrMagnitude < 0.0001f))
			{
				return ((Vector3)(ref val)).normalized;
			}
			return ((Component)to).transform.forward;
		}

		internal static void Trace(string message)
		{
			if (Settings.DebugLog.Value)
			{
				Plugin.Log.LogInfo((object)("[ParryStrike] " + message));
			}
		}
	}
	[HarmonyPatch(typeof(Character), "RPC_Damage")]
	internal static class ParryStrikePatch
	{
		private static bool Prefix(Character __instance, HitData hit)
		{
			if (!Settings.Enabled.Value || hit == null)
			{
				return true;
			}
			Player val = (Player)(object)((__instance is Player) ? __instance : null);
			if (val == null)
			{
				return true;
			}
			if ((Object)(object)val != (Object)(object)Player.m_localPlayer || !((Character)val).IsOwner() || ((Character)val).IsDead())
			{
				return true;
			}
			if (!ParryWindow.IsActiveFor(val))
			{
				return true;
			}
			Character attacker = hit.GetAttacker();
			if (!IsParryCandidate(val, attacker, hit))
			{
				return true;
			}
			if (!TryParry(val, attacker, hit))
			{
				return true;
			}
			return false;
		}

		private static bool IsParryCandidate(Player player, Character attacker, HitData hit)
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Invalid comparison between Unknown and I4
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Invalid comparison between Unknown and I4
			if ((Object)(object)attacker == (Object)null || (Object)(object)attacker == (Object)(object)player || attacker.IsDead())
			{
				return false;
			}
			if (!hit.m_blockable)
			{
				return false;
			}
			if (Settings.OnlyMelee.Value && hit.m_ranged)
			{
				return false;
			}
			if ((int)hit.m_hitType != 1 && (int)hit.m_hitType != 2)
			{
				return false;
			}
			if (attacker.IsPlayer() && !((Character)player).IsPVPEnabled())
			{
				return false;
			}
			if (!ParryHelper.IsFacingHit((Character)(object)player, hit))
			{
				return false;
			}
			return !((Character)player).IsBlocking();
		}

		private static bool TryParry(Player player, Character attacker, 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_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: 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_00d2: 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_00e1: 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_0139: Unknown result type (might be due to invalid IL or missing references)
			//IL_0141: Unknown result type (might be due to invalid IL or missing references)
			//IL_0144: Unknown result type (might be due to invalid IL or missing references)
			//IL_0149: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Expected O, but got Unknown
			//IL_016e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Unknown result type (might be due to invalid IL or missing references)
			ItemData currentWeapon = ((Humanoid)player).GetCurrentWeapon();
			ItemData val = currentWeapon ?? ((Humanoid)player).LeftItem;
			if (val == null)
			{
				return false;
			}
			float num = Mathf.Max(0f, Settings.StaminaCost.Value);
			if (num > 0f && !((Character)player).HaveStamina(num))
			{
				ParryHelper.Trace("Parry failed: not enough stamina.");
				return false;
			}
			if (num > 0f)
			{
				((Character)player).UseStamina(num);
			}
			float totalDamage = hit.GetTotalDamage();
			val.m_shared.m_blockEffect.Create(hit.m_point, Quaternion.identity, (Transform)null, 1f, -1, ((Character)player).GetZDOID());
			((Humanoid)player).m_perfectBlockEffect.Create(hit.m_point, Quaternion.identity, (Transform)null, 1f, -1, ((Character)player).GetZDOID());
			if (Settings.ShowDamageText.Value && (Object)(object)DamageText.instance != (Object)null)
			{
				DamageText.instance.ShowText((TextType)6, hit.m_point + Vector3.up * 0.5f, totalDamage, true);
			}
			if (Settings.DeflectAttacker.Value)
			{
				float num2 = Mathf.Max(new float[3]
				{
					val.GetDeflectionForce(),
					hit.m_pushForce,
					5f
				}) * Settings.DeflectForceMultiplier.Value;
				if (num2 > 0f)
				{
					HitData val2 = new HitData
					{
						m_pushForce = num2,
						m_dir = ParryHelper.DirectionTo((Character)(object)player, attacker)
					};
					attacker.Damage(val2);
				}
			}
			if (Settings.StaggerAttacker.Value && attacker.m_staggerWhenBlocked)
			{
				attacker.Stagger(-hit.m_dir);
			}
			if (Settings.RaiseBlockingSkill.Value)
			{
				((Character)player).RaiseSkill((SkillType)6, 2f);
			}
			if (Settings.AdrenalineGain.Value > 0f)
			{
				((Character)player).AddAdrenaline(Settings.AdrenalineGain.Value);
			}
			if (Settings.DurabilityLoss.Value && currentWeapon != null && currentWeapon.m_shared.m_useDurability)
			{
				currentWeapon.m_durability -= currentWeapon.m_shared.m_useDurabilityDrain * Game.m_durabilityRate;
			}
			if ((Object)(object)val.m_shared.m_perfectBlockStatusEffect != (Object)null)
			{
				((Character)player).GetSEMan().AddStatusEffect(val.m_shared.m_perfectBlockStatusEffect, true, val.m_worldLevel, ((Character)player).GetSkillLevel((SkillType)6), (short)(-1));
			}
			if (Settings.FreezeFrame.Value)
			{
				((Character)player).FreezeFrame(0.1f);
			}
			if (Settings.OneParryPerSwing.Value)
			{
				ParryWindow.Lock();
			}
			Plugin.ReportParry(((Object)attacker).name, totalDamage);
			return true;
		}
	}
	internal static class ParryWindow
	{
		private const float WindupGrace = 0.15f;

		private const float MaxWindupTime = 2f;

		private static Player m_player;

		private static float m_expireAt;

		private static float m_graceUntil;

		private static bool m_open;

		private static bool m_pastHitFrame;

		private static bool m_locked;

		internal static void OnNewSwing(Player player, bool openDuringWindup)
		{
			m_player = player;
			m_locked = false;
			m_pastHitFrame = false;
			m_open = openDuringWindup;
			m_graceUntil = Time.time + 0.15f;
			m_expireAt = (openDuringWindup ? (Time.time + 2f) : 0f);
		}

		internal static void OnHitFrame(Player player, float duration)
		{
			if (!m_locked && !((Object)(object)player == (Object)null))
			{
				if ((Object)(object)m_player != (Object)(object)player)
				{
					m_player = player;
					m_locked = false;
				}
				m_open = true;
				m_pastHitFrame = true;
				m_expireAt = Time.time + Mathf.Max(0.01f, duration);
			}
		}

		internal static bool IsActiveFor(Player player)
		{
			if (!m_open || m_locked || (Object)(object)player == (Object)null || (Object)(object)m_player != (Object)(object)player)
			{
				return false;
			}
			if (((Character)player).IsDead() || ((Character)player).IsStaggering() || ((Character)player).InDodge() || ((Character)player).IsEncumbered())
			{
				Close();
				return false;
			}
			if (Time.time > m_expireAt)
			{
				Close();
				return false;
			}
			if (!m_pastHitFrame && !((Character)player).InAttack() && Time.time > m_graceUntil)
			{
				Close();
				return false;
			}
			return true;
		}

		internal static void Lock()
		{
			m_open = false;
			m_locked = true;
			m_expireAt = 0f;
		}

		internal static void Close()
		{
			m_player = null;
			m_expireAt = 0f;
			m_graceUntil = 0f;
			m_open = false;
			m_pastHitFrame = false;
		}
	}
	[HarmonyPatch(typeof(Humanoid), "StartAttack")]
	internal static class ParryWindowStartPatch
	{
		private static void Postfix(Humanoid __instance, bool __result)
		{
			if (__result && Settings.Enabled.Value)
			{
				Player val = (Player)(object)((__instance is Player) ? __instance : null);
				if (val != null && ParryHelper.IsLocalPlayer(val))
				{
					bool flag = ParryHelper.CanUseWeaponForParry(val);
					ParryWindow.OnNewSwing(val, flag && Settings.ParryOnWindup.Value);
					ParryHelper.Trace(flag ? "New swing, window reset." : "New swing, parrying is unavailable with this weapon.");
				}
			}
		}
	}
	[HarmonyPatch(typeof(Humanoid), "OnAttackTrigger")]
	internal static class ParryWindowTriggerPatch
	{
		private static void Postfix(Humanoid __instance)
		{
			if (!Settings.Enabled.Value)
			{
				return;
			}
			Player val = (Player)(object)((__instance is Player) ? __instance : null);
			if (val != null && ParryHelper.IsLocalPlayer(val))
			{
				if (!ParryHelper.CanUseWeaponForParry(val))
				{
					ParryWindow.Close();
					return;
				}
				ParryWindow.OnHitFrame(val, Settings.WindowDuration.Value);
				ParryHelper.Trace($"Parry window opened ({Settings.WindowDuration.Value:0.00} s).");
			}
		}
	}
	[BepInPlugin("narukami.parrystrike", "Parry Strike", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "narukami.parrystrike";

		public const string DisplayName = "Parry Strike";

		public const string Version = "0.1.0";

		private static bool m_firstParryLogged;

		internal static Plugin Instance { get; private set; }

		internal static ManualLogSource Log { get; private set; }

		internal static Harmony Harmony { get; private set; }

		private void Awake()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			Settings.Bind(((BaseUnityPlugin)this).Config);
			Harmony = new Harmony("narukami.parrystrike");
			Harmony.PatchAll();
			Log.LogInfo((object)("Parry Strike v0.1.0 loaded. Strike parrying: " + (Settings.Enabled.Value ? "on" : "off") + ", " + $"window {Settings.WindowDuration.Value:0.00} s."));
		}

		private void OnDestroy()
		{
			ParryWindow.Close();
			Harmony harmony = Harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}

		internal static void ReportParry(string attackerName, float prevented)
		{
			if (!m_firstParryLogged)
			{
				m_firstParryLogged = true;
				Log.LogInfo((object)($"First parry: {attackerName}, prevented {prevented:0.#} damage. " + "The mechanic is working."));
			}
			if (Settings.DebugLog.Value)
			{
				Log.LogInfo((object)$"[ParryStrike] Parry: {attackerName}, prevented {prevented:0.#} damage.");
			}
		}
	}
	internal static class Settings
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<float> WindowDuration;

		internal static ConfigEntry<bool> ParryOnWindup;

		internal static ConfigEntry<bool> OnlyMelee;

		internal static ConfigEntry<bool> OneParryPerSwing;

		internal static ConfigEntry<bool> AllowBowParry;

		internal static ConfigEntry<float> StaminaCost;

		internal static ConfigEntry<bool> StaggerAttacker;

		internal static ConfigEntry<bool> DeflectAttacker;

		internal static ConfigEntry<float> DeflectForceMultiplier;

		internal static ConfigEntry<bool> RaiseBlockingSkill;

		internal static ConfigEntry<float> AdrenalineGain;

		internal static ConfigEntry<bool> DurabilityLoss;

		internal static ConfigEntry<bool> ShowDamageText;

		internal static ConfigEntry<bool> FreezeFrame;

		internal static ConfigEntry<bool> DebugLog;

		internal static void Bind(ConfigFile cfg)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f2: Expected O, but got Unknown
			//IL_0156: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Expected O, but got Unknown
			//IL_01a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b3: Expected O, but got Unknown
			Enabled = cfg.Bind<bool>("General", "Enabled", true, "Enable parrying enemy attacks with your own swing.\nВключить парирование атаки врага собственной атакой.");
			WindowDuration = cfg.Bind<float>("General", "ParryWindow", 0.3f, new ConfigDescription("Parry window length in seconds. Counted from the hit frame of your attack (and from the start of the swing when ParryOnWindup is on).\nДлительность окна парирования в секундах. Отсчитывается от кадра удара (и от начала замаха, если включено ParryOnWindup).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 1.5f), Array.Empty<object>()));
			ParryOnWindup = cfg.Bind<bool>("General", "ParryOnWindup", true, "Open the parry window already during the wind-up, not only on the hit frame. Turn off for strict strike-for-strike parrying.\nОткрывать окно парирования уже на замахе, а не только в момент удара. Выключите для строгого парирования «удар в удар».");
			OnlyMelee = cfg.Bind<bool>("General", "OnlyMelee", true, "Only melee hits can be parried. Arrows and projectiles cannot.\nПарировать можно только ближние атаки. Стрелы и снаряды парировать нельзя.");
			OneParryPerSwing = cfg.Bind<bool>("General", "OneParryPerSwing", true, "A single swing can parry only one enemy hit.\nОдна ваша атака может парировать только один удар врага.");
			AllowBowParry = cfg.Bind<bool>("General", "AllowBowParry", false, "Allow parrying while drawing a bow or crossbow.\nРазрешить парирование во время выстрела из лука или арбалета.");
			StaminaCost = cfg.Bind<float>("Balance", "StaminaCost", 10f, new ConfigDescription("Stamina spent per successful parry. 0 means free.\nСколько выносливости тратится за успешное парирование. 0 — бесплатно.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>()));
			StaggerAttacker = cfg.Bind<bool>("Balance", "StaggerAttacker", true, "Stagger the attacker on a successful parry.\nОглушать врага при успешном парировании.");
			DeflectAttacker = cfg.Bind<bool>("Balance", "DeflectAttacker", true, "Knock the attacker back on a successful parry.\nОтбрасывать врага назад при успешном парировании.");
			DeflectForceMultiplier = cfg.Bind<float>("Balance", "DeflectForceMultiplier", 1f, new ConfigDescription("Multiplier for the knockback force applied to the attacker.\nМножитель силы отбрасывания врага при парировании.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>()));
			RaiseBlockingSkill = cfg.Bind<bool>("Balance", "RaiseBlockingSkill", true, "Raise the Blocking skill on a successful parry.\nПовышать навык «Блокирование» при успешном парировании.");
			AdrenalineGain = cfg.Bind<float>("Balance", "AdrenalineGain", 5f, new ConfigDescription("Adrenaline granted per successful parry.\nСколько адреналина даётся за успешное парирование.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 50f), Array.Empty<object>()));
			DurabilityLoss = cfg.Bind<bool>("Balance", "DurabilityLoss", true, "Spend weapon durability on a successful parry.\nТратить прочность оружия при успешном парировании.");
			ShowDamageText = cfg.Bind<bool>("Visual", "ShowDamageText", true, "Show the floating \"Blocked\" number on a parry.\nПоказывать всплывающий текст «Заблокировано» при парировании.");
			FreezeFrame = cfg.Bind<bool>("Visual", "FreezeFrame", true, "Brief hit-stop on a parry for impact.\nКороткая заморозка анимации в момент парирования (эффект «звон клинков»).");
			DebugLog = cfg.Bind<bool>("Debug", "DebugLog", false, "Verbose parry logging to the BepInEx console.\nПодробный лог парирований в консоль BepInEx.");
		}
	}
}