Skip to content

Commit 487cfa0

Browse files
committed
Refactor grapple rope physics for improved realism and control
Enhances player swinging mechanics by precisely managing position and tangential velocity when the rope is at maximum length and anchored to terrain. This ensures smooth movement along the tethered arc. Improves how maximum rope length is enforced during hook flight and when attached to objects, deferring to physics constraints for a more natural tethering effect rather than abruptly stopping the hook. Refines the initialization of rope segment positions and their historical states for both the player and hook ends. This leads to more accurate initial rope deployment and better response to movement. Standardizes the number of physics iterations for consistent simulation quality and simplifies rope rendering by always drawing segments. Additionally, adjusts the shift-scroll speed for finer rope length control and modifies target acquisition to better handle complex, multi-part objects.
1 parent 2e3f722 commit 487cfa0

File tree

3 files changed

+122
-125
lines changed

3 files changed

+122
-125
lines changed

Data/Base.rte/Devices/Tools/GrappleGun/Grapple.lua

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function Create(self)
5555
self.currentSegments = self.minSegments -- Current number of segments
5656

5757
-- Mousewheel control variables
58-
self.shiftScrollSpeed = 8.0 -- Faster rope control with Shift+Mousewheel
58+
self.shiftScrollSpeed = 1.0 -- Faster rope control with Shift+Mousewheel
5959

6060
--ESTABLISH LINE
6161
self.apx = {}
@@ -74,8 +74,8 @@ function Create(self)
7474
self.lastY[i] = py
7575
end
7676

77-
self.lastX[self.minSegments] = px - self.Vel.X
78-
self.lastY[self.minSegments] = py - self.Vel.Y
77+
-- self.lastX[self.minSegments] = px - self.Vel.X -- This will be set after parent is found and hook Vel is determined
78+
-- self.lastY[self.minSegments] = py - self.Vel.Y
7979
self.currentSegments = self.minSegments -- Start with minimum segments
8080
--slots 0 and currentSegments are ANCHOR POINTS
8181

@@ -92,7 +92,20 @@ function Create(self)
9292
self.parent = ToACrab(self.parent)
9393
end
9494

95+
-- Initialize player anchor point (segment 0) based on parent's state
96+
self.apx[0] = self.parent.Pos.X
97+
self.apy[0] = self.parent.Pos.Y
98+
self.lastX[0] = self.parent.Pos.X - self.parent.Vel.X
99+
self.lastY[0] = self.parent.Pos.Y - self.parent.Vel.Y
100+
95101
self.Vel = self.parent.Vel + Vector(self.fireVel, 0):RadRotate(self.parent:GetAimAngle(true)) -- Changed: Use full parent velocity
102+
103+
-- Now that hook's self.Vel is set, initialize its lastX/Y for initial trajectory for the hook end
104+
-- Note: self.currentSegments is self.minSegments at this point.
105+
self.lastX[self.currentSegments] = px - self.Vel.X
106+
self.lastY[self.currentSegments] = py - self.Vel.Y
107+
108+
96109
self.parentGun:RemoveNumberValue("GrappleMode")
97110
for part in self.parent.Attachables do
98111
local radcheck = SceneMan:ShortestDistance(self.parent.Pos, part.Pos, self.mapWrapsX).Magnitude + part.Radius
@@ -132,32 +145,27 @@ function Update(self)
132145
if self.actionMode == 1 then
133146
-- Immediately update rope length based on actual hook position
134147
self.lineVec = SceneMan:ShortestDistance(self.parent.Pos, self.Pos, self.mapWrapsX)
135-
self.lineLength = self.lineVec.Magnitude
136-
self.currentLineLength = self.lineLength
137-
138-
-- Check if we've reached the maximum shooting distance during flight
148+
self.lineLength = self.lineVec.Magnitude -- Actual current distance
149+
150+
-- Determine currentLineLength and limitReached status based on maxShootDistance
139151
if self.lineLength >= self.maxShootDistance then
140-
-- Stop the claw at max shooting distance but keep it in flight mode
141-
local maxShootVec = self.lineVec:SetMagnitude(self.maxShootDistance)
142-
self.Pos = self.parent.Pos + maxShootVec
143-
self.Vel = Vector(0, 0) -- Stop the claw
144-
self.currentLineLength = self.maxShootDistance
152+
if not self.limitReached then -- Play sound only on the frame it first reaches the limit
153+
self.clickSound:Play(self.parent.Pos)
154+
end
155+
self.currentLineLength = self.maxShootDistance -- Cap the effective rope length for physics
145156
self.limitReached = true
146-
-- Keep actionMode = 1 (flight) so it can still detect collisions
147-
self.clickSound:Play(self.parent.Pos)
157+
-- By not setting self.Pos or self.Vel directly, we let RopePhysics.applyRopeConstraints
158+
-- handle the "binding" at maxShootDistance, creating a tethered effect.
159+
else
160+
self.currentLineLength = self.lineLength -- Rope is shorter than max, so its length is the actual distance
161+
self.limitReached = false
148162
end
149163

