Skip to content

Commit f13173c

Browse files
committed
Refine grapple physics, collision detection, and controls
Improves rope behavior during flight by implementing Verlet physics for intermediate segments, resulting in a more natural drape and movement. Increases precision of hook attachment by refining raycasting logic with multiple checks and adjusted parameters for terrain and movable objects, including filtering out very small objects.
1 parent fb203ab commit f13173c

File tree

4 files changed

+365
-192
lines changed

4 files changed

+365
-192
lines changed

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

Lines changed: 158 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
---@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
33
-- Main logic for the grapple claw MovableObject.
44

55
-- Load Modules
66
local RopePhysics = require("Devices.Tools.GrappleGun.Scripts.RopePhysics")
77
local RopeRenderer = require("Devices.Tools.GrappleGun.Scripts.RopeRenderer")
88
local RopeInputController = require("Devices.Tools.GrappleGun.Scripts.RopeInputController")
99
local RopeStateManager = require("Devices.Tools.GrappleGun.Scripts.RopeStateManager")
10-
1110
function Create(self)
1211
self.lastPos = self.Pos
1312

@@ -18,11 +17,12 @@ function Create(self)
1817

1918
-- Initialize state using the state manager. This sets self.actionMode = 0.
2019
RopeStateManager.initState(self)
21-
2220
-- self.initializationOk = true -- This flag is effectively replaced by checking self.actionMode == 0 in Update.
2321

2422
-- 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+
2626
self.maxLineLength = 600 -- Maximum allowed length of the rope.
2727
self.maxShootDistance = self.maxLineLength * 0.95 -- Hook will detach if it travels further than this before sticking.
2828
self.setLineLength = 0 -- Target length set by input/logic.
@@ -33,10 +33,13 @@ function Create(self)
3333
self.stretchPullRatio = 0.0 -- No stretching for rigid rope.
3434
self.pieSelection = 0 -- Current pie menu selection (0: none, 1: full retract, etc.).
3535

36+
3637
-- Timing and interval properties for rope actions
3738
self.climbDelay = 8 -- Delay between climb ticks.
3839
self.tapTime = 150 -- Max time between taps for double-tap unhook.
3940
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.
4043
self.mouseClimbLength = 200 -- Duration mouse scroll input is considered active.
4144
self.climbInterval = 4.0 -- Amount rope length changes per climb tick.
4245
self.autoClimbIntervalA = 5.0 -- Auto-retract speed (primary).
@@ -82,56 +85,79 @@ function Create(self)
8285
end
8386

8487
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
8689

8790
-- First-time setup: Find parent, initialize velocity, anchor points, etc.
8891
if self.actionMode == 0 then
8992
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
9194
if gun_mo and gun_mo.ClassName == "HDFirearm" and gun_mo.PresetName == "Grapple Gun" then
9295
local hdfGun = ToHDFirearm(gun_mo)
9396
if hdfGun and SceneMan:ShortestDistance(self.Pos, hdfGun.MuzzlePos, self.mapWrapsX):MagnitudeIsLessThan(20) then
9497
self.parentGun = hdfGun
9598
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
127126
end
128127
end
129128
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)
135161
end -- if hdfGun and distance check
136162
end -- if gun_mo is grapple gun
137163
end -- for gun_mo
@@ -167,6 +193,22 @@ function Update(self)
167193
end
168194
local player = controller.Player or 0
169195

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+
170212
-- Standard update flags
171213
self.ToSettle = false -- Grapple claw should not settle
172214

@@ -182,7 +224,14 @@ function Update(self)
182224
-- Hook position is determined by its own physics
183225
self.apx[self.currentSegments] = self.Pos.X
184226
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
186235
elseif self.actionMode == 2 then -- Grabbed terrain
187236
-- Hook position is fixed where it grabbed
188237
self.Pos.X = self.apx[self.currentSegments] -- Ensure self.Pos matches anchor
@@ -233,13 +282,35 @@ function Update(self)
233282

234283
-- Dynamic rope segment calculation
235284
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
237299
RopePhysics.resizeRopeSegments(self, desiredSegments)
238300
end
239301

240302
-- Core rope physics simulation
241303
RopePhysics.updateRopePhysics(self, parentActor.Pos, self.Pos, self.currentLineLength)
242304

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+
243314
-- Apply constraints and check for breaking
244315
local ropeBreaks = RopePhysics.applyRopeConstraints(self, self.currentLineLength)
245316
if ropeBreaks or self.shouldBreak then -- self.shouldBreak can be set by other logic
@@ -284,16 +355,34 @@ function Update(self)
284355
end
285356

286357
-- 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
293366
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
296375
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
297386
end
298387
end
299388
-- Gun stance offset when holding the gun
@@ -306,52 +395,26 @@ function Update(self)
306395
end
307396
end
308397

309-
-- Handle Pie Menu actions
398+
-- Delegate all input handling to RopeInputController
399+
-- 1. Pie menu selection (unhook, retract, extend)
310400
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
312404
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
326409
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
353414
end
354-
415+
-- 4. Mousewheel and directional controls for rope length
416+
RopeInputController.handleRopePulling(self)
417+
355418
-- Render the rope
356419
RopeRenderer.drawRope(self, player)
357420

@@ -383,4 +446,4 @@ function Destroy(self)
383446
ToMOSParticle(self.parentGun.Magazine).Scale = 1 -- Ensure magazine is visible
384447
end
385448
end
386-
end
449+
end

0 commit comments

Comments
 (0)