using KFCommonUtilityLib.Scripts.Attributes; using HarmonyLib; using UnityEngine; using KFCommonUtilityLib.Scripts.StaticManagers; using KFCommonUtilityLib; using System.Reflection.Emit; using System.Collections.Generic; using UniLinq; using static ActionModuleTagged; using KFCommonUtilityLib.Scripts.Utilities; public struct ShotIndexRange { public IntRange IndexRange; public Vector2 RecoilPositionStrength; public Vector2 RecoilRotationStrength; public Vector2 RecoilAngleRange; public static ShotIndexRange Parse(DynamicProperties prop) { var ret = new ShotIndexRange(); StringParsers.TryParseRange(prop.GetString("IndexRange"), out ret.IndexRange); prop.ParseVec("RecoilPositionStrength", ref ret.RecoilPositionStrength); prop.ParseVec("RecoilRotationStrength", ref ret.RecoilRotationStrength); prop.ParseVec("RecoilAngleRange", ref ret.RecoilAngleRange); return ret; } } public class ShotIndexRangeGroup { private ShotIndexRange[] shotRange; private ShotIndexRangeGroup() { } public static ShotIndexRangeGroup Parse(DynamicProperties props) { ShotIndexRangeGroup group = new ShotIndexRangeGroup(); List list = new List(); for (int i = 0; i < 99; i++) { if (props.Classes.TryGetValue($"ShotsGroupSettings{i}", out var shotProps)) { list.Add(ShotIndexRange.Parse(shotProps)); } else { break; } } if (list.Count > 0) { group.shotRange = list.ToArray(); } return group; } public bool FindIndexGroup(int index, out ShotIndexRange range) { range = default; if (shotRange == null) { return false; } foreach (var shotRange in shotRange) { if (shotRange.IndexRange.min <= index && shotRange.IndexRange.max >= index) { range = shotRange; return true; } } return false; } } [TypeTarget(typeof(ItemActionRanged)), TypeDataTarget(typeof(EFTProceduralRecoilData))] public class ActionModuleProceduralRecoil { //====== public const float BASE_RECOIL_ROTATION_STR_MIN = 0.9f; public const float BASE_RECOIL_ROTATION_STR_MAX = 1.15f; public const float BASE_RECOIL_POSITION_STR_MIN = 0.65f; public const float BASE_RECOIL_POSITION_STR_MAX = 1.05f; public const float CONSTANT_ROTATION_STR_MULTIPLIER = 0.1399f; public const float INTENSITY_MULTIPLIER_CROUCHING = 0.85f; public struct RecoilPassiveTags { public FastTags WeaponRecoilModifier; public FastTags DeltaAnglePerShot; public FastTags DeltaAngleMin; public FastTags DeltaAngleMax; public FastTags CameraRecoilConversionPerc; public FastTags WeaponRotIntensity; public FastTags WeaponPosIntensity; public FastTags CameraRotIntensity; public FastTags WeaponRotIntensityMultiplier; public FastTags WeaponRotationForceDamping; public FastTags WeaponRotationForceReturnSpeed; public FastTags WeaponPositionForceDamping; public FastTags WeaponPositionForceReturnSpeed; public FastTags CameraRotationForceDamping; public FastTags CameraRotationForceReturnSpeed; public FastTags RecoilReturnBias; public FastTags RecoilReturnBiasDamping; public FastTags RecoilForceStrength; public FastTags RecoilAimingIntensity; } public static readonly FastTags WeaponRecoilModifer = FastTags.Parse("RecoilIntensityModifier"); public static readonly FastTags DeltaAnglePerShot = FastTags.Parse("RecoilStableAngleIncreaseStep"); public static readonly FastTags DeltaAngleMin = FastTags.Parse("ProgressRecoilAngleOnStableMin"); public static readonly FastTags DeltaAngleMax = FastTags.Parse("ProgressRecoilAngleOnStableMax"); public static readonly FastTags CameraRecoilConversionPerc = FastTags.Parse("RecoilCamera"); public static readonly FastTags WeaponRotIntensity = FastTags.Parse("WeaponRotIntensity"); public static readonly FastTags WeaponPosIntensity = FastTags.Parse("WeaponPosIntensity"); public static readonly FastTags CameraRotIntensity = FastTags.Parse("CameraRotIntensity"); public static readonly FastTags WeaponRotIntensityMultiplier = FastTags.Parse("RecoilCategoryMultiplierHandRotation"); public static readonly FastTags WeaponRotationForceDamping = FastTags.Parse("RecoilDampingHandRotation"); public static readonly FastTags WeaponRotationForceReturnSpeed = FastTags.Parse("RecoilReturnSpeedHandRotation"); public static readonly FastTags WeaponPositionForceDamping = FastTags.Parse("RecoilDampingHandPosition"); public static readonly FastTags WeaponPositionForceReturnSpeed = FastTags.Parse("RecoilReturnSpeedHandPosition"); public static readonly FastTags CameraRotationForceDamping = FastTags.Parse("RecoilDampingCameraRotation"); public static readonly FastTags CameraRotationForceReturnSpeed = FastTags.Parse("RecoilReturnSpeedCameraRotation"); public static readonly FastTags RecoilReturnBias = FastTags.Parse("RecoilReturnPathOffsetHandRotation"); public static readonly FastTags RecoilReturnBiasDamping = FastTags.Parse("RecoilReturnPathDampingHandRotation"); public static readonly FastTags RecoilAimingIntensity = FastTags.Parse("RecoilAimingIntensity"); public RecoilPassiveTags tags; [HarmonyPatch(nameof(ItemAction.ReadFrom)), MethodTargetPostfix] public void Postfix_ReadFrom(DynamicProperties _props, ItemAction __instance) { var itemTags = __instance.item.ItemTags; tags = new RecoilPassiveTags() { WeaponRecoilModifier = itemTags | WeaponRecoilModifer, DeltaAnglePerShot = itemTags | DeltaAnglePerShot, DeltaAngleMin = itemTags | DeltaAngleMin, DeltaAngleMax = itemTags | DeltaAngleMax, CameraRecoilConversionPerc = itemTags | CameraRecoilConversionPerc, WeaponRotIntensity = itemTags | WeaponRotIntensity, WeaponPosIntensity = itemTags | WeaponPosIntensity, CameraRotIntensity = itemTags | CameraRotIntensity, WeaponRotIntensityMultiplier = itemTags | WeaponRotIntensityMultiplier, WeaponRotationForceDamping = itemTags | WeaponRotationForceDamping, WeaponRotationForceReturnSpeed = itemTags | WeaponRotationForceReturnSpeed, WeaponPositionForceDamping = itemTags | WeaponPositionForceDamping, WeaponPositionForceReturnSpeed = itemTags | WeaponPositionForceReturnSpeed, CameraRotationForceDamping = itemTags | CameraRotationForceDamping, CameraRotationForceReturnSpeed = itemTags | CameraRotationForceReturnSpeed, RecoilReturnBias = itemTags | RecoilReturnBias, RecoilReturnBiasDamping = itemTags | RecoilReturnBiasDamping, RecoilForceStrength = FastTags.Parse("RecoilForceStrength"), RecoilAimingIntensity = itemTags | RecoilAimingIntensity }; } [HarmonyPatch(nameof(ItemAction.OnModificationsChanged)), MethodTargetPostfix] public void Postfix_OnModificationChanged(ItemActionRanged __instance, ItemActionData _data, EFTProceduralRecoilData __customData) { Vector2 recoilForce = default; string originalValue = "0,0"; DynamicProperties props = __instance.Properties; props.ParseString("WeaponRecoilForce", ref originalValue); recoilForce = StringParsers.ParseVector2(_data.invData.itemValue.GetPropertyOverrideForAction("WeaponRecoilForce", originalValue, __instance.ActionIndex)); __customData.WeaponRecoilForceUp = recoilForce.x; __customData.WeaponRecoilForceBack = recoilForce.y; originalValue = "80,100"; props.ParseString("RecoilAngleRange", ref originalValue); __customData.BaseRecoilRadianRange = StringParsers.ParseVector2(_data.invData.itemValue.GetPropertyOverrideForAction("RecoilAngleRange", originalValue, __instance.ActionIndex)) * Mathf.Deg2Rad; originalValue = "5"; props.ParseString("StableShotIndex", ref originalValue); __customData.StableShotIndex = StringParsers.ParseSInt32(_data.invData.itemValue.GetPropertyOverrideForAction("StableShotIndex", originalValue, __instance.ActionIndex)); originalValue = "true"; props.ParseString("RampUpRecoil", ref originalValue); __customData.RampRecoil = StringParsers.ParseBool(_data.invData.itemValue.GetPropertyOverrideForAction("RampUpRecoil", originalValue, __instance.ActionIndex)); originalValue = "3"; props.ParseString("RampRecoilIndex", ref originalValue); __customData.RampRecoilIndex = StringParsers.ParseSInt32(_data.invData.itemValue.GetPropertyOverrideForAction("RampRecoilIndex", originalValue, __instance.ActionIndex)); __customData.ShotRangeGroup = ShotIndexRangeGroup.Parse(props); __customData.playerOriginTransform = null; if (_data.invData.holdingEntity is EntityPlayerLocal player && player.bFirstPersonView) { __customData.playerCameraTransform = player.cameraTransform; var targets = AnimationRiggingManager.GetRigTargetsFromPlayer(_data.invData.holdingEntity); __customData.recoilPivotTransform = null; __customData.hasPivotOverride = false; if (targets) { __customData.recoilPivotTransform = AnimationRiggingManager.GetAddPartTransformOverride(targets.transform, "RecoilPivot"); } if (__customData.recoilPivotTransform) { __customData.hasPivotOverride = true; } else { __customData.recoilPivotTransform = player.cameraTransform.FindInAllChildren("RightHand"); } if (targets && targets.ItemFpv && targets is RigTargets) { __customData.playerOriginTransform = targets.ItemAnimator.transform; __customData.isRigWeapon = true; } else { __customData.playerOriginTransform = player.cameraTransform.FindInAllChildren("Hips"); __customData.isRigWeapon = false; } var oldRecoil = targets.ItemAnimator?.GetComponent(); if (oldRecoil) { oldRecoil.enabled = false; } } __customData.ResetRecoil(); if (!EFTProceduralRecoilData.dontUpdateParam || (EFTProceduralRecoilData.dontUpdateParam && !__customData.passivesInited)) { CalcRecoilParams(__customData, _data as ItemActionRanged.ItemActionDataRanged); __customData.passivesInited = true; } __customData.isHolding = true; //CalcDampFactors(__customData, _data as ItemActionRanged.ItemActionDataRanged); } [HarmonyPatch(nameof(ItemAction.StopHolding)), MethodTargetPostfix] public void Postfix_StopHolding(EFTProceduralRecoilData __customData) { __customData.ResetRecoil(); __customData.isHolding = false; } //[HarmonyPatch(nameof(ItemAction.OnHoldingUpdate)), MethodTargetPostfix] //public void Postfix_OnHoldingUpdate(EFTProceduralRecoilData __customData, ItemActionData _actionData) //{ // if (_actionData.invData.holdingEntity is EntityPlayerLocal player && player.bFirstPersonView) // { // bool aimingGun = player.AimingGun; // if (aimingGun) // { // __customData.WeaponRotIntensity = 0.75f; // //__customData.WeaponPosIntensity = 0f; // } // else // { // __customData.WeaponRotIntensity = 1f; // //__customData.WeaponPosIntensity = 1f; // } // } //} [HarmonyPatch(nameof(ItemActionRanged.onHoldingEntityFired)), MethodTargetPostfix] public void Postfix_onHoldingEntityFired(ItemActionData _actionData, EFTProceduralRecoilData __customData) { if (_actionData.invData.holdingEntity is EntityPlayerLocal player && player.bFirstPersonView) { if (!EFTProceduralRecoilData.dontUpdateParam) { CalcRecoilParams(__customData, __customData.rangedData); } __customData.AddRecoilForce(EffectManager.GetValue(CustomEnums.CustomTaggedEffect, _actionData.invData.itemValue, 1, player, null, tags.RecoilForceStrength, false, true, false, false, false, 1, false, false)); } } [HarmonyPatch(typeof(ItemActionRanged), nameof(ItemActionRanged.fireShot)), MethodTargetTranspiler] public static IEnumerable Transpiler_ItemActionRanged_fireShot(IEnumerable instructions) { var codes = instructions.ToList(); var mtd_ray = AccessTools.Method(typeof(EntityAlive), nameof(EntityAlive.GetLookRay)); for (int i = 0; i < codes.Count; i++) { if (codes[i].Calls(mtd_ray)) { codes.RemoveRange(i - 1, 2); codes.InsertRange(i - 1, new[] { new CodeInstruction(OpCodes.Ldarg_2), CodeInstruction.Call(typeof(ActionModuleProceduralRecoil), nameof(GetLookRayOverride)) }); break; } } return codes; } [HarmonyPatch(typeof(ItemActionLauncher), nameof(ItemActionLauncher.getImageActionEffectsStartPosAndDirection)), MethodTargetTranspiler] public static IEnumerable Transpiler_ItemActionLauncher_getImageActionEffectsStartPosAndDirection(IEnumerable instructions) { var codes = instructions.ToList(); var mtd_ray = AccessTools.Method(typeof(EntityAlive), nameof(EntityAlive.GetLookRay)); for (int i = 0; i < codes.Count; i++) { if (codes[i].Calls(mtd_ray)) { codes.RemoveRange(i - 3, 4); codes.InsertRange(i - 3, new[] { new CodeInstruction(OpCodes.Ldarg_1), CodeInstruction.Call(typeof(ActionModuleProceduralRecoil), nameof(GetLookRayOverride)) }); break; } } return codes; } public static Ray GetLookRayOverride(ItemActionData data) { if (data.invData.holdingEntity is EntityPlayerLocal player && player.bFirstPersonView && data is IModuleContainerFor dataModule && data.invData.holdingEntity.AimingGun) { var aimingModule = (data.invData.actionData[1] as IModuleContainerFor)?.Instance; if (aimingModule != null) { Transform transform = aimingModule.CurAimRef.transform; return new Ray(transform.position + Origin.position, transform.forward); } } return data.invData.holdingEntity.GetLookRay(); } public void CalcRecoilParams(EFTProceduralRecoilData recoilData, ItemActionRanged.ItemActionDataRanged rangedData) { recoilData.WeaponRecoilModifier = Mathf.Max(0, EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 1, rangedData.invData.holdingEntity, null, tags.WeaponRecoilModifier)); recoilData.BaseWeaponRecoilStrRot = new Vector2(BASE_RECOIL_ROTATION_STR_MIN, BASE_RECOIL_ROTATION_STR_MAX) * (recoilData.WeaponRecoilForceUp * recoilData.WeaponRecoilModifier + 20) * CONSTANT_ROTATION_STR_MULTIPLIER; recoilData.BaseWeaponRecoilStrPos = new Vector2(BASE_RECOIL_POSITION_STR_MIN, BASE_RECOIL_POSITION_STR_MAX) * (recoilData.WeaponRecoilForceBack * recoilData.WeaponRecoilModifier + 20) * CONSTANT_ROTATION_STR_MULTIPLIER; recoilData.DeltaAnglePerShot = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 2.5f, rangedData.invData.holdingEntity, null, tags.DeltaAnglePerShot); recoilData.DeltaAngleRange.x = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0, rangedData.invData.holdingEntity, null, tags.DeltaAngleMin); recoilData.DeltaAngleRange.y = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 30, rangedData.invData.holdingEntity, null, tags.DeltaAngleMax); recoilData.CameraRecoilConversionPerc = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.04f, rangedData.invData.holdingEntity, null, tags.CameraRecoilConversionPerc); recoilData.WeaponRotIntensity = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 1, rangedData.invData.holdingEntity, null, tags.WeaponRotIntensity); recoilData.WeaponPosIntensity = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 1, rangedData.invData.holdingEntity, null, tags.WeaponPosIntensity); recoilData.CameraRotIntensity = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 1, rangedData.invData.holdingEntity, null, tags.CameraRotIntensity); recoilData.WeaponRotIntensityMultiplier = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.2f, rangedData.invData.holdingEntity, null, tags.WeaponRotIntensityMultiplier); recoilData.WeaponRotationForceDamping = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.85f, rangedData.invData.holdingEntity, null, tags.WeaponRotationForceDamping); recoilData.WeaponRotationForceReturnSpeed = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 3, rangedData.invData.holdingEntity, null, tags.WeaponRotationForceReturnSpeed); recoilData.WeaponPositionForceDamping = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.5f, rangedData.invData.holdingEntity, null, tags.WeaponPositionForceDamping); recoilData.WeaponPositionForceReturnSpeed = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.08f, rangedData.invData.holdingEntity, null, tags.WeaponPositionForceReturnSpeed); recoilData.CameraRotationForceDamping = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.5f, rangedData.invData.holdingEntity, null, tags.CameraRotationForceDamping); recoilData.CameraRotationForceReturnSpeed = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.05f, rangedData.invData.holdingEntity, null, tags.CameraRotationForceReturnSpeed); recoilData.RecoilReturnBias = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.01f, rangedData.invData.holdingEntity, null, tags.RecoilReturnBias); recoilData.BiasDamping = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 0.48f, rangedData.invData.holdingEntity, null, tags.RecoilReturnBiasDamping); recoilData.HandRotValueAimIntensity = EffectManager.GetValue(CustomEnums.CustomTaggedEffect, rangedData.invData.itemValue, 1f, rangedData.invData.holdingEntity, null, tags.RecoilAimingIntensity); } //public void CalcDampFactors(ProceduralRecoilData recoilData, ItemActionRanged.ItemActionDataRanged rangedData) //{ // recoilData.recoilFollowDampPerc = EffectManager.GetValue(CustomEnums.PRDampPerc, rangedData.invData.itemValue, 0f, rangedData.invData.holdingEntity); // recoilData.recoilFollowReturnSpeed = EffectManager.GetValue(CustomEnums.PRReturnSpeed, rangedData.invData.itemValue, 1f, rangedData.invData.holdingEntity); //} public class EFTProceduralRecoilData { public ItemActionRanged.ItemActionDataRanged rangedData; public ActionModuleProceduralRecoil module; public ItemInventoryData invData; public Transform playerOriginTransform; public Transform recoilPivotTransform; public Transform playerCameraTransform; public bool isRigWeapon; public bool hasPivotOverride; public int actionIndex; public bool isHolding; public static bool dontUpdateParam; public bool passivesInited; // initial weapon recoil property public float WeaponRecoilForceUp, WeaponRecoilForceBack; public ShotIndexRangeGroup ShotRangeGroup; //====== // the recoil angle range in radian, where 90 is up and 0 is right public Vector2 BaseRecoilRadianRange; // the amount of shots before recoil state is considered stable public int StableShotIndex = 3; // whether there is a ramping stage after reaching RampRecoilIndex // where the recoil force is multiplied by clamp(1, StableShotIndex) / StableShotIndex public bool RampRecoil = true; public int RampRecoilIndex = 1; //====== temp // modifier for the weapon recoil force public float WeaponRecoilModifier; //====== // calculated basic weapon recoil values, multiplied by mod/perk modifiers public Vector2 BaseWeaponRecoilStrRot, BaseWeaponRecoilStrPos; // angle increase per shot public float DeltaAnglePerShot; // delta angle clamp value public Vector2 DeltaAngleRange; // the percentage of the recoil strength that is converted to camera kick, // which in our case should also be applied inversely to the weapon public float CameraRecoilConversionPerc; // the intensity modifier of the accumulated recoil force public float WeaponRotIntensity = 1, WeaponPosIntensity = 1, CameraRotIntensity = 1; // the base intensity multiplier of incoming weapon rotation force public float WeaponRotIntensityMultiplier; // damping and return speed, only need to set for weapon rotation public float WeaponRotationForceDamping, WeaponRotationForceReturnSpeed; public float WeaponPositionForceDamping = 0.5f, WeaponPositionForceReturnSpeed = 0.08f; public float CameraRotationForceDamping = 0.5f, CameraRotationForceReturnSpeed = 0.05f; //====== // offset perc during recoil return to apply public float RecoilReturnBias = 0.01f; // recoil return bias damping public float BiasDamping; //====== // x -> up (negative) | y -> hor (positive = right) | z -> back (positive) public Vector3 RecoilDirection; //====== hand rotation fields public bool IsStable; public bool IsReturning; public bool IsHandRotDirty; public Vector2 CurrentStableRotationOffset; public float RampMultiplier; public Vector2 CurrentRotationOffset; public float CurrentRotationAccumulated; public float HandRotValueAimIntensity = 1f; public Vector2 HandRotValueCur, HandRotValuePrev, HandRotValueApply, HandRotVelocity, HandRotForce; public float HandRotValueXAfterRecoil = 0.01f; public AnimationCurve HandRotReturnSpeedCurve = new AnimationCurve(new Keyframe(0, 0.008f, 0.0002f, 0.0002f) { inWeight = 0, outWeight = 0.0775f }, new Keyframe(2.5f, 0.008f, 0.0001f, 0.0001f) { inWeight = 0.0717f, outWeight = 0 }) { preWrapMode = WrapMode.ClampForever, postWrapMode = WrapMode.ClampForever }; public float HandRotCurveTime; public Vector2 HandRotStableOffsetRange = new Vector2(0.1f, 8f); public Vector2 TargetStableRotationOffset; public float AutoFireReturnSpeed; public int LastReturnOffsetSign; public float RecoilOffsetImpulse; public float RecoilOffsetLerpSpeed = 0.01f; //====== hand position fields public bool IsHandPosDirty; public float HandPosValue, HandPosVelocity, HandPosForce; //====== cam rotation fields public bool IsCamRotDirty; public Vector2 CamRotValue, CamRotVelocity, CamRotForce; public EFTProceduralRecoilData(ItemActionData actionData, ItemInventoryData _invData, int _indexInEntityOfAction, ActionModuleProceduralRecoil _module) { rangedData = actionData as ItemActionRanged.ItemActionDataRanged; module = _module; invData = _invData; actionIndex = _indexInEntityOfAction; } public void AddRecoilForce(float forceStr = 1) { if (rangedData.curBurstCount >= StableShotIndex) { SetStable(true); } if (RampRecoil && rangedData.curBurstCount >= RampRecoilIndex) { RampMultiplier = Mathf.Clamp01((float)rangedData.curBurstCount / StableShotIndex); } else { RampMultiplier = 1; } CalcRecoilForceStr(forceStr, out float rotStr, out float posStr); CalcRecoilDirRadian(out Vector2 dirRad); CalcFinalRecoilDirection(dirRad, rotStr, posStr); RedirectRecoilForceToHandRot(); RedirectRecoilForceToPosAndCam(); ProceduralRecoilUpdater.LastShotTime = Time.time + 0.2f; } public void ResetRecoil() { IsStable = false; IsReturning = false; IsHandRotDirty = false; CurrentStableRotationOffset = Vector2.zero; CurrentRotationOffset = Vector2.zero; CurrentRotationAccumulated = 0; HandRotValueCur = HandRotValuePrev = HandRotVelocity = HandRotForce = Vector2.zero; HandRotValueXAfterRecoil = 0.01f; HandRotCurveTime = 0f; TargetStableRotationOffset = Vector2.zero; AutoFireReturnSpeed = 0f; LastReturnOffsetSign = 0; IsHandPosDirty = false; HandPosValue = HandPosVelocity = HandPosForce = 0f; IsCamRotDirty = false; CamRotValue = CamRotVelocity = CamRotForce = Vector2.zero; //ProceduralRecoilUpdater.SetTargetRecoilPosOffset(Vector3.zero); } public void FixedUpdate(float dt) { if (!isHolding) { return; } if (rangedData.state == ItemActionFiringState.Off) { SetStable(false); } FixedUpdateHandRot(dt); FixedUpdateHandPos(dt); FixedUpdateCamRot(dt); } public void LateUpdate(float dt) { if (!isHolding) { return; } var aimingData = ((IModuleContainerFor)invData.actionData[1]).Instance; //ProceduralRecoilUpdater.InverseWorldCamKickOffsetCur.ToAngleAxis(out float camRotAngle, out Vector3 camRotAxis); //playerOriginTransform.RotateAround(aimingData.aimRefTransform.position, camRotAxis, camRotAngle); AimReference aimref = aimingData.CurAimRef; Transform aimRefTransform = aimref?.transform ?? aimingData.aimRefTransform; if (!aimRefTransform) { return; } Vector3 prevAimRefPos = aimRefTransform.position; // right hand forward = right, right = -up, up = -forward Vector3 targetHandPosOffset = -playerCameraTransform.forward * HandPosValue; //Vector3 targetHandForward = Quaternion.AngleAxis(HandRotValueCur.x, playerCameraTransform.right) // * (Quaternion.AngleAxis(HandRotValueCur.y, playerCameraTransform.up) * playerCameraTransform.forward); //Quaternion.FromToRotation(playerCameraTransform.forward, targetHandForward).ToAngleAxis(out float worldAngleOffset, out Vector3 worldRotAxis); ((playerCameraTransform.rotation * Quaternion.Euler(HandRotValueApply)) * Quaternion.Inverse(playerCameraTransform.rotation)).ToAngleAxis(out float worldAngleOffset, out Vector3 worldRotAxis); if (isRigWeapon && !hasPivotOverride) { playerOriginTransform.RotateAround(recoilPivotTransform.position, worldRotAxis, worldAngleOffset); playerOriginTransform.position += targetHandPosOffset; } else { playerOriginTransform.position += targetHandPosOffset; playerOriginTransform.RotateAround(recoilPivotTransform.position, worldRotAxis, worldAngleOffset); } Vector3 alignmentPos = aimref?.alignmentTarget?.position ?? (aimRefTransform.position - aimingData.AimRefOffset * aimRefTransform.forward); ((playerCameraTransform.rotation * Quaternion.Euler(ProceduralRecoilUpdater.CamAimRecoilRotOffsetCur)) * Quaternion.Inverse(playerCameraTransform.rotation)).ToAngleAxis(out worldAngleOffset, out worldRotAxis); playerOriginTransform.RotateAround(alignmentPos, worldRotAxis, worldAngleOffset); ProceduralRecoilUpdater.LateUpdateCamRecoilPosOffset(aimRefTransform.position - prevAimRefPos, dt, invData.holdingEntity.AimingGun); playerOriginTransform.position -= ProceduralRecoilUpdater.GetCurRecoilPosOffset(); //ProceduralRecoilUpdater.InverseWorldCamKickOffsetCur.ToAngleAxis(out float camRotAngle, out Vector3 camRotAxis); //playerOriginTransform.RotateAround(aimRefTransform.position - aimingData.AimRefOffset * aimRefTransform.forward, camRotAxis, camRotAngle); } private void FixedUpdateHandRot(float dt) { bool isAiming = invData.holdingEntity.AimingGun; if (IsHandRotDirty) { float weaponRotIntensity = WeaponRotIntensity; if (isAiming) { weaponRotIntensity *= 0.75f; } HandRotVelocity += HandRotForce * weaponRotIntensity; HandRotForce = Vector2.zero; HandRotCurveTime = Mathf.Min(HandRotValueCur.magnitude, HandRotCurveTime); if (IsStable) { float stableOffset = Random.Range(HandRotStableOffsetRange.x, HandRotStableOffsetRange.y); TargetStableRotationOffset = CurrentStableRotationOffset + HandRotVelocity * stableOffset; Vector2 stableOffsetDir = TargetStableRotationOffset - HandRotValueCur; if (stableOffsetDir.magnitude > 0.95f) { stableOffsetDir *= 0.95f; TargetStableRotationOffset = HandRotValueCur + stableOffsetDir; } } IsHandRotDirty = false; } if (rangedData.state == ItemActionFiringState.Off) { AutoFireReturnSpeed = WeaponRotationForceReturnSpeed * HandRotReturnSpeedCurve.Evaluate(HandRotCurveTime); //mount? } if (IsHandRotDirty && rangedData.state != ItemActionFiringState.Off && !IsStable) { AutoFireReturnSpeed *= RampMultiplier; } HandRotCurveTime += dt; HandRotVelocity *= WeaponRotationForceDamping; if (!IsStable) { HandRotVelocity -= HandRotValueCur * AutoFireReturnSpeed; HandRotValueCur += HandRotVelocity; } else { HandRotValueCur = HandRotValueCur + (TargetStableRotationOffset - HandRotValueCur) * 0.2f; } UpdateReturnBias(dt); if (rangedData.state == ItemActionFiringState.Off) { if (HandRotValueXAfterRecoil != 0) { float speedDamp = Mathf.Abs(Mathf.Clamp(HandRotValueCur.x, HandRotValueXAfterRecoil, -0.01f) / HandRotValueXAfterRecoil); RecoilOffsetLerpSpeed = Mathf.Clamp(1 - speedDamp, 0.01f, 1f) * 0.01f; HandRotValueCur = Vector2.Lerp(HandRotValueCur, Vector2.zero, RecoilOffsetLerpSpeed); //Log.Out($"LerpBack RecoilOffsetLerpSpeed {RecoilOffsetLerpSpeed} HandRotValueCurY {HandRotValueCur.y}"); } } HandRotValuePrev = HandRotValueCur; if (isAiming) { //HandRotValueApply = Vector2.Lerp(HandRotValueApply, new Vector2(HandRotValueCur.x * HandRotValueAimIntensity, HandRotValueCur.y), 8 * dt); HandRotValueApply = new Vector2(HandRotValueCur.x * HandRotValueAimIntensity, HandRotValueCur.y); } else { //HandRotValueApply = Vector2.Lerp(HandRotValueApply, HandRotValueCur, 8 * dt); HandRotValueApply = HandRotValueCur; } } private void FixedUpdateHandPos(float dt) { if (IsHandPosDirty) { HandPosVelocity += HandPosForce * WeaponPosIntensity; HandPosForce = 0; IsHandPosDirty = false; } HandPosVelocity -= HandPosValue * WeaponPositionForceReturnSpeed; HandPosVelocity *= WeaponPositionForceDamping; HandPosValue += HandPosVelocity; } private void FixedUpdateCamRot(float dt) { if (IsCamRotDirty) { CamRotVelocity += CamRotForce * CameraRotIntensity; CamRotForce = Vector2.zero; IsCamRotDirty = false; } CamRotVelocity -= CamRotValue * CameraRotationForceReturnSpeed; CamRotVelocity *= CameraRotationForceDamping; CamRotValue += CamRotVelocity; } public void SetStable(bool stable) { if (stable) { if (!IsStable) { IsStable = true; CurrentStableRotationOffset = CurrentRotationOffset; } } else if (IsStable) { CurrentRotationAccumulated = 0; IsStable = false; } } private void CalcRecoilForceStr(float forceStr, out float rotStr, out float posStr) { //todo: random range according to burst count Vector2 rotRange = BaseWeaponRecoilStrRot; Vector2 posRange = BaseWeaponRecoilStrPos; if (ShotRangeGroup.FindIndexGroup(rangedData.curBurstCount, out var range)) { rotRange += range.RecoilRotationStrength; posRange += range.RecoilPositionStrength; } rotStr = Random.Range(rotRange.x, rotRange.y) * forceStr; posStr = Random.Range(posRange.x, posRange.y) * forceStr; } private void CalcRecoilDirRadian(out Vector2 dirRad) { Vector2 randomRange = default; if (ShotRangeGroup.FindIndexGroup(rangedData.curBurstCount, out var range)) { randomRange = range.RecoilAngleRange; } if (IsStable) { CurrentRotationAccumulated = Mathf.Clamp(CurrentRotationAccumulated + DeltaAnglePerShot, DeltaAngleRange.x, DeltaAngleRange.y); randomRange += new Vector2(CurrentRotationAccumulated, -CurrentRotationAccumulated); } dirRad = BaseRecoilRadianRange + randomRange * Mathf.Deg2Rad; } private void CalcFinalRecoilDirection(Vector2 recoilDirRad, float recoilRotStr, float recoilPosStr) { float recoilRad = Random.Range(recoilDirRad.x, recoilDirRad.y); float poseFactor = rangedData.invData.holdingEntity.IsCrouching ? ActionModuleProceduralRecoil.INTENSITY_MULTIPLIER_CROUCHING : 1f; RecoilDirection = new Vector3(-Mathf.Sin(recoilRad) * recoilRotStr * poseFactor, Mathf.Cos(recoilRad) * recoilRotStr * poseFactor, recoilPosStr * poseFactor); //aiming intensity? } private void RedirectRecoilForceToHandRot() { HandRotForce += new Vector2(RecoilDirection.x, RecoilDirection.y) * 0.15f * WeaponRotIntensityMultiplier * RampMultiplier; //perhaps mount multiplier in the future? IsReturning = true; if (IsStable) { HandRotVelocity = Vector2.zero; } IsHandRotDirty = true; } private void RedirectRecoilForceToPosAndCam() { HandPosForce += RecoilDirection.z * 0.0007f; IsHandPosDirty = true; CamRotForce += new Vector2(RecoilDirection.x, -RecoilDirection.y) * CameraRecoilConversionPerc; IsCamRotDirty = true; } private void UpdateReturnBias(float dt) { if (HandRotValueCur.x < HandRotValuePrev.x && IsReturning) { //skipped the random offset HandRotValueXAfterRecoil = HandRotValueCur.x; RecoilOffsetImpulse = Mathf.Abs(HandRotValueCur.x * WeaponRotationForceReturnSpeed) * 0.01f; //mount? if (HandRotValueCur.y > 0) { LastReturnOffsetSign = 1; } else if (HandRotValueCur.y < 0) { LastReturnOffsetSign = -1; } else { LastReturnOffsetSign = Random.Range(-1f, 1f) >= 0f ? 1 : -1; } //Log.Out($"Upkick RecoilOffsetImpulse {RecoilOffsetImpulse} Sign {LastReturnOffsetSign} HandRotVelocityY {HandRotVelocity.y} HandRotValueCurY {HandRotValueCur.y}"); return; } if (rangedData.state == ItemActionFiringState.Off && IsReturning) { RecoilOffsetImpulse *= BiasDamping; if (RecoilOffsetImpulse <= 0.001f) { IsReturning = false; //RecoilOffsetImpulse = 0f; //LastReturnOffsetSign = 0; return; } HandRotVelocity.y += RecoilOffsetImpulse * LastReturnOffsetSign; //Log.Out($"Apply RecoilOffsetImpulse {RecoilOffsetImpulse} Sign {LastReturnOffsetSign} HandRotVelocityY {HandRotVelocity.y} HandRotValueCurY {HandRotValueCur.y}"); } } } } public static class ProceduralRecoilUpdater { public static EntityPlayerLocal player; public static ActionModuleProceduralRecoil.EFTProceduralRecoilData RecoilData { get { return (player?.inventory?.holdingItemData?.actionData?[MultiActionManager.GetActionIndexForEntity(player)] as IModuleContainerFor)?.Instance; } } private static float SetAimIntensity { set { if (RecoilData != null) { RecoilData.HandRotValueAimIntensity = value; } } } private static bool SetDontUpdate { set { ActionModuleProceduralRecoil.EFTProceduralRecoilData.dontUpdateParam = value; } } //====== cam rotation apply public static Vector3 PreUpdateCamFwd; public static float CamRecoilLerpSpeed, CamRecoilLerpSpeedStep = 0.1f; public static Vector2 CamRecoilLerpSpeedRange = new Vector2(0.1f, 0.2f); public static Vector2 CamRecoilOffsetStable, CamRecoilOffsetCur; public static Quaternion LocalCamRotOffsetCur, InverseWorldCamShakeOffsetCur, InverseWorldCamKickOffsetCur, InverseWorldCamTotalOffsetCur; public static float CamAimRecoilPosSmoothIn = 8f, CamAimRecoilPosSmoothOut = 6f; private static Vector3 CamAimRecoilPosOffsetCur, CamAimRecoilPosOffsetStable/*, CamAimRecoilPosOffsetTarget*/; public static float LastShotTime, CamAimRecoilPosLerpSpeedXYMin = 7, CamAimRecoilPosLerpSpeedXYMax = 8, CamAimRecoilPosLerpSpeedStep = 5; public static Vector2 CamAimRecoilRotOffsetCur; public static float CamAimRecoilRotOffsetLerpSpeed = 15f; public static void InitPlayer(EntityPlayerLocal player) { ProceduralRecoilUpdater.player = player; PreUpdateCamFwd = player.cameraTransform?.forward ?? Vector3.forward; CamRecoilLerpSpeed = 0.01f; CamRecoilOffsetCur = Vector2.zero; CamRecoilOffsetStable = Vector2.zero; LocalCamRotOffsetCur = Quaternion.identity; InverseWorldCamShakeOffsetCur = Quaternion.identity; InverseWorldCamKickOffsetCur = Quaternion.identity; InverseWorldCamTotalOffsetCur = Quaternion.identity; CamAimRecoilPosOffsetCur = Vector3.zero; CamAimRecoilPosOffsetStable = Vector3.zero; //CamAimRecoilPosOffsetTarget = Vector3.zero; LastShotTime = 0; CamAimRecoilRotOffsetCur = Vector2.zero; } //public static void SetTargetRecoilPosOffset(Vector3 offset) //{ // CamAimRecoilPosOffsetTarget = player.cameraTransform.InverseTransformDirection(offset); //} public static Vector3 GetCurRecoilPosOffset() { return player.cameraTransform.TransformDirection(CamAimRecoilPosOffsetCur); } public static void FixedUpdate(float dt) { if (!player) { return; } ActionModuleProceduralRecoil.EFTProceduralRecoilData recoilData = RecoilData; if (recoilData != null) { recoilData.FixedUpdate(dt); } } public static void LateUpdateCameraRot(float dt) { if (!player) { return; } PreUpdateCamFwd = player.cameraTransform.forward; ActionModuleProceduralRecoil.EFTProceduralRecoilData recoilData = RecoilData; if (recoilData != null) { var rangedData = recoilData.rangedData; bool aiming = rangedData.invData.holdingEntity.AimingGun; if (recoilData.HandRotValueCur != Vector2.zero) { //when shooting while aiming, adds a portion of hand rot to camera; //return to 0 when not aiming or shooting if (rangedData.state != ItemActionFiringState.Off && aiming) { CamRecoilLerpSpeed = Mathf.Clamp(CamRecoilLerpSpeed + CamRecoilLerpSpeedStep * dt, CamRecoilLerpSpeedRange.x, CamRecoilLerpSpeedRange.y); if (!recoilData.IsStable) { CamRecoilOffsetStable = recoilData.HandRotValueCur; } CamRecoilOffsetCur = Vector2.Lerp(CamRecoilOffsetCur, CamRecoilOffsetStable, CamRecoilLerpSpeed); } else { CamRecoilLerpSpeed = Mathf.Clamp(CamRecoilLerpSpeed - CamRecoilLerpSpeedStep * dt, CamRecoilLerpSpeedRange.x, CamRecoilLerpSpeedRange.y); if (rangedData.state == ItemActionFiringState.Off && aiming) { CamRecoilOffsetStable = CamRecoilOffsetCur = Vector2.Lerp(CamRecoilOffsetCur, recoilData.HandRotValueCur, CamRecoilLerpSpeed); } else { CamRecoilOffsetStable = CamRecoilOffsetCur = Vector2.Lerp(CamRecoilOffsetCur, Vector2.zero, CamRecoilLerpSpeed); } } } else { CamRecoilLerpSpeed = Mathf.Clamp(CamRecoilLerpSpeed - CamRecoilLerpSpeedStep * dt, CamRecoilLerpSpeedRange.x, CamRecoilLerpSpeedRange.y); CamRecoilOffsetStable = CamRecoilOffsetCur = Vector2.Lerp(CamRecoilOffsetCur, Vector2.zero, CamRecoilLerpSpeed); } //if (CamRecoilOffsetCur.sqrMagnitude > 0.0001f) //{ // Log.Out($"CamRecoilOffsetCur {CamRecoilOffsetCur}, CamRecoilOffsetStable {CamRecoilOffsetStable}, HandRotValueCur {recoilData.HandRotValueCur}"); //} if (aiming && rangedData.state != ItemActionFiringState.Off) { CamAimRecoilRotOffsetCur = Vector2.Lerp(CamAimRecoilRotOffsetCur, -recoilData.HandRotValueApply, CamAimRecoilRotOffsetLerpSpeed * dt); } else { CamAimRecoilRotOffsetCur = Vector2.Lerp(CamAimRecoilRotOffsetCur, Vector2.zero, 5 * dt); } } else { CamRecoilLerpSpeed = Mathf.Clamp(CamRecoilLerpSpeed - CamRecoilLerpSpeedStep * dt, CamRecoilLerpSpeedRange.x, CamRecoilLerpSpeedRange.y); CamRecoilOffsetStable = CamRecoilOffsetCur = Vector2.Lerp(CamRecoilOffsetCur, Vector2.zero, CamRecoilLerpSpeed); CamAimRecoilPosOffsetStable = CamAimRecoilPosOffsetCur = Vector3.Lerp(CamAimRecoilPosOffsetCur, Vector3.zero, CamAimRecoilPosSmoothOut * dt); CamAimRecoilRotOffsetCur = Vector2.Lerp(CamAimRecoilRotOffsetCur, Vector2.zero, 5 * dt); } Vector2 realCamRecoilOffset = CamRecoilOffsetCur; if (recoilData != null) { realCamRecoilOffset += recoilData.CamRotValue; } //realCamRecoilOffset.x = Mathf.Min(0, realCamRecoilOffset.x); LocalCamRotOffsetCur = Quaternion.Euler(realCamRecoilOffset); Quaternion targetCameraRotation = Quaternion.Euler(CamRecoilOffsetCur) * player.cameraTransform.localRotation; Quaternion cameraParentRotation = player.cameraTransform.parent?.rotation ?? Quaternion.identity; InverseWorldCamKickOffsetCur = player.cameraTransform.rotation * Quaternion.Inverse(cameraParentRotation * targetCameraRotation); if (recoilData != null) { targetCameraRotation = Quaternion.Euler(recoilData.CamRotValue) * player.cameraTransform.localRotation; InverseWorldCamShakeOffsetCur = player.cameraTransform.rotation * Quaternion.Inverse(cameraParentRotation * targetCameraRotation); } else { InverseWorldCamShakeOffsetCur = Quaternion.identity; } targetCameraRotation = LocalCamRotOffsetCur * player.cameraTransform.localRotation; InverseWorldCamTotalOffsetCur = player.cameraTransform.rotation * Quaternion.Inverse(cameraParentRotation * targetCameraRotation); } public static void LateUpdateCamRecoilPosOffset(Vector3 CamAimRecoilPosOffsetTarget, float dt, bool aiming) { CamAimRecoilPosOffsetTarget = player.cameraTransform.InverseTransformDirection(CamAimRecoilPosOffsetTarget); if (CamAimRecoilPosOffsetTarget != Vector3.zero && aiming) { float lerpStepXY = Mathf.Lerp(CamAimRecoilPosLerpSpeedXYMin, CamAimRecoilPosLerpSpeedXYMax, (Time.time - LastShotTime) * CamAimRecoilPosLerpSpeedStep); ActionModuleProceduralRecoil.EFTProceduralRecoilData recoilData = RecoilData; ItemActionRanged.ItemActionDataRanged rangedData = recoilData.rangedData; if (rangedData.state == ItemActionFiringState.Loop && aiming) { if (!recoilData.IsStable) { CamAimRecoilPosOffsetStable = CamAimRecoilPosOffsetTarget; } CamAimRecoilPosOffsetCur = new Vector3(Mathf.Lerp(CamAimRecoilPosOffsetCur.x, CamAimRecoilPosOffsetTarget.x, lerpStepXY * dt), Mathf.Lerp(CamAimRecoilPosOffsetCur.y, CamAimRecoilPosOffsetTarget.y, lerpStepXY * dt), Mathf.Lerp(CamAimRecoilPosOffsetCur.z, CamAimRecoilPosOffsetTarget.z, CamAimRecoilPosSmoothIn * dt)); } else { if (rangedData.state != ItemActionFiringState.Loop && aiming) { CamAimRecoilPosOffsetCur = new Vector3(Mathf.Lerp(CamAimRecoilPosOffsetCur.x, CamAimRecoilPosOffsetTarget.x, lerpStepXY * dt), Mathf.Lerp(CamAimRecoilPosOffsetCur.y, CamAimRecoilPosOffsetTarget.y, lerpStepXY * dt), Mathf.Lerp(CamAimRecoilPosOffsetCur.z, CamAimRecoilPosOffsetTarget.z, CamAimRecoilPosSmoothIn * dt)); } else { CamAimRecoilPosOffsetCur = Vector3.Lerp(CamAimRecoilPosOffsetCur, Vector3.zero, CamAimRecoilPosSmoothOut * dt); } CamAimRecoilPosOffsetStable = CamAimRecoilPosOffsetCur; } } else { CamAimRecoilPosOffsetStable = CamAimRecoilPosOffsetCur = Vector3.Lerp(CamAimRecoilPosOffsetCur, Vector3.zero, CamAimRecoilPosSmoothOut * dt); } } public static void LateUpdateApplyCamRot() { if (!player || !player.bFirstPersonView) { return; } player.cameraTransform.localRotation *= LocalCamRotOffsetCur; player.cameraTransform.localPosition += CamAimRecoilPosOffsetCur; } } [HarmonyPatch] public static class ProceduralRecoilPatches { [HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.Awake))] [HarmonyPostfix] private static void Postfix_Awake_EntityPlayerLocal(EntityPlayerLocal __instance) { ProceduralRecoilUpdater.InitPlayer(__instance); } [HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.LateUpdate))] [HarmonyPostfix] private static void Postfix_LateUpdate_EntityPlayerLocal(EntityPlayerLocal __instance) { ProceduralRecoilUpdater.LateUpdateCameraRot(Time.deltaTime); if (__instance.bFirstPersonView) { ProceduralRecoilUpdater.RecoilData?.LateUpdate(Time.deltaTime); } } [HarmonyPatch(typeof(vp_FPCamera), nameof(vp_FPCamera.LateUpdate))] [HarmonyPostfix] private static void Postfix_LateUpdate_vp_FPCamera() { ProceduralRecoilUpdater.LateUpdateApplyCamRot(); } [HarmonyPatch(typeof(vp_FPWeapon), nameof(vp_FPWeapon.FixedUpdate))] [HarmonyPostfix] private static void Postfix_FixedUpdate_vp_FPWeapon() { ProceduralRecoilUpdater.FixedUpdate(Time.fixedDeltaTime); } }