150164
-- Update rope anchor points directly for flight mode
151165
self.apx[0] = self.parent.Pos.X
152166
self.apy[0] = self.parent.Pos.Y
153167
self.apx[self.currentSegments] = self.Pos.X
154168
self.apy[self.currentSegments] = self.Pos.Y
155-
156-
-- Set all lastX/lastY positions to prevent velocity inheritance from previous mode
157-
for i = 0, self.currentSegments do
158-
self.lastX[i] = self.apx[i]
159-
self.lastY[i] = self.apy[i]
160-
end
161169
end
162170

163171
-- Calculate optimal number of segments based on rope length using our module function
@@ -194,22 +202,28 @@ function Update(self)
194202

195203
-- Special handling for attached targets (MO grabbing)
196204
if self.actionMode == 3 and self.target and self.target.ID ~= rte.NoMOID then
197-
local target = self.target
198-
if target.ID ~= target.RootID then
199-
local mo = target:GetRootParent()
200-
if mo.ID ~= rte.NoMOID and IsAttachable(target) then
201-
target = mo
205+
local effective_target = self.target -- Start with the direct hit object
206+
207+
-- If the direct hit target is part of a larger entity (e.g., a limb of an actor),
208+
-- try to use its root parent as the effective target, provided the root is "attachable".
209+
if self.target.ID ~= self.target.RootID then
210+
local root_parent = self.target:GetRootParent()
211+
-- Check if root_parent is valid and if it's considered attachable
212+
if root_parent and root_parent.ID ~= rte.NoMOID and IsAttachable(root_parent) then
213+
effective_target = root_parent -- Use the attachable root parent
214+
-- Else, if root_parent is not attachable (or doesn't exist),
215+
-- we continue using the original self.target as effective_target.
202216
end
203217
end
204218

205-
-- Update hook position to follow the target
206-
self.Pos = target.Pos
207-
self.apx[self.currentSegments] = target.Pos.X
208-
self.apy[self.currentSegments] = target.Pos.Y
219+
-- Update hook position to follow the 'effective_target'
220+
self.Pos = effective_target.Pos
221+
self.apx[self.currentSegments] = effective_target.Pos.X
222+
self.apy[self.currentSegments] = effective_target.Pos.Y
209223

210-
-- Apply target velocity to the hook anchor for physics continuity
211-
self.lastX[self.currentSegments] = self.apx[self.currentSegments] - target.Vel.X
212-
self.lastY[self.currentSegments] = self.apy[self.currentSegments] - target.Vel.Y
224+
-- Apply 'effective_target' velocity to the hook anchor for physics continuity
225+
self.lastX[self.currentSegments] = self.apx[self.currentSegments] - effective_target.Vel.X
226+
self.lastY[self.currentSegments] = self.apy[self.currentSegments] - effective_target.Vel.Y
213227
else
214228
-- Update hook position from rope physics when not attached to MO
215229
if self.actionMode > 1 then -- Hook is stuck to terrain
@@ -253,6 +267,21 @@ function Update(self)
253267
if self.actionMode > 1 then -- Only reset limit flag when attached, not during flight
254268
self.limitReached = false
255269
end
270+
271+
-- Update rope anchor points
272+
-- Player end (anchor 0)
273+
self.apx[0] = self.parent.Pos.X
274+
self.apy[0] = self.parent.Pos.Y
275+
-- Set lastX/Y for the player anchor to reflect its movement.
276+
-- This makes the rope correctly inherit player\'s motion at the anchor point.
277+
self.lastX[0] = self.parent.Pos.X - self.parent.Vel.X
278+
self.lastY[0] = self.parent.Pos.Y - self.parent.Vel.Y
279+
280+
-- Hook end (anchor currentSegments)
281+
self.apx[self.currentSegments] = self.Pos.X
282+
self.apy[self.currentSegments] = self.Pos.Y
283+
-- lastX/Y for the hook end are implicitly handled by the Verlet integration
284+
-- as we are no longer resetting them in a loop for actionMode == 1.
256285
end
257286

258287
if self.parentGun and self.parentGun.ID ~= rte.NoMOID then

Data/Base.rte/Devices/Tools/GrappleGun/Scripts/RopePhysics.lua

Lines changed: 58 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,14 @@ end
107107
-- @return The number of physics iterations to use for this rope
108108
function RopePhysics.optimizePhysicsIterations(self)
109109
-- Base iteration count - balance between performance and physics accuracy
110-
if self.currentSegments < 15 and self.currentLineLength < 150 then
111-
return 36 -- More iterations for shorter, more active ropes (higher accuracy)
112-
elseif self.currentSegments > 30 or self.currentLineLength > 300 then
113-
return 9 -- Fewer iterations for very long ropes to save performance
114-
end
115-
116-
return 18 -- Default for medium-length ropes
110+
-- if self.currentSegments < 15 and self.currentLineLength < 150 then
111+
-- return 36 -- More iterations for shorter, more active ropes (higher accuracy)
112+
-- elseif self.currentSegments > 30 or self.currentLineLength > 300 then
113+
-- return 9 -- Fewer iterations for very long ropes to save performance
114+
-- end
115+
--
116+
-- return 18 -- Default for medium-length ropes
117+
return 32 -- User request: Set all iterations to 32
117118
end
118119

119120
-- Resize the rope segments (add/remove/reposition)
@@ -331,103 +332,78 @@ function RopePhysics.applyRopeConstraints(grappleInstance, currentTotalCableLeng
331332

332333
if not grappleInstance.parent then return false end
333334

334-
-- Use the centrally controlled rope length as the maximum constraint
335335
local maxRopeLength = grappleInstance.currentLineLength or grappleInstance.maxLineLength
336336

337-
-- FIRST: Enforce rigid maximum distance constraint through rope physics only
338-
-- Pure Verlet implementation - no direct player position manipulation
339-
local playerPos = grappleInstance.parent.Pos
337+
local playerPos = grappleInstance.parent.Pos
340338
local hookPos = Vector(grappleInstance.apx[segments], grappleInstance.apy[segments])
341-
local ropeVector = SceneMan:ShortestDistance(playerPos, hookPos, grappleInstance.mapWrapsX)
342-
local totalRopeDistance = ropeVector.Magnitude
343339

344-
-- Update anchor positions for constraint calculations
340+
-- Ensure player anchor point is up-to-date for constraint calculations
345341
grappleInstance.apx[0] = playerPos.X
346342
grappleInstance.apy[0] = playerPos.Y
347343

348-
-- GLOBAL CONSTRAINT: Handle rope length constraints smoothly
344+
local ropeVector = SceneMan:ShortestDistance(playerPos, hookPos, grappleInstance.mapWrapsX)
345+
local totalRopeDistance = ropeVector.Magnitude
346+
347+
-- GLOBAL CONSTRAINT: Handle rope length limits
349348
if totalRopeDistance > maxRopeLength then
350349
local excessDistance = totalRopeDistance - maxRopeLength
351-
local constraintDirection = ropeVector:SetMagnitude(1)
352-
353-
-- Check if hook is anchored (attached to terrain or MO)
354-
-- if grappleInstance.actionMode >= 2 then -- Original condition
355-
if grappleInstance.actionMode == 2 then -- Changed: Actor anchored to claw only in mode 2
356-
-- Hook is anchored - apply PROPER SWINGING CONSTRAINT
357-
-- This allows free tangential movement (swinging) while constraining radial movement
350+
local constraintDirection = ropeVector:SetMagnitude(1) -- Vector from player to hook
351+
352+
if grappleInstance.actionMode == 2 then -- Hook is anchored to terrain; player swings.
353+
-- Player is overstretched. Correct position and velocity for a rigid swing.
358354

359-
local currentVelocity = grappleInstance.parent.Vel
355+
-- 1. Correct Player Position: Snap player precisely to the maxRopeLength arc.
356+
local vec_from_hook_to_player = playerPos - hookPos -- Vector from hook to current player position
357+
grappleInstance.parent.Pos = hookPos + vec_from_hook_to_player:SetMagnitude(maxRopeLength)
358+
359+
-- Update player's rope anchor point and local playerPos variable to reflect the correction.
360+
grappleInstance.apx[0] = grappleInstance.parent.Pos.X
361+
grappleInstance.apy[0] = grappleInstance.parent.Pos.Y
362+
playerPos = grappleInstance.parent.Pos
363+
364+
-- 2. Correct Player Velocity: Make it purely tangential to the swing arc.
365+
local currentVel = grappleInstance.parent.Vel
366+
-- Define rope direction from the *newly corrected* player position to the hook.
367+
local ropeDirFromPlayerToHook = (hookPos - playerPos):SetMagnitude(1)
360368

361-
-- Calculate velocity component toward/away from hook
362-
-- constraintDirection points FROM player TO hook
363-
local radialVelocity = currentVelocity:Dot(constraintDirection)
369+
local radialVelScalar = currentVel:Dot(ropeDirFromPlayerToHook)
370+
-- radialVelScalar is the component of currentVel along the rope direction (player to hook).
371+
-- If > 0, moving towards hook. If < 0, moving away from hook.
372+
-- For a rigid tether at max length, all velocity along the rope axis should be nullified.
373+
local radialVelocityVector = ropeDirFromPlayerToHook * radialVelScalar
374+
local tangentialVelocity = currentVel - radialVelocityVector
364375

365-
-- Only constrain the radial component if moving away from hook (stretching rope)
366-
if radialVelocity < 0 then
367-
-- Player is moving away from hook - remove ONLY the radial component
368-
-- Keep all tangential velocity for swinging motion
369-
local radialVelocityVector = constraintDirection * radialVelocity
370-
local tangentialVelocity = currentVelocity - radialVelocityVector
371-
372-
-- Set velocity to pure tangential motion (perfect swinging)
373-
grappleInstance.parent.Vel = tangentialVelocity
374-
375-
-- Set tension for physics feedback
376-
grappleInstance.ropeTensionForce = -radialVelocity * 0.5
377-
grappleInstance.ropeTensionDirection = constraintDirection
376+
grappleInstance.parent.Vel = tangentialVelocity
377+
378+
-- Tension feedback: Indicate that the rope resisted outward motion.
379+
-- resisted_outgoing_speed will be positive if player was moving away from hook.
380+
local resisted_outgoing_speed = -radialVelScalar
381+
if resisted_outgoing_speed > 0.01 then
382+
grappleInstance.ropeTensionForce = resisted_outgoing_speed * 0.5 -- Magnitude based on resisted speed
383+
grappleInstance.ropeTensionDirection = ropeDirFromPlayerToHook -- Force on player is towards hook
378384
else
379-
-- Player is moving toward hook or tangentially - no constraint needed
380-
-- This allows free movement inward and pure swinging motion
381385
grappleInstance.ropeTensionForce = nil
382386
grappleInstance.ropeTensionDirection = nil
383387
end
384-
385-
-- CRITICAL: Also enforce position constraint to prevent gradual stretching
386-
-- After constraining velocity, ensure player doesn't drift beyond max rope length
387-
if totalRopeDistance > maxRopeLength then
388-
local correctionDistance = totalRopeDistance - maxRopeLength
389-
local correctionVector = constraintDirection * correctionDistance
390-
391-
-- Move player back to exact rope radius (smooth correction)
392-
local correctionStrength = 0.8 -- Strong but not instant correction
393-
grappleInstance.parent.Pos = grappleInstance.parent.Pos + correctionVector * correctionStrength
394-
395-
-- Update rope anchor to match corrected player position
396-
grappleInstance.apx[0] = grappleInstance.parent.Pos.X
397-
grappleInstance.apy[0] = grappleInstance.parent.Pos.Y
398-
end
399-
elseif grappleInstance.actionMode == 1 then -- Added: Claw anchored to actor in mode 1
400-
-- Hook is in flight, anchor it to the player
401-
local correctionVector = constraintDirection * excessDistance
402-
-- Move the player instead of the hook
403-
grappleInstance.parent.Pos = grappleInstance.parent.Pos + correctionVector
404-
-- Update rope anchor to match corrected player position
405-
grappleInstance.apx[0] = grappleInstance.parent.Pos.X
406-
grappleInstance.apy[0] = grappleInstance.parent.Pos.Y
407-
408-
-- Clear any tension forces since rope is not under tension
409-
grappleInstance.ropeTensionForce = nil
410-
grappleInstance.ropeTensionDirection = nil
411388

412-
-- Recalculate after constraint
413-
playerPos = grappleInstance.parent.Pos -- update playerPos for subsequent calculations
414-
ropeVector = SceneMan:ShortestDistance(playerPos, hookPos, grappleInstance.mapWrapsX)
415-
totalRopeDistance = ropeVector.Magnitude
416-
else
417-
-- Hook is in flight - we can move it to maintain rope length (default case)
418-
local correctionVector = constraintDirection * excessDistance
419-
grappleInstance.apx[segments] = grappleInstance.apx[segments] - correctionVector.X
389+
else -- Handles actionMode == 1 (hook flying), actionMode == 3 (hook on MO), and any other defaults.
390+
-- In these cases, the player is the anchor, and the hook end of the rope is corrected.
391+
local correctionVector = constraintDirection * excessDistance -- constraintDirection is player -> hook
392+
393+
-- Move hook segment towards player
394+
grappleInstance.apx[segments] = grappleInstance.apx[segments] - correctionVector.X
420395
grappleInstance.apy[segments] = grappleInstance.apy[segments] - correctionVector.Y
421396

422-
-- Clear any tension forces since rope is not under tension
423-
grappleInstance.ropeTensionForce = nil
397+
grappleInstance.ropeTensionForce = nil
424398
grappleInstance.ropeTensionDirection = nil
425399

426-
-- Recalculate after constraint
427-
hookPos = Vector(grappleInstance.apx[segments], grappleInstance.apy[segments])
428-
ropeVector = SceneMan:ShortestDistance(playerPos, hookPos, grappleInstance.mapWrapsX)
429-
totalRopeDistance = ropeVector.Magnitude
400+
hookPos = Vector(grappleInstance.apx[segments], grappleInstance.apy[segments]) -- Update hookPos for subsequent segment constraints
430401
end
402+
403+
-- After any correction, update totalRopeDistance for the segment constraint part
404+
-- This ensures the segment distribution logic uses the corrected overall length.
405+
ropeVector = SceneMan:ShortestDistance(playerPos, hookPos, grappleInstance.mapWrapsX)
406+
totalRopeDistance = ropeVector.Magnitude
431407
else
432408
-- Rope is not at maximum length - clear tension forces
433409
grappleInstance.ropeTensionForce = nil

Data/Base.rte/Devices/Tools/GrappleGun/Scripts/RopeRenderer.lua

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,9 @@ end
3636

3737
-- Draw the complete rope with debug information
3838
function RopeRenderer.drawRope(grappleInstance, player)
39-
-- If we're in flight mode, draw a simple direct line
40-
if grappleInstance.actionMode == 1 then
41-
-- Draw a direct line from player to hook for visibility during flight
42-
if grappleInstance.parent then
43-
PrimitiveMan:DrawLinePrimitive(player, grappleInstance.parent.Pos, grappleInstance.Pos, 97)
44-
end
45-
else
46-
-- Draw regular rope segments with physics
47-
for i = 0, grappleInstance.currentSegments - 1 do
48-
RopeRenderer.drawSegment(grappleInstance, i, i + 1, player)
49-
end
39+
-- Always draw regular rope segments with physics, regardless of actionMode
40+
for i = 0, grappleInstance.currentSegments - 1 do
41+
RopeRenderer.drawSegment(grappleInstance, i, i + 1, player)
5042
end
5143

5244
-- Always draw debug information when player is controlling

0 commit comments

Comments
 (0)