Skip to content

Commit 9132a76

Browse files
committed
Refines grapple gun mechanics and input handling
Improves the grapple gun's behavior for a more consistent and reliable experience. - Standardizes unhooking: R-key (reload) unhooks when the gun is equipped, while a double-crouch tap unhooks when the gun is not actively held but present in inventory. - Allows the grapple line to persist and be controlled even if the gun is unequipped, as long as it remains in the player's inventory. - Significantly tightens grapple attachment collision detection for more precise and predictable sticking to terrain and objects. - Ensures the gun's magazine visually reflects the grapple's state, appearing empty and hidden when the grapple is active, and full when retracted. - Adds a safeguard in firearm logic to prevent issues if a magazine attempts to pop a non-existent round.
1 parent 4bad057 commit 9132a76

File tree

6 files changed

+211
-268
lines changed

6 files changed

+211
-268
lines changed

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

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ function Update(self)
192192

193193
local parentActor = self.parent -- self.parent is already an Actor type from the setup block
194194

195-
if not self.parentGun or self.parentGun.ID == rte.NoMOID or not parentActor:HasObject("Grapple Gun") then
195+
-- Remove the HasObject check - allow grapple to persist even when gun is not equipped
196+
if not self.parentGun or self.parentGun.ID == rte.NoMOID then
196197
self.ToDelete = true
197198
return
198199
end
@@ -347,40 +348,38 @@ function Update(self)
347348
if parentActor:IsPlayerControlled() then
348349
local controller = self.parent:GetController()
349350
if controller then
350-
-- ONLY use RopeInputController for all input handling
351-
352-
-- 1. R key to unhook (when holding gun)
351+
-- 1. Handle R key (reload) to unhook - use the module function
353352
if RopeInputController.handleReloadKeyUnhook(self, controller) then
354-
print("Unhooking via R key!")
355353
self.ToDelete = true
356354
return
357355
end
358356

359-
-- 2. Double crouch-tap to unhook (when NOT holding gun)
360-
if RopeInputController.handleTapDetection(self, controller) then
361-
print("Unhooking via double crouch!")
357+
-- 2. Handle pie menu unhook commands
358+
if RopeInputController.handlePieMenuSelection(self) then
362359
self.ToDelete = true
363360
return
364361
end
365362

366-
-- 3. Pie menu unhook
367-
if RopeInputController.handlePieMenuSelection(self) then
368-
print("Unhooking via pie menu!")
363+
-- 3. Handle double-tap crouch to unhook - use the module function
364+
if RopeInputController.handleTapDetection(self, controller) then
369365
self.ToDelete = true
370366
return
371367
end
372368

373-
-- 4. Other rope controls
369+
-- Set magazine to empty when grapple is active
370+
if self.parentGun.Magazine then
371+
self.parentGun.Magazine.RoundCount = 0
372+
self.parentGun.Magazine.Scale = 0 -- Hide the magazine
373+
end
374+
375+
-- 4. Other rope controls (climbing, length adjustment)
374376
RopeInputController.handleRopePulling(self)
375377
RopeInputController.handleAutoRetraction(self, false)
376378
end
377379
end
378380

379381
-- Gun stance offset when holding the gun
380382
if self.parentGun and self.parentGun.RootID == parentActor.ID then
381-
if MovableMan:IsParticle(self.parentGun.Magazine) then -- Check if Magazine is a particle
382-
ToMOSParticle(self.parentGun.Magazine).RoundCount = 0 -- Visually empty
383-
end
384383
local offsetAngle = parentActor.FlipFactor * (self.lineVec.AbsRadAngle - parentActor:GetAimAngle(true))
385384
self.parentGun.StanceOffset = Vector(self.lineLength, 0):RadRotate(offsetAngle)
386385
end
@@ -391,12 +390,12 @@ function Update(self)
391390

392391
-- Final deletion check and cleanup
393392
if self.ToDelete then
394-
if self.parentGun and MovableMan:IsParticle(self.parentGun.Magazine) then
395-
local mag = ToMOSParticle(self.parentGun.Magazine)
396-
-- Show magazine briefly as if hook is retracting
397-
mag.Pos = parentActor.Pos + (self.lineVec * 0.5)
398-
mag.Scale = 1
399-
mag.Frame = 0 -- Assuming frame 0 is the visible magazine
393+
if self.parentGun and self.parentGun.Magazine then
394+
-- Show the magazine as if the hook is being retracted
395+
local drawPos = parentActor.Pos + (self.lineVec * 0.5)
396+
self.parentGun.Magazine.Pos = drawPos
397+
self.parentGun.Magazine.Scale = 1
398+
self.parentGun.Magazine.Frame = 0
400399
end
401400
if self.returnSound then self.returnSound:Play(parentActor.Pos) end
402401
end
@@ -409,12 +408,14 @@ function Destroy(self)
409408

410409
-- Clean up references on the parent gun
411410
if self.parentGun and self.parentGun.ID ~= rte.NoMOID then
412-
self.parentGun.HUDVisible = true -- Assuming it was hidden
411+
self.parentGun.HUDVisible = true
413412
self.parentGun:RemoveNumberValue("GrappleMode")
414-
-- Reset stance offset if it was modified
415-
self.parentGun.StanceOffset = Vector(0,0)
416-
if MovableMan:IsParticle(self.parentGun.Magazine) then
417-
ToMOSParticle(self.parentGun.Magazine).Scale = 1 -- Ensure magazine is visible
413+
self.parentGun.StanceOffset = Vector(0,0)
414+
415+
-- Restore and show magazine when grapple is destroyed
416+
if self.parentGun.Magazine then
417+
self.parentGun.Magazine.RoundCount = 1 -- Restore to 1 round when grapple returns
418+
self.parentGun.Magazine.Scale = 1 -- Make magazine visible again
418419
end
419420
end
420421
end

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

Lines changed: 38 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -40,72 +40,70 @@ function Create(self)
4040
-- Log an error or warning if preset is missing/incorrect
4141
-- print("Warning: Grapple Gun Guide Arrow preset not found or incorrect type.")
4242
end
43+
44+
self.originalRoundCount = 1
45+
self.hasGrappleActive = false
4346
end
4447

4548
function Update(self)
4649
local parent = self:GetRootParent()
4750

4851
-- Ensure the gun is held by a valid, player-controlled Actor.
4952
if not parent or not IsActor(parent) then
50-
self:Deactivate() -- If not held by an actor, deactivate.
53+
self:Deactivate()
5154
return
5255
end
5356

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
57+
local parentActor = ToActor(parent)
5658

5759
if not parentActor:IsPlayerControlled() or parentActor.Status >= Actor.DYING then
58-
self:Deactivate() -- Deactivate if not player controlled or if player is dying.
60+
self:Deactivate()
5961
return
6062
end
6163

6264
local controller = parentActor:GetController()
6365
if not controller then
64-
self:Deactivate() -- Should not happen if IsPlayerControlled is true, but good check.
66+
self:Deactivate()
6567
return
6668
end
6769

68-
-- REMOVE/COMMENT OUT this section that deactivates in background:
69-
--[[
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-
76-
-- Allow gun to stay active in background for rope functionality
77-
7870
-- Magazine handling (visual representation of the hook's availability)
7971
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
8072
local magazineParticle = ToMOSParticle(self.Magazine)
8173

82-
-- Double tapping crouch retrieves the hook (if a grapple is active)
83-
-- This logic seems to be for initiating a retrieve action from the gun itself.
84-
-- The actual unhooking is handled by the Grapple.lua script's tap detection.
85-
-- This section might be redundant if Grapple.lua's tap detection is comprehensive.
86-
if magazineParticle.Scale == 1 then -- Assuming Scale 1 means hook is "loaded" / available to fire
87-
-- The following stance offsets seem to be for when the hook is *not* fired yet.
88-
-- Consider if this is the correct condition.
89-
local parentSprite = ToMOSprite(self:GetParent()) -- Assuming self:GetParent() is the gun's sprite component
74+
-- Check if we have an active grapple
75+
local hasActiveGrapple = false
76+
for mo in MovableMan.AddedActors do
77+
if mo and mo.PresetName == "Grapple Gun Claw" and mo.parentGun and mo.parentGun.ID == self.ID then
78+
hasActiveGrapple = true
79+
break
80+
end
81+
end
82+
83+
-- Update magazine based on grapple state
84+
if hasActiveGrapple then
85+
magazineParticle.RoundCount = 0 -- Empty when grapple is out
86+
magazineParticle.Scale = 0 -- Hidden
87+
self.hasGrappleActive = true
88+
elseif self.hasGrappleActive and not hasActiveGrapple then
89+
-- Grapple just returned, restore ammo
90+
magazineParticle.RoundCount = 1
91+
magazineParticle.Scale = 1
92+
magazineParticle.Frame = 0
93+
self.hasGrappleActive = false
94+
end
95+
96+
-- Set stance offset when hook is loaded
97+
if magazineParticle.Scale == 1 then
98+
local parentSprite = ToMOSprite(self:GetParent())
9099
if parentSprite then
91100
local spriteWidth = parentSprite:GetSpriteWidth() or 0
92101
self.StanceOffset = Vector(spriteWidth, 1)
93102
self.SharpStanceOffset = Vector(spriteWidth, 1)
94103
end
95-
96-
-- REMOVE the entire crouch-tap section from the gun - it should only be in the hook
97-
-- The gun should NOT handle unhooking directly
98-
99-
-- Only keep this for other gun functionality, NOT for unhooking:
100-
if controller:IsState(Controller.WEAPON_RELOAD) then
101-
-- Gun's own reload logic here (if any)
102-
-- Do NOT send unhook signals from here
103-
end
104-
105104
end
106105

107106
-- Guide arrow visibility logic
108-
-- Show if magazine scale is 0 (hook is fired) AND not sharp aiming, OR if parent is moving fast.
109107
local shouldShowGuide = false
110108
if magazineParticle.Scale == 0 and not controller:IsState(Controller.AIM_SHARP) then
111109
shouldShowGuide = true
@@ -114,55 +112,26 @@ function Update(self)
114112
end
115113
self.guide = shouldShowGuide
116114
else
117-
self.guide = false -- No magazine or not a particle, so no guide based on it.
115+
self.guide = false
118116
end
119117

120118
-- Draw the guide arrow if enabled and valid
121119
if self.guide and self.arrow and self.arrow.ID ~= rte.NoMOID then
122120
local frame = 0
123121
if parentActor.Vel and parentActor.Vel:MagnitudeIsGreaterThan(12) then
124-
frame = 1 -- Use a different arrow frame for higher speeds
122+
frame = 1
125123
end
126124

127-
-- Calculate positions for drawing the arrow
128-
-- EyePos might not exist on all Actor types, ensure parentActor has it or use a fallback.
129-
local eyePos = parentActor.EyePos or Vector(0,0)
130-
local startPos = (parentActor.Pos + eyePos + self.Pos)/3 -- Averaged position
125+
local eyePos = parentActor.EyePos or Vector(0,0)
126+
local startPos = (parentActor.Pos + eyePos + self.Pos)/3
131127
local aimAngle = parentActor:GetAimAngle(true)
132-
local aimDistance = parentActor.AimDistance or 50 -- Default AimDistance if not present
128+
local aimDistance = parentActor.AimDistance or 50
133129
local guidePos = startPos + Vector(aimDistance + (parentActor.Vel and parentActor.Vel.Magnitude or 0), 0):RadRotate(aimAngle)
134130

135-
-- Ensure the arrow MO still exists before trying to draw with it
136131
if MovableMan:IsValid(self.arrow) then
137132
PrimitiveMan:DrawBitmapPrimitive(ActivityMan:GetActivity():ScreenOfPlayer(controller.Player), guidePos, self.arrow, aimAngle, frame)
138133
else
139-
self.arrow = nil -- Arrow MO was deleted, nullify reference
140-
end
141-
end
142-
143-
-- Ensure magazine is visually "full" and ready if no grapple is active.
144-
-- This assumes the HDFirearm's standard magazine logic handles firing.
145-
-- If a grapple claw MO (the projectile) is active, Grapple.lua will hide the magazine.
146-
-- This section ensures it's visible when no grapple is out.
147-
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
148-
local magParticle = ToMOSParticle(self.Magazine)
149-
local isActiveGrapple = false
150-
-- Check if there's an active grapple associated with this gun
151-
for mo_instance in MovableMan:GetMOsByPreset("Grapple Gun Claw") do
152-
if mo_instance and mo_instance.parentGun and mo_instance.parentGun.ID == self.ID then
153-
isActiveGrapple = true
154-
break
155-
end
156-
end
157-
158-
if not isActiveGrapple then
159-
magParticle.RoundCount = 1 -- Visually full
160-
magParticle.Scale = 1 -- Visible
161-
magParticle.Frame = 0 -- Standard frame
162-
else
163-
magParticle.Scale = 0 -- Hidden by active grapple (Grapple.lua also does this)
164-
magParticle.RoundCount = 0 -- Visually empty
165-
134+
self.arrow = nil
166135
end
167136
end
168137
end

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

Lines changed: 26 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,6 @@
22
-- RopeStateManager might not be directly needed here if we only set GrappleMode on the gun.
33
-- local RopeStateManager = require("Devices.Tools.GrappleGun.Scripts.RopeStateManager")
44

5-
-- Action for Retract slice in the pie menu.
6-
function GrapplePieRetract(pieMenuOwner, pieMenu, pieSlice)
7-
if pieMenuOwner and pieMenuOwner.EquippedItem then
8-
local gun = ToMOSRotating(pieMenuOwner.EquippedItem) -- Assume it's a MOSRotating
9-
if gun and gun.PresetName == "Grapple Gun" then -- Ensure it's the correct gun
10-
gun:SetNumberValue("GrappleMode", 1) -- 1 signifies Retract
11-
end
12-
end
13-
end
14-
15-
-- Action for Extend slice in the pie menu.
16-
function GrapplePieExtend(pieMenuOwner, pieMenu, pieSlice)
17-
if pieMenuOwner and pieMenuOwner.EquippedItem then
18-
local gun = ToMOSRotating(pieMenuOwner.EquippedItem)
19-
if gun and gun.PresetName == "Grapple Gun" then
20-
gun:SetNumberValue("GrappleMode", 2) -- 2 signifies Extend
21-
end
22-
end
23-
end
24-
255
-- Utility function to safely check if an object has a specific property (key) in its Lua script table.
266
-- This is useful for checking if a script-defined variable exists on an MO.
277
function HasScriptProperty(obj, propName)
@@ -34,61 +14,40 @@ function HasScriptProperty(obj, propName)
3414
return status and result
3515
end
3616

37-
38-
-- Action for Unhook slice in the pie menu.
39-
function GrapplePieUnhook(pieMenuOwner, pieMenu, pieSlice)
17+
-- Helper function to validate grapple gun
18+
local function ValidateGrappleGun(pieMenuOwner)
4019
if not pieMenuOwner or not pieMenuOwner.EquippedItem then
41-
return
20+
return nil
4221
end
43-
22+
4423
local gun = ToMOSRotating(pieMenuOwner.EquippedItem)
45-
if not (gun and gun.PresetName == "Grapple Gun") then
46-
return -- Not the grapple gun
24+
if gun and gun.PresetName == "Grapple Gun" then
25+
return gun
4726
end
27+
28+
return nil
29+
end
4830

49-
local activeGrappleMO = nil
50-
-- Find the active grapple claw associated with this specific gun instance.
51-
for mo_instance in MovableMan:GetMOsByPreset("Grapple Gun Claw") do
52-
-- Check if the instance is valid, has the parentGun property, and it matches our gun.
53-
if mo_instance and mo_instance.ID ~= rte.NoMOID and
54-
HasScriptProperty(mo_instance, "parentGun") and -- Use HasScriptProperty for Lua members
55-
mo_instance.parentGun and mo_instance.parentGun.ID == gun.ID then
56-
activeGrappleMO = mo_instance
57-
break
58-
end
31+
-- Action for Retract slice in the pie menu.
32+
function GrapplePieRetract(pieMenuOwner, pieMenu, pieSlice)
33+
local gun = ValidateGrappleGun(pieMenuOwner)
34+
if gun then
35+
gun:SetNumberValue("GrappleMode", 1) -- 1 signifies Retract
5936
end
37+
end
6038

61-
local allowUnhook = true -- Default to allowing unhook.
62-
if activeGrappleMO then
63-
-- Check the 'canRelease' property on the grapple claw instance itself.
64-
-- This property is set by the Grapple.lua script based on its state (e.g., after sticking).
65-
if HasScriptProperty(activeGrappleMO, "canRelease") then
66-
allowUnhook = (activeGrappleMO.canRelease == true)
67-
else
68-
-- If canRelease property doesn't exist, but grapple is active,
69-
-- it might imply it's in a state where it can be unhooked (e.g., already stuck).
70-
-- However, for safety, if 'canRelease' is the definitive flag, stick to it.
71-
-- If the grapple is flying (actionMode 1), 'canRelease' might be false.
72-
-- If it's stuck (actionMode > 1), 'canRelease' should become true.
73-
-- If actionMode is 1 (flying), pie menu unhook might not be desired or should just delete it.
74-
if HasScriptProperty(activeGrappleMO, "actionMode") and activeGrappleMO.actionMode == 1 then
75-
allowUnhook = true -- Allow "unhooking" (deleting) a flying hook via pie menu
76-
elseif not HasScriptProperty(activeGrappleMO, "canRelease") then
77-
allowUnhook = false -- If stuck and no canRelease flag, assume cannot release.
78-
end
79-
end
80-
else
81-
-- No active grapple found for this gun. Unhook action is irrelevant.
82-
allowUnhook = false
39+
-- Action for Extend slice in the pie menu.
40+
function GrapplePieExtend(pieMenuOwner, pieMenu, pieSlice)
41+
local gun = ValidateGrappleGun(pieMenuOwner)
42+
if gun then
43+
gun:SetNumberValue("GrappleMode", 2) -- 2 signifies Extend
8344
end
45+
end
8446

85-
if allowUnhook then
86-
gun:SetNumberValue("GrappleMode", 3) -- 3 signifies Unhook. Grapple.lua will handle this.
87-
else
88-
-- Play a denial sound if unhook is not allowed (e.g., hook is still flying and not releasable yet).
89-
local denySound = CreateSoundContainer("Grapple Gun Click", "Base.rte") -- Or a specific "deny" sound
90-
if denySound then
91-
denySound:Play(gun.Pos)
92-
end
47+
-- Action for Unhook slice in the pie menu.
48+
function GrapplePieUnhook(pieMenuOwner, pieMenu, pieSlice)
49+
local gun = ValidateGrappleGun(pieMenuOwner)
50+
if gun then
51+
gun:SetNumberValue("GrappleMode", 3) -- 3 signifies Unhook
9352
end
9453
end

0 commit comments

Comments
 (0)