Skip to content

Enhance Grapple Gun with unhook functionality and modular improvements #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a5abad0
Implement unhook functionality and pie menu integration for Grapple Gun
OpenTools-run May 29, 2025
9b4a8f0
Implement Grapple Gun Enhancements
OpenTools-run May 30, 2025
9e0e520
Refactor module paths and enhance grapple robustness
OpenTools-run May 30, 2025
10300a8
Enhance RopePhysics with improved collision handling and constraint a…
OpenTools-run May 30, 2025
169cb05
Refactor grapple to use rigid, unbreakable Verlet rope physics
OpenTools-run May 30, 2025
55fbce4
Enhance grapple flight physics and max shoot distance
OpenTools-run May 31, 2025
f842201
Refactor: Document rope physics, tune iterations
OpenTools-run May 31, 2025
2e3f722
Refines Grapple Gun mechanics and rope physics
OpenTools-run May 31, 2025
487cfa0
Refactor grapple rope physics for improved realism and control
OpenTools-run May 31, 2025
fb203ab
Refine Grapple Gun mechanics for enhanced stability and rope physics
OpenTools-run Jun 1, 2025
f13173c
Refine grapple physics, collision detection, and controls
OpenTools-run Jun 3, 2025
4bad057
Refine grapple controls and enable background functionality
OpenTools-run Jun 4, 2025
9132a76
Refines grapple gun mechanics and input handling
OpenTools-run Jun 4, 2025
83d4c6c
Refine grapple gun logic for improved ownership checks and background…
OpenTools-run Jun 4, 2025
6488b40
Add logging functionality to GrappleGun's RopeStateManager
OpenTools-run Jun 5, 2025
c85575d
Improves grapple gun magazine reset and sounds
OpenTools-run Jun 5, 2025
8caba67
Refines Grapple Gun Shift+Wheel control and prevents weapon switch
OpenTools-run Jun 5, 2025
9fe0d84
Enhance input settings and improve UI stability
OpenTools-run Jun 5, 2025
84b68ff
Refactor logging to use Logger module
OpenTools-run Jun 5, 2025
264b042
Merge branch 'development' into improve-grappling-gun
OpenTools-run Jun 5, 2025
624897e
Update Source/Lua/LuaBindingsInput.cpp
OpenTools-run Jun 5, 2025
81a918a
GrappleGun.lua nitpicking
OpenTools-run Jun 5, 2025
ee4131d
Refactor logging and adjust shift key input handling
OpenTools-run Jun 6, 2025
db856e7
Refactor Logger module usage and improve shift key input handling in …
OpenTools-run Jun 6, 2025
c1009b3
Removes unused shift input and control state
OpenTools-run Jun 6, 2025
b63b247
Removes obsolete "Shift" key binding
OpenTools-run Jun 6, 2025
a6dd3dd
Update .gitignore to include imgui.ini and fix SHIFT key state retrie…
OpenTools-run Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ LogLoadingWarning.txt
LogConsole.txt
Console.dump.log
Console.input.log
imgui.ini
1,320 changes: 894 additions & 426 deletions Data/Base.rte/Devices/Tools/GrappleGun/Grapple.lua

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Data/Base.rte/Devices/Tools/GrappleGun/GrappleGun.ini
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ AddDevice = HDFirearm
CopyOf = Hand Open
ScriptPath = Base.rte/Devices/Shared/Scripts/ToggleDualWield.lua
FunctionName = ToggleDualWield
AddPieSlice = PieSlice
Description = Unhook
Direction = Right
Icon = Icon
PresetName = Grapple Gun Unhook
FrameCount = 2
BitmapFile = ContentFile
FilePath = Base.rte/Devices/Tools/GrappleGun/PieIcons/Unhook.png
ScriptPath = Base.rte/Devices/Tools/GrappleGun/Pie.lua
FunctionName = GrapplePieUnhook
AddGib = Gib
GibParticle = MOPixel
CopyOf = Spark Yellow 1
Expand Down
259 changes: 185 additions & 74 deletions Data/Base.rte/Devices/Tools/GrappleGun/GrappleGun.lua
Original file line number Diff line number Diff line change
@@ -1,89 +1,200 @@
---@diagnostic disable: undefined-global
-- Localize Cortex Command globals
local Timer = Timer
local PresetMan = PresetMan
local CreateMOSRotating = CreateMOSRotating
local IsActor = IsActor
local Actor = Actor
local ToMOSParticle = ToMOSParticle
local ToMOSprite = ToMOSprite
local PrimitiveMan = PrimitiveMan
local ActivityMan = ActivityMan
local MovableMan = MovableMan
local Vector = Vector
local Controller = Controller -- For Controller.BODY_PRONE etc.
local rte = rte

function Create(self)
self.tapTimerAim = Timer();
self.tapTimerJump = Timer();
self.tapCounter = 0;
self.didTap = false;
self.canTap = false;
-- Timers and counters for tap-based controls (e.g., double-tap to retrieve hook)
self.tapTimerJump = Timer() -- Used for crouch-tap detection.
self.tapCounter = 0
-- self.didTap = false -- Seems unused, consider removing.
self.canTap = false -- Flag to register the first tap in a sequence.

self.tapTime = 200;
self.tapAmount = 2;
self.guide = false;
self.tapTime = 200 -- Max milliseconds between taps for them to count as a sequence.
self.tapAmount = 2 -- Number of taps required.

self.guide = false -- Whether to show the aiming guide arrow.

self.arrow = CreateMOSRotating("Grapple Gun Guide Arrow");
-- Create the guide arrow MOSRotating. This is a visual aid.
-- Ensure "Grapple Gun Guide Arrow" preset exists and is a MOSRotating.
local arrowPreset = PresetMan:GetPreset("Grapple Gun Guide Arrow", "MOSRotating", "Grapple Gun Guide Arrow")
if arrowPreset and arrowPreset.ClassName == "MOSRotating" then
self.arrow = CreateMOSRotating("Grapple Gun Guide Arrow")
if self.arrow then
self.arrow.GlobalAccurateDelete = true -- Ensure it cleans up properly
end
else
self.arrow = nil -- Preset not found or incorrect type
-- Log an error or warning if preset is missing/incorrect
-- print("Warning: Grapple Gun Guide Arrow preset not found or incorrect type.")
end

self.originalRoundCount = 1
self.hasGrappleActive = false
end

function Update(self)
local parent = self:GetRootParent();
if parent and IsActor(parent) then
if IsAHuman(parent) then
parent = ToAHuman(parent);
elseif IsACrab(parent) then
parent = ToACrab(parent);
else
parent = ToActor(parent);
end
if parent:IsPlayerControlled() and parent.Status < Actor.DYING then
local controller = parent:GetController();
local mouse = controller:IsMouseControlled();
-- Deactivate when equipped in BG arm to allow FG arm shooting
if parent.EquippedBGItem and parent.EquippedItem then
if parent.EquippedBGItem.ID == self.ID then
self:Deactivate();
end
end
local parent = self:GetRootParent()

-- Ensure the gun is held by a valid, player-controlled Actor.
if not parent or not IsActor(parent) then
self:Deactivate() -- If not held by an actor, deactivate.
return
end

local parentActor = ToActor(parent) -- Cast to Actor base type
-- Specific casting to AHuman or ACrab can be done if needed for type-specific logic

if self.Magazine then
-- Double tapping crouch retrieves the hook
if self.Magazine.Scale == 1 then
self.StanceOffset = Vector(ToMOSprite(self:GetParent()):GetSpriteWidth(), 1);
self.SharpStanceOffset = Vector(ToMOSprite(self:GetParent()):GetSpriteWidth(), 1);
if controller and controller:IsState(Controller.BODY_PRONE) then
if self.canTap then
controller:SetState(Controller.BODY_PRONE, false);
self.tapTimerJump:Reset();
self.didTap = true;
self.canTap = false;
self.tapCounter = self.tapCounter + 1;
end
else
self.canTap = true;
end

if self.tapTimerJump:IsPastSimMS(self.tapTime) then
self.tapCounter = 0;
else
if self.tapCounter >= self.tapAmount then
self:Activate();
self.tapCounter = 0;
end
end
end

-- A guide arrow appears at higher speeds
if (self.Magazine.Scale == 0 and not controller:IsState(Controller.AIM_SHARP)) or parent.Vel:MagnitudeIsGreaterThan(6) then
self.guide = true;
else
self.guide = false;
end
if not parentActor:IsPlayerControlled() or parentActor.Status >= Actor.DYING then
self:Deactivate() -- Deactivate if not player controlled or if player is dying.
return
end

local controller = parentActor:GetController()
if not controller then
self:Deactivate() -- Should not happen if IsPlayerControlled is true, but good check.
return
end

-- REMOVE/COMMENT OUT this section that deactivates in background:
--[[
if parentActor.EquippedBGItem and parentActor.EquippedBGItem.ID == self.ID and parentActor.EquippedItem then
self:Deactivate()
// Potentially return here if no further logic should run for a BG equipped grapple gun.
end
--]]

-- Allow gun to stay active in background for rope functionality

-- Magazine handling (visual representation of the hook's availability)
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
local magazineParticle = ToMOSParticle(self.Magazine)

-- Double tapping crouch retrieves the hook (if a grapple is active)
-- This logic seems to be for initiating a retrieve action from the gun itself.
-- The actual unhooking is handled by the Grapple.lua script's tap detection.
-- This section might be redundant if Grapple.lua's tap detection is comprehensive.
if magazineParticle.Scale == 1 then -- Assuming Scale 1 means hook is "loaded" / available to fire
-- The following stance offsets seem to be for when the hook is *not* fired yet.
-- Consider if this is the correct condition.
local parentSprite = ToMOSprite(self:GetParent()) -- Assuming self:GetParent() is the gun's sprite component
if parentSprite then
local spriteWidth = parentSprite:GetSpriteWidth() or 0
self.StanceOffset = Vector(spriteWidth, 1)
self.SharpStanceOffset = Vector(spriteWidth, 1)
end

if self.guide then
local frame = 0;
if parent.Vel:MagnitudeIsGreaterThan(12) then
frame = 1;
end
local startPos = (parent.Pos + parent.EyePos + self.Pos)/3;
local guidePos = startPos + Vector(parent.AimDistance + (parent.Vel.Magnitude), 0):RadRotate(parent:GetAimAngle(true));
PrimitiveMan:DrawBitmapPrimitive(ActivityMan:GetActivity():ScreenOfPlayer(controller.Player), guidePos, self.arrow, parent:GetAimAngle(true), frame);
-- REMOVE the entire crouch-tap section from the gun - it should only be in the hook
-- The gun should NOT handle unhooking directly

-- Only keep this for other gun functionality, NOT for unhooking:
if controller:IsState(Controller.WEAPON_RELOAD) then
-- Gun's own reload logic here (if any)
-- Do NOT send unhook signals from here
end
else
self:Deactivate();

end

-- Guide arrow visibility logic
-- Show if magazine scale is 0 (hook is fired) AND not sharp aiming, OR if parent is moving fast.
local shouldShowGuide = false
if magazineParticle.Scale == 0 and not controller:IsState(Controller.AIM_SHARP) then
shouldShowGuide = true
elseif parentActor.Vel and parentActor.Vel:MagnitudeIsGreaterThan(6) then
shouldShowGuide = true
end
self.guide = shouldShowGuide
else
self.guide = false -- No magazine or not a particle, so no guide based on it.
end

-- Draw the guide arrow if enabled and valid
if self.guide and self.arrow and self.arrow.ID ~= rte.NoMOID then
local frame = 0
if parentActor.Vel and parentActor.Vel:MagnitudeIsGreaterThan(12) then
frame = 1 -- Use a different arrow frame for higher speeds
end

if self.Magazine then
self.Magazine.RoundCount = 1;
self.Magazine.Scale = 1;
self.Magazine.Frame = 0;
-- Calculate positions for drawing the arrow
-- EyePos might not exist on all Actor types, ensure parentActor has it or use a fallback.
local eyePos = parentActor.EyePos or Vector(0,0)
local startPos = (parentActor.Pos + eyePos + self.Pos)/3 -- Averaged position
local aimAngle = parentActor:GetAimAngle(true)
local aimDistance = parentActor.AimDistance or 50 -- Default AimDistance if not present
local guidePos = startPos + Vector(aimDistance + (parentActor.Vel and parentActor.Vel.Magnitude or 0), 0):RadRotate(aimAngle)

-- Ensure the arrow MO still exists before trying to draw with it
if MovableMan:IsValid(self.arrow) then
PrimitiveMan:DrawBitmapPrimitive(ActivityMan:GetActivity():ScreenOfPlayer(controller.Player), guidePos, self.arrow, aimAngle, frame)
else
self.arrow = nil -- Arrow MO was deleted, nullify reference
end
end

-- Check if we have an active grapple
local hasActiveGrapple = false
for mo in MovableMan.AddedActors do
if mo and mo.PresetName == "Grapple Gun Claw" and mo.parentGun and mo.parentGun.ID == self.ID then
hasActiveGrapple = true
break
end
end

-- Update magazine based on grapple state
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
local mag = ToMOSParticle(self.Magazine)
if hasActiveGrapple then
mag.RoundCount = 0 -- Empty when grapple is out
self.hasGrappleActive = true
elseif self.hasGrappleActive and not hasActiveGrapple then
-- Grapple just returned, restore ammo
mag.RoundCount = 1
self.hasGrappleActive = false
end
end

-- Ensure magazine is visually "full" and ready if no grapple is active.
-- This assumes the HDFirearm's standard magazine logic handles firing.
-- If a grapple claw MO (the projectile) is active, Grapple.lua will hide the magazine.
-- This section ensures it's visible when no grapple is out.
if self.Magazine and MovableMan:IsParticle(self.Magazine) then
local magParticle = ToMOSParticle(self.Magazine)
local isActiveGrapple = false
-- Check if there's an active grapple associated with this gun
for mo_instance in MovableMan:GetMOsByPreset("Grapple Gun Claw") do
if mo_instance and mo_instance.parentGun and mo_instance.parentGun.ID == self.ID then
isActiveGrapple = true
break
end
end

if not isActiveGrapple then
magParticle.RoundCount = 1 -- Visually full
magParticle.Scale = 1 -- Visible
magParticle.Frame = 0 -- Standard frame
else
magParticle.Scale = 0 -- Hidden by active grapple (Grapple.lua also does this)
magParticle.RoundCount = 0 -- Visually empty

end
end
end

function Destroy(self)
-- Clean up the guide arrow if it exists
if self.arrow and self.arrow.ID ~= rte.NoMOID and MovableMan:IsValid(self.arrow) then
MovableMan:RemoveMO(self.arrow)
self.arrow = nil
end
end
48 changes: 44 additions & 4 deletions Data/Base.rte/Devices/Tools/GrappleGun/Pie.lua
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
-- Load required modules
-- RopeStateManager might not be directly needed here if we only set GrappleMode on the gun.
-- local RopeStateManager = require("Devices.Tools.GrappleGun.Scripts.RopeStateManager")

-- Utility function to safely check if an object has a specific property (key) in its Lua script table.
-- This is useful for checking if a script-defined variable exists on an MO.
local function HasScriptProperty(obj, propName)
if type(obj) ~= "table" or type(propName) ~= "string" then
return false
end
-- pcall to safely access potentially non-existent script members.
-- This is more about checking Lua script-defined members rather than engine properties.
local status, result = pcall(function() return rawget(obj, propName) ~= nil end)
return status and result
end

-- Helper function to validate grapple gun
local function ValidateGrappleGun(pieMenuOwner)
if not pieMenuOwner or not pieMenuOwner.EquippedItem then
return nil
end

local gun = ToMOSRotating(pieMenuOwner.EquippedItem)
if gun and gun.PresetName == "Grapple Gun" then
return gun
end

return nil
end

-- Action for Retract slice in the pie menu.
function GrapplePieRetract(pieMenuOwner, pieMenu, pieSlice)
local gun = pieMenuOwner.EquippedItem;
local gun = ValidateGrappleGun(pieMenuOwner)
if gun then
ToMOSRotating(gun):SetNumberValue("GrappleMode", 1);
gun:SetNumberValue("GrappleMode", 1) -- 1 signifies Retract
end
end

-- Action for Extend slice in the pie menu.
function GrapplePieExtend(pieMenuOwner, pieMenu, pieSlice)
local gun = pieMenuOwner.EquippedItem;
local gun = ValidateGrappleGun(pieMenuOwner)
if gun then
gun:SetNumberValue("GrappleMode", 2) -- 2 signifies Extend
end
end

-- Action for Unhook slice in the pie menu.
function GrapplePieUnhook(pieMenuOwner, pieMenu, pieSlice)
local gun = ValidateGrappleGun(pieMenuOwner)
if gun then
ToMOSRotating(gun):SetNumberValue("GrappleMode", 2);
gun:SetNumberValue("GrappleMode", 3) -- 3 signifies Unhook
end
end
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading