HookGun.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>Held utility that launches a latching hook and pulls the player to the hit point.</summary>
public sealed class HookGun : Component
{
private enum HookState
{
Idle,
Flying,
Pulling,
}
/// <summary>Transform at the barrel; the hook spawns here in VR.</summary>
[Property] public GameObject MuzzleTransform { get; set; }
/// <summary>Prefab cloned when the hook is fired.</summary>
[Property] public GameObject HookPrefab { get; set; }
/// <summary>Hook travel speed in world units per second.</summary>
[Property] public float HookSpeed { get; set; } = 2500f;
/// <summary>Max distance the hook can travel before it retracts.</summary>
[Property] public float MaxRange { get; set; } = 2500f;
/// <summary>Player pull speed toward the latch point in world units per second.</summary>
[Property, Group( "Grapple" )]
public float PullSpeed { get; set; } = 700f;
/// <summary>Distance from the latch point at which the pull stops yanking.</summary>
[Property, Group( "Grapple" )]
public float StopDistance { get; set; } = 48f;
/// <summary>Line drawn from the muzzle to the hook tip / latch point.</summary>
[Property] public LineRenderer Rope { get; set; }
[Sync] private bool RopeActive { get; set; }
[Sync] private Vector3 RopeEnd { get; set; }
private HookState _state = HookState.Idle;
private HookProjectile _activeHook;
private Vector3 _attachPoint;
private bool _ownedGrapple;
private bool IsHeld =>
GameObject.Components.GetAll<GrabPoint>( FindMode.InDescendants ).Any( g => g.IsGrabbed );
protected override void OnUpdate()
{
if ( !IsProxy )
{
if ( _state != HookState.Idle && !IsHeld )
Retract();
else if ( _ownedGrapple && !IsPlayerStillGrappling() )
Retract();
}
UpdateRopeEnd();
UpdateRopeVisual();
}
protected override void OnDestroy()
{
if ( !IsProxy )
Retract();
}
/// <summary>Launch the hook when idle, or retract it while flying / pulling.</summary>
public void Fire()
{
if ( IsProxy )
return;
if ( _state == HookState.Idle )
Launch();
else
Retract();
}
public void OnAttached( Vector3 hitPosition )
{
if ( IsProxy || _state != HookState.Flying )
return;
_attachPoint = hitPosition;
_state = HookState.Pulling;
RopeActive = true;
RopeEnd = hitPosition;
var playerController = ResolvePlayerController();
if ( playerController.IsValid() )
{
playerController.BeginGrapple( hitPosition, PullSpeed, StopDistance );
_ownedGrapple = true;
}
}
public void OnHookMissed()
{
if ( IsProxy )
return;
if ( _state != HookState.Flying )
return;
_activeHook = null;
_state = HookState.Idle;
RopeActive = false;
}
private void Launch()
{
if ( HookPrefab is null || MuzzleTransform is null )
return;
Vector3 spawnPosition;
Vector3 shootDirection;
if ( Game.IsRunningInVR )
{
spawnPosition = MuzzleTransform.WorldPosition;
shootDirection = MuzzleTransform.WorldRotation.Forward;
}
else
{
if ( PlayerViewModel.Instance?.Camera is null )
return;
var ray = PlayerViewModel.Instance.GetAimRay( Vector2.Zero );
spawnPosition = ray.Position;
shootDirection = ray.Forward;
}
var hookObject = HookPrefab.Clone( spawnPosition, Rotation.LookAt( shootDirection ) );
var projectile = hookObject.Components.Get<HookProjectile>();
if ( projectile is null )
{
hookObject.Destroy();
return;
}
projectile.Velocity = shootDirection.Normal * HookSpeed;
projectile.WeaponObject = GameObject;
projectile.OwnerGun = this;
projectile.SpawnOrigin = spawnPosition;
projectile.MaxRange = MaxRange;
_activeHook = projectile;
_state = HookState.Flying;
RopeActive = true;
RopeEnd = spawnPosition;
}
private void Retract()
{
if ( _activeHook.IsValid() )
{
var hook = _activeHook;
_activeHook = null;
hook.OwnerGun = null;
hook.GameObject.Destroy();
}
_activeHook = null;
EndOwnedGrapple();
_state = HookState.Idle;
RopeActive = false;
}
private void EndOwnedGrapple()
{
if ( !_ownedGrapple )
return;
var playerController = ResolvePlayerController();
if ( playerController.IsValid() )
playerController.EndGrapple();
_ownedGrapple = false;
}
private bool IsPlayerStillGrappling()
{
var playerController = ResolvePlayerController();
return playerController.IsValid() && playerController.IsGrappling;
}
private void UpdateRopeEnd()
{
if ( IsProxy || !RopeActive )
return;
if ( _state == HookState.Pulling )
{
RopeEnd = _attachPoint;
var playerController = ResolvePlayerController();
if ( playerController.IsValid() && _ownedGrapple )
playerController.GrappleTarget = _attachPoint;
return;
}
if ( _activeHook.IsValid() )
RopeEnd = _activeHook.WorldPosition;
}
private void UpdateRopeVisual()
{
if ( Rope is null )
return;
var ropeStart = ResolveRopeStart();
if ( ropeStart is null )
return;
Rope.Enabled = RopeActive;
if ( !RopeActive )
return;
Rope.UseVectorPoints = true;
Rope.VectorPoints = new List<Vector3>
{
ropeStart.WorldPosition,
RopeEnd,
};
}
/// <summary>
/// Local desktop owner: viewmodel muzzle. Everyone else (including other players on your client): world weapon muzzle.
/// </summary>
private GameObject ResolveRopeStart()
{
if ( !IsProxy && !Game.IsRunningInVR )
{
var viewMuzzle = PlayerViewModel.Instance?.Muzzle;
if ( viewMuzzle.IsValid() )
return viewMuzzle;
}
return MuzzleTransform;
}
private PlayerCharacterController ResolvePlayerController()
{
var fromHierarchy = GetComponentInParent<PlayerCharacterController>();
if ( fromHierarchy.IsValid() )
return fromHierarchy;
if ( Player.CurrentPlayer.IsValid() )
return Player.CurrentPlayer.Components.Get<PlayerCharacterController>();
return null;
}
}