Skip to content

Commit fb203ab

Browse files
committed
Refine Grapple Gun mechanics for enhanced stability and rope physics
- Defers grapple initialization to the first `Update` call, ensuring parent actor and gun are valid, which prevents potential early-frame errors. - Overhauls the rope physics module with more robust Verlet integration and constraint satisfaction, leading to more stable and realistic rigid rope behavior. - Improves state management, particularly for attachment collision detection and handling of grappled objects. - Streamlines input handling and clarifies module responsibilities for better maintainability. - Updates visual guide arrow logic and pie menu actions for consistency and reliability.
1 parent 487cfa0 commit fb203ab

File tree

7 files changed

+1493
-1735
lines changed

7 files changed

+1493
-1735
lines changed

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

Lines changed: 338 additions & 394 deletions
Large diffs are not rendered by default.
Lines changed: 180 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,187 @@
1+
---@diagnostic disable: undefined-global
2+
-- Localize Cortex Command globals
3+
local Timer = Timer
4+
local PresetMan = PresetMan
5+
local CreateMOSRotating = CreateMOSRotating
6+
local IsActor = IsActor
7+
local Actor = Actor
8+
local ToMOSParticle = ToMOSParticle
9+
local ToMOSprite = ToMOSprite
10+
local PrimitiveMan = PrimitiveMan
11+
local ActivityMan = ActivityMan
12+
local MovableMan = MovableMan
13+
local Vector = Vector
14+
local Controller = Controller -- For Controller.BODY_PRONE etc.
15+
local rte = rte
16+
117
function Create(self)
2-
self.tapTimerAim = Timer();
3-
self.tapTimerJump = Timer();
4-
self.tapCounter = 0;
5-
self.didTap = false;
6-
self.canTap = false;
18+
-- Timers and counters for tap-based controls (e.g., double-tap to retrieve hook)
19+
self.tapTimerAim = Timer() -- Unused? Or intended for a different tap action.
20+
self.tapTimerJump = Timer() -- Used for crouch-tap detection.
21+
self.tapCounter = 0
22+
-- self.didTap = false -- Seems unused, consider removing.
23+
self.canTap = false -- Flag to register the first tap in a sequence.
724

8-
self.tapTime = 200;
9-
self.tapAmount = 2;
10-
self.guide = false;
25+
self.tapTime = 200 -- Max milliseconds between taps for them to count as a sequence.
26+
self.tapAmount = 2 -- Number of taps required.
27+
28+
self.guide = false -- Whether to show the aiming guide arrow.
1129

12-
self.arrow = CreateMOSRotating("Grapple Gun Guide Arrow");
30+
-- Create the guide arrow MOSRotating. This is a visual aid.
31+
-- Ensure "Grapple Gun Guide Arrow" preset exists and is a MOSRotating.
32+
local arrowPreset = PresetMan:GetPreset("Grapple Gun Guide Arrow", "MOSRotating", "Grapple Gun Guide Arrow")
33+
if arrowPreset and arrowPreset.ClassName == "MOSRotating" then
34+
self.arrow = CreateMOSRotating("Grapple Gun Guide Arrow")
35+
if self.arrow then
36+
self.arrow.GlobalAccurateDelete = true -- Ensure it cleans up properly
37+
end
38+
else
39+
self.arrow = nil -- Preset not found or incorrect type
40+
-- Log an error or warning if preset is missing/incorrect
41+
-- print("Warning: Grapple Gun Guide Arrow preset not found or incorrect type.")
42+
end
1343
end
1444

1545
function Update(self)
16-
local parent = self:GetRootParent();
17-
if parent and IsActor(parent) then
18-
if IsAHuman(parent) then
19-
parent = ToAHuman(parent);
20-
elseif IsACrab(parent) then
21-
parent = ToACrab(parent);
22-
else
23-
parent = ToActor(parent);
24-
end
25-
if parent:IsPlayerControlled() and parent.Status < Actor.DYING then
26-
local controller = parent:GetController();
27-
local mouse = controller:IsMouseControlled();
28-
-- Deactivate when equipped in BG arm to allow FG arm shooting
29-
if parent.EquippedBGItem and parent.EquippedItem then
30-
if parent.EquippedBGItem.ID == self.ID then
31-
self:Deactivate();
32-
end
33-
end
34-
35-
if self.Magazine then
36-
-- Double tapping crouch retrieves the hook
37-
if self.Magazine.Scale == 1 then
38-
self.StanceOffset = Vector(ToMOSprite(self:GetParent()):GetSpriteWidth(), 1);
39-
self.SharpStanceOffset = Vector(ToMOSprite(self:GetParent()):GetSpriteWidth(), 1);
40-
if controller and controller:IsState(Controller.BODY_PRONE) then
41-
if self.canTap then
42-
controller:SetState(Controller.BODY_PRONE, false);
43-
self.tapTimerJump:Reset();
44-
self.didTap = true;
45-
self.canTap = false;
46-
self.tapCounter = self.tapCounter + 1;
47-
end
48-
else
49-
self.canTap = true;
50-
end
51-
52-
if self.tapTimerJump:IsPastSimMS(self.tapTime) then
53-
self.tapCounter = 0;
54-
else
55-
if self.tapCounter >= self.tapAmount then
56-
self:Activate();
57-
self.tapCounter = 0;
58-
end
59-
end
60-
end
61-
62-
-- A guide arrow appears at higher speeds
63-
if (self.Magazine.Scale == 0 and not controller:IsState(Controller.AIM_SHARP)) or parent.Vel:MagnitudeIsGreaterThan(6) then
64-
self.guide = true;
65-
else
66-
self.guide = false;
67-
end
68-
end
69-
70-
if self.guide then
71-
local frame = 0;
72-
if parent.Vel:MagnitudeIsGreaterThan(12) then
73-
frame = 1;
74-
end
75-
local startPos = (parent.Pos + parent.EyePos + self.Pos)/3;
76-
local guidePos = startPos + Vector(parent.AimDistance + (parent.Vel.Magnitude), 0):RadRotate(parent:GetAimAngle(true));
77-
PrimitiveMan:DrawBitmapPrimitive(ActivityMan:GetActivity():ScreenOfPlayer(controller.Player), guidePos, self.arrow, parent:GetAimAngle(true), frame);
78-
end
79-
else
80-
self:Deactivate();
81-
end
82-
83-
if self.Magazine then
84-
self.Magazine.RoundCount = 1;
85-
self.Magazine.Scale = 1;
86-
self.Magazine.Frame = 0;
87-
end
88-
end
46+
local parent = self:GetRootParent()
47+
48+
-- Ensure the gun is held by a valid, player-controlled Actor.
49+
if not parent or not IsActor(parent) then
50+
self:Deactivate() -- If not held by an actor, deactivate.
51+
return
52+
end
53+
54+
local parentActor = ToActor(parent) -- Cast to Actor base type.
55+
-- Specific casting to AHuman or ACrab can be done if needed for type-specific logic.
56+
57+
if not parentActor:IsPlayerControlled() or parentActor.Status >= Actor.DYING then
58+
self:Deactivate() -- Deactivate if not player controlled or if player is dying.
59+
return
60+
end
61+
62+
local controller = parentActor:GetController()
63+
if not controller then
64+
self:Deactivate() -- Should not happen if IsPlayerControlled is true, but good check.
65+
return
66+
end
67+
68+
-- Deactivate if equipped in the background arm and a foreground item exists,
69+
-- to allow the foreground item (e.g., another weapon) to be used.
70+
if parentActor.EquippedBGItem and parentActor.EquippedBGItem.ID == self.ID and parentActor.EquippedItem then
71+
self:Deactivate()
72+
-- Potentially return here if no further logic should run for a BG equipped grapple gun.
73+
end
74+
75+
-- Magazine handling (visual representation of the hook's availability)
76+
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
77+
local magazineParticle = ToMOSParticle(self.Magazine)
78+
79+
-- Double tapping crouch retrieves the hook (if a grapple is active)
80+
-- This logic seems to be for initiating a retrieve action from the gun itself.
81+
-- The actual unhooking is handled by the Grapple.lua script's tap detection.
82+
-- This section might be redundant if Grapple.lua's tap detection is comprehensive.
83+
if magazineParticle.Scale == 1 then -- Assuming Scale 1 means hook is "loaded" / available to fire
84+
-- The following stance offsets seem to be for when the hook is *not* fired yet.
85+
-- Consider if this is the correct condition.
86+
local parentSprite = ToMOSprite(self:GetParent()) -- Assuming self:GetParent() is the gun's sprite component
87+
if parentSprite then
88+
local spriteWidth = parentSprite:GetSpriteWidth() or 0
89+
self.StanceOffset = Vector(spriteWidth, 1)
90+
self.SharpStanceOffset = Vector(spriteWidth, 1)
91+
end
92+
93+
-- Crouch-tap logic (potentially for recalling an active hook)
94+
if controller:IsState(Controller.BODY_PRONE) then
95+
if self.canTap then
96+
controller:SetState(Controller.BODY_PRONE, false) -- Prevent continuous prone state
97+
self.tapTimerJump:Reset()
98+
-- self.didTap = true; -- Mark that a tap occurred (if used elsewhere)
99+
self.canTap = false
100+
self.tapCounter = self.tapCounter + 1
101+
end
102+
else
103+
self.canTap = true -- Allow first tap when not prone
104+
end
105+
106+
if self.tapTimerJump:IsPastSimMS(self.tapTime) then
107+
self.tapCounter = 0 -- Reset counter if too much time has passed
108+
else
109+
if self.tapCounter >= self.tapAmount then
110+
-- If enough taps, activate the gun. This might be intended to fire/recall.
111+
-- If a grapple is already out, Grapple.lua's tap detection should handle recall.
112+
-- If no grapple is out, this would fire a new one.
113+
-- Clarify the intent: is this to fire, or to send a signal to an existing grapple?
114+
self:Activate() -- This will typically fire the HDFirearm.
115+
self.tapCounter = 0
116+
end
117+
end
118+
end
119+
120+
-- Guide arrow visibility logic
121+
-- Show if magazine scale is 0 (hook is fired) AND not sharp aiming, OR if parent is moving fast.
122+
local shouldShowGuide = false
123+
if magazineParticle.Scale == 0 and not controller:IsState(Controller.AIM_SHARP) then
124+
shouldShowGuide = true
125+
elseif parentActor.Vel and parentActor.Vel:MagnitudeIsGreaterThan(6) then
126+
shouldShowGuide = true
127+
end
128+
self.guide = shouldShowGuide
129+
else
130+
self.guide = false -- No magazine or not a particle, so no guide based on it.
131+
end
132+
133+
-- Draw the guide arrow if enabled and valid
134+
if self.guide and self.arrow and self.arrow.ID ~= rte.NoMOID then
135+
local frame = 0
136+
if parentActor.Vel and parentActor.Vel:MagnitudeIsGreaterThan(12) then
137+
frame = 1 -- Use a different arrow frame for higher speeds
138+
end
139+
140+
-- Calculate positions for drawing the arrow
141+
-- EyePos might not exist on all Actor types, ensure parentActor has it or use a fallback.
142+
local eyePos = parentActor.EyePos or Vector(0,0)
143+
local startPos = (parentActor.Pos + eyePos + self.Pos)/3 -- Averaged position
144+
local aimAngle = parentActor:GetAimAngle(true)
145+
local aimDistance = parentActor.AimDistance or 50 -- Default AimDistance if not present
146+
local guidePos = startPos + Vector(aimDistance + (parentActor.Vel and parentActor.Vel.Magnitude or 0), 0):RadRotate(aimAngle)
147+
148+
-- Ensure the arrow MO still exists before trying to draw with it
149+
if MovableMan:IsValid(self.arrow) then
150+
PrimitiveMan:DrawBitmapPrimitive(ActivityMan:GetActivity():ScreenOfPlayer(controller.Player), guidePos, self.arrow, aimAngle, frame)
151+
else
152+
self.arrow = nil -- Arrow MO was deleted, nullify reference
153+
end
154+
end
155+
156+
-- Ensure magazine is visually "full" and ready if no grapple is active.
157+
-- This assumes the HDFirearm's standard magazine logic handles firing.
158+
-- If a grapple claw MO (the projectile) is active, Grapple.lua will hide the magazine.
159+
-- This section ensures it's visible when no grapple is out.
160+
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
161+
local magParticle = ToMOSParticle(self.Magazine)
162+
local isActiveGrapple = false
163+
-- Check if there's an active grapple associated with this gun
164+
for mo_instance in MovableMan:GetMOsByPreset("Grapple Gun Claw") do
165+
if mo_instance and mo_instance.parentGun and mo_instance.parentGun.ID == self.ID then
166+
isActiveGrapple = true
167+
break
168+
end
169+
end
170+
171+
if not isActiveGrapple then
172+
magParticle.RoundCount = 1 -- Visually full
173+
magParticle.Scale = 1 -- Visible
174+
magParticle.Frame = 0 -- Standard frame
175+
else
176+
magParticle.Scale = 0 -- Hidden by active grapple (Grapple.lua also does this)
177+
end
178+
end
179+
end
180+
181+
function Destroy(self)
182+
-- Clean up the guide arrow if it exists
183+
if self.arrow and self.arrow.ID ~= rte.NoMOID and MovableMan:IsValid(self.arrow) then
184+
MovableMan:RemoveMO(self.arrow)
185+
self.arrow = nil
186+
end
89187
end

0 commit comments

Comments
 (0)