1+ using HarmonyLib ;
2+ using LCVR . Assets ;
3+ using LCVR . Managers ;
4+ using LCVR . Patches ;
5+ using LCVR . Player ;
6+ using UnityEngine ;
7+ using Object = UnityEngine . Object ;
8+
9+ namespace LCVR . Physics . Interactions ;
10+
11+ public class VRLadder : MonoBehaviour , VRInteractable
12+ {
13+ private const float MAX_CLIMB_SPEED = 3.0f ;
14+ private const float CLIMB_STRENGTH = 1.0f ;
15+
16+ private InteractTrigger ladderTrigger ;
17+
18+ private Vector3 ? leftHandGripPoint ;
19+ private Vector3 ? rightHandGripPoint ;
20+
21+ private VRInteractor leftHandInteractor ;
22+ private VRInteractor rightHandInteractor ;
23+
24+ private bool isActiveLadder ;
25+ private float climbStartTime ;
26+
27+ public InteractableFlags Flags => InteractableFlags . BothHands ;
28+
29+ private void Awake ( )
30+ {
31+ ladderTrigger = GetComponentInParent < InteractTrigger > ( ) ;
32+ }
33+
34+ private void Update ( )
35+ {
36+ if ( VRSession . Instance is not { } instance )
37+ return ;
38+
39+ var player = instance . LocalPlayer . PlayerController ;
40+
41+ if ( ! player . isClimbingLadder || ! isActiveLadder )
42+ return ;
43+
44+ var totalMovement = Vector3 . zero ;
45+ var grippingHands = 0 ;
46+
47+ if ( leftHandGripPoint . HasValue )
48+ {
49+ var leftHand = VRSession . Instance . LocalPlayer . LeftHandVRTarget ;
50+ var worldGripPoint = transform . TransformPoint ( leftHandGripPoint . Value ) ;
51+ var pullVector = worldGripPoint - leftHand . position ;
52+ totalMovement += pullVector ;
53+ grippingHands ++ ;
54+ }
55+
56+ if ( rightHandGripPoint . HasValue )
57+ {
58+ var rightHand = VRSession . Instance . LocalPlayer . RightHandVRTarget ;
59+ var worldGripPoint = transform . TransformPoint ( rightHandGripPoint . Value ) ;
60+ var pullVector = worldGripPoint - rightHand . position ;
61+ totalMovement += pullVector ;
62+ grippingHands ++ ;
63+ }
64+
65+ totalMovement *= CLIMB_STRENGTH / grippingHands ;
66+ totalMovement . x = 0 ;
67+ totalMovement . z = 0 ;
68+
69+ var maxMovementThisFrame = MAX_CLIMB_SPEED * Time . deltaTime ;
70+ if ( Mathf . Abs ( totalMovement . y ) > maxMovementThisFrame )
71+ totalMovement . y = Mathf . Sign ( totalMovement . y ) * maxMovementThisFrame ;
72+
73+ if ( Mathf . Abs ( totalMovement . y ) > 0.001f )
74+ player . thisPlayerBody . position += totalMovement ;
75+
76+ if ( Time . time - climbStartTime < 0.5f || ladderTrigger . topOfLadderPosition == null )
77+ return ;
78+
79+ var topY = ladderTrigger . topOfLadderPosition . position . y ;
80+ var playerHeadY = player . gameplayCamera . transform . position . y ;
81+
82+ if ( playerHeadY < topY - 0.3f )
83+ return ;
84+
85+ Vector3 exitPosition ;
86+
87+ if ( ladderTrigger . useRaycastToGetTopPosition )
88+ {
89+ var rayStart = player . transform . position + Vector3 . up * 0.5f ;
90+ var rayEnd = ladderTrigger . topOfLadderPosition . position + Vector3 . up * 0.5f ;
91+
92+ exitPosition = UnityEngine . Physics . Linecast ( rayStart , rayEnd , out var hit ,
93+ StartOfRound . Instance . collidersAndRoomMaskAndDefault ,
94+ QueryTriggerInteraction . Ignore )
95+ ? hit . point
96+ : ladderTrigger . topOfLadderPosition . position ;
97+ }
98+ else
99+ {
100+ exitPosition = ladderTrigger . topOfLadderPosition . position ;
101+ }
102+
103+ ExitLadder ( player , exitPosition ) ;
104+ }
105+
106+ private void ExitLadder ( GameNetcodeStuff . PlayerControllerB player , Vector3 exitPosition )
107+ {
108+ leftHandGripPoint = null ;
109+ rightHandGripPoint = null ;
110+
111+ if ( leftHandInteractor != null )
112+ {
113+ leftHandInteractor . FingerCurler . ForceFist ( false ) ;
114+ leftHandInteractor . isHeld = false ;
115+ leftHandInteractor = null ;
116+ }
117+
118+ if ( rightHandInteractor != null )
119+ {
120+ rightHandInteractor . FingerCurler . ForceFist ( false ) ;
121+ rightHandInteractor . isHeld = false ;
122+ rightHandInteractor = null ;
123+ }
124+
125+ isActiveLadder = false ;
126+
127+ player . isClimbingLadder = false ;
128+ player . thisController . enabled = true ;
129+ player . inSpecialInteractAnimation = false ;
130+ player . UpdateSpecialAnimationValue ( false ) ;
131+
132+ player . takingFallDamage = false ;
133+ player . fallValue = 0f ;
134+ player . fallValueUncapped = 0f ;
135+
136+ // TODO: Coroutine that smoothly places player at exit position
137+ player . TeleportPlayer ( exitPosition ) ;
138+
139+ ladderTrigger . usingLadder = false ;
140+ ladderTrigger . isPlayingSpecialAnimation = false ;
141+ ladderTrigger . lockedPlayer = null ;
142+ }
143+
144+ public bool OnButtonPress ( VRInteractor interactor )
145+ {
146+ var player = VRSession . Instance . LocalPlayer . PlayerController ;
147+
148+ // Store grip point in ladder's local space
149+ if ( interactor . IsRightHand )
150+ {
151+ rightHandGripPoint =
152+ transform . InverseTransformPoint ( VRSession . Instance . LocalPlayer . RightHandVRTarget . position ) ;
153+ rightHandInteractor = interactor ;
154+ }
155+ else
156+ {
157+ leftHandGripPoint =
158+ transform . InverseTransformPoint ( VRSession . Instance . LocalPlayer . LeftHandVRTarget . position ) ;
159+ leftHandInteractor = interactor ;
160+ }
161+
162+ if ( ! player . isClimbingLadder )
163+ {
164+ if ( ladderTrigger != null && ladderTrigger . interactable )
165+ {
166+ isActiveLadder = true ;
167+ climbStartTime = Time . time ;
168+ player . isClimbingLadder = true ;
169+ player . thisController . enabled = false ;
170+
171+ player . takingFallDamage = false ;
172+ player . fallValue = 0f ;
173+ player . fallValueUncapped = 0f ;
174+ }
175+ else
176+ {
177+ return false ;
178+ }
179+ }
180+ else if ( player . isClimbingLadder && ! isActiveLadder )
181+ {
182+ return false ;
183+ }
184+
185+ interactor . FingerCurler . ForceFist ( true ) ;
186+ return true ;
187+ }
188+
189+ public void OnButtonRelease ( VRInteractor interactor )
190+ {
191+ if ( interactor . IsRightHand )
192+ {
193+ rightHandGripPoint = null ;
194+ rightHandInteractor = null ;
195+ }
196+ else
197+ {
198+ leftHandGripPoint = null ;
199+ leftHandInteractor = null ;
200+ }
201+
202+ interactor . FingerCurler . ForceFist ( false ) ;
203+
204+ if ( leftHandGripPoint . HasValue || rightHandGripPoint . HasValue || ! isActiveLadder )
205+ return ;
206+
207+ var player = VRSession . Instance . LocalPlayer . PlayerController ;
208+
209+ isActiveLadder = false ;
210+ player . isClimbingLadder = false ;
211+ player . thisController . enabled = true ;
212+ player . inSpecialInteractAnimation = false ;
213+ player . UpdateSpecialAnimationValue ( false , 0 , 0f , false ) ;
214+
215+ player . takingFallDamage = false ;
216+ player . fallValue = 0f ;
217+ player . fallValueUncapped = 0f ;
218+ }
219+
220+ public void OnColliderEnter ( VRInteractor interactor ) { }
221+ public void OnColliderExit ( VRInteractor interactor ) { }
222+ }
223+
224+ // Lightweight wrapper that forwards to the shared ladder component
225+ internal class VRLadderInteractable : MonoBehaviour , VRInteractable
226+ {
227+ public VRLadder ladder ;
228+
229+ public InteractableFlags Flags => InteractableFlags . BothHands ;
230+
231+ public bool OnButtonPress ( VRInteractor interactor ) => ladder . OnButtonPress ( interactor ) ;
232+ public void OnButtonRelease ( VRInteractor interactor ) => ladder . OnButtonRelease ( interactor ) ;
233+ public void OnColliderEnter ( VRInteractor interactor ) { }
234+ public void OnColliderExit ( VRInteractor interactor ) { }
235+ }
236+
237+ [ LCVRPatch ]
238+ [ HarmonyPatch ]
239+ internal static class LadderPatches
240+ {
241+ [ HarmonyPatch ( typeof ( InteractTrigger ) , nameof ( InteractTrigger . Start ) ) ]
242+ [ HarmonyPostfix ]
243+ private static void OnLadderStart ( InteractTrigger __instance )
244+ {
245+ if ( ! __instance . isLadder )
246+ return ;
247+
248+ if ( Plugin . Config . DisableLadderClimbingInteraction . Value )
249+ return ;
250+
251+ var ladderComponent = __instance . gameObject . AddComponent < VRLadder > ( ) ;
252+
253+ // Create two separate colliders offset to left and right
254+ // This allows both hands to interact simultaneously
255+ var leftHandCollider = Object . Instantiate ( AssetManager . Interactable , __instance . transform ) ;
256+ var rightHandCollider = Object . Instantiate ( AssetManager . Interactable , __instance . transform ) ;
257+
258+ if ( __instance . topOfLadderPosition != null && __instance . bottomOfLadderPosition != null )
259+ {
260+ var topPos = __instance . topOfLadderPosition . localPosition ;
261+ var bottomPos = __instance . bottomOfLadderPosition . localPosition ;
262+ var midPoint = ( topPos + bottomPos ) / 2f ;
263+ var height = Mathf . Abs ( topPos . y - bottomPos . y ) ;
264+
265+ // Offset left collider to the left side
266+ leftHandCollider . transform . localPosition = midPoint + new Vector3 ( - 0.3f , 0f , 0.3f ) ;
267+ leftHandCollider . transform . localScale = new Vector3 ( 0.8f , height , 0.8f ) ;
268+
269+ // Offset right collider to the right side
270+ rightHandCollider . transform . localPosition = midPoint + new Vector3 ( 0.3f , 0f , 0.3f ) ;
271+ rightHandCollider . transform . localScale = new Vector3 ( 0.8f , height , 0.8f ) ;
272+ }
273+ else
274+ {
275+ leftHandCollider . transform . localPosition = new Vector3 ( - 0.3f , 0f , 0.3f ) ;
276+ leftHandCollider . transform . localScale = new Vector3 ( 0.8f , 3f , 0.8f ) ;
277+
278+ rightHandCollider . transform . localPosition = new Vector3 ( 0.3f , 0f , 0.3f ) ;
279+ rightHandCollider . transform . localScale = new Vector3 ( 0.8f , 3f , 0.8f ) ;
280+ }
281+
282+ // Both colliders reference the same ladder component
283+ leftHandCollider . AddComponent < VRLadderInteractable > ( ) . ladder = ladderComponent ;
284+ rightHandCollider . AddComponent < VRLadderInteractable > ( ) . ladder = ladderComponent ;
285+
286+ foreach ( var collider in __instance . GetComponents < Collider > ( ) )
287+ collider . enabled = false ;
288+ }
289+
290+ [ HarmonyPatch ( typeof ( InteractTrigger ) , nameof ( InteractTrigger . Interact ) ) ]
291+ [ HarmonyPrefix ]
292+ private static bool PreventLadderInteract ( InteractTrigger __instance )
293+ {
294+ if ( ! __instance . isLadder )
295+ return true ;
296+
297+ if ( Plugin . Config . DisableLadderClimbingInteraction . Value )
298+ return true ;
299+
300+ return false ;
301+ }
302+ }
0 commit comments