1
1
--- @diagnostic disable : undefined-global
2
- -- filepath: /home/cretin/git/ Cortex-Command-Community-Project/Data/Base.rte/Devices/Tools/GrappleGun/Grapple.lua
2
+ -- filepath: /Cortex-Command-Community-Project/Data/Base.rte/Devices/Tools/GrappleGun/Grapple.lua
3
3
-- Main logic for the grapple claw MovableObject.
4
4
5
5
-- Load Modules
6
6
local RopePhysics = require (" Devices.Tools.GrappleGun.Scripts.RopePhysics" )
7
7
local RopeRenderer = require (" Devices.Tools.GrappleGun.Scripts.RopeRenderer" )
8
8
local RopeInputController = require (" Devices.Tools.GrappleGun.Scripts.RopeInputController" )
9
9
local RopeStateManager = require (" Devices.Tools.GrappleGun.Scripts.RopeStateManager" )
10
-
11
10
function Create (self )
12
11
self .lastPos = self .Pos
13
12
@@ -18,11 +17,12 @@ function Create(self)
18
17
19
18
-- Initialize state using the state manager. This sets self.actionMode = 0.
20
19
RopeStateManager .initState (self )
21
-
22
20
-- self.initializationOk = true -- This flag is effectively replaced by checking self.actionMode == 0 in Update.
23
21
24
22
-- Core grapple properties
25
- self .fireVel = 40 -- Initial velocity of the hook. Overwrites .ini FireVel. Crucial for HDFirearm.
23
+ self .fireVel = 30 -- Initial velocity of the hook. Overwrites .ini FireVel.
24
+ self .hookRadius = 50 -- Reduced from 360 for more precise parent finding
25
+
26
26
self .maxLineLength = 600 -- Maximum allowed length of the rope.
27
27
self .maxShootDistance = self .maxLineLength * 0.95 -- Hook will detach if it travels further than this before sticking.
28
28
self .setLineLength = 0 -- Target length set by input/logic.
@@ -33,10 +33,13 @@ function Create(self)
33
33
self .stretchPullRatio = 0.0 -- No stretching for rigid rope.
34
34
self .pieSelection = 0 -- Current pie menu selection (0: none, 1: full retract, etc.).
35
35
36
+
36
37
-- Timing and interval properties for rope actions
37
38
self .climbDelay = 8 -- Delay between climb ticks.
38
39
self .tapTime = 150 -- Max time between taps for double-tap unhook.
39
40
self .tapAmount = 2 -- Number of taps required for unhook.
41
+ self .tapCounter = 0 -- Current tap count for multi-tap detection.
42
+ self .canTap = false -- Flag to register the first tap in a sequence.
40
43
self .mouseClimbLength = 200 -- Duration mouse scroll input is considered active.
41
44
self .climbInterval = 4.0 -- Amount rope length changes per climb tick.
42
45
self .autoClimbIntervalA = 5.0 -- Auto-retract speed (primary).
@@ -82,56 +85,79 @@ function Create(self)
82
85
end
83
86
84
87
function Update (self )
85
- if self .ToDelete then return end -- Already marked for deletion from a previous frame or early in this one.
88
+ if self .ToDelete then return end
86
89
87
90
-- First-time setup: Find parent, initialize velocity, anchor points, etc.
88
91
if self .actionMode == 0 then
89
92
local foundAndValidParent = false
90
- for gun_mo in MovableMan :GetMOsInRadius (self .Pos , 75 ) do
93
+ for gun_mo in MovableMan :GetMOsInRadius (self .Pos , self . hookRadius ) do
91
94
if gun_mo and gun_mo .ClassName == " HDFirearm" and gun_mo .PresetName == " Grapple Gun" then
92
95
local hdfGun = ToHDFirearm (gun_mo )
93
96
if hdfGun and SceneMan :ShortestDistance (self .Pos , hdfGun .MuzzlePos , self .mapWrapsX ):MagnitudeIsLessThan (20 ) then
94
97
self .parentGun = hdfGun
95
98
local rootParentMO = MovableMan :GetMOFromID (hdfGun .RootID )
96
- if rootParentMO then
97
- if MovableMan :IsActor (rootParentMO ) then
98
- self .parent = ToActor (rootParentMO ) -- Store as Actor type
99
-
100
- -- Initialize player anchor point (segment 0)
101
- self .apx [0 ] = self .parent .Pos .X
102
- self .apy [0 ] = self .parent .Pos .Y
103
- self .lastX [0 ] = self .parent .Pos .X - (self .parent .Vel .X or 0 )
104
- self .lastY [0 ] = self .parent .Pos .Y - (self .parent .Vel .Y or 0 )
105
-
106
- -- Set initial velocity of the hook based on parent's aim and velocity
107
- local aimAngle = self .parent :GetAimAngle (true )
108
- self .Vel = (self .parent .Vel or Vector (0 ,0 )) + Vector (self .fireVel , 0 ):RadRotate (aimAngle )
109
-
110
- -- Initialize hook's lastX/Y for its initial trajectory
111
- self .lastX [self .currentSegments ] = self .Pos .X - self .Vel .X
112
- self .lastY [self .currentSegments ] = self .Pos .Y - self .Vel .Y
113
-
114
- if self .parentGun then -- Should be valid here
115
- self .parentGun :RemoveNumberValue (" GrappleMode" ) -- Clear any previous mode
116
- end
117
-
118
- -- Determine parent's effective radius for terrain checks
119
- self .parentRadius = 5 -- Default radius
120
- if self .parent .Attachables and type (self .parent .Attachables ) == " table" then
121
- for _ , part in ipairs (self .parent .Attachables ) do
122
- if part and part .Pos and part .Radius then
123
- local radcheck = SceneMan :ShortestDistance (self .parent .Pos , part .Pos , self .mapWrapsX ).Magnitude + part .Radius
124
- if self .parentRadius == nil or radcheck > self .parentRadius then
125
- self .parentRadius = radcheck
126
- end
99
+ if rootParentMO and MovableMan :IsActor (rootParentMO ) then
100
+ self .parent = ToActor (rootParentMO )
101
+ self .apx [0 ] = self .parent .Pos .X
102
+ self .apy [0 ] = self .parent .Pos .Y
103
+ self .lastX [0 ] = self .parent .Pos .X - (self .parent .Vel .X or 0 )
104
+ self .lastY [0 ] = self .parent .Pos .Y - (self .parent .Vel .Y or 0 )
105
+
106
+ -- Set initial velocity of the hook based on parent's aim and velocity
107
+ local aimAngle = self .parent :GetAimAngle (true )
108
+ self .Vel = (self .parent .Vel or Vector (0 ,0 )) + Vector (self .fireVel , 0 ):RadRotate (aimAngle )
109
+
110
+ -- Initialize hook's lastX/Y for its initial trajectory
111
+ self .lastX [self .currentSegments ] = self .Pos .X - self .Vel .X
112
+ self .lastY [self .currentSegments ] = self .Pos .Y - self .Vel .Y
113
+
114
+ if self .parentGun then -- Should be valid here
115
+ self .parentGun :RemoveNumberValue (" GrappleMode" ) -- Clear any previous mode
116
+ end
117
+
118
+ -- Determine parent's effective radius for terrain checks
119
+ self .parentRadius = 5 -- Default radius
120
+ if self .parent .Attachables and type (self .parent .Attachables ) == " table" then
121
+ for _ , part in ipairs (self .parent .Attachables ) do
122
+ if part and part .Pos and part .Radius then
123
+ local radcheck = SceneMan :ShortestDistance (self .parent .Pos , part .Pos , self .mapWrapsX ).Magnitude + part .Radius
124
+ if self .parentRadius == nil or radcheck > self .parentRadius then
125
+ self .parentRadius = radcheck
127
126
end
128
127
end
129
128
end
130
- self .actionMode = 1 -- Set to flying, initialization successful
131
- foundAndValidParent = true
132
- end -- if MovableMan:IsActor(rootParentMO)
133
- end -- if rootParentMO
134
- break -- Found our gun, processed it.
129
+ end
130
+ self .actionMode = 1 -- Set to flying, initialization successful
131
+
132
+ -- Initialize rope segments for display during flight with proper physics
133
+ -- First segment is at the shooter's position, last segment is at hook position
134
+ -- Use more segments for better physics and visuals
135
+ self .currentSegments = 4 -- Start with more segments for better physics during flight
136
+ self .apx [0 ] = self .parent .Pos .X
137
+ self .apy [0 ] = self .parent .Pos .Y
138
+ self .lastX [0 ] = self .parent .Pos .X - (self .parent .Vel .X or 0 )
139
+ self .lastY [0 ] = self .parent .Pos .Y - (self .parent .Vel .Y or 0 )
140
+
141
+ -- Initialize the hook segment
142
+ self .apx [self .currentSegments ] = self .Pos .X
143
+ self .apy [self .currentSegments ] = self .Pos .Y
144
+ self .lastX [self .currentSegments ] = self .Pos .X - (self .Vel .X or 0 )
145
+ self .lastY [self .currentSegments ] = self .Pos .Y - (self .Vel .Y or 0 )
146
+
147
+ -- Initialize intermediate segments with a natural drape
148
+ for i = 1 , self .currentSegments - 1 do
149
+ local t = i / self .currentSegments
150
+ self .apx [i ] = self .parent .Pos .X + t * (self .Pos .X - self .parent .Pos .X )
151
+ self .apy [i ] = self .parent .Pos .Y + t * (self .Pos .Y - self .parent .Pos .Y )
152
+ -- Add slight droop for natural look
153
+ self .apy [i ] = self .apy [i ] + math.sin (t * math.pi ) * 2
154
+ -- Initialize lastX/Y with small velocity matching the overall direction
155
+ self .lastX [i ] = self .apx [i ] - (self .Vel .X or 0 ) * 0.2
156
+ self .lastY [i ] = self .apy [i ] - (self .Vel .Y or 0 ) * 0.2
157
+ end
158
+
159
+ foundAndValidParent = true
160
+ end -- if MovableMan:IsActor(rootParentMO)
135
161
end -- if hdfGun and distance check
136
162
end -- if gun_mo is grapple gun
137
163
end -- for gun_mo
@@ -167,6 +193,22 @@ function Update(self)
167
193
end
168
194
local player = controller .Player or 0
169
195
196
+ -- Handle pie menu modes
197
+ if self .parentGun then
198
+ local mode = self .parentGun :GetNumberValue (" GrappleMode" )
199
+ if mode ~= 0 then
200
+ if mode == 3 then -- Unhook via Pie Menu
201
+ self .ToDelete = true
202
+ if self .parentGun then
203
+ self .parentGun :RemoveNumberValue (" GrappleMode" )
204
+ end
205
+ else
206
+ self .pieSelection = mode
207
+ self .parentGun :RemoveNumberValue (" GrappleMode" )
208
+ end
209
+ end
210
+ end
211
+
170
212
-- Standard update flags
171
213
self .ToSettle = false -- Grapple claw should not settle
172
214
@@ -182,7 +224,14 @@ function Update(self)
182
224
-- Hook position is determined by its own physics
183
225
self .apx [self .currentSegments ] = self .Pos .X
184
226
self .apy [self .currentSegments ] = self .Pos .Y
185
- -- lastX/Y for the hook end are updated by its own Verlet integration
227
+ -- Initialize lastX/Y for the hook end if not set
228
+ if not self .lastX [self .currentSegments ] then
229
+ self .lastX [self .currentSegments ] = self .Pos .X - (self .Vel .X or 0 )
230
+ self .lastY [self .currentSegments ] = self .Pos .Y - (self .Vel .Y or 0 )
231
+ end
232
+
233
+ -- Use full Verlet physics during flight, not just simple line positioning
234
+ -- This ensures consistent rope behavior across all action modes
186
235
elseif self .actionMode == 2 then -- Grabbed terrain
187
236
-- Hook position is fixed where it grabbed
188
237
self .Pos .X = self .apx [self .currentSegments ] -- Ensure self.Pos matches anchor
@@ -233,13 +282,35 @@ function Update(self)
233
282
234
283
-- Dynamic rope segment calculation
235
284
local desiredSegments = RopePhysics .calculateOptimalSegments (self , math.max (1 , self .currentLineLength ))
236
- if desiredSegments ~= self .currentSegments and math.abs (desiredSegments - self .currentSegments ) > 1 then -- Hysteresis
285
+
286
+ -- In flying mode, ensure we have enough intermediate segments for proper Verlet physics
287
+ if self .actionMode == 1 then
288
+ -- For short distances, use at least 6 segments
289
+ -- For longer distances, use enough segments for proper rope physics
290
+ -- This higher segment count is essential for proper Verlet physics simulation
291
+ local minSegmentsForFlight = math.max (6 , math.floor (self .lineLength / 25 ))
292
+ desiredSegments = math.max (minSegmentsForFlight , desiredSegments )
293
+ end
294
+
295
+ -- Update segments if needed, with reduced hysteresis threshold for flight mode
296
+ -- This ensures smoother transitions as the rope extends
297
+ local segmentUpdateThreshold = self .actionMode == 1 and 1 or 2
298
+ if desiredSegments ~= self .currentSegments and math.abs (desiredSegments - self .currentSegments ) >= segmentUpdateThreshold then
237
299
RopePhysics .resizeRopeSegments (self , desiredSegments )
238
300
end
239
301
240
302
-- Core rope physics simulation
241
303
RopePhysics .updateRopePhysics (self , parentActor .Pos , self .Pos , self .currentLineLength )
242
304
305
+ -- Check for hook attachment collisions (only when flying)
306
+ if self .actionMode == 1 then
307
+ local stateChanged = RopeStateManager .checkAttachmentCollisions (self )
308
+ if stateChanged then
309
+ -- Rope physics may need re-initialization after attachment
310
+ self .ropePhysicsInitialized = false
311
+ end
312
+ end
313
+
243
314
-- Apply constraints and check for breaking
244
315
local ropeBreaks = RopePhysics .applyRopeConstraints (self , self .currentLineLength )
245
316
if ropeBreaks or self .shouldBreak then -- self.shouldBreak can be set by other logic
@@ -284,16 +355,34 @@ function Update(self)
284
355
end
285
356
286
357
-- Player-specific controls and unhooking mechanisms
287
- if IsAHuman (parentActor ) then -- Or IsACrab, if they can use it
288
- local parentHuman = ToAHuman (parentActor ) -- Cast for specific human properties if needed
289
- if parentHuman :IsPlayerControlled () then
290
- -- Unhook with Reload key (R)
291
- if RopeInputController .handleReloadKeyUnhook (self , controller ) then
292
- self .ToDelete = true
358
+ if IsAHuman (parentActor ) or IsACrab (parentActor ) then
359
+ if parentActor :IsPlayerControlled () then
360
+ -- R key unhooking functionality
361
+ local isHoldingGrapple = false
362
+
363
+ -- Check if holding grapple in main hand
364
+ if self .parent .EquippedItem and self .parentGun and self .parent .EquippedItem .ID == self .parentGun .ID then
365
+ isHoldingGrapple = true
293
366
end
294
- -- Unhook with double-tap crouch (if not holding the gun)
295
- if RopeInputController .handleTapDetection (self , controller ) then
367
+
368
+ -- Check if holding grapple in off-hand
369
+ local isHoldingInBG = self .parent .EquippedBGItem and self .parentGun and
370
+ self .parent .EquippedBGItem .ID == self .parentGun .ID
371
+
372
+ -- If reload key pressed while holding grapple gun, unhook
373
+ if controller :IsState (Controller .WEAPON_RELOAD ) and (isHoldingGrapple or isHoldingInBG ) then
374
+ print (" R key unhook triggered!" ) -- Debug message
296
375
self .ToDelete = true
376
+ return -- Exit immediately to prevent other checks
377
+ end
378
+
379
+ -- Unhook with double-tap crouch (ONLY when NOT holding the gun)
380
+ if not isHoldingGrapple and not isHoldingInBG then
381
+ if RopeInputController .handleTapDetection (self , controller ) then
382
+ print (" Double-tap unhook triggered!" ) -- Debug message
383
+ self .ToDelete = true
384
+ return -- Exit immediately
385
+ end
297
386
end
298
387
end
299
388
-- Gun stance offset when holding the gun
@@ -306,52 +395,26 @@ function Update(self)
306
395
end
307
396
end
308
397
309
- -- Handle Pie Menu actions
398
+ -- Delegate all input handling to RopeInputController
399
+ -- 1. Pie menu selection (unhook, retract, extend)
310
400
if RopeInputController .handlePieMenuSelection (self ) then
311
- self .ToDelete = true -- Pie menu selected "Unhook"
401
+ self .ToDelete = true
402
+ if self .parentGun then self .parentGun :RemoveNumberValue (" GrappleMode" ) end
403
+ return
312
404
end
313
-
314
- -- Manage crank sound
315
- if not self .crankSoundInstance or self .crankSoundInstance .ToDelete then
316
- self .crankSoundInstance = CreateAEmitter (" Grapple Gun Sound Crank" )
317
- self .crankSoundInstance .Pos = parentActor .Pos
318
- MovableMan :AddParticle (self .crankSoundInstance )
319
- else
320
- self .crankSoundInstance .Pos = parentActor .Pos
321
- if self .lastSetLineLength and math.abs (self .lastSetLineLength - self .currentLineLength ) > 0.1 then
322
- self .crankSoundInstance :EnableEmission (true )
323
- else
324
- self .crankSoundInstance :EnableEmission (false )
325
- end
405
+ -- 2. R key (reload) to unhook
406
+ if RopeInputController .handleReloadKeyUnhook (self , controller ) then
407
+ self .ToDelete = true
408
+ return
326
409
end
327
- self .lastSetLineLength = self .currentLineLength
328
-
329
-
330
- -- State-specific updates
331
- if self .actionMode == 1 then -- Hook is in flight
332
- RopeStateManager .applyStretchMode (self ) -- (Currently does nothing if stretchMode is false)
333
- RopeStateManager .checkAttachmentCollisions (self ) -- This can change self.actionMode
334
- -- RopeStateManager.checkLengthLimit(self) -- Length limit during flight is handled above
335
- elseif self .actionMode > 1 then -- Hook has stuck (terrain or MO)
336
- -- Calculate forces affecting player (used by input controller for climb speed)
337
- self .parentForces = 1 + (parentActor .Vel .Magnitude * 10 + parentActor .Mass ) / (1 + self .lineLength )
338
-
339
- local terrCheck = false
340
- if self .parentRadius then
341
- terrCheck = SceneMan :CastStrengthRay (parentActor .Pos ,
342
- self .lineVec :SetMagnitude (self .parentRadius ),
343
- 0 , Vector (), 2 , rte .airID , self .mapWrapsX )
344
- end
345
-
346
- RopeInputController .handleAutoRetraction (self , terrCheck )
347
- RopeInputController .handleRopePulling (self ) -- Handles manual climb/extend inputs
348
-
349
- -- Physics for attached states (pulling player/MO) are now primarily handled by RopePhysics.applyRopeConstraints
350
- -- and the resulting tension. Direct force application here should be minimal or for specific effects.
351
- -- RopeStateManager.applyTerrainPullPhysics(self) -- If direct forces are still desired
352
- -- RopeStateManager.applyMOPullPhysics(self)
410
+ -- 3. Double-tap crouch to unhook (only if not holding gun)
411
+ if RopeInputController .handleTapDetection (self , controller ) then
412
+ self .ToDelete = true
413
+ return
353
414
end
354
-
415
+ -- 4. Mousewheel and directional controls for rope length
416
+ RopeInputController .handleRopePulling (self )
417
+
355
418
-- Render the rope
356
419
RopeRenderer .drawRope (self , player )
357
420
@@ -383,4 +446,4 @@ function Destroy(self)
383
446
ToMOSParticle (self .parentGun .Magazine ).Scale = 1 -- Ensure magazine is visible
384
447
end
385
448
end
386
- end
449
+ end
0 commit comments