Carriable/FishingRod.cs
using System;
using Braxnet;
using Clover.Animals;
using Clover.Data;
using Clover.Objects;
using Clover.Persistence;
using Clover.Player;
namespace Clover.Carriable;
[Category( "Clover/Carriable" )]
public class FishingRod : BaseCarriable
{
public enum RodState
{
Idle,
Casting,
Reeling,
Hooking,
Fighting
}
[Property] public GameObject BobberPrefab { get; set; }
// [Property] public GameObject
[Property] public GameObject LineStartPoint { get; set; }
[Property] public SoundEvent CastSound { get; set; }
[Property] public SoundEvent ReelSound { get; set; }
[Property] public SoundEvent HookSound { get; set; }
[Property] public SoundEvent SplashSound { get; set; }
[Property] public GameObject SplashParticle { get; set; }
[Property] public LineRenderer LineRenderer { get; set; }
public float LineLength = 500f;
public bool HasCasted = false;
private bool _isWindup = false;
private bool _isCasting = false;
// private bool _isBusy = false;
private TimeSince _timeSinceWindup;
private float _castDistance = 0f;
private float _trashChance = 0.1f; // TODO: base this on luck?
private SoundHandle _reelSound;
private const float MinCastDistance = 32f;
private const float MaxCastDistance = 192f;
private const float WaterCheckHeight = 64f;
private float LineSlackDistance => Sandbox.Utility.Noise.Perlin( Time.Now * 10f ) * 16f;
private GameObject _lineSlackDummy;
/*public class CurrentFishData
{
public FishData Data;
public float Weight;
public float Stamina;
public float StaminaMax;
}*/
private FishingBobber Bobber;
public CatchableFish CurrentFish;
public float Stamina = 100f;
public float LineStrength = 100f;
protected override void OnStart()
{
base.OnStart();
_reelSound = GameObject.PlaySound( ReelSound );
if ( _reelSound.IsValid() ) _reelSound.Volume = 0f;
}
public override void OnUseDown()
{
if ( !CanUse() )
{
Log.Warning( "Cannot use." );
return;
}
if ( !Networking.IsHost )
{
Log.Error( "Only the host can use world altering items for now." );
return;
}
if ( _isCasting )
{
Log.Warning( "Already casting." );
return;
}
if ( !HasCasted )
{
_isWindup = true;
_timeSinceWindup = 0f;
NextUse = 0f;
return;
}
NextUse = 1f;
if ( HasCasted && !Bobber.IsValid() )
{
Log.Warning( "Bobber is not valid." );
HasCasted = false;
}
if ( HasCasted )
{
if ( Stamina <= 5 ) return;
if ( Bobber.Fish.IsValid() )
{
Bobber.Fish.OnLinePull();
}
ReelIn( 32f );
}
}
public override void OnUseUp()
{
base.OnUseUp();
if ( _isWindup )
{
_isWindup = false;
_isCasting = true;
NextUse = 0.5f;
Cast();
}
}
protected override void OnDestroy()
{
base.OnDestroy();
_reelSound?.Stop();
DestroyBobber();
}
protected override void OnFixedUpdate()
{
if ( IsProxy ) return;
if ( _lineSlackDummy.IsValid() )
{
_lineSlackDummy.WorldPosition = LineStartPoint.WorldPosition.LerpTo( Bobber.WorldPosition, 0.5f ) +
Vector3.Down * LineSlackDistance;
}
if ( _isWindup )
{
var castDistance = Math.Clamp( 32f + (_timeSinceWindup * 90f), MinCastDistance, MaxCastDistance );
// Log.Info( $"Windup: {castDistance}, {_timeSinceWindup}" );
SetRodAngle( (castDistance / MaxCastDistance) * 90f );
return;
}
if ( HasCasted && CanUse() )
{
if ( Input.AnalogMove.x < 0 )
{
ReelIn( 8f, Vector3.Backward );
NextUse = 0.3f;
}
else if ( Input.AnalogMove.y > 0 )
{
ReelIn( 4f, Vector3.Left );
NextUse = 0.3f;
}
else if ( Input.AnalogMove.y < 0 )
{
ReelIn( 4f, Vector3.Right );
NextUse = 0.3f;
}
}
if ( _reelSound.IsValid() )
{
_reelSound.Volume = _reelSound.Volume.LerpTo( 0f, Sandbox.Utility.Easing.QuadraticIn( Time.Delta * 15f ) );
// _reelSound.Pitch = _reelSound.Pitch.LerpTo( 1f, Time.Delta * 3f );
}
if ( Stamina < 100f )
{
// Stamina = Stamina.LerpTo( 100f, Time.Delta * 5f );
Stamina += Time.Delta * 7.5f;
if ( Bobber.IsValid() && Bobber.Fish.IsValid() )
{
if ( Bobber.Fish.Stamina <= 0 )
{
Stamina += Time.Delta * 17f;
}
}
}
if ( LineStrength <= 0 )
{
ResetAll();
}
}
private Vector3 GetCastPosition()
{
return Player.WorldPosition + Player.Model.WorldRotation.Forward * _castDistance;
}
public override bool ShouldDisableMovement()
{
return HasCasted || _isCasting;
}
private async void Cast()
{
Log.Info( "Casting." );
if ( Player == null ) throw new Exception( "Player is null." );
_castDistance = Math.Clamp( 32f + (_timeSinceWindup * 90f), MinCastDistance, MaxCastDistance );
if ( !CheckForWater( GetCastPosition() ) )
{
Log.Warning( $"CAST: No water found at {GetCastPosition()}." );
ResetAll();
return;
}
_isCasting = true;
// play the cast animation
// GetNode<AnimationPlayer>( "AnimationPlayer" ).Play( "cast" );
// Model.LocalRotation = Rotation.FromPitch( 90f );
// wait for the animation to finish
// await ToSignal( GetNode<AnimationPlayer>( "AnimationPlayer" ), AnimationPlayer.SignalName.AnimationFinished );
// await Task.DelayRealtimeSeconds( 0.5f );
var tween1 = TweenManager.CreateTween();
tween1.AddLocalRotation( Model, Rotation.FromPitch( 90f ), 0.5f )
.SetEasing( Sandbox.Utility.Easing.QuadraticOut );
tween1.AddLocalRotation( Model, Rotation.FromPitch( 0f ), 0.3f )
.SetEasing( Sandbox.Utility.Easing.QuadraticIn );
await tween1.Wait();
GameObject.PlaySound( CastSound );
var tween2 = TweenManager.CreateTween();
tween2.AddLocalRotation( Model, Rotation.FromPitch( -20f ), 0.3f )
.SetEasing( Sandbox.Utility.Easing.QuadraticOut );
await tween2.Wait();
var tween3 = TweenManager.CreateTween();
tween3.AddLocalRotation( Model, Rotation.FromPitch( 0f ), 0.3f )
.SetEasing( Sandbox.Utility.Easing.QuadraticIn );
// Model.LocalRotation = Rotation.FromPitch( 0f );
if ( !Bobber.IsValid() )
{
var waterPosition = GetWaterSurface( GetCastPosition() );
Bobber = BobberPrefab.Clone().Components.Get<FishingBobber>();
Bobber.Rod = this;
LineRenderer?.Points.Add( Bobber.Tip );
Bobber.WorldPosition = LineStartPoint.WorldPosition;
Bobber.WorldRotation = Rotation.FromYaw( Player.Model.WorldRotation.Yaw() );
CameraMan.Instance.AddTarget( Bobber.GameObject );
Bobber.GameObject.NetworkSpawn();
// place slack dummy between the line start and the bobber
_lineSlackDummy = Scene.CreateObject();
_lineSlackDummy.WorldPosition = LineStartPoint.WorldPosition.LerpTo( Bobber.WorldPosition, 0.5f ) +
Vector3.Down * LineSlackDistance;
LineRenderer?.Points.Insert( 1, _lineSlackDummy );
// tween the bobber to the water in an arc
/*var tween = GetTree().CreateTween();
tween.TweenMethod( Callable.From<float>( ( float i ) =>
{
Bobber.GlobalPosition = LinePoint.GlobalPosition.CubicInterpolate( waterPosition,
LinePoint.GlobalPosition + Vector3.Down * 1f, waterPosition + Vector3.Down * 10f, i );
} ), 0f, 1f, 0.5f ).SetEase( Tween.EaseType.Out );
await ToSignal( tween, Tween.SignalName.Finished );*/
var tween = TweenManager.CreateTween();
tween.AddPosition( Bobber.GameObject, waterPosition, 0.5f ).SetEasing( Sandbox.Utility.Easing.QuadraticIn );
await tween.Wait();
Bobber.OnHitWater();
/*var splash = SplashScene.Instantiate<Node3D>();
Player.World.AddChild( splash );
splash.GlobalPosition = waterPosition;*/
SplashParticle.Clone( waterPosition, Rotation.FromPitch( -90f ) );
}
else
{
Log.Error( "Bobber is already valid." );
ResetAll();
return;
}
// CreateLine();
_isCasting = false;
HasCasted = true;
Stamina = 100f;
}
public static bool CheckForWater( Vector3 position )
{
var traceWater = Game.ActiveScene.Trace
.Ray( position + Vector3.Up * 32f, position + Vector3.Down * WaterCheckHeight )
.WithTag( "water" )
.Run();
if ( !traceWater.Hit )
{
// Log.Warning( $"No water found at {position}." );
return false;
}
var traceTerrain = Game.ActiveScene.Trace
.Ray( traceWater.HitPosition + Vector3.Up * 32f, traceWater.HitPosition + Vector3.Down * 16f )
.WithTag( "terrain" )
.Run();
if ( traceTerrain.Hit )
{
// Log.Warning( $"Terrain found at {position}." );
return false;
}
// TODO: check if it's the waterfall or something
/* if ( traceWater.Normal != Vector3.Up )
{
Log.Warning( $"Water normal is not up ({traceWater.Normal})." );
return false;
} */
return true;
}
public Vector3 GetWaterSurface( Vector3 position )
{
var traceWater = Scene.Trace.Ray( position + Vector3.Up * WaterCheckHeight,
position + Vector3.Down * (WaterCheckHeight * 2) )
.WithTag( "water" )
.Run();
if ( !traceWater.Hit )
{
Log.Warning( $"No water found at {position}." );
return Vector3.Zero;
}
return traceWater.HitPosition;
}
private void DestroyBobber()
{
if ( Bobber.IsValid() ) LineRenderer?.Points.Remove( Bobber.Tip );
if ( _lineSlackDummy.IsValid() ) LineRenderer?.Points.Remove( _lineSlackDummy );
Bobber?.DestroyGameObject();
Bobber = null;
_lineSlackDummy?.Destroy();
_lineSlackDummy = null;
}
private void ResetAll()
{
HasCasted = false;
_isCasting = false;
// _isBusy = false;
Stamina = 100f;
LineStrength = 100f;
SetRodAngle( 0 );
DestroyBobber();
}
private void ReelIn( float amount, Vector3 direction = default )
{
var forward = Player.Model.WorldRotation.Forward;
var reelDirection = (Player.WorldPosition - Bobber.WorldPosition).Normal;
if ( direction != Vector3.Zero )
{
if ( direction == Vector3.Right )
{
reelDirection += Player.Model.WorldRotation.Right;
}
else if ( direction == Vector3.Left )
{
reelDirection += Player.Model.WorldRotation.Left;
}
else if ( direction == Vector3.Backward )
{
reelDirection += Player.Model.WorldRotation.Backward;
}
}
var reelPosition = Bobber.WorldPosition + reelDirection * amount;
// Gizmo.Draw.LineSphere( reelPosition, 8f );
var dist = reelPosition.Distance( WorldPosition );
var reelPosInWater = CheckForWater( reelPosition );
if ( !reelPosInWater || dist < 32f )
{
// Log.Info( "Reel position too close or not in water. Reeling in." );
// ResetAll();
if ( Bobber.Fish.IsValid() && Bobber.Fish.State == CatchableFish.FishState.Fighting )
{
if ( dist < 35 || !reelPosInWater ) // TODO: why this again
{
if ( Bobber.Fish.Stamina <= 1 )
{
Log.Info( "Reeling in, fish is tired." );
CatchFish( Bobber.Fish );
}
else
{
Log.Info( "Reeling in, fish is fighting." );
LineStrength -= 2f;
}
}
else
{
Log.Info( "Fish too far away." );
}
}
else
{
Log.Info( "Reeling in, too close or not in water." );
ResetAll();
}
return;
}
var waterSurface = GetWaterSurface( reelPosition );
reelPosition.z = waterSurface.z;
var pitch = Math.Clamp( (dist * -0.5f) + 90f, -20f, 100f );
// Log.Info( $"Reeling in. Distance: {dist}, Pitch: {pitch} ({(dist * -0.5f) + 90f})" );
SetRodAngle( pitch );
var tween = TweenManager.CreateTween();
tween.AddPosition( Bobber.GameObject, reelPosition, 0.4f ).SetEasing( Sandbox.Utility.Easing.QuadraticOut );
if ( _reelSound.IsValid() ) _reelSound.Volume = 1f;
// _reelSound.Pitch = 2f;
NextUse = 0.5f;
Stamina -= amount * 0.5f;
LineStrength -= 2f;
if ( Bobber.Fish.IsValid() )
{
Bobber.Fish.Stamina -= amount * 0.01f;
}
}
public void Fight()
{
}
public async void CatchFish( CatchableFish fishInWater )
{
if ( !fishInWater.IsValid() )
{
Log.Warning( "Fish is not valid." );
return;
}
Log.Info( "Caught fish." );
NextUse = 3f;
// GetNode<AudioStreamPlayer3D>( "Splash" ).Play();
GameObject.PlaySound( SplashSound );
// GetNode<AnimationPlayer>( "AnimationPlayer" ).Play( "catch" );
// var isTrash = GD.Randf() < _trashChance;
var isTrash = Random.Shared.Float() < _trashChance;
if ( !isTrash )
{
/*var carryableFish = fishInWater.Data.CarryScene.Instantiate<Fish>();
Player.World.AddChild( carryableFish );
carryableFish.GlobalTransform = fishInWater.GlobalTransform;
fishInWater.QueueFree();
// tween the fish to the player
var tween = GetTree().CreateTween();
tween.TweenProperty( carryableFish, "position", Player.GlobalPosition + Vector3.Up * 0.5f, 0.5f )
.SetTrans( Tween.TransitionType.Quad ).SetEase( Tween.EaseType.Out );
// tween.TweenCallback( Callable.From( carryableFish.QueueFree ) );
await ToSignal( tween, Tween.SignalName.Finished );
GiveFish( carryableFish );*/
var data = fishInWater.Data;
var model = data.ModelScene.Clone();
model.WorldPosition = fishInWater.WorldPosition;
model.WorldRotation = fishInWater.WorldRotation;
fishInWater.DestroyGameObject();
var tween = TweenManager.CreateTween();
tween.AddPosition( model, Player.WorldPosition + Vector3.Up * 16f, 0.5f )
.SetEasing( Sandbox.Utility.Easing.QuadraticOut );
await tween.Wait();
model.Destroy();
var item = PersistentItem.Create( data );
Player.Inventory.PickUpItem( item );
}
else
{
/*var trashItemData =
Loader.LoadResource<ItemData>( ResourceManager.Instance.GetItemPathByName( "item:shoe" ) );
var trash = trashItemData.DropScene.Instantiate<DroppedItem>();
// Player.World.AddChild( trash );
trash.GlobalTransform = fishInWater.GlobalTransform;
trash.DisableCollisions();
fishInWater.QueueFree();
// tween the trash to the player
var tween = GetTree().CreateTween();
tween.TweenProperty( trash, "position", Player.GlobalPosition + Vector3.Up * 0.5f, 0.5f )
.SetTrans( Tween.TransitionType.Quad ).SetEase( Tween.EaseType.Out );
// tween.TweenCallback( Callable.From( carryableFish.QueueFree ) );
await ToSignal( tween, Tween.SignalName.Finished );
GiveTrash( trash );*/
}
// ReelIn( 128f );
ResetAll();
}
private void SetRodAngle( float angle )
{
// Model.LocalRotation = Rotation.FromPitch( angle * -1f );
var tween = TweenManager.CreateTween();
tween.AddLocalRotation( Model, Rotation.FromPitch( angle * -1f ), 0.1f )
.SetEasing( Sandbox.Utility.Easing.QuadraticOut );
}
/*private void GiveFish( Fish fish )
{
var playerInventory = Player.Inventory;
if ( playerInventory == null )
{
Log.Warning( "Player inventory is null." );
return;
}
var carry = PersistentItem.Create( fish );
// carry.ItemDataPath = fish.Data.ResourcePath;
// carry.ItemScenePath = fish.Data.DropScene != null && !string.IsNullOrEmpty( fish.Data.DropScene.ResourcePath ) ? fish.Data.DropScene.ResourcePath : World.DefaultDropScene;
// carry.PlacementType = World.ItemPlacementType.Dropped;
playerInventory.PickUpItem( carry );
fish.QueueFree();
}*/
/*private void GiveTrash( DroppedItem trash )
{
var playerInventory = Player.Inventory;
if ( playerInventory == null )
{
Log.Warning( "Player inventory is null." );
return;
}
var carry = PersistentItem.Create( trash );
// carry.ItemDataPath = trash.Data.ResourcePath;
// carry.ItemScenePath = trash.Data.DropScene != null && !string.IsNullOrEmpty( trash.Data.DropScene.ResourcePath ) ? trash.Data.DropScene.ResourcePath : World.DefaultDropScene;
// carry.PlacementType = World.ItemPlacementType.Dropped;
playerInventory.PickUpItem( carry );
trash.QueueFree();
}*/
public void FishGotAway()
{
Log.Info( "Fish got away." );
NextUse = 1f;
ReelIn( 128f );
}
public override string GetUseName()
{
if ( HasCasted )
{
return "Reel In";
}
return "Cast (hold)";
}
/*protected override void OnUpdate()
{
base.OnUpdate();
if ( Bobber.IsValid() )
{
Gizmo.Draw.Line( LineStartPoint.WorldPosition, Bobber.WorldPosition );
}
}*/
}