using System; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using ImmersiveCombat.Common; using ImmersiveCombat.Config; using ImmersiveCombat.Features.AttackCancel; using ImmersiveCombat.Features.HoldAttack; 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: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: AssemblyCompany("ImmersiveCombat")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.5.2.0")] [assembly: AssemblyInformationalVersion("0.5.2+4cba1c5655706eff9e6ad677cccff9b72fe6cd3c")] [assembly: AssemblyProduct("ImmersiveCombat")] [assembly: AssemblyTitle("ImmersiveCombat")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.5.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 ImmersiveCombat { [BepInPlugin("com.lubertmiliutin.immersivecombat", "ImmersiveCombat", "0.5.3")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.lubertmiliutin.immersivecombat"; public const string PluginName = "ImmersiveCombat"; public const string PluginVersion = "0.5.3"; internal static ManualLogSource Log; private Harmony _harmony; private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); _harmony = new Harmony("com.lubertmiliutin.immersivecombat"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); int num = 0; foreach (MethodBase allPatchedMethod in Harmony.GetAllPatchedMethods()) { Patches patchInfo = Harmony.GetPatchInfo(allPatchedMethod); if (patchInfo != null && (patchInfo.Prefixes.Any((Patch p) => p.owner == "com.lubertmiliutin.immersivecombat") || patchInfo.Postfixes.Any((Patch p) => p.owner == "com.lubertmiliutin.immersivecombat") || patchInfo.Transpilers.Any((Patch p) => p.owner == "com.lubertmiliutin.immersivecombat"))) { num++; Log.LogInfo((object)(" [patch] " + allPatchedMethod.DeclaringType?.FullName + "::" + allPatchedMethod.Name)); } } Log.LogInfo((object)string.Format("{0} v{1} loaded with {2} patched method(s).", "ImmersiveCombat", "0.5.3", num)); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } namespace ImmersiveCombat.Features.QuickDodge { [HarmonyPatch(typeof(Player), "Update")] internal static class QuickDodgePatch { [HarmonyPrefix] private static void Prefix(Player __instance) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: 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_00cc: 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_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.QuickDodgeEnabled.Value && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !IsTextInputActive() && Input.GetKeyDown(ModConfig.QuickDodgeKey.Value) && !((Character)__instance).IsDead() && !((Character)__instance).IsStaggering() && !((Character)__instance).InCutscene() && !((Character)__instance).InDodge() && __instance.GetDoodadController() == null && !HoldAttackState.IsActive) { if (((Character)__instance).InAttack() && ((Humanoid)__instance).m_currentAttack != null) { AttackCancelLogic.CancelAttack(__instance, ((Humanoid)__instance).m_currentAttack, refundStamina: false); } Vector3 val = ((Character)__instance).m_moveDir; if (((Vector3)(ref val)).sqrMagnitude < 0.01f) { val = -((Component)__instance).transform.forward; } else { ((Vector3)(ref val)).Normalize(); } QuickDodgeState.HalfStaminaActive = true; QuickDodgeState.IframeStartPending = true; __instance.Dodge(val * ModConfig.QuickDodgeDistanceScale.Value); if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)($"[QuickDodge] queued. dir=({val.x:F2},{val.z:F2}) " + $"scale={ModConfig.QuickDodgeDistanceScale.Value:F2} " + $"staminaFrac={ModConfig.QuickDodgeStaminaFraction.Value:F2}")); } } } [HarmonyPostfix] private static void Postfix(Player __instance) { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return; } if (QuickDodgeState.HalfStaminaActive && (((Character)__instance).InDodge() || __instance.m_queuedDodgeTimer <= 0f)) { QuickDodgeState.HalfStaminaActive = false; } if (QuickDodgeState.IframeStartPending && __instance.m_dodgeInvincible) { QuickDodgeState.IframeStartPending = false; QuickDodgeState.IframeEndTime = Time.time + ModConfig.QuickDodgeIframeSeconds.Value; QuickDodgeState.ScaleRootMotion = true; if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)($"[QuickDodge] iframes armed for {ModConfig.QuickDodgeIframeSeconds.Value:F2}s, " + $"root-motion scaling = {ModConfig.QuickDodgeDistanceScale.Value:F2}")); } } if (QuickDodgeState.ScaleRootMotion && ((Character)__instance).InDodge() && (Object)(object)((Character)__instance).m_animator != (Object)null) { float num = Mathf.Max(0.05f, ModConfig.QuickDodgeDistanceScale.Value); ((Character)__instance).m_animator.speed = 1f / num; } if (QuickDodgeState.ScaleRootMotion && !((Character)__instance).InDodge()) { QuickDodgeState.ScaleRootMotion = false; if ((Object)(object)((Character)__instance).m_animator != (Object)null) { ((Character)__instance).m_animator.speed = 1f; } if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)"[QuickDodge] root-motion scaling disengaged (dodge anim ended)"); } } if (QuickDodgeState.IframeEndTime > 0f && Time.time >= QuickDodgeState.IframeEndTime) { if (__instance.m_dodgeInvincible) { __instance.OnDodgeMortal(); } QuickDodgeState.IframeEndTime = 0f; } } private static bool IsTextInputActive() { if ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus()) { return true; } if (Console.IsVisible()) { return true; } if (TextInput.IsVisible()) { return true; } return false; } } [HarmonyPatch(typeof(Character), "AddRootMotion")] internal static class QuickDodgeRootMotionScalePatch { [HarmonyPrefix] private static void Prefix(Character __instance, ref Vector3 vel) { //IL_0021: 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) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (QuickDodgeState.ScaleRootMotion && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { vel *= ModConfig.QuickDodgeDistanceScale.Value; } } } [HarmonyPatch(typeof(Player), "GetDodgeStaminaUse")] internal static class QuickDodgeStaminaShimPatch { [HarmonyPostfix] private static void Postfix(ref float __result) { if (QuickDodgeState.HalfStaminaActive) { __result *= ModConfig.QuickDodgeStaminaFraction.Value; } } } internal static class QuickDodgeState { public static bool HalfStaminaActive; public static bool IframeStartPending; public static float IframeEndTime; public static bool ScaleRootMotion; public static void Reset() { HalfStaminaActive = false; IframeStartPending = false; IframeEndTime = 0f; ScaleRootMotion = false; } } } namespace ImmersiveCombat.Features.HoldAttack { internal static class HoldAttackHelpers { public static bool IsProjectile(Attack a) { //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_0010: Invalid comparison between Unknown and I4 if ((int)a.m_attackType != 2 && (int)a.m_attackType != 5) { return (Object)(object)a.m_attackProjectile != (Object)null; } return true; } public static bool IsLocalPlayer(Humanoid humanoid) { if ((Object)(object)humanoid != (Object)null) { return (Object)(object)humanoid == (Object)(object)Player.m_localPlayer; } return false; } public static CharacterAnimEvent GetAnimEvent(Humanoid humanoid) { if (!((Object)(object)humanoid == (Object)null)) { return ((Component)humanoid).GetComponentInChildren(); } return null; } } [HarmonyPatch(typeof(Player), "SetControls")] internal static class HoldAttackInputLockPatch { [HarmonyPrefix] private static void Prefix(Player __instance, ref Vector3 movedir, ref bool jump, ref bool dodge, ref bool run, ref bool autoRun) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.HoldEnabled.Value && ModConfig.HoldLockMovement.Value && HoldAttackState.IsActive && HoldAttackHelpers.IsLocalPlayer((Humanoid)(object)__instance)) { movedir = Vector3.zero; jump = false; dodge = false; run = false; autoRun = false; } } } [HarmonyPatch(typeof(Player), "Update")] internal static class HoldAttackReleasePatch { [HarmonyPostfix] private static void Postfix(Player __instance) { HoldAttackState.TriggerReentryGuard = false; if (!HoldAttackState.IsActive || !HoldAttackHelpers.IsLocalPlayer((Humanoid)(object)__instance)) { return; } if (((Humanoid)__instance).m_currentAttack != HoldAttackState.PendingAttack || ((Character)__instance).IsDead() || ((Character)__instance).IsStaggering() || ((Character)__instance).InCutscene()) { if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)"[Hold] safety bailout — clearing deferred attack"); } HoldAttackState.Bailout(); return; } bool flag = ValheimButtons.AttackHeld(((Humanoid)__instance).m_currentAttackIsSecondary); bool flag2 = Time.time - HoldAttackState.HoldStartTime >= ModConfig.HoldMaxSeconds.Value; if (ModConfig.HoldDrainStamina.Value && flag && !flag2) { float num = ModConfig.HoldDrainStaminaPerSecond.Value * Time.deltaTime; if (num > 0f) { ((Character)__instance).UseStamina(num); if (__instance.GetStamina() <= 0f) { flag2 = true; } } } if (!flag || flag2) { Release(flag2); } } private static void Release(bool forced) { Attack pendingAttack = HoldAttackState.PendingAttack; bool frozenAtTrigger = HoldAttackState.FrozenAtTrigger; if (pendingAttack != null) { HoldAttackState.MarkReleased(); if (ModConfig.DebugLog.Value) { float num = Time.time - HoldAttackState.HoldStartTime; string arg = (frozenAtTrigger ? "trigger-fallback" : "windup"); Plugin.Log.LogInfo((object)$"[Hold] released after {num:F2}s (forced={forced}, path={arg})"); } if (frozenAtTrigger) { pendingAttack.OnAttackTrigger(); } } } } [HarmonyPatch(typeof(Humanoid), "StartAttack")] internal static class HoldAttackStartGuardPatch { [HarmonyPrefix] private static bool Prefix(Humanoid __instance, ref bool __result) { if (!ModConfig.HoldEnabled.Value) { return true; } if (!HoldAttackState.IsActive) { return true; } if ((Object)(object)__instance == (Object)null) { return true; } if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer) { return true; } __result = false; if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)"[Hold/StartGuard] blocked StartAttack while holding deferred swing"); } return false; } } internal static class HoldAttackState { public static Attack PendingAttack; public static Attack DecidedAttack; public static CharacterAnimEvent FrozenAnimEvent; public static float HoldStartTime; public static bool FrozenAtTrigger; private const float FreezeDurationSeconds = 600f; public static bool TriggerReentryGuard; public static bool IsActive => PendingAttack != null; public static void Freeze(Attack attack, CharacterAnimEvent animEvent, bool atTrigger) { PendingAttack = attack; DecidedAttack = attack; HoldStartTime = Time.time; FrozenAtTrigger = atTrigger; FrozenAnimEvent = animEvent; if ((Object)(object)animEvent != (Object)null) { animEvent.FreezeFrame(600f); } } public static void MarkLetThrough(Attack attack) { DecidedAttack = attack; } public static void MarkReleased() { Thaw(); DecidedAttack = PendingAttack; PendingAttack = null; FrozenAnimEvent = null; } public static void Bailout() { Thaw(); PendingAttack = null; DecidedAttack = null; FrozenAtTrigger = false; HoldStartTime = 0f; FrozenAnimEvent = null; } private static void Thaw() { if (!((Object)(object)FrozenAnimEvent == (Object)null)) { if ((Object)(object)FrozenAnimEvent.m_animator != (Object)null) { float speed = ((FrozenAnimEvent.m_pauseSpeed > 0.01f) ? FrozenAnimEvent.m_pauseSpeed : 1f); FrozenAnimEvent.m_animator.speed = speed; } FrozenAnimEvent.m_pauseTimer = 0f; } } } internal static class HoldAttackTriggerDeferLogic { public static bool TryDefer(Humanoid attacker, Attack attack, string source) { if (!ModConfig.HoldEnabled.Value) { return true; } if (attack == null) { return true; } if (!HoldAttackHelpers.IsLocalPlayer(attacker)) { return true; } if (HoldAttackState.TriggerReentryGuard) { return true; } if (HoldAttackState.PendingAttack == attack) { return true; } if (HoldAttackState.DecidedAttack == attack) { if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)("[Hold/" + source + "] DAMAGE — trigger fired for already-decided attack")); } return true; } if (HoldAttackHelpers.IsProjectile(attack)) { return true; } bool currentAttackIsSecondary = attacker.m_currentAttackIsSecondary; if (currentAttackIsSecondary && !ModConfig.HoldSecondary.Value) { return true; } if (!currentAttackIsSecondary && !ModConfig.HoldPrimary.Value) { return true; } bool flag = ValheimButtons.AttackHeld(currentAttackIsSecondary); if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)($"[Hold/{source}] trigger fired. secondary={currentAttackIsSecondary} " + $"zinputHeld={flag} m_attackHold={((Character)attacker).m_attackHold} " + $"m_secAttackHold={((Character)attacker).m_secondaryAttackHold}")); } if (!flag) { return true; } CharacterAnimEvent animEvent = HoldAttackHelpers.GetAnimEvent(attacker); HoldAttackState.Freeze(attack, animEvent, atTrigger: true); HoldAttackState.TriggerReentryGuard = true; if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)("[Hold/" + source + "] DEFERRED at trigger frame (fallback path)")); } return false; } } [HarmonyPatch(typeof(Attack), "OnAttackTrigger")] internal static class AttackOnAttackTriggerPatch { [HarmonyPrefix] private static bool Prefix(Attack __instance) { return HoldAttackTriggerDeferLogic.TryDefer(__instance.m_character, __instance, "Attack"); } } [HarmonyPatch(typeof(Humanoid), "OnAttackTrigger")] internal static class HumanoidOnAttackTriggerPatch { [HarmonyPrefix] private static bool Prefix(Humanoid __instance) { return HoldAttackTriggerDeferLogic.TryDefer(__instance, __instance.m_currentAttack, "Humanoid"); } } [HarmonyPatch(typeof(Humanoid), "UpdateAttack")] internal static class HoldAttackWindupFreezePatch { [HarmonyPostfix] private static void Postfix(Humanoid __instance) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.HoldEnabled.Value || HoldAttackState.IsActive || !HoldAttackHelpers.IsLocalPlayer(__instance)) { return; } Attack currentAttack = __instance.m_currentAttack; if (currentAttack == null || currentAttack == HoldAttackState.DecidedAttack || HoldAttackHelpers.IsProjectile(currentAttack)) { return; } bool currentAttackIsSecondary = __instance.m_currentAttackIsSecondary; if ((currentAttackIsSecondary && !ModConfig.HoldSecondary.Value) || (!currentAttackIsSecondary && !ModConfig.HoldPrimary.Value)) { return; } Animator animator = ((Character)__instance).m_animator; if ((Object)(object)animator == (Object)null || animator.IsInTransition(0)) { return; } AnimatorStateInfo currentAnimatorStateInfo = animator.GetCurrentAnimatorStateInfo(0); if (((AnimatorStateInfo)(ref currentAnimatorStateInfo)).tagHash != Humanoid.s_animatorTagAttack) { return; } float normalizedTime = ((AnimatorStateInfo)(ref currentAnimatorStateInfo)).normalizedTime; if (normalizedTime >= 1f) { return; } float value = ModConfig.HoldFreezeAt.Value; if (normalizedTime < value) { return; } if (!ValheimButtons.AttackHeld(currentAttackIsSecondary)) { HoldAttackState.MarkLetThrough(currentAttack); if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)$"[Hold/Windup] tap detected at t={normalizedTime:F2} — passing through"); } return; } CharacterAnimEvent animEvent = HoldAttackHelpers.GetAnimEvent(__instance); HoldAttackState.Freeze(currentAttack, animEvent, atTrigger: false); if (ModConfig.DebugLog.Value) { Plugin.Log.LogInfo((object)$"[Hold/Windup] frozen at t={normalizedTime:F2} (threshold={value:F2})"); } } } } namespace ImmersiveCombat.Features.AttackCancel { internal static class AttackCancelLogic { public static void CancelAttack(Player player, Attack attack, bool refundStamina) { if (HoldAttackState.PendingAttack == attack) { HoldAttackState.Bailout(); } if (refundStamina) { float num = attack.GetAttackStamina() * ModConfig.RefundStaminaPercent.Value; if (num > 0f) { ((Character)player).AddStamina(num); } } attack.m_abortAttack = true; attack.Stop(); ((Humanoid)player).m_currentAttack = null; ((Humanoid)player).m_currentAttackIsSecondary = false; ((Humanoid)player).m_previousAttack = null; player.m_queuedAttackTimer = 0f; player.m_queuedSecondAttackTimer = 0f; Animator animator = ((Character)player).m_animator; if ((Object)(object)animator != (Object)null) { animator.CrossFadeInFixedTime("Movement", 0.05f, 0); } } } [HarmonyPatch(typeof(Player), "Update")] internal static class AttackCancelPatch { [HarmonyPrefix] private static void Prefix(Player __instance) { if (!ModConfig.CancelEnabled.Value || (Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)Player.m_localPlayer || !((Character)__instance).InAttack()) { return; } Attack currentAttack = ((Humanoid)__instance).m_currentAttack; if (currentAttack == null) { return; } bool flag = ModConfig.CancelOnBlock.Value && ValheimButtons.BlockPressed(); bool flag2 = ModConfig.CancelOnDodge.Value && ValheimButtons.DodgePressed(); if (flag2 && HoldAttackState.IsActive && ModConfig.HoldLockMovement.Value) { flag2 = false; } if ((flag || flag2) && (!ModConfig.CancelOnlyDuringWindup.Value || !currentAttack.m_attackDone)) { AttackCancelLogic.CancelAttack(__instance, currentAttack, ModConfig.RefundStamina.Value); if (ModConfig.DebugLog.Value) { string arg = (flag ? "Block" : "Dodge"); Plugin.Log.LogInfo((object)$"[Cancel] Aborted swing via {arg}. attackDone={currentAttack.m_attackDone}"); } } } } } namespace ImmersiveCombat.Config { internal static class ModConfig { private const string SecDebug = "0 - Debug"; private const string SecCancel = "1 - Attack Cancel"; private const string SecHold = "2 - Hold To Release Attack"; private const string SecQuickDodge = "3 - Quick Dodge (BETA)"; public static ConfigEntry DebugLog { get; private set; } public static ConfigEntry CancelEnabled { get; private set; } public static ConfigEntry CancelOnBlock { get; private set; } public static ConfigEntry CancelOnDodge { get; private set; } public static ConfigEntry CancelOnlyDuringWindup { get; private set; } public static ConfigEntry RefundStamina { get; private set; } public static ConfigEntry RefundStaminaPercent { get; private set; } public static ConfigEntry QuickDodgeEnabled { get; private set; } public static ConfigEntry QuickDodgeKey { get; private set; } public static ConfigEntry QuickDodgeDistanceScale { get; private set; } public static ConfigEntry QuickDodgeStaminaFraction { get; private set; } public static ConfigEntry QuickDodgeIframeSeconds { get; private set; } public static ConfigEntry HoldEnabled { get; private set; } public static ConfigEntry HoldPrimary { get; private set; } public static ConfigEntry HoldSecondary { get; private set; } public static ConfigEntry HoldFreezeAt { get; private set; } public static ConfigEntry HoldMaxSeconds { get; private set; } public static ConfigEntry HoldLockMovement { get; private set; } public static ConfigEntry HoldDrainStamina { get; private set; } public static ConfigEntry HoldDrainStaminaPerSecond { get; private set; } public static void Bind(ConfigFile cfg) { //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Expected O, but got Unknown //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Expected O, but got Unknown //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Expected O, but got Unknown //IL_01fa: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Expected O, but got Unknown //IL_026c: Unknown result type (might be due to invalid IL or missing references) //IL_0276: Expected O, but got Unknown //IL_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02ae: Expected O, but got Unknown //IL_02dc: Unknown result type (might be due to invalid IL or missing references) //IL_02e6: Expected O, but got Unknown DebugLog = cfg.Bind("0 - Debug", "DebugLog", false, "Verbose logging to BepInEx\\LogOutput.log."); CancelEnabled = cfg.Bind("1 - Attack Cancel", "Enabled", true, "Master toggle for the Attack Cancel feature."); CancelOnBlock = cfg.Bind("1 - Attack Cancel", "CancelOnBlock", true, "Pressing Block during a swing cancels the swing."); CancelOnDodge = cfg.Bind("1 - Attack Cancel", "CancelOnDodge", true, "Pressing Dodge (Jump key + WASD) during a swing cancels the swing."); CancelOnlyDuringWindup = cfg.Bind("1 - Attack Cancel", "CancelOnlyDuringWindup", false, "Only allow cancelling before the damage trigger has fired."); RefundStamina = cfg.Bind("1 - Attack Cancel", "RefundStamina", true, "Refund part of the stamina consumed by the cancelled swing."); RefundStaminaPercent = cfg.Bind("1 - Attack Cancel", "RefundStaminaPercent", 0.5f, new ConfigDescription("Fraction of the attack's stamina cost to refund (0..1).", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); HoldEnabled = cfg.Bind("2 - Hold To Release Attack", "Enabled", true, "Master toggle. Holding the attack button freezes the swing in wind-up; releasing it plays the strike."); HoldPrimary = cfg.Bind("2 - Hold To Release Attack", "HoldPrimary", true, "Apply hold-to-release to primary (LMB) attacks."); HoldSecondary = cfg.Bind("2 - Hold To Release Attack", "HoldSecondary", false, "Apply hold-to-release to secondary (RMB) attacks. Off by default — vanilla secondary moves are designed to fire instantly."); HoldFreezeAt = cfg.Bind("2 - Hold To Release Attack", "FreezeAtNormalizedTime", 0.35f, new ConfigDescription("Where in the swing animation (0..1) to freeze. Lower = earlier wind-up pose.", (AcceptableValueBase)(object)new AcceptableValueRange(0.05f, 0.95f), Array.Empty())); HoldMaxSeconds = cfg.Bind("2 - Hold To Release Attack", "MaxHoldSeconds", 5f, new ConfigDescription("Auto-release after this many seconds (safety cap).", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 30f), Array.Empty())); HoldLockMovement = cfg.Bind("2 - Hold To Release Attack", "LockMovementWhileHeld", true, "Zero out movement / jump / dodge inputs while the wind-up is frozen, so accidental key presses don't waste stamina. Block still works (cancels)."); HoldDrainStamina = cfg.Bind("2 - Hold To Release Attack", "DrainStaminaWhileHolding", false, "Drain stamina each second while holding. Off by default."); HoldDrainStaminaPerSecond = cfg.Bind("2 - Hold To Release Attack", "DrainStaminaPerSecond", 4f, new ConfigDescription("Stamina per second drained while holding (only used if DrainStaminaWhileHolding).", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 30f), Array.Empty())); QuickDodgeEnabled = cfg.Bind("3 - Quick Dodge (BETA)", "Enabled", false, "BETA. Short Witcher 3-style sidestep on a dedicated key, half stamina, shortened iframes. Off by default while this is still in beta."); QuickDodgeKey = cfg.Bind("3 - Quick Dodge (BETA)", "KeyBind", (KeyCode)308, "BETA. Key that triggers Quick Dodge. Combine with WASD to pick direction; no WASD = backward."); QuickDodgeDistanceScale = cfg.Bind("3 - Quick Dodge (BETA)", "DistanceScale", 0.4f, new ConfigDescription("BETA. Fraction of a normal dodge's distance AND duration.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 1f), Array.Empty())); QuickDodgeStaminaFraction = cfg.Bind("3 - Quick Dodge (BETA)", "StaminaFraction", 0.5f, new ConfigDescription("BETA. Fraction of vanilla dodge stamina cost.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 1f), Array.Empty())); QuickDodgeIframeSeconds = cfg.Bind("3 - Quick Dodge (BETA)", "IframeSeconds", 0.05f, new ConfigDescription("BETA. Seconds of invincibility after dodge starts (vanilla ≈ 0.2s).", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 0.5f), Array.Empty())); } } } namespace ImmersiveCombat.Common { internal static class AnimatorConstants { public const string MovementState = "Movement"; public const int BodyLayer = 0; public const float ShortCrossFade = 0.05f; } internal static class ValheimButtons { public const string Jump = "Jump"; public const string JumpJoy = "JoyDodge"; public const string Block = "Block"; public const string BlockJoy = "JoyBlock"; public const string Attack = "Attack"; public const string AttackJoy = "JoyAttack"; public const string SecAttack = "SecondaryAttack"; public const string SecAttackJoy = "JoySecondaryAttack"; public static bool Pressed(string kb, string joy) { if (!ZInput.GetButtonDown(kb)) { return ZInput.GetButtonDown(joy); } return true; } public static bool Held(string kb, string joy) { if (!ZInput.GetButton(kb)) { return ZInput.GetButton(joy); } return true; } public static bool BlockPressed() { return Pressed("Block", "JoyBlock"); } public static bool DodgePressed() { return Pressed("Jump", "JoyDodge"); } public static bool AttackHeld(bool secondary) { if (!secondary) { return Held("Attack", "JoyAttack"); } return Held("SecondaryAttack", "JoySecondaryAttack"); } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { internal IgnoresAccessChecksToAttribute(string assemblyName) { } } }