diff --git a/code/Carriable.cs b/code/Carriable.cs new file mode 100644 index 0000000..e173cdb --- /dev/null +++ b/code/Carriable.cs @@ -0,0 +1,31 @@ +using Sandbox; + +public partial class Carriable : BaseCarriable, IUse +{ + public override void CreateViewModel() + { + Host.AssertClient(); + + if ( string.IsNullOrEmpty( ViewModelPath ) ) + return; + + ViewModelEntity = new ViewModel + { + Position = Position, + Owner = Owner, + EnableViewmodelRendering = true + }; + + ViewModelEntity.SetModel( ViewModelPath ); + } + + public bool OnUse( Entity user ) + { + return false; + } + + public virtual bool IsUsable( Entity user ) + { + return Owner == null; + } +} diff --git a/code/Game.cs b/code/Game.cs new file mode 100644 index 0000000..cad17e6 --- /dev/null +++ b/code/Game.cs @@ -0,0 +1,97 @@ +using Sandbox; + +[Library( "sandbox", Title = "Sandbox" )] +partial class SandboxGame : Game +{ + public SandboxGame() + { + if ( IsServer ) + { + // Create the HUD + _ = new SandboxHud(); + } + } + + public override void ClientJoined( Client cl ) + { + base.ClientJoined( cl ); + var player = new SandboxPlayer(); + player.Respawn(); + + cl.Pawn = player; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + } + + [ServerCmd( "spawn" )] + public static void Spawn( string modelname ) + { + var owner = ConsoleSystem.Caller?.Pawn; + + if ( ConsoleSystem.Caller == null ) + return; + + var tr = Trace.Ray( owner.EyePos, owner.EyePos + owner.EyeRot.Forward * 500 ) + .UseHitboxes() + .Ignore( owner ) + .Run(); + + var ent = new Prop(); + ent.Position = tr.EndPos; + ent.Rotation = Rotation.From( new Angles( 0, owner.EyeRot.Angles().yaw, 0 ) ) * Rotation.FromAxis( Vector3.Up, 180 ); + ent.SetModel( modelname ); + ent.Position = tr.EndPos - Vector3.Up * ent.CollisionBounds.Mins.z; + } + + [ServerCmd( "spawn_entity" )] + public static void SpawnEntity( string entName ) + { + var owner = ConsoleSystem.Caller.Pawn; + + if ( owner == null ) + return; + + var attribute = Library.GetAttribute( entName ); + + if ( attribute == null || !attribute.Spawnable ) + return; + + var tr = Trace.Ray( owner.EyePos, owner.EyePos + owner.EyeRot.Forward * 200 ) + .UseHitboxes() + .Ignore( owner ) + .Size( 2 ) + .Run(); + + var ent = Library.Create( entName ); + if ( ent is BaseCarriable && owner.Inventory != null ) + { + if ( owner.Inventory.Add( ent, true ) ) + return; + } + + ent.Position = tr.EndPos; + ent.Rotation = Rotation.From( new Angles( 0, owner.EyeRot.Angles().yaw, 0 ) ); + + //Log.Info( $"ent: {ent}" ); + } + + public override void DoPlayerNoclip( Client player ) + { + if ( player.Pawn is Player basePlayer ) + { + if ( basePlayer.DevController is NoclipController ) + { + Log.Info( "Noclip Mode Off" ); + basePlayer.DevController = null; + } + else + { + Log.Info( "Noclip Mode On" ); + basePlayer.DevController = new NoclipController(); + } + } + } +} diff --git a/code/Inventory.cs b/code/Inventory.cs new file mode 100644 index 0000000..daac496 --- /dev/null +++ b/code/Inventory.cs @@ -0,0 +1,50 @@ +using Sandbox; +using System; +using System.Linq; + +partial class Inventory : BaseInventory +{ + public Inventory( Player player ) : base( player ) + { + } + + public override bool CanAdd( Entity entity ) + { + if ( !entity.IsValid() ) + return false; + + if ( !base.CanAdd( entity ) ) + return false; + + return !IsCarryingType( entity.GetType() ); + } + + public override bool Add( Entity entity, bool makeActive = false ) + { + if ( !entity.IsValid() ) + return false; + + if ( IsCarryingType( entity.GetType() ) ) + return false; + + return base.Add( entity, makeActive ); + } + + public bool IsCarryingType( Type t ) + { + return List.Any( x => x?.GetType() == t ); + } + + public override bool Drop( Entity ent ) + { + if ( !Host.IsServer ) + return false; + + if ( !Contains( ent ) ) + return false; + + ent.OnCarryDrop( Owner ); + + return ent.Parent == null; + } +} diff --git a/code/Player.Clothes.cs b/code/Player.Clothes.cs new file mode 100644 index 0000000..fd8e8c3 --- /dev/null +++ b/code/Player.Clothes.cs @@ -0,0 +1,113 @@ +using Sandbox; + +partial class SandboxPlayer +{ + ModelEntity pants; + ModelEntity jacket; + ModelEntity shoes; + ModelEntity hat; + + bool dressed = false; + + public void Dress() + { + if ( dressed ) return; + dressed = true; + + if ( true ) + { + var model = Rand.FromArray( new[] + { + "models/citizen_clothes/trousers/trousers.jeans.vmdl", + "models/citizen_clothes/trousers/trousers.lab.vmdl", + "models/citizen_clothes/trousers/trousers.police.vmdl", + "models/citizen_clothes/trousers/trousers.smart.vmdl", + "models/citizen_clothes/trousers/trousers.smarttan.vmdl", + "models/citizen/clothes/trousers_tracksuit.vmdl", + "models/citizen_clothes/trousers/trousers_tracksuitblue.vmdl", + "models/citizen_clothes/trousers/trousers_tracksuit.vmdl", + "models/citizen_clothes/shoes/shorts.cargo.vmdl", + } ); + + pants = new ModelEntity(); + pants.SetModel( model ); + pants.SetParent( this, true ); + pants.EnableShadowInFirstPerson = true; + pants.EnableHideInFirstPerson = true; + + SetBodyGroup( "Legs", 1 ); + } + + if ( true ) + { + var model = Rand.FromArray( new[] + { + "models/citizen_clothes/jacket/labcoat.vmdl", + "models/citizen_clothes/jacket/jacket.red.vmdl", + "models/citizen_clothes/jacket/jacket.tuxedo.vmdl", + "models/citizen_clothes/jacket/jacket_heavy.vmdl", + } ); + + jacket = new ModelEntity(); + jacket.SetModel( model ); + jacket.SetParent( this, true ); + jacket.EnableShadowInFirstPerson = true; + jacket.EnableHideInFirstPerson = true; + + var propInfo = jacket.GetModel().GetPropData(); + if ( propInfo.ParentBodyGroupName != null ) + { + SetBodyGroup( propInfo.ParentBodyGroupName, propInfo.ParentBodyGroupValue ); + } + else + { + SetBodyGroup( "Chest", 0 ); + } + } + + if ( true ) + { + var model = Rand.FromArray( new[] + { + "models/citizen_clothes/shoes/trainers.vmdl", + "models/citizen_clothes/shoes/shoes.workboots.vmdl" + } ); + + shoes = new ModelEntity(); + shoes.SetModel( model ); + shoes.SetParent( this, true ); + shoes.EnableShadowInFirstPerson = true; + shoes.EnableHideInFirstPerson = true; + + SetBodyGroup( "Feet", 1 ); + } + + if ( true ) + { + var model = Rand.FromArray( new[] + { + "models/citizen_clothes/hat/hat_hardhat.vmdl", + "models/citizen_clothes/hat/hat_woolly.vmdl", + "models/citizen_clothes/hat/hat_securityhelmet.vmdl", + "models/citizen_clothes/hair/hair_malestyle02.vmdl", + "models/citizen_clothes/hair/hair_femalebun.black.vmdl", + "models/citizen_clothes/hat/hat_beret.red.vmdl", + "models/citizen_clothes/hat/hat.tophat.vmdl", + "models/citizen_clothes/hat/hat_beret.black.vmdl", + "models/citizen_clothes/hat/hat_cap.vmdl", + "models/citizen_clothes/hat/hat_leathercap.vmdl", + "models/citizen_clothes/hat/hat_leathercapnobadge.vmdl", + "models/citizen_clothes/hat/hat_securityhelmetnostrap.vmdl", + "models/citizen_clothes/hat/hat_service.vmdl", + "models/citizen_clothes/hat/hat_uniform.police.vmdl", + "models/citizen_clothes/hat/hat_woollybobble.vmdl", + } ); + + hat = new ModelEntity(); + hat.SetModel( model ); + hat.SetParent( this, true ); + hat.EnableShadowInFirstPerson = true; + hat.EnableHideInFirstPerson = true; + } + } +} diff --git a/code/Player.Ragdoll.cs b/code/Player.Ragdoll.cs new file mode 100644 index 0000000..470771c --- /dev/null +++ b/code/Player.Ragdoll.cs @@ -0,0 +1,85 @@ +using Sandbox; + +partial class SandboxPlayer +{ + [ClientRpc] + private void BecomeRagdollOnClient( Vector3 velocity, DamageFlags damageFlags, Vector3 forcePos, Vector3 force, int bone ) + { + var ent = new ModelEntity(); + ent.Position = Position; + ent.Rotation = Rotation; + ent.Scale = Scale; + ent.MoveType = MoveType.Physics; + ent.UsePhysicsCollision = true; + ent.EnableAllCollisions = true; + ent.CollisionGroup = CollisionGroup.Debris; + ent.SetModel( GetModelName() ); + ent.CopyBonesFrom( this ); + ent.CopyBodyGroups( this ); + ent.CopyMaterialGroup( this ); + ent.TakeDecalsFrom( this ); + ent.EnableHitboxes = true; + ent.EnableAllCollisions = true; + ent.SurroundingBoundsMode = SurroundingBoundsType.Physics; + ent.RenderColorAndAlpha = RenderColorAndAlpha; + ent.PhysicsGroup.Velocity = velocity; + + if ( Local.Pawn == this ) + { + //ent.EnableDrawing = false; wtf + } + + ent.SetInteractsAs( CollisionLayer.Debris ); + ent.SetInteractsWith( CollisionLayer.WORLD_GEOMETRY ); + ent.SetInteractsExclude( CollisionLayer.Player | CollisionLayer.Debris ); + + foreach ( var child in Children ) + { + if ( child is ModelEntity e ) + { + var model = e.GetModelName(); + if ( model != null && !model.Contains( "clothes" ) ) + continue; + + var clothing = new ModelEntity(); + clothing.SetModel( model ); + clothing.SetParent( ent, true ); + clothing.RenderColorAndAlpha = e.RenderColorAndAlpha; + + if ( Local.Pawn == this ) + { + // clothing.EnableDrawing = false; wtf + } + } + } + + if ( damageFlags.HasFlag( DamageFlags.Bullet ) || + damageFlags.HasFlag( DamageFlags.PhysicsImpact ) ) + { + PhysicsBody body = bone > 0 ? ent.GetBonePhysicsBody( bone ) : null; + + if ( body != null ) + { + body.ApplyImpulseAt( forcePos, force * body.Mass ); + } + else + { + ent.PhysicsGroup.ApplyImpulse( force ); + } + } + + if ( damageFlags.HasFlag( DamageFlags.Blast ) ) + { + if ( ent.PhysicsGroup != null ) + { + ent.PhysicsGroup.AddVelocity( (Position - (forcePos + Vector3.Down * 100.0f)).Normal * (force.Length * 0.2f) ); + var angularDir = (Rotation.FromYaw( 90 ) * force.WithZ( 0 ).Normal).Normal; + ent.PhysicsGroup.AddAngularVelocity( angularDir * (force.Length * 0.02f) ); + } + } + + Corpse = ent; + + ent.DeleteAsync( 10.0f ); + } +} diff --git a/code/Player.Use.cs b/code/Player.Use.cs new file mode 100644 index 0000000..8bf24e5 --- /dev/null +++ b/code/Player.Use.cs @@ -0,0 +1,35 @@ +using Sandbox; + +partial class SandboxPlayer +{ + public bool IsUseDisabled() + { + return ActiveChild is IUse use && use.IsUsable( this ); + } + + protected override Entity FindUsable() + { + if ( IsUseDisabled() ) + return null; + + var tr = Trace.Ray( EyePos, EyePos + EyeRot.Forward * (85 * Scale) ) + .Radius( 2 ) + .HitLayer( CollisionLayer.Debris ) + .Ignore( this ) + .Run(); + + if ( tr.Entity == null ) return null; + if ( tr.Entity is not IUse use ) return null; + if ( !use.IsUsable( this ) ) return null; + + return tr.Entity; + } + + protected override void UseFail() + { + if ( IsUseDisabled() ) + return; + + base.UseFail(); + } +} diff --git a/code/Player.cs b/code/Player.cs new file mode 100644 index 0000000..5a81cfd --- /dev/null +++ b/code/Player.cs @@ -0,0 +1,240 @@ +using Sandbox; + +partial class SandboxPlayer : Player +{ + private TimeSince timeSinceDropped; + private TimeSince timeSinceJumpReleased; + + private DamageInfo lastDamage; + + [Net] public PawnController VehicleController { get; set; } + [Net] public PawnAnimator VehicleAnimator { get; set; } + [Net, Predicted] public ICamera VehicleCamera { get; set; } + [Net, Predicted] public Entity Vehicle { get; set; } + [Net, Predicted] public ICamera MainCamera { get; set; } + + public ICamera LastCamera { get; set; } + + public SandboxPlayer() + { + Inventory = new Inventory( this ); + } + + public override void Spawn() + { + MainCamera = new FirstPersonCamera(); + LastCamera = MainCamera; + + base.Spawn(); + } + + public override void Respawn() + { + SetModel( "models/citizen/citizen.vmdl" ); + + Controller = new WalkController(); + Animator = new StandardPlayerAnimator(); + + MainCamera = LastCamera; + Camera = MainCamera; + + if ( DevController is NoclipController ) + { + DevController = null; + } + + EnableAllCollisions = true; + EnableDrawing = true; + EnableHideInFirstPerson = true; + EnableShadowInFirstPerson = true; + + Dress(); + + Inventory.Add( new PhysGun(), true ); + Inventory.Add( new GravGun() ); + Inventory.Add( new Tool() ); + Inventory.Add( new Pistol() ); + Inventory.Add( new Flashlight() ); + + base.Respawn(); + } + + public override void OnKilled() + { + base.OnKilled(); + + if ( lastDamage.Flags.HasFlag( DamageFlags.Vehicle ) ) + { + Particles.Create( "particles/impact.flesh.bloodpuff-big.vpcf", lastDamage.Position ); + Particles.Create( "particles/impact.flesh-big.vpcf", lastDamage.Position ); + PlaySound( "kersplat" ); + } + + VehicleController = null; + VehicleAnimator = null; + VehicleCamera = null; + Vehicle = null; + + BecomeRagdollOnClient( Velocity, lastDamage.Flags, lastDamage.Position, lastDamage.Force, GetHitboxBone( lastDamage.HitboxIndex ) ); + LastCamera = MainCamera; + MainCamera = new SpectateRagdollCamera(); + Camera = MainCamera; + Controller = null; + + EnableAllCollisions = false; + EnableDrawing = false; + + Inventory.DropActive(); + Inventory.DeleteContents(); + } + + public override void TakeDamage( DamageInfo info ) + { + if ( GetHitboxGroup( info.HitboxIndex ) == 1 ) + { + info.Damage *= 10.0f; + } + + lastDamage = info; + + TookDamage( lastDamage.Flags, lastDamage.Position, lastDamage.Force ); + + base.TakeDamage( info ); + } + + [ClientRpc] + public void TookDamage( DamageFlags damageFlags, Vector3 forcePos, Vector3 force ) + { + } + + public override PawnController GetActiveController() + { + if ( VehicleController != null ) return VehicleController; + if ( DevController != null ) return DevController; + + return base.GetActiveController(); + } + + public override PawnAnimator GetActiveAnimator() + { + if ( VehicleAnimator != null ) return VehicleAnimator; + + return base.GetActiveAnimator(); + } + + public ICamera GetActiveCamera() + { + if ( VehicleCamera != null ) return VehicleCamera; + + return MainCamera; + } + + public override void Simulate( Client cl ) + { + base.Simulate( cl ); + + if ( Input.ActiveChild != null ) + { + ActiveChild = Input.ActiveChild; + } + + if ( LifeState != LifeState.Alive ) + return; + + if ( VehicleController != null && DevController is NoclipController ) + { + DevController = null; + } + + var controller = GetActiveController(); + if ( controller != null ) + EnableSolidCollisions = !controller.HasTag( "noclip" ); + + TickPlayerUse(); + SimulateActiveChild( cl, ActiveChild ); + + if ( Input.Pressed( InputButton.View ) ) + { + if ( MainCamera is not FirstPersonCamera ) + { + MainCamera = new FirstPersonCamera(); + } + else + { + MainCamera = new ThirdPersonCamera(); + } + } + + Camera = GetActiveCamera(); + + if ( Input.Pressed( InputButton.Drop ) ) + { + var dropped = Inventory.DropActive(); + if ( dropped != null ) + { + dropped.PhysicsGroup.ApplyImpulse( Velocity + EyeRot.Forward * 500.0f + Vector3.Up * 100.0f, true ); + dropped.PhysicsGroup.ApplyAngularImpulse( Vector3.Random * 100.0f, true ); + + timeSinceDropped = 0; + } + } + + if ( Input.Released( InputButton.Jump ) ) + { + if ( timeSinceJumpReleased < 0.3f ) + { + Game.Current?.DoPlayerNoclip( cl ); + } + + timeSinceJumpReleased = 0; + } + + if ( Input.Left != 0 || Input.Forward != 0 ) + { + timeSinceJumpReleased = 1; + } + } + + public override void StartTouch( Entity other ) + { + if ( timeSinceDropped < 1 ) return; + + base.StartTouch( other ); + } + + [ServerCmd( "inventory_current" )] + public static void SetInventoryCurrent( string entName ) + { + var target = ConsoleSystem.Caller.Pawn; + if ( target == null ) return; + + var inventory = target.Inventory; + if ( inventory == null ) + return; + + for ( int i = 0; i < inventory.Count(); ++i ) + { + var slot = inventory.GetSlot( i ); + if ( !slot.IsValid() ) + continue; + + if ( !slot.ClassInfo.IsNamed( entName ) ) + continue; + + inventory.SetActiveSlot( i, false ); + + break; + } + } + + // TODO + + //public override bool HasPermission( string mode ) + //{ + // if ( mode == "noclip" ) return true; + // if ( mode == "devcam" ) return true; + // if ( mode == "suicide" ) return true; + // + // return base.HasPermission( mode ); + // } +} diff --git a/code/PreviewEntity.cs b/code/PreviewEntity.cs new file mode 100644 index 0000000..5f08eb0 --- /dev/null +++ b/code/PreviewEntity.cs @@ -0,0 +1,39 @@ + +namespace Sandbox.Tools +{ + public class PreviewEntity : ModelEntity + { + public bool RelativeToNormal { get; set; } = true; + public bool OffsetBounds { get; set; } = false; + public Rotation RotationOffset { get; set; } = Rotation.Identity; + public Vector3 PositionOffset { get; set; } = Vector3.Zero; + + internal bool UpdateFromTrace( TraceResult tr ) + { + if ( !IsTraceValid( tr ) ) + { + return false; + } + + if ( RelativeToNormal ) + { + Rotation = Rotation.LookAt( tr.Normal, tr.Direction ) * RotationOffset; + Position = tr.EndPos + Rotation * PositionOffset; + } + else + { + Rotation = Rotation.Identity * RotationOffset; + Position = tr.EndPos + PositionOffset; + } + + if ( OffsetBounds ) + { + Position += tr.Normal * CollisionBounds.Size * 0.5f; + } + + return true; + } + + protected virtual bool IsTraceValid( TraceResult tr ) => tr.Hit; + } +} diff --git a/code/Tool.Effects.cs b/code/Tool.Effects.cs new file mode 100644 index 0000000..f2c38c4 --- /dev/null +++ b/code/Tool.Effects.cs @@ -0,0 +1,14 @@ +using Sandbox; + +public partial class Tool +{ + [ClientRpc] + public void CreateHitEffects( Vector3 hitPos ) + { + var particle = Particles.Create( "particles/tool_hit.vpcf", hitPos ); + particle.SetPosition( 0, hitPos ); + particle.Destroy( false ); + + PlaySound( "balloon_pop_cute" ); + } +} diff --git a/code/Tool.Preview.cs b/code/Tool.Preview.cs new file mode 100644 index 0000000..f7f69f1 --- /dev/null +++ b/code/Tool.Preview.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; + +namespace Sandbox.Tools +{ + public partial class BaseTool + { + internal List Previews; + + protected virtual bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !tr.Hit ) + return false; + + if ( !tr.Entity.IsValid() ) + return false; + + return true; + } + + public virtual void CreatePreviews() + { + // Nothing + } + + public virtual void DeletePreviews() + { + if ( Previews == null || Previews.Count == 0 ) + return; + + foreach ( var preview in Previews ) + { + preview.Delete(); + } + + Previews.Clear(); + } + + + public virtual bool TryCreatePreview( ref PreviewEntity ent, string model ) + { + if ( !ent.IsValid() ) + { + ent = new PreviewEntity(); + ent.SetModel( model ); + } + + if ( Previews == null ) + { + Previews = new List(); + } + + if ( !Previews.Contains( ent ) ) + { + Previews.Add( ent ); + } + + return ent.IsValid(); + } + + + private void UpdatePreviews() + { + if ( Previews == null || Previews.Count == 0 ) + return; + + if ( !Owner.IsValid() ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * 10000.0f ) + .Ignore( Owner ) + .Run(); + + foreach ( var preview in Previews ) + { + if ( !preview.IsValid() ) + continue; + + if ( IsPreviewTraceValid( tr ) && preview.UpdateFromTrace( tr ) ) + { + preview.RenderAlpha = 0.5f; + } + else + { + preview.RenderAlpha = 0.0f; + } + } + } + } +} diff --git a/code/Tool.cs b/code/Tool.cs new file mode 100644 index 0000000..fd51a4e --- /dev/null +++ b/code/Tool.cs @@ -0,0 +1,124 @@ +using Sandbox; +using Sandbox.Tools; + +[Library( "weapon_tool", Title = "Toolgun" )] +partial class Tool : Carriable +{ + [ConVar.ClientData( "tool_current" )] + public static string UserToolCurrent { get; set; } = "tool_boxgun"; + + public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl"; + + [Net, Predicted] + public BaseTool CurrentTool { get; set; } + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pistol/rust_pistol.vmdl" ); + } + + public override void Simulate( Client owner ) + { + UpdateCurrentTool( owner ); + + CurrentTool?.Simulate(); + } + + private void UpdateCurrentTool( Client owner ) + { + var toolName = owner.GetUserString( "tool_current", "tool_boxgun" ); + if ( toolName == null ) + return; + + // Already the right tool + if ( CurrentTool != null && CurrentTool.Parent == this && CurrentTool.Owner == owner.Pawn && CurrentTool.ClassInfo.IsNamed( toolName ) ) + return; + + if ( CurrentTool != null ) + { + CurrentTool?.Deactivate(); + CurrentTool = null; + } + + CurrentTool = Library.Create( toolName, false ); + + if ( CurrentTool != null ) + { + CurrentTool.Parent = this; + CurrentTool.Owner = owner.Pawn as Player; + CurrentTool.Activate(); + } + } + + public override void ActiveStart( Entity ent ) + { + base.ActiveStart( ent ); + + CurrentTool?.Activate(); + } + + public override void ActiveEnd( Entity ent, bool dropped ) + { + base.ActiveEnd( ent, dropped ); + + CurrentTool?.Deactivate(); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + CurrentTool?.Deactivate(); + CurrentTool = null; + } + + public override void OnCarryDrop( Entity dropper ) + { + } + + [Event.Frame] + public void OnFrame() + { + if ( !IsActiveChild() ) return; + + CurrentTool?.OnFrame(); + } +} + +namespace Sandbox.Tools +{ + public partial class BaseTool : NetworkComponent + { + public Tool Parent { get; set; } + public Player Owner { get; set; } + + protected virtual float MaxTraceDistance => 10000.0f; + + public virtual void Activate() + { + CreatePreviews(); + } + + public virtual void Deactivate() + { + DeletePreviews(); + } + + public virtual void Simulate() + { + + } + + public virtual void OnFrame() + { + UpdatePreviews(); + } + + public virtual void CreateHitEffects( Vector3 pos ) + { + Parent?.CreateHitEffects( pos ); + } + } +} diff --git a/code/ViewModel.cs b/code/ViewModel.cs new file mode 100644 index 0000000..0d65e7d --- /dev/null +++ b/code/ViewModel.cs @@ -0,0 +1,102 @@ +using Sandbox; + +public class ViewModel : BaseViewModel +{ + protected float SwingInfluence => 0.05f; + protected float ReturnSpeed => 5.0f; + protected float MaxOffsetLength => 10.0f; + protected float BobCycleTime => 7; + protected Vector3 BobDirection => new Vector3( 0.0f, 1.0f, 0.5f ); + + private Vector3 swingOffset; + private float lastPitch; + private float lastYaw; + private float bobAnim; + + private bool activated = false; + + public override void PostCameraSetup( ref CameraSetup camSetup ) + { + base.PostCameraSetup( ref camSetup ); + + if ( !Local.Pawn.IsValid() ) + return; + + if ( !activated ) + { + lastPitch = camSetup.Rotation.Pitch(); + lastYaw = camSetup.Rotation.Yaw(); + + activated = true; + } + + Position = camSetup.Position; + Rotation = camSetup.Rotation; + + camSetup.ViewModel.FieldOfView = FieldOfView; + + var playerVelocity = Local.Pawn.Velocity; + + if ( Local.Pawn is Player player ) + { + var controller = player.GetActiveController(); + if ( controller != null && controller.HasTag( "noclip" ) ) + { + playerVelocity = Vector3.Zero; + } + } + + var newPitch = Rotation.Pitch(); + var newYaw = Rotation.Yaw(); + + var pitchDelta = Angles.NormalizeAngle( newPitch - lastPitch ); + var yawDelta = Angles.NormalizeAngle( lastYaw - newYaw ); + + var verticalDelta = playerVelocity.z * Time.Delta; + var viewDown = Rotation.FromPitch( newPitch ).Up * -1.0f; + verticalDelta *= (1.0f - System.MathF.Abs( viewDown.Cross( Vector3.Down ).y )); + pitchDelta -= verticalDelta * 1; + + var offset = CalcSwingOffset( pitchDelta, yawDelta ); + offset += CalcBobbingOffset( playerVelocity ); + + Position += Rotation * offset; + + lastPitch = newPitch; + lastYaw = newYaw; + } + + protected Vector3 CalcSwingOffset( float pitchDelta, float yawDelta ) + { + Vector3 swingVelocity = new Vector3( 0, yawDelta, pitchDelta ); + + swingOffset -= swingOffset * ReturnSpeed * Time.Delta; + swingOffset += (swingVelocity * SwingInfluence); + + if ( swingOffset.Length > MaxOffsetLength ) + { + swingOffset = swingOffset.Normal * MaxOffsetLength; + } + + return swingOffset; + } + + protected Vector3 CalcBobbingOffset( Vector3 velocity ) + { + bobAnim += Time.Delta * BobCycleTime; + + var twoPI = System.MathF.PI * 2.0f; + + if ( bobAnim > twoPI ) + { + bobAnim -= twoPI; + } + + var speed = new Vector2( velocity.x, velocity.y ).Length; + speed = speed > 10.0 ? speed : 0.0f; + var offset = BobDirection * (speed * 0.005f) * System.MathF.Cos( bobAnim ); + offset = offset.WithZ( -System.MathF.Abs( offset.z ) ); + + return offset; + } +} diff --git a/code/Weapon.cs b/code/Weapon.cs new file mode 100644 index 0000000..755f72e --- /dev/null +++ b/code/Weapon.cs @@ -0,0 +1,202 @@ +using Sandbox; + +public partial class Weapon : BaseWeapon, IUse +{ + public virtual float ReloadTime => 3.0f; + + public PickupTrigger PickupTrigger { get; protected set; } + + [Net, Predicted] + public TimeSince TimeSinceReload { get; set; } + + [Net, Predicted] + public bool IsReloading { get; set; } + + [Net, Predicted] + public TimeSince TimeSinceDeployed { get; set; } + + public override void Spawn() + { + base.Spawn(); + + PickupTrigger = new PickupTrigger + { + Parent = this, + Position = Position, + EnableTouch = true, + EnableSelfCollisions = false + }; + + PickupTrigger.PhysicsBody.EnableAutoSleeping = false; + } + + public override void ActiveStart( Entity ent ) + { + base.ActiveStart( ent ); + + TimeSinceDeployed = 0; + } + + public override void Reload() + { + if ( IsReloading ) + return; + + TimeSinceReload = 0; + IsReloading = true; + + (Owner as AnimEntity)?.SetAnimBool( "b_reload", true ); + + StartReloadEffects(); + } + + public override void Simulate( Client owner ) + { + if ( TimeSinceDeployed < 0.6f ) + return; + + if ( !IsReloading ) + { + base.Simulate( owner ); + } + + if ( IsReloading && TimeSinceReload > ReloadTime ) + { + OnReloadFinish(); + } + } + + public virtual void OnReloadFinish() + { + IsReloading = false; + } + + [ClientRpc] + public virtual void StartReloadEffects() + { + ViewModelEntity?.SetAnimBool( "reload", true ); + + // TODO - player third person model reload + } + + public override void CreateViewModel() + { + Host.AssertClient(); + + if ( string.IsNullOrEmpty( ViewModelPath ) ) + return; + + ViewModelEntity = new ViewModel + { + Position = Position, + Owner = Owner, + EnableViewmodelRendering = true + }; + + ViewModelEntity.SetModel( ViewModelPath ); + } + + public bool OnUse( Entity user ) + { + if ( Owner != null ) + return false; + + if ( !user.IsValid() ) + return false; + + user.StartTouch( this ); + + return false; + } + + public virtual bool IsUsable( Entity user ) + { + if ( Owner != null ) return false; + + if ( user.Inventory is Inventory inventory ) + { + return inventory.CanAdd( this ); + } + + return true; + } + + public void Remove() + { + PhysicsGroup?.Wake(); + Delete(); + } + + [ClientRpc] + protected virtual void ShootEffects() + { + Host.AssertClient(); + + Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" ); + + if ( IsLocalPawn ) + { + _ = new Sandbox.ScreenShake.Perlin(); + } + + ViewModelEntity?.SetAnimBool( "fire", true ); + CrosshairPanel?.CreateEvent( "fire" ); + } + + /// + /// Shoot a single bullet + /// + public virtual void ShootBullet( Vector3 pos, Vector3 dir, float spread, float force, float damage, float bulletSize ) + { + var forward = dir; + forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * spread * 0.25f; + forward = forward.Normal; + + // + // ShootBullet is coded in a way where we can have bullets pass through shit + // or bounce off shit, in which case it'll return multiple results + // + foreach ( var tr in TraceBullet( pos, pos + forward * 5000, bulletSize ) ) + { + tr.Surface.DoBulletImpact( tr ); + + if ( !IsServer ) continue; + if ( !tr.Entity.IsValid() ) continue; + + // + // We turn predictiuon off for this, so any exploding effects don't get culled etc + // + using ( Prediction.Off() ) + { + var damageInfo = DamageInfo.FromBullet( tr.EndPos, forward * 100 * force, damage ) + .UsingTraceResult( tr ) + .WithAttacker( Owner ) + .WithWeapon( this ); + + tr.Entity.TakeDamage( damageInfo ); + } + } + } + + /// + /// Shoot a single bullet from owners view point + /// + public virtual void ShootBullet( float spread, float force, float damage, float bulletSize ) + { + ShootBullet( Owner.EyePos, Owner.EyeRot.Forward, spread, force, damage, bulletSize ); + } + + /// + /// Shoot a multiple bullets from owners view point + /// + public virtual void ShootBullets( int numBullets, float spread, float force, float damage, float bulletSize ) + { + var pos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + for ( int i = 0; i < numBullets; i++ ) + { + ShootBullet( pos, dir, spread, force / numBullets, damage, bulletSize ); + } + } +} diff --git a/code/entities/BalloonEntity.cs b/code/entities/BalloonEntity.cs new file mode 100644 index 0000000..1e3d246 --- /dev/null +++ b/code/entities/BalloonEntity.cs @@ -0,0 +1,37 @@ +using Sandbox; + +[Library( "ent_balloon", Title = "Balloon", Spawnable = true )] +public partial class BalloonEntity : Prop +{ + private static float GravityScale => -0.2f; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "models/citizen_props/balloonregular01.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + PhysicsBody.GravityScale = GravityScale; + RenderColor = Color.Random.ToColor32(); + } + + public override void OnKilled() + { + base.OnKilled(); + + PlaySound( "balloon_pop_cute" ); + } + + [Event.Physics.PostStep] + public void OnPostPhysicsStep() + { + if ( !this.IsValid() ) + return; + + var body = PhysicsBody; + if ( !body.IsValid() ) + return; + + body.GravityScale = GravityScale; + } +} diff --git a/code/entities/BouncyBall.cs b/code/entities/BouncyBall.cs new file mode 100644 index 0000000..d61184c --- /dev/null +++ b/code/entities/BouncyBall.cs @@ -0,0 +1,43 @@ +using Sandbox; +using System; + +[Library( "ent_bouncyball", Title = "Bouncy Ball", Spawnable = true )] +public partial class BouncyBallEntity : Prop, IUse +{ + public float MaxSpeed { get; set; } = 1000.0f; + public float SpeedMul { get; set; } = 1.2f; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "models/ball/ball.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + Scale = Rand.Float( 0.5f, 2.0f ); + RenderColor = Color.Random.ToColor32(); + } + + protected override void OnPhysicsCollision( CollisionEventData eventData ) + { + var speed = eventData.PreVelocity.Length; + var direction = Vector3.Reflect( eventData.PreVelocity.Normal, eventData.Normal.Normal ).Normal; + Velocity = direction * MathF.Min( speed * SpeedMul, MaxSpeed ); + } + + public bool IsUsable( Entity user ) + { + return true; + } + + public bool OnUse( Entity user ) + { + if ( user is Player player ) + { + player.Health += 10; + + Delete(); + } + + return false; + } +} diff --git a/code/entities/DirectionalGravity.cs b/code/entities/DirectionalGravity.cs new file mode 100644 index 0000000..28f5b69 --- /dev/null +++ b/code/entities/DirectionalGravity.cs @@ -0,0 +1,64 @@ +using Sandbox; +using System.Linq; + +[Library( "directional_gravity", Title = "Directional Gravity", Spawnable = true )] +public partial class DirectionalGravity : Prop +{ + bool enabled = false; + + public override void Spawn() + { + base.Spawn(); + + DeleteOthers(); + + SetModel( "models/arrow.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + + enabled = true; + } + + private void DeleteOthers() + { + // Only allow one of these to be spawned at a time + foreach ( var ent in All.OfType() + .Where( x => x.IsValid() && x != this ) ) + { + ent.Delete(); + } + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if ( IsServer ) + { + PhysicsWorld.UseDefaultGravity(); + PhysicsWorld.WakeAllBodies(); + } + + enabled = false; + } + + [Event.Physics.PostStep] + public void OnPostPhysicsStep() + { + if ( !IsServer ) + return; + + if ( !enabled ) + return; + + if ( !this.IsValid() ) + return; + + var gravity = Rotation.Down * 800.0f; + + if ( gravity != PhysicsWorld.Gravity ) + { + PhysicsWorld.Gravity = gravity; + PhysicsWorld.WakeAllBodies(); + } + } +} diff --git a/code/entities/DroneEntity.cs b/code/entities/DroneEntity.cs new file mode 100644 index 0000000..e5a4df6 --- /dev/null +++ b/code/entities/DroneEntity.cs @@ -0,0 +1,146 @@ +using Sandbox; +using System; + +[Library( "ent_drone", Title = "Drone", Spawnable = true )] +public partial class DroneEntity : Prop +{ + public virtual float altitudeAcceleration => 2000; + public virtual float movementAcceleration => 5000; + public virtual float yawSpeed => 150; + public virtual float uprightSpeed => 5000; + public virtual float uprightDot => 0.5f; + public virtual float leanWeight => 0.5f; + public virtual float leanMaxVelocity => 1000; + + private struct DroneInputState + { + public Vector3 movement; + public float throttle; + public float pitch; + public float yaw; + + public void Reset() + { + movement = Vector3.Zero; + pitch = 0; + yaw = 0; + } + } + + private DroneInputState currentInput; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "entities/drone/drone.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + } + + [Event.Physics.PostStep] + public void OnPostPhysicsStep() + { + if ( !PhysicsBody.IsValid() ) + { + return; + } + + var body = PhysicsBody; + var transform = Transform; + + body.LinearDrag = 1.0f; + body.AngularDrag = 1.0f; + body.LinearDamping = 4.0f; + body.AngularDamping = 4.0f; + + var yawRot = Rotation.From( new Angles( 0, Rotation.Angles().yaw, 0 ) ); + var worldMovement = yawRot * currentInput.movement; + var velocityDirection = body.Velocity.WithZ( 0 ); + var velocityMagnitude = velocityDirection.Length; + velocityDirection = velocityDirection.Normal; + + var velocityScale = (velocityMagnitude / leanMaxVelocity).Clamp( 0, 1 ); + var leanDirection = worldMovement.LengthSquared == 0.0f + ? -velocityScale * velocityDirection + : worldMovement; + + var targetUp = (Vector3.Up + leanDirection * leanWeight * velocityScale).Normal; + var currentUp = transform.NormalToWorld( Vector3.Up ); + var alignment = Math.Max( Vector3.Dot( targetUp, currentUp ), 0 ); + + bool hasCollision = false; + bool isGrounded = false; + + if ( !hasCollision || isGrounded ) + { + var hoverForce = isGrounded && currentInput.throttle <= 0 ? Vector3.Zero : -1 * transform.NormalToWorld( Vector3.Up ) * -800.0f; + var movementForce = isGrounded ? Vector3.Zero : worldMovement * movementAcceleration; + var altitudeForce = transform.NormalToWorld( Vector3.Up ) * currentInput.throttle * altitudeAcceleration; + var totalForce = hoverForce + movementForce + altitudeForce; + body.ApplyForce( (totalForce * alignment) * body.Mass ); + } + + if ( !hasCollision && !isGrounded ) + { + var spinTorque = Transform.NormalToWorld( new Vector3( 0, 0, currentInput.yaw * yawSpeed ) ); + var uprightTorque = Vector3.Cross( currentUp, targetUp ) * uprightSpeed; + var uprightAlignment = alignment < uprightDot ? 0 : alignment; + var totalTorque = spinTorque * alignment + uprightTorque * uprightAlignment; + body.ApplyTorque( (totalTorque * alignment) * body.Mass ); + } + } + + public override void Simulate( Client owner ) + { + if ( owner == null ) return; + if ( !IsServer ) return; + + using ( Prediction.Off() ) + { + currentInput.Reset(); + var x = (Input.Down( InputButton.Forward ) ? -1 : 0) + (Input.Down( InputButton.Back ) ? 1 : 0); + var y = (Input.Down( InputButton.Right ) ? 1 : 0) + (Input.Down( InputButton.Left ) ? -1 : 0); + currentInput.movement = new Vector3( x, y, 0 ).Normal; + currentInput.throttle = (Input.Down( InputButton.Run ) ? 1 : 0) + (Input.Down( InputButton.Duck ) ? -1 : 0); + currentInput.yaw = -Input.MouseDelta.x; + } + } + + public void ResetInput() + { + currentInput.Reset(); + } + + private readonly Vector3[] turbinePositions = new Vector3[] + { + new Vector3( -35.37f, 35.37f, 10.0f ), + new Vector3( 35.37f, 35.37f, 10.0f ), + new Vector3( 35.37f, -35.37f, 10.0f ), + new Vector3( -35.37f, -35.37f, 10.0f ) + }; + + public override void OnNewModel( Model model ) + { + base.OnNewModel( model ); + + if ( IsClient ) + { + } + } + + private float spinAngle; + + [Event.Frame] + public void OnFrame() + { + spinAngle += 10000.0f * Time.Delta; + spinAngle %= 360.0f; + + for ( int i = 0; i < turbinePositions.Length; ++i ) + { + var transform = Transform.ToWorld( new Transform( turbinePositions[i] * Scale, Rotation.From( new Angles( 0, spinAngle, 0 ) ) ) ); + transform.Scale = Scale; + SetBoneTransform( i, transform ); + } + } +} diff --git a/code/entities/LampEntity.cs b/code/entities/LampEntity.cs new file mode 100644 index 0000000..bda8b41 --- /dev/null +++ b/code/entities/LampEntity.cs @@ -0,0 +1,33 @@ +using Sandbox; + +[Library( "ent_lamp", Title = "Lamp", Spawnable = true )] +public partial class LampEntity : SpotLightEntity, IUse +{ + public override void Spawn() + { + base.Spawn(); + + SetModel( "models/torch/torch.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + } + + public bool IsUsable( Entity user ) + { + return true; + } + + public bool OnUse( Entity user ) + { + Enabled = !Enabled; + + PlaySound( Enabled ? "flashlight-on" : "flashlight-off" ); + + return false; + } + + public void Remove() + { + PhysicsGroup?.Wake(); + Delete(); + } +} diff --git a/code/entities/LightEntity.cs b/code/entities/LightEntity.cs new file mode 100644 index 0000000..4447c88 --- /dev/null +++ b/code/entities/LightEntity.cs @@ -0,0 +1,33 @@ +using Sandbox; + +[Library( "ent_light", Title = "Light", Spawnable = true )] +public partial class LightEntity : PointLightEntity, IUse +{ + public override void Spawn() + { + base.Spawn(); + + SetModel( "models/light/light_tubular.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + } + + public bool IsUsable( Entity user ) + { + return true; + } + + public bool OnUse( Entity user ) + { + Enabled = !Enabled; + + PlaySound( Enabled ? "flashlight-on" : "flashlight-off" ); + + return false; + } + + public void Remove() + { + PhysicsGroup?.Wake(); + Delete(); + } +} diff --git a/code/entities/NoiseTest.cs b/code/entities/NoiseTest.cs new file mode 100644 index 0000000..dc967e2 --- /dev/null +++ b/code/entities/NoiseTest.cs @@ -0,0 +1,103 @@ +using Sandbox; + +[Library( "noise_test", Title = "Noise Test", Spawnable = true )] +public partial class NoiseTest : Prop +{ + public override void Spawn() + { + base.Spawn(); + + SetModel( "models/citizen_props/balloonregular01.vmdl" ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + } + + [Event.Frame] + public void OnFrame() + { + var pos = Position; + var right = Rotation.Right * 4; + var forward = Rotation.Forward * 4; + var up = Rotation.Up * 50; + var offset = Time.Now * 2.0f; + var offsetz = Time.Now * 0.1f; + + var mode = (int)((Time.Now * 0.3f) % 5); + + switch ( mode ) + { + case 0: + { + DebugOverlay.Text( pos, "Perlin" ); + break; + } + + case 1: + { + DebugOverlay.Text( pos, "SparseConvolution" ); + break; + } + + case 2: + { + DebugOverlay.Text( pos, "SparseConvolutionNormalized" ); + break; + } + + case 3: + { + DebugOverlay.Text( pos, "Turbulence" ); + break; + } + + case 4: + { + DebugOverlay.Text( pos, "Fractal" ); + break; + } + } + + + var size = 100; + + pos -= right * size * 0.5f; + pos -= forward * size * 0.5f; + + for ( float x = 0; x < size; x++ ) + for ( float y = 0; y < size; y++ ) + { + float val = 0; + + switch ( mode ) + { + case 0: + { + val = Noise.Perlin( x * 0.1f + offset, y * 0.1f, offsetz ) * 0.5f; + break; + } + case 1: + { + val = Noise.SparseConvolution( x * 0.1f + offset, y * 0.1f, offsetz ) * 0.5f; + break; + } + case 2: + { + val = Noise.SparseConvolutionNormalized( x * 0.1f + offset, y * 0.1f, offsetz ) * 0.5f; + break; + } + case 3: + { + val = Noise.Turbulence( 2, x * 0.1f + offset, y * 0.1f, offsetz ) * 0.5f; + break; + } + case 4: + { + val = Noise.Fractal( 2, x * 0.1f + offset, y * 0.1f, offsetz ) * 0.5f; + break; + } + } + + var start = pos + x * right + y * forward; + DebugOverlay.Line( start, start + up * val, Color.Lerp( Color.Red, Color.Green, (val + 1.0f) / 2.0f ) ); + } + } +} diff --git a/code/entities/ThrusterEntity.Effects.cs b/code/entities/ThrusterEntity.Effects.cs new file mode 100644 index 0000000..bc67d0e --- /dev/null +++ b/code/entities/ThrusterEntity.Effects.cs @@ -0,0 +1,46 @@ +using Sandbox; + +public partial class ThrusterEntity +{ + private Particles effects; + + [Event.Frame] + public void OnFrame() + { + UpdateEffects(); + } + + protected void CreateEffects() + { + if ( effects != null ) + return; + + effects = Particles.Create( "particles/physgun_end_nohit.vpcf" ); + } + + protected virtual void KillEffects() + { + if ( effects == null ) + return; + + effects.Destroy( false ); + effects = null; + } + + protected virtual void UpdateEffects() + { + if ( Enabled ) + { + CreateEffects(); + } + else + { + KillEffects(); + } + + if ( effects == null ) + return; + + effects.SetPosition( 0, Position + Rotation.Up * 20 ); + } +} diff --git a/code/entities/ThrusterEntity.cs b/code/entities/ThrusterEntity.cs new file mode 100644 index 0000000..d801942 --- /dev/null +++ b/code/entities/ThrusterEntity.cs @@ -0,0 +1,50 @@ +using Sandbox; + +[Library( "ent_thruster" )] +public partial class ThrusterEntity : Prop, IUse +{ + public float Force = 1000.0f; + public bool Massless = false; + public PhysicsBody TargetBody; + + [Net] + public bool Enabled { get; set; } = true; + + [Event.Physics.PostStep] + public virtual void OnPostPhysicsStep() + { + if ( IsServer && Enabled ) + { + if ( TargetBody.IsValid() ) + { + TargetBody.ApplyForceAt( Position, Rotation.Down * (Massless ? Force * TargetBody.Mass : Force) ); + } + else if ( PhysicsBody.IsValid() ) + { + PhysicsBody.ApplyForce( Rotation.Down * (Massless ? Force * PhysicsBody.Mass : Force) ); + } + } + } + + public bool IsUsable( Entity user ) + { + return true; + } + + public bool OnUse( Entity user ) + { + Enabled = !Enabled; + + return false; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if ( IsClient ) + { + KillEffects(); + } + } +} diff --git a/code/entities/WheelEntity.cs b/code/entities/WheelEntity.cs new file mode 100644 index 0000000..005d66c --- /dev/null +++ b/code/entities/WheelEntity.cs @@ -0,0 +1,25 @@ +using Sandbox; +using Sandbox.Joints; + +[Library( "ent_wheel" )] +public partial class WheelEntity : Prop +{ + public RevoluteJoint Joint; + + protected override void OnDestroy() + { + base.OnDestroy(); + + if ( Joint.IsValid ) + { + Joint.Remove(); + } + } + + protected override void UpdatePropData( Model model ) + { + base.UpdatePropData( model ); + + Health = -1; + } +} diff --git a/code/entities/car/CarAnimator.cs b/code/entities/car/CarAnimator.cs new file mode 100644 index 0000000..4aa3e0e --- /dev/null +++ b/code/entities/car/CarAnimator.cs @@ -0,0 +1,34 @@ + +namespace Sandbox +{ + public class CarAnimator : PawnAnimator + { + public override void Simulate() + { + ResetParams(); + + SetParam( "b_grounded", true ); + SetParam( "b_sit", true ); + + var eyeAngles = (Pawn.Rotation.Inverse * Pawn.EyeRot).Angles(); + eyeAngles.pitch = eyeAngles.pitch.Clamp( -25, 70 ); + eyeAngles.yaw = eyeAngles.yaw.Clamp( -90, 90 ); + + var aimPos = Pawn.EyePos + (Pawn.Rotation * Rotation.From( eyeAngles )).Forward * 200; + + SetLookAt( "aim_eyes", aimPos ); + SetLookAt( "aim_head", aimPos ); + SetLookAt( "aim_body", aimPos ); + + if ( Pawn.ActiveChild is BaseCarriable carry ) + { + carry.SimulateAnimator( this ); + } + else + { + SetParam( "holdtype", 0 ); + SetParam( "aim_body_weight", 0.5f ); + } + } + } +} diff --git a/code/entities/car/CarCamera.cs b/code/entities/car/CarCamera.cs new file mode 100644 index 0000000..3204318 --- /dev/null +++ b/code/entities/car/CarCamera.cs @@ -0,0 +1,228 @@ +using Sandbox; +using System; + +public class CarCamera : Camera +{ + protected virtual float MinFov => 80.0f; + protected virtual float MaxFov => 100.0f; + protected virtual float MaxFovSpeed => 1000.0f; + protected virtual float FovSmoothingSpeed => 4.0f; + protected virtual float OrbitCooldown => 0.6f; + protected virtual float OrbitSmoothingSpeed => 25.0f; + protected virtual float OrbitReturnSmoothingSpeed => 4.0f; + protected virtual float MinOrbitPitch => -25.0f; + protected virtual float MaxOrbitPitch => 70.0f; + protected virtual float FixedOrbitPitch => 10.0f; + protected virtual float OrbitHeight => 35.0f; + protected virtual float OrbitDistance => 150.0f; + protected virtual float MaxOrbitReturnSpeed => 100.0f; + protected virtual float MinCarPitch => -60.0f; + protected virtual float MaxCarPitch => 60.0f; + protected virtual float FirstPersonPitch => 10.0f; + protected virtual float CarPitchSmoothingSpeed => 1.0f; + protected virtual float CollisionRadius => 8.0f; + protected virtual float ShakeSpeed => 10.0f; + protected virtual float ShakeSpeedThreshold => 1500.0f; + protected virtual float ShakeMaxSpeed => 2500.0f; + protected virtual float ShakeMaxLength => 1.0f; + + private bool orbitEnabled; + private TimeSince timeSinceOrbit; + private Angles orbitAngles; + private Rotation orbitYawRot; + private Rotation orbitPitchRot; + private float currentFov; + private float carPitch; + private bool firstPerson; + + public override void Activated() + { + var pawn = Local.Pawn; + if ( pawn == null ) return; + + orbitEnabled = false; + timeSinceOrbit = 0.0f; + orbitAngles = Angles.Zero; + orbitYawRot = Rotation.Identity; + orbitPitchRot = Rotation.Identity; + currentFov = MinFov; + carPitch = 0; + firstPerson = false; + + var car = (pawn as SandboxPlayer)?.Vehicle as CarEntity; + if ( !car.IsValid() ) return; + + orbitYawRot = firstPerson ? Rotation.Identity : Rotation.FromYaw( car.Rotation.Yaw() ); + orbitPitchRot = firstPerson ? Rotation.FromPitch( FirstPersonPitch ) : Rotation.Identity; + orbitAngles = (orbitYawRot * orbitPitchRot).Angles(); + } + + public override void Update() + { + var pawn = Local.Pawn; + if ( pawn == null ) return; + + var car = (pawn as SandboxPlayer)?.Vehicle as CarEntity; + if ( !car.IsValid() ) return; + + var body = car.PhysicsBody; + if ( !body.IsValid() ) + return; + + var speed = car.MovementSpeed; + var speedAbs = Math.Abs( speed ); + + if ( orbitEnabled && timeSinceOrbit > OrbitCooldown ) + orbitEnabled = false; + + var carRot = car.Rotation; + carPitch = carPitch.LerpTo( car.Grounded ? carRot.Pitch().Clamp( MinCarPitch, MaxCarPitch ) * (speed < 0.0f ? -1.0f : 1.0f) : 0.0f, Time.Delta * CarPitchSmoothingSpeed ); + + if ( orbitEnabled ) + { + var slerpAmount = Time.Delta * OrbitSmoothingSpeed; + + orbitYawRot = Rotation.Slerp( orbitYawRot, Rotation.From( 0.0f, orbitAngles.yaw, 0.0f ), slerpAmount ); + orbitPitchRot = Rotation.Slerp( orbitPitchRot, Rotation.From( orbitAngles.pitch + carPitch, 0.0f, 0.0f ), slerpAmount ); + } + else + { + if ( firstPerson ) + { + var targetYaw = 0; + var targetPitch = FirstPersonPitch; + var slerpAmount = Time.Delta * OrbitReturnSmoothingSpeed; + + orbitYawRot = Rotation.Slerp( orbitYawRot, Rotation.FromYaw( targetYaw ), slerpAmount ); + orbitPitchRot = Rotation.Slerp( orbitPitchRot, Rotation.FromPitch( targetPitch ), slerpAmount ); + } + else + { + var targetPitch = FixedOrbitPitch.Clamp( MinOrbitPitch, MaxOrbitPitch ); + var targetYaw = !firstPerson && speed < 0.0f ? carRot.Yaw() + 180.0f : carRot.Yaw(); + var slerpAmount = MaxOrbitReturnSpeed > 0.0f ? Time.Delta * (speedAbs / MaxOrbitReturnSpeed).Clamp( 0.0f, OrbitReturnSmoothingSpeed ) : 1.0f; + + orbitYawRot = Rotation.Slerp( orbitYawRot, Rotation.FromYaw( targetYaw ), slerpAmount ); + orbitPitchRot = Rotation.Slerp( orbitPitchRot, Rotation.FromPitch( targetPitch + carPitch ), slerpAmount ); + } + + orbitAngles.pitch = orbitPitchRot.Pitch(); + orbitAngles.yaw = orbitYawRot.Yaw(); + orbitAngles = orbitAngles.Normal; + } + + if ( firstPerson ) + { + DoFirstPerson(); + } + else + { + DoThirdPerson( car, body ); + } + + currentFov = MaxFovSpeed > 0.0f ? currentFov.LerpTo( MinFov.LerpTo( MaxFov, speedAbs / MaxFovSpeed ), Time.Delta * FovSmoothingSpeed ) : MaxFov; + FieldOfView = currentFov; + + ApplyShake( speedAbs ); + } + + private void DoFirstPerson() + { + var pawn = Local.Pawn; + if ( pawn == null ) return; + + Pos = pawn.EyePos; + Rot = pawn.Rotation * (orbitYawRot * orbitPitchRot); + + Viewer = pawn; + } + + private void DoThirdPerson( CarEntity car, PhysicsBody body ) + { + Rot = orbitYawRot * orbitPitchRot; + + var carPos = car.Position + car.Rotation * (body.LocalMassCenter * car.Scale); + var startPos = carPos; + var targetPos = startPos + Rot.Backward * (OrbitDistance * car.Scale) + (Vector3.Up * (OrbitHeight * car.Scale)); + + var tr = Trace.Ray( startPos, targetPos ) + .Ignore( car ) + .Radius( Math.Clamp( CollisionRadius * car.Scale, 2.0f, 10.0f ) ) + .WorldOnly() + .Run(); + + Pos = tr.EndPos; + + Viewer = null; + } + + public override void BuildInput( InputBuilder input ) + { + base.BuildInput( input ); + + var pawn = Local.Pawn; + if ( pawn == null ) return; + + var car = (pawn as SandboxPlayer)?.Vehicle as CarEntity; + if ( !car.IsValid() ) return; + + if ( input.Pressed( InputButton.View ) ) + { + firstPerson = !firstPerson; + orbitYawRot = firstPerson ? Rotation.Identity : Rotation.FromYaw( car.Rotation.Yaw() ); + orbitPitchRot = firstPerson ? Rotation.FromPitch( FirstPersonPitch ) : Rotation.Identity; + orbitAngles = (orbitYawRot * orbitPitchRot).Angles(); + orbitEnabled = false; + timeSinceOrbit = 0.0f; + } + + if ( (Math.Abs( input.AnalogLook.pitch ) + Math.Abs( input.AnalogLook.yaw )) > 0.0f ) + { + if ( !orbitEnabled ) + { + orbitAngles = (orbitYawRot * orbitPitchRot).Angles(); + orbitAngles = orbitAngles.Normal; + + orbitYawRot = Rotation.From( 0.0f, orbitAngles.yaw, 0.0f ); + orbitPitchRot = Rotation.From( orbitAngles.pitch, 0.0f, 0.0f ); + } + + orbitEnabled = true; + timeSinceOrbit = 0.0f; + + orbitAngles.yaw += input.AnalogLook.yaw; + orbitAngles.pitch += input.AnalogLook.pitch; + orbitAngles = orbitAngles.Normal; + orbitAngles.pitch = orbitAngles.pitch.Clamp( MinOrbitPitch, MaxOrbitPitch ); + } + + if ( firstPerson ) + { + input.ViewAngles = (car.Rotation * Rotation.From( orbitAngles )).Angles(); + } + else + { + input.ViewAngles = orbitEnabled ? orbitAngles : car.Rotation.Angles(); + } + + input.ViewAngles = input.ViewAngles.Normal; + } + + private void ApplyShake( float speed ) + { + if ( speed < ShakeSpeedThreshold ) + return; + + var pos = (Time.Now % MathF.PI) * ShakeSpeed; + var length = (speed - ShakeSpeedThreshold) / (ShakeMaxSpeed - ShakeSpeedThreshold); + length = length.Clamp( 0, ShakeMaxLength ); + + float x = Noise.Perlin( pos, 0, 0 ) * length; + float y = Noise.Perlin( pos, 5.0f, 0 ) * length; + + Pos += Rot.Right * x + Rot.Up * y; + Rot *= Rotation.FromAxis( Vector3.Up, x ); + Rot *= Rotation.FromAxis( Vector3.Right, y ); + } +} + diff --git a/code/entities/car/CarController.cs b/code/entities/car/CarController.cs new file mode 100644 index 0000000..2df03d2 --- /dev/null +++ b/code/entities/car/CarController.cs @@ -0,0 +1,37 @@ +using Sandbox; + +[Library] +public class CarController : PawnController +{ + public override void FrameSimulate() + { + base.FrameSimulate(); + + Simulate(); + } + + public override void Simulate() + { + var player = Pawn as SandboxPlayer; + if ( !player.IsValid() ) return; + + var car = player.Vehicle as CarEntity; + if ( !car.IsValid() ) return; + + car.Simulate( Client ); + + if ( player.Vehicle == null ) + { + Position = car.Position + car.Rotation.Up * (100 * car.Scale); + Velocity += car.Rotation.Right * (200 * car.Scale); + return; + } + + EyeRot = Input.Rotation; + EyePosLocal = Vector3.Up * (64 - 10) * car.Scale; + Velocity = car.Velocity; + + SetTag( "noclip" ); + SetTag( "sitting" ); + } +} diff --git a/code/entities/car/CarEntity.cs b/code/entities/car/CarEntity.cs new file mode 100644 index 0000000..e25880f --- /dev/null +++ b/code/entities/car/CarEntity.cs @@ -0,0 +1,561 @@ +using Sandbox; +using System; +using System.Collections.Generic; + +[Library( "ent_car", Title = "Car", Spawnable = true )] +public partial class CarEntity : Prop, IUse +{ + [ConVar.Replicated( "debug_car" )] + public static bool debug_car { get; set; } = false; + + [ConVar.Replicated( "car_accelspeed" )] + public static float car_accelspeed { get; set; } = 500.0f; + + private CarWheel frontLeft; + private CarWheel frontRight; + private CarWheel backLeft; + private CarWheel backRight; + + private float frontLeftDistance; + private float frontRightDistance; + private float backLeftDistance; + private float backRightDistance; + + private bool frontWheelsOnGround; + private bool backWheelsOnGround; + private float accelerateDirection; + private float airRoll; + private float airTilt; + private float grip; + private TimeSince timeSinceDriverLeft; + + [Net] private float WheelSpeed { get; set; } + [Net] private float TurnDirection { get; set; } + [Net] private float AccelerationTilt { get; set; } + [Net] private float TurnLean { get; set; } + + [Net] public float MovementSpeed { get; private set; } + [Net] public bool Grounded { get; private set; } + + private struct InputState + { + public float throttle; + public float turning; + public float breaking; + public float tilt; + public float roll; + + public void Reset() + { + throttle = 0; + turning = 0; + breaking = 0; + tilt = 0; + roll = 0; + } + } + + private InputState currentInput; + + public CarEntity() + { + frontLeft = new CarWheel( this ); + frontRight = new CarWheel( this ); + backLeft = new CarWheel( this ); + backRight = new CarWheel( this ); + } + + [Net] public Player driver { get; private set; } + + private ModelEntity chassis_axle_rear; + private ModelEntity chassis_axle_front; + private ModelEntity wheel0; + private ModelEntity wheel1; + private ModelEntity wheel2; + private ModelEntity wheel3; + + public override void Spawn() + { + base.Spawn(); + + var modelName = "models/car/car.vmdl"; + + SetModel( modelName ); + SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + SetInteractsExclude( CollisionLayer.Player ); + EnableSelfCollisions = false; + + var trigger = new ModelEntity + { + Parent = this, + Position = Position, + Rotation = Rotation, + EnableTouch = true, + CollisionGroup = CollisionGroup.Trigger, + Transmit = TransmitType.Never, + EnableSelfCollisions = false, + }; + + trigger.SetModel( modelName ); + trigger.SetupPhysicsFromModel( PhysicsMotionType.Keyframed, false ); + } + + public override void ClientSpawn() + { + base.ClientSpawn(); + + { + var vehicle_fuel_tank = new ModelEntity(); + vehicle_fuel_tank.SetModel( "entities/modular_vehicle/vehicle_fuel_tank.vmdl" ); + vehicle_fuel_tank.Transform = Transform; + vehicle_fuel_tank.Parent = this; + vehicle_fuel_tank.LocalPosition = new Vector3( 0.75f, 0, 0 ) * 40.0f; + } + + { + chassis_axle_front = new ModelEntity(); + chassis_axle_front.SetModel( "entities/modular_vehicle/chassis_axle_front.vmdl" ); + chassis_axle_front.Transform = Transform; + chassis_axle_front.Parent = this; + chassis_axle_front.LocalPosition = new Vector3( 1.05f, 0, 0.35f ) * 40.0f; + + { + wheel0 = new ModelEntity(); + wheel0.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); + wheel0.SetParent( chassis_axle_front, "Wheel_Steer_R", new Transform( Vector3.Zero, Rotation.From( 0, 180, 0 ) ) ); + } + + { + wheel1 = new ModelEntity(); + wheel1.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); + wheel1.SetParent( chassis_axle_front, "Wheel_Steer_L", new Transform( Vector3.Zero, Rotation.From( 0, 0, 0 ) ) ); + } + + { + var chassis_steering = new ModelEntity(); + chassis_steering.SetModel( "entities/modular_vehicle/chassis_steering.vmdl" ); + chassis_steering.SetParent( chassis_axle_front, "Axle_front_Center", new Transform( Vector3.Zero, Rotation.From( -90, 180, 0 ) ) ); + } + } + + { + chassis_axle_rear = new ModelEntity(); + chassis_axle_rear.SetModel( "entities/modular_vehicle/chassis_axle_rear.vmdl" ); + chassis_axle_rear.Transform = Transform; + chassis_axle_rear.Parent = this; + chassis_axle_rear.LocalPosition = new Vector3( -1.05f, 0, 0.35f ) * 40.0f; + + { + var chassis_transmission = new ModelEntity(); + chassis_transmission.SetModel( "entities/modular_vehicle/chassis_transmission.vmdl" ); + chassis_transmission.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Zero, Rotation.From( -90, 180, 0 ) ) ); + } + + { + wheel2 = new ModelEntity(); + wheel2.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); + wheel2.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Left * (0.7f * 40), Rotation.From( 0, 90, 0 ) ) ); + } + + { + wheel3 = new ModelEntity(); + wheel3.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); + wheel3.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Right * (0.7f * 40), Rotation.From( 0, -90, 0 ) ) ); + } + } + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if ( driver is SandboxPlayer player ) + { + RemoveDriver( player ); + } + } + + public void ResetInput() + { + currentInput.Reset(); + } + + [Event.Tick.Server] + protected void Tick() + { + if ( driver is SandboxPlayer player ) + { + if ( player.LifeState != LifeState.Alive || player.Vehicle != this ) + { + RemoveDriver( player ); + } + } + } + + public override void Simulate( Client owner ) + { + if ( owner == null ) return; + if ( !IsServer ) return; + + using ( Prediction.Off() ) + { + currentInput.Reset(); + + if ( Input.Pressed( InputButton.Use ) ) + { + if ( owner.Pawn is SandboxPlayer player && !player.IsUseDisabled() ) + { + RemoveDriver( player ); + + return; + } + } + + currentInput.throttle = (Input.Down( InputButton.Forward ) ? 1 : 0) + (Input.Down( InputButton.Back ) ? -1 : 0); + currentInput.turning = (Input.Down( InputButton.Left ) ? 1 : 0) + (Input.Down( InputButton.Right ) ? -1 : 0); + currentInput.breaking = (Input.Down( InputButton.Jump ) ? 1 : 0); + currentInput.tilt = (Input.Down( InputButton.Run ) ? 1 : 0) + (Input.Down( InputButton.Duck ) ? -1 : 0); + currentInput.roll = (Input.Down( InputButton.Left ) ? 1 : 0) + (Input.Down( InputButton.Right ) ? -1 : 0); + } + } + + [Event.Physics.PreStep] + public void OnPrePhysicsStep() + { + if ( !IsServer ) + return; + + var selfBody = PhysicsBody; + if ( !selfBody.IsValid() ) + return; + + var body = selfBody.SelfOrParent; + if ( !body.IsValid() ) + return; + + var dt = Time.Delta; + + body.DragEnabled = false; + + var rotation = selfBody.Rotation; + + accelerateDirection = currentInput.throttle.Clamp( -1, 1 ) * (1.0f - currentInput.breaking); + TurnDirection = TurnDirection.LerpTo( currentInput.turning.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.001f, dt ) ); + + airRoll = airRoll.LerpTo( currentInput.roll.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.0001f, dt ) ); + airTilt = airTilt.LerpTo( currentInput.tilt.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.0001f, dt ) ); + + float targetTilt = 0; + float targetLean = 0; + + var localVelocity = rotation.Inverse * body.Velocity; + + if ( backWheelsOnGround || frontWheelsOnGround ) + { + var forwardSpeed = MathF.Abs( localVelocity.x ); + var speedFraction = MathF.Min( forwardSpeed / 500.0f, 1 ); + + targetTilt = accelerateDirection.Clamp( -1.0f, 1.0f ); + targetLean = speedFraction * TurnDirection; + } + + AccelerationTilt = AccelerationTilt.LerpTo( targetTilt, 1.0f - MathF.Pow( 0.01f, dt ) ); + TurnLean = TurnLean.LerpTo( targetLean, 1.0f - MathF.Pow( 0.01f, dt ) ); + + if ( backWheelsOnGround ) + { + var forwardSpeed = MathF.Abs( localVelocity.x ); + var speedFactor = 1.0f - (forwardSpeed / 5000.0f).Clamp( 0.0f, 1.0f ); + var acceleration = speedFactor * (accelerateDirection < 0.0f ? car_accelspeed * 0.5f : car_accelspeed) * accelerateDirection * dt; + var impulse = rotation * new Vector3( acceleration, 0, 0 ); + body.Velocity += impulse; + } + + RaycastWheels( rotation, true, out frontWheelsOnGround, out backWheelsOnGround, dt ); + var onGround = frontWheelsOnGround || backWheelsOnGround; + var fullyGrounded = (frontWheelsOnGround && backWheelsOnGround); + Grounded = onGround; + + if ( fullyGrounded ) + { + body.Velocity += PhysicsWorld.Gravity * dt; + } + + body.GravityScale = fullyGrounded ? 0 : 1; + + bool canAirControl = false; + + var v = rotation * localVelocity.WithZ( 0 ); + var vDelta = MathF.Pow((v.Length / 1000.0f).Clamp( 0, 1 ), 5.0f).Clamp(0, 1); + if ( vDelta < 0.01f ) vDelta = 0; + + if ( debug_car ) + { + DebugOverlay.Line( body.MassCenter, body.MassCenter + rotation.Forward.Normal * 100, Color.White, 0, false ); + DebugOverlay.Line( body.MassCenter, body.MassCenter + v.Normal * 100, Color.Green, 0, false ); + } + + var angle = ( rotation.Forward.Normal * MathF.Sign( localVelocity.x )).Normal.Dot( v.Normal ).Clamp( 0.0f, 1.0f ); + angle = angle.LerpTo( 1.0f, 1.0f - vDelta ); + grip = grip.LerpTo( angle, 1.0f - MathF.Pow( 0.001f, dt ) ); + + if ( debug_car ) + { + DebugOverlay.ScreenText( new Vector2( 200, 200 ), $"{grip}" ); + } + + var angularDamping = 0.0f; + angularDamping = angularDamping.LerpTo( 5.0f, grip ); + + body.LinearDamping = 0.0f; + body.AngularDamping = fullyGrounded ? angularDamping : 0.5f; + + if ( onGround ) + { + localVelocity = rotation.Inverse * body.Velocity; + WheelSpeed = localVelocity.x; + var turnAmount = frontWheelsOnGround ? (MathF.Sign( localVelocity.x ) * 25.0f * CalculateTurnFactor( TurnDirection, MathF.Abs( localVelocity.x ) ) * dt) : 0.0f; + body.AngularVelocity += rotation * new Vector3( 0, 0, turnAmount ); + + airRoll = 0; + airTilt = 0; + + var forwardGrip = 0.1f; + forwardGrip = forwardGrip.LerpTo( 0.9f, currentInput.breaking ); + body.Velocity = VelocityDamping( Velocity, rotation, new Vector3( forwardGrip, grip, 0 ), dt ); + } + else + { + var s = selfBody.Position + (rotation * selfBody.LocalMassCenter); + var tr = Trace.Ray( s, s + rotation.Down * 50 ) + .Ignore( this ) + .Run(); + + if ( debug_car ) + DebugOverlay.Line( tr.StartPos, tr.EndPos, tr.Hit ? Color.Red : Color.Green ); + + canAirControl = !tr.Hit; + } + + if ( canAirControl && (airRoll != 0 || airTilt != 0) ) + { + var offset = 50 * Scale; + var s = selfBody.Position + (rotation * selfBody.LocalMassCenter) + (rotation.Right * airRoll * offset) + (rotation.Down * (10 * Scale)); + var tr = Trace.Ray( s, s + rotation.Up * (25 * Scale) ) + .Ignore( this ) + .Run(); + + if ( debug_car ) + DebugOverlay.Line( tr.StartPos, tr.EndPos ); + + bool dampen = false; + + if ( currentInput.roll.Clamp( -1, 1 ) != 0 ) + { + var force = tr.Hit ? 400.0f : 100.0f; + var roll = tr.Hit ? currentInput.roll.Clamp( -1, 1 ) : airRoll; + body.ApplyForceAt( selfBody.MassCenter + rotation.Left * (offset * roll), (rotation.Down * roll) * (roll * (body.Mass * force)) ); + + if ( debug_car ) + DebugOverlay.Sphere( selfBody.MassCenter + rotation.Left * (offset * roll), 8, Color.Red ); + + dampen = true; + } + + if ( !tr.Hit && currentInput.tilt.Clamp( -1, 1 ) != 0 ) + { + var force = 200.0f; + body.ApplyForceAt( selfBody.MassCenter + rotation.Forward * (offset * airTilt), (rotation.Down * airTilt) * (airTilt * (body.Mass * force)) ); + + if ( debug_car ) + DebugOverlay.Sphere( selfBody.MassCenter + rotation.Forward * (offset * airTilt), 8, Color.Green ); + + dampen = true; + } + + if ( dampen ) + body.AngularVelocity = VelocityDamping( body.AngularVelocity, rotation, 0.95f, dt ); + } + + localVelocity = rotation.Inverse * body.Velocity; + MovementSpeed = localVelocity.x; + } + + private static float CalculateTurnFactor( float direction, float speed ) + { + var turnFactor = MathF.Min( speed / 500.0f, 1 ); + var yawSpeedFactor = 1.0f - (speed / 1000.0f).Clamp( 0, 0.6f ); + + return direction * turnFactor * yawSpeedFactor; + } + + private static Vector3 VelocityDamping( Vector3 velocity, Rotation rotation, Vector3 damping, float dt ) + { + var localVelocity = rotation.Inverse * velocity; + var dampingPow = new Vector3( MathF.Pow( 1.0f - damping.x, dt ), MathF.Pow( 1.0f - damping.y, dt ), MathF.Pow( 1.0f - damping.z, dt ) ); + return rotation * (localVelocity * dampingPow); + } + + private void RaycastWheels( Rotation rotation, bool doPhysics, out bool frontWheels, out bool backWheels, float dt ) + { + float forward = 42; + float right = 32; + + var frontLeftPos = rotation.Forward * forward + rotation.Right * right + rotation.Up * 20; + var frontRightPos = rotation.Forward * forward - rotation.Right * right + rotation.Up * 20; + var backLeftPos = -rotation.Forward * forward + rotation.Right * right + rotation.Up * 20; + var backRightPos = -rotation.Forward * forward - rotation.Right * right + rotation.Up * 20; + + var tiltAmount = AccelerationTilt * 2.5f; + var leanAmount = TurnLean * 2.5f; + + float length = 20.0f; + + frontWheels = + frontLeft.Raycast( length + tiltAmount - leanAmount, doPhysics, frontLeftPos * Scale, ref frontLeftDistance, dt ) | + frontRight.Raycast( length + tiltAmount + leanAmount, doPhysics, frontRightPos * Scale, ref frontRightDistance, dt ); + + backWheels = + backLeft.Raycast( length - tiltAmount - leanAmount, doPhysics, backLeftPos * Scale, ref backLeftDistance, dt ) | + backRight.Raycast( length - tiltAmount + leanAmount, doPhysics, backRightPos * Scale, ref backRightDistance, dt ); + } + + float wheelAngle = 0.0f; + float wheelRevolute = 0.0f; + + [Event.Frame] + public void OnFrame() + { + wheelAngle = wheelAngle.LerpTo( TurnDirection * 25, 1.0f - MathF.Pow( 0.001f, Time.Delta ) ); + wheelRevolute += (WheelSpeed / (14.0f * Scale)).RadianToDegree() * Time.Delta; + + var wheelRotRight = Rotation.From( -wheelAngle, 180, -wheelRevolute ); + var wheelRotLeft = Rotation.From( wheelAngle, 0, wheelRevolute ); + var wheelRotBackRight = Rotation.From( 0, 90, -wheelRevolute ); + var wheelRotBackLeft = Rotation.From( 0, -90, wheelRevolute ); + + RaycastWheels( Rotation, false, out _, out _, Time.Delta ); + + float frontOffset = 20.0f - Math.Min( frontLeftDistance, frontRightDistance ); + float backOffset = 20.0f - Math.Min( backLeftDistance, backRightDistance ); + + chassis_axle_front.SetBoneTransform( "Axle_front_Center", new Transform( Vector3.Up * frontOffset ), false ); + chassis_axle_rear.SetBoneTransform( "Axle_Rear_Center", new Transform( Vector3.Up * backOffset ), false ); + + wheel0.LocalRotation = wheelRotRight; + wheel1.LocalRotation = wheelRotLeft; + wheel2.LocalRotation = wheelRotBackRight; + wheel3.LocalRotation = wheelRotBackLeft; + } + + private void RemoveDriver( SandboxPlayer player ) + { + driver = null; + player.Vehicle = null; + player.VehicleController = null; + player.VehicleAnimator = null; + player.VehicleCamera = null; + player.Parent = null; + player.PhysicsBody.Enabled = true; + player.PhysicsBody.Position = player.Position; + + timeSinceDriverLeft = 0; + + ResetInput(); + } + + public bool OnUse( Entity user ) + { + if ( user is SandboxPlayer player && player.Vehicle == null && timeSinceDriverLeft > 1.0f ) + { + player.Vehicle = this; + player.VehicleController = new CarController(); + player.VehicleAnimator = new CarAnimator(); + player.VehicleCamera = new CarCamera(); + player.Parent = this; + player.LocalPosition = Vector3.Up * 10; + player.LocalRotation = Rotation.Identity; + player.LocalScale = 1; + player.PhysicsBody.Enabled = false; + + driver = player; + } + + return true; + } + + public bool IsUsable( Entity user ) + { + return driver == null; + } + + public override void StartTouch( Entity other ) + { + base.StartTouch( other ); + + if ( !IsServer ) + return; + + var body = PhysicsBody; + if ( !body.IsValid() ) + return; + + body = body.SelfOrParent; + if ( !body.IsValid() ) + return; + + if ( other is SandboxPlayer player && player.Vehicle == null ) + { + var speed = body.Velocity.Length; + var forceOrigin = Position + Rotation.Down * Rand.Float( 20, 30 ); + var velocity = (player.Position - forceOrigin).Normal * speed; + var angularVelocity = body.AngularVelocity; + + OnPhysicsCollision( new CollisionEventData + { + Entity = player, + Pos = player.Position + Vector3.Up * 50, + Velocity = velocity, + PreVelocity = velocity, + PostVelocity = velocity, + PreAngularVelocity = angularVelocity, + Speed = speed, + } ); + } + } + + protected override void OnPhysicsCollision( CollisionEventData eventData ) + { + if ( !IsServer ) + return; + + if ( eventData.Entity is SandboxPlayer player && player.Vehicle != null ) + { + return; + } + + var propData = GetModelPropData(); + + var minImpactSpeed = propData.MinImpactDamageSpeed; + if ( minImpactSpeed <= 0.0f ) minImpactSpeed = 500; + + var impactDmg = propData.ImpactDamage; + if ( impactDmg <= 0.0f ) impactDmg = 10; + + var speed = eventData.Speed; + + if ( speed > minImpactSpeed ) + { + if ( eventData.Entity.IsValid() && eventData.Entity != this ) + { + var damage = speed / minImpactSpeed * impactDmg * 1.2f; + eventData.Entity.TakeDamage( DamageInfo.Generic( damage ) + .WithFlag( DamageFlags.PhysicsImpact ) + .WithFlag( DamageFlags.Vehicle ) + .WithAttacker( driver != null ? driver : this, driver != null ? this : null ) + .WithPosition( eventData.Pos ) + .WithForce( eventData.PreVelocity ) ); + } + } + } +} diff --git a/code/entities/car/CarWheel.cs b/code/entities/car/CarWheel.cs new file mode 100644 index 0000000..c14ce06 --- /dev/null +++ b/code/entities/car/CarWheel.cs @@ -0,0 +1,75 @@ +using Sandbox; +using System; + +struct CarWheel +{ + private readonly CarEntity parent; + + private float _previousLength; + private float _currentLength; + + public CarWheel( CarEntity parent ) + { + this.parent = parent; + _previousLength = 0; + _currentLength = 0; + } + + public bool Raycast( float length, bool doPhysics, Vector3 offset, ref float wheel, float dt ) + { + var position = parent.Position; + var rotation = parent.Rotation; + + var wheelAttachPos = position + offset; + var wheelExtend = wheelAttachPos - rotation.Up * (length * parent.Scale); + + var tr = Trace.Ray( wheelAttachPos, wheelExtend ) + .Ignore( parent ) + .Ignore( parent.driver ) + .Run(); + + wheel = length * tr.Fraction; + var wheelRadius = (14 * parent.Scale); + + if ( !doPhysics && CarEntity.debug_car ) + { + var wheelPosition = tr.Hit ? tr.EndPos : wheelExtend; + wheelPosition += rotation.Up * wheelRadius; + + if ( tr.Hit ) + { + DebugOverlay.Circle( wheelPosition, rotation * Rotation.FromYaw( 90 ), wheelRadius, Color.Red.WithAlpha( 0.5f ), false ); + DebugOverlay.Line( tr.StartPos, tr.EndPos, Color.Red, 0, false ); + } + else + { + DebugOverlay.Circle( wheelPosition, rotation * Rotation.FromYaw( 90 ), wheelRadius, Color.Green.WithAlpha( 0.5f ), false ); + DebugOverlay.Line( wheelAttachPos, wheelExtend, Color.Green, 0, false ); + } + } + + if ( !tr.Hit || !doPhysics ) + { + return tr.Hit; + } + + var body = parent.PhysicsBody.SelfOrParent; + + _previousLength = _currentLength; + _currentLength = (length * parent.Scale) - tr.Distance; + + var springVelocity = (_currentLength - _previousLength) / dt; + var springForce = body.Mass * 50.0f * _currentLength; + var damperForce = body.Mass * (1.5f + (1.0f - tr.Fraction) * 3.0f) * springVelocity; + var velocity = body.GetVelocityAtPoint( wheelAttachPos ); + var speed = velocity.Length; + var speedDot = MathF.Abs( speed ) > 0.0f ? MathF.Abs( MathF.Min( Vector3.Dot( velocity, rotation.Up.Normal ) / speed, 0.0f ) ) : 0.0f; + var speedAlongNormal = speedDot * speed; + var correctionMultiplier = (1.0f - tr.Fraction) * (speedAlongNormal / 1000.0f); + var correctionForce = correctionMultiplier * 50.0f * speedAlongNormal / dt; + + body.ApplyImpulseAt( wheelAttachPos, tr.Normal * (springForce + damperForce + correctionForce) * dt ); + + return true; + } +} diff --git a/code/tools/Balloon.cs b/code/tools/Balloon.cs new file mode 100644 index 0000000..9765bc1 --- /dev/null +++ b/code/tools/Balloon.cs @@ -0,0 +1,123 @@ +namespace Sandbox.Tools +{ + [Library( "tool_balloon", Title = "Balloons", Description = "Create Balloons!", Group = "construction" )] + public partial class BalloonTool : BaseTool + { + [Net] + public Color32 Tint { get; set; } + + PreviewEntity previewModel; + + public override void Activate() + { + base.Activate(); + + if ( Host.IsServer ) + { + Tint = Color.Random.ToColor32(); + } + } + + protected override bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !base.IsPreviewTraceValid( tr ) ) + return false; + + if ( tr.Entity is BalloonEntity ) + return false; + + return true; + } + + public override void CreatePreviews() + { + if ( TryCreatePreview( ref previewModel, "models/citizen_props/balloonregular01.vmdl" ) ) + { + previewModel.RelativeToNormal = false; + } + } + + public override void Simulate() + { + if ( previewModel.IsValid() ) + { + previewModel.RenderColor = Tint; + } + + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + bool useRope = Input.Pressed( InputButton.Attack1 ); + if ( !useRope && !Input.Pressed( InputButton.Attack2 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit ) + return; + + if ( !tr.Entity.IsValid() ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity is BalloonEntity ) + return; + + var ent = new BalloonEntity + { + Position = tr.EndPos, + }; + + ent.SetModel( "models/citizen_props/balloonregular01.vmdl" ); + ent.PhysicsBody.GravityScale = -0.2f; + ent.RenderColor = Tint; + + Tint = Color.Random.ToColor32(); + + if ( !useRope ) + return; + + var rope = Particles.Create( "particles/rope.vpcf" ); + rope.SetEntity( 0, ent ); + + var attachEnt = tr.Body.IsValid() ? tr.Body.Entity : tr.Entity; + var attachLocalPos = tr.Body.Transform.PointToLocal( tr.EndPos ) * (1.0f / tr.Entity.Scale); + + if ( attachEnt.IsWorld ) + { + rope.SetPosition( 1, attachLocalPos ); + } + else + { + rope.SetEntityBone( 1, attachEnt, tr.Bone, new Transform( attachLocalPos ) ); + } + + var spring = PhysicsJoint.Spring + .From( ent.PhysicsBody ) + .To( tr.Body, tr.Body.Transform.PointToLocal( tr.EndPos ) ) + .WithFrequency( 5.0f ) + .WithDampingRatio( 0.7f ) + .WithReferenceMass( ent.PhysicsBody.Mass ) + .WithMinRestLength( 0 ) + .WithMaxRestLength( 100 ) + .WithCollisionsEnabled() + .Create(); + + spring.EnableAngularConstraint = false; + spring.OnBreak( () => + { + rope?.Destroy( true ); + spring.Remove(); + } ); + } + } + } +} diff --git a/code/tools/BoxShooter.cs b/code/tools/BoxShooter.cs new file mode 100644 index 0000000..780eb8a --- /dev/null +++ b/code/tools/BoxShooter.cs @@ -0,0 +1,38 @@ +namespace Sandbox.Tools +{ + [Library( "tool_boxgun", Title = "Box Shooter", Description = "Shoot boxes", Group = "fun" )] + public class BoxShooter : BaseTool + { + TimeSince timeSinceShoot; + + public override void Simulate() + { + if ( Host.IsServer ) + { + if ( Input.Pressed( InputButton.Attack1 ) ) + { + ShootBox(); + } + + if ( Input.Down( InputButton.Attack2 ) && timeSinceShoot > 0.05f ) + { + timeSinceShoot = 0; + ShootBox(); + } + } + } + + void ShootBox() + { + var ent = new Prop + { + Position = Owner.EyePos + Owner.EyeRot.Forward * 50, + Rotation = Owner.EyeRot + }; + + ent.SetModel( "models/citizen_props/crate01.vmdl" ); + ent.Velocity = Owner.EyeRot.Forward * 1000; + } + } + +} diff --git a/code/tools/Color.cs b/code/tools/Color.cs new file mode 100644 index 0000000..c290fd5 --- /dev/null +++ b/code/tools/Color.cs @@ -0,0 +1,38 @@ +using System; + +namespace Sandbox.Tools +{ + [Library( "tool_color", Title = "Color", Description = "Change render color and alpha of entities", Group = "construction" )] + public partial class ColorTool : BaseTool + { + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + if ( !Input.Pressed( InputButton.Attack1 ) ) return; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .UseHitboxes() + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() ) + return; + + if ( tr.Entity is not ModelEntity modelEnt ) + return; + + modelEnt.RenderColor = Color.Random.ToColor32(); + + CreateHitEffects( tr.EndPos ); + } + } + } +} diff --git a/code/tools/GravGun.cs b/code/tools/GravGun.cs new file mode 100644 index 0000000..7095890 --- /dev/null +++ b/code/tools/GravGun.cs @@ -0,0 +1,280 @@ +using Sandbox; +using Sandbox.Joints; +using System; +using System.Linq; + +[Library( "gravgun" )] +public partial class GravGun : Carriable +{ + public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl"; + + private PhysicsBody holdBody; + private WeldJoint holdJoint; + + public PhysicsBody HeldBody { get; private set; } + public Rotation HeldRot { get; private set; } + public ModelEntity HeldEntity { get; private set; } + + protected virtual float MaxPullDistance => 2000.0f; + protected virtual float MaxPushDistance => 500.0f; + protected virtual float LinearFrequency => 10.0f; + protected virtual float LinearDampingRatio => 1.0f; + protected virtual float AngularFrequency => 10.0f; + protected virtual float AngularDampingRatio => 1.0f; + protected virtual float PullForce => 20.0f; + protected virtual float PushForce => 1000.0f; + protected virtual float ThrowForce => 2000.0f; + protected virtual float HoldDistance => 100.0f; + protected virtual float AttachDistance => 150.0f; + protected virtual float DropCooldown => 0.5f; + protected virtual float BreakLinearForce => 2000.0f; + + private TimeSince timeSinceDrop; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pistol/rust_pistol.vmdl" ); + + CollisionGroup = CollisionGroup.Weapon; + SetInteractsAs( CollisionLayer.Debris ); + } + + public override void Simulate( Client client ) + { + if ( Owner is not Player owner ) return; + + if ( !IsServer ) + return; + + using ( Prediction.Off() ) + { + var eyePos = owner.EyePos; + var eyeRot = owner.EyeRot; + var eyeDir = owner.EyeRot.Forward; + + if ( HeldBody.IsValid() && HeldBody.PhysicsGroup != null ) + { + if ( holdJoint.IsValid && !holdJoint.IsActive ) + { + GrabEnd(); + } + else if ( Input.Pressed( InputButton.Attack1 ) ) + { + if ( HeldBody.PhysicsGroup.BodyCount > 1 ) + { + // Don't throw ragdolls as hard + HeldBody.PhysicsGroup.ApplyImpulse( eyeDir * (ThrowForce * 0.5f), true ); + HeldBody.PhysicsGroup.ApplyAngularImpulse( Vector3.Random * ThrowForce, true ); + } + else + { + HeldBody.ApplyImpulse( eyeDir * (HeldBody.Mass * ThrowForce) ); + HeldBody.ApplyAngularImpulse( Vector3.Random * (HeldBody.Mass * ThrowForce) ); + } + + GrabEnd(); + } + else if ( Input.Pressed( InputButton.Attack2 ) ) + { + timeSinceDrop = 0; + + GrabEnd(); + } + else + { + GrabMove( eyePos, eyeDir, eyeRot ); + } + + return; + } + + if ( timeSinceDrop < DropCooldown ) + return; + + var tr = Trace.Ray( eyePos, eyePos + eyeDir * MaxPullDistance ) + .UseHitboxes() + .Ignore( owner, false ) + .Radius( 2.0f ) + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Body.IsValid() || !tr.Entity.IsValid() || tr.Entity.IsWorld ) + return; + + if ( tr.Entity.PhysicsGroup == null ) + return; + + var modelEnt = tr.Entity as ModelEntity; + if ( !modelEnt.IsValid() ) + return; + + var body = tr.Body; + + if ( Input.Pressed( InputButton.Attack1 ) ) + { + if ( tr.Distance < MaxPushDistance && !IsBodyGrabbed( body ) ) + { + var pushScale = 1.0f - Math.Clamp( tr.Distance / MaxPushDistance, 0.0f, 1.0f ); + body.ApplyImpulseAt( tr.EndPos, eyeDir * (body.Mass * (PushForce * pushScale)) ); + } + } + else if ( Input.Down( InputButton.Attack2 ) ) + { + var physicsGroup = tr.Entity.PhysicsGroup; + + if ( physicsGroup.BodyCount > 1 ) + { + body = modelEnt.PhysicsBody; + if ( !body.IsValid() ) + return; + } + + if ( eyePos.Distance( body.Position ) <= AttachDistance ) + { + GrabStart( modelEnt, body, eyePos + eyeDir * HoldDistance, eyeRot ); + } + else if ( !IsBodyGrabbed( body ) ) + { + physicsGroup.ApplyImpulse( eyeDir * -PullForce, true ); + } + } + } + } + + private void Activate() + { + if ( !holdBody.IsValid() ) + { + holdBody = new PhysicsBody + { + BodyType = PhysicsBodyType.Keyframed + }; + } + } + + private void Deactivate() + { + GrabEnd(); + + holdBody?.Remove(); + holdBody = null; + } + + public override void ActiveStart( Entity ent ) + { + base.ActiveStart( ent ); + + if ( IsServer ) + { + Activate(); + } + } + + public override void ActiveEnd( Entity ent, bool dropped ) + { + base.ActiveEnd( ent, dropped ); + + if ( IsServer ) + { + Deactivate(); + } + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if ( IsServer ) + { + Deactivate(); + } + } + + public override void OnCarryDrop( Entity dropper ) + { + } + + private static bool IsBodyGrabbed( PhysicsBody body ) + { + // There for sure is a better way to deal with this + if ( All.OfType().Any( x => x?.HeldBody?.PhysicsGroup == body?.PhysicsGroup ) ) return true; + if ( All.OfType().Any( x => x?.HeldBody?.PhysicsGroup == body?.PhysicsGroup ) ) return true; + + return false; + } + + private void GrabStart( ModelEntity entity, PhysicsBody body, Vector3 grabPos, Rotation grabRot ) + { + if ( !body.IsValid() ) + return; + + if ( body.PhysicsGroup == null ) + return; + + if ( IsBodyGrabbed( body ) ) + return; + + GrabEnd(); + + HeldBody = body; + HeldRot = grabRot.Inverse * HeldBody.Rotation; + + holdBody.Position = grabPos; + holdBody.Rotation = HeldBody.Rotation; + + HeldBody.Wake(); + HeldBody.EnableAutoSleeping = false; + + holdJoint = PhysicsJoint.Weld + .From( holdBody ) + .To( HeldBody, HeldBody.LocalMassCenter ) + .WithLinearSpring( LinearFrequency, LinearDampingRatio, 0.0f ) + .WithAngularSpring( AngularFrequency, AngularDampingRatio, 0.0f ) + .Breakable( HeldBody.Mass * BreakLinearForce, 0 ) + .Create(); + + HeldEntity = entity; + + var client = GetClientOwner(); + client?.Pvs.Add( HeldEntity ); + } + + private void GrabEnd() + { + if ( holdJoint.IsValid ) + { + holdJoint.Remove(); + } + + if ( HeldBody.IsValid() ) + { + HeldBody.EnableAutoSleeping = true; + } + + if ( HeldEntity.IsValid() ) + { + var client = GetClientOwner(); + client?.Pvs.Remove( HeldEntity ); + } + + HeldBody = null; + HeldRot = Rotation.Identity; + HeldEntity = null; + } + + private void GrabMove( Vector3 startPos, Vector3 dir, Rotation rot ) + { + if ( !HeldBody.IsValid() ) + return; + + holdBody.Position = startPos + dir * HoldDistance; + holdBody.Rotation = rot * HeldRot; + } + + public override bool IsUsable( Entity user ) + { + return Owner == null || HeldBody.IsValid(); + } +} diff --git a/code/tools/Lamp.cs b/code/tools/Lamp.cs new file mode 100644 index 0000000..1845bc4 --- /dev/null +++ b/code/tools/Lamp.cs @@ -0,0 +1,83 @@ +namespace Sandbox.Tools +{ + [Library( "tool_lamp", Title = "Lamps", Description = "Directional light source that casts shadows", Group = "construction" )] + public partial class LampTool : BaseTool + { + PreviewEntity previewModel; + + private string Model => "models/torch/torch.vmdl"; + + protected override bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !base.IsPreviewTraceValid( tr ) ) + return false; + + if ( tr.Entity is LampEntity ) + return false; + + return true; + } + + public override void CreatePreviews() + { + if ( TryCreatePreview( ref previewModel, Model ) ) + { + previewModel.RelativeToNormal = false; + previewModel.OffsetBounds = true; + previewModel.PositionOffset = -previewModel.CollisionBounds.Center; + } + } + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + if ( !Input.Pressed( InputButton.Attack1 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity is LampEntity lamp ) + { + // TODO: Set properties + + lamp.Flicker = !lamp.Flicker; + + return; + } + + lamp = new LampEntity + { + Enabled = true, + DynamicShadows = true, + Range = 512, + Falloff = 1.0f, + LinearAttenuation = 0.0f, + QuadraticAttenuation = 1.0f, + InnerConeAngle = 25, + OuterConeAngle = 45, + Brightness = 10, + Color = Color.Random, + Rotation = Rotation.Identity + }; + + lamp.SetModel( Model ); + lamp.SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + lamp.Position = tr.EndPos + -lamp.CollisionBounds.Center + tr.Normal * lamp.CollisionBounds.Size * 0.5f; + } + } + } +} diff --git a/code/tools/LeafBlower.cs b/code/tools/LeafBlower.cs new file mode 100644 index 0000000..ab2b4fc --- /dev/null +++ b/code/tools/LeafBlower.cs @@ -0,0 +1,57 @@ +namespace Sandbox.Tools +{ + [Library( "tool_leafblower", Title = "Leaf Blower", Description = "Blow me", Group = "fun" )] + public partial class LeafBlowerTool : BaseTool + { + protected virtual float Force => 128; + protected virtual float MaxDistance => 512; + protected virtual bool Massless => true; + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + bool push = Input.Down( InputButton.Attack1 ); + if ( !push && !Input.Down( InputButton.Attack2 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit ) + return; + + if ( !tr.Entity.IsValid() ) + return; + + if ( tr.Entity.IsWorld ) + return; + + var body = tr.Body; + + if ( !body.IsValid() ) + return; + + var direction = tr.EndPos - tr.StartPos; + var distance = direction.Length; + var ratio = (1.0f - (distance / MaxDistance)).Clamp( 0, 1 ) * (push ? 1.0f : -1.0f); + var force = direction * (Force * ratio); + + if ( Massless ) + { + force *= body.Mass; + } + + body.ApplyForceAt( tr.EndPos, force ); + } + } + } +} diff --git a/code/tools/Light.cs b/code/tools/Light.cs new file mode 100644 index 0000000..9ac7565 --- /dev/null +++ b/code/tools/Light.cs @@ -0,0 +1,116 @@ +namespace Sandbox.Tools +{ + [Library( "tool_light", Title = "Lights", Description = "A dynamic point light", Group = "construction" )] + public partial class LightTool : BaseTool + { + PreviewEntity previewModel; + + private string Model => "models/light/light_tubular.vmdl"; + + protected override bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !base.IsPreviewTraceValid( tr ) ) + return false; + + if ( tr.Entity is LightEntity ) + return false; + + return true; + } + + public override void CreatePreviews() + { + if ( TryCreatePreview( ref previewModel, Model ) ) + { + previewModel.RelativeToNormal = false; + previewModel.OffsetBounds = true; + previewModel.PositionOffset = -previewModel.CollisionBounds.Center; + } + } + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + bool useRope = Input.Pressed( InputButton.Attack1 ); + if ( !useRope && !Input.Pressed( InputButton.Attack2 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity is LightEntity ) + { + // TODO: Set properties + + return; + } + + var light = new LightEntity + { + Enabled = true, + DynamicShadows = false, + Range = 128, + Falloff = 1.0f, + LinearAttenuation = 0.0f, + QuadraticAttenuation = 1.0f, + Brightness = 1, + Color = Color.Random, + }; + + light.UseFogNoShadows(); + light.SetModel( Model ); + light.SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); + light.Position = tr.EndPos + -light.CollisionBounds.Center + tr.Normal * light.CollisionBounds.Size * 0.5f; + + if ( !useRope ) + return; + + var rope = Particles.Create( "particles/rope.vpcf" ); + rope.SetEntity( 0, light, Vector3.Down * 6.5f ); // Should be an attachment point + + var attachEnt = tr.Body.IsValid() ? tr.Body.Entity : tr.Entity; + var attachLocalPos = tr.Body.Transform.PointToLocal( tr.EndPos ) * (1.0f / tr.Entity.Scale); + + if ( attachEnt.IsWorld ) + { + rope.SetPosition( 1, attachLocalPos ); + } + else + { + rope.SetEntityBone( 1, attachEnt, tr.Bone, new Transform( attachLocalPos ) ); + } + + var spring = PhysicsJoint.Spring + .From( light.PhysicsBody, Vector3.Down * 6.5f ) + .To( tr.Body, tr.Body.Transform.PointToLocal( tr.EndPos ) ) + .WithFrequency( 5.0f ) + .WithDampingRatio( 0.7f ) + .WithReferenceMass( light.PhysicsBody.Mass ) + .WithMinRestLength( 0 ) + .WithMaxRestLength( 100 ) + .WithCollisionsEnabled() + .Create(); + + spring.EnableAngularConstraint = false; + spring.OnBreak( () => + { + rope?.Destroy( true ); + spring.Remove(); + } ); + } + } + } +} diff --git a/code/tools/PhysGun.Effects.cs b/code/tools/PhysGun.Effects.cs new file mode 100644 index 0000000..b3a7dc7 --- /dev/null +++ b/code/tools/PhysGun.Effects.cs @@ -0,0 +1,124 @@ +using Sandbox; +using System.Linq; + +public partial class PhysGun +{ + Particles Beam; + Particles EndNoHit; + + Vector3 lastBeamPos; + ModelEntity lastGrabbedEntity; + + [Event.Frame] + public void OnFrame() + { + UpdateEffects(); + } + + protected virtual void KillEffects() + { + Beam?.Destroy( true ); + Beam = null; + BeamActive = false; + + EndNoHit?.Destroy( false ); + EndNoHit = null; + + if ( lastGrabbedEntity.IsValid() ) + { + foreach ( var child in lastGrabbedEntity.Children.OfType() ) + { + if ( child is Player ) + continue; + + child.GlowActive = false; + child.GlowState = GlowStates.GlowStateOff; + } + + lastGrabbedEntity.GlowActive = false; + lastGrabbedEntity.GlowState = GlowStates.GlowStateOff; + lastGrabbedEntity = null; + } + } + + protected virtual void UpdateEffects() + { + var owner = Owner; + + if ( owner == null || !BeamActive || !IsActiveChild() ) + { + KillEffects(); + return; + } + + var startPos = owner.EyePos; + var dir = owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTargetDistance ) + .UseHitboxes() + .Ignore( owner ) + .Run(); + + if ( Beam == null ) + { + Beam = Particles.Create( "particles/physgun_beam.vpcf", tr.EndPos ); + } + + Beam.SetEntityAttachment( 0, EffectEntity, "muzzle", true ); + + if ( GrabbedEntity.IsValid() && !GrabbedEntity.IsWorld ) + { + var physGroup = GrabbedEntity.PhysicsGroup; + + if ( physGroup != null && GrabbedBone >= 0 ) + { + var physBody = physGroup.GetBody( GrabbedBone ); + if ( physBody != null ) + { + Beam.SetPosition( 1, physBody.Transform.PointToWorld( GrabbedPos ) ); + } + } + else + { + Beam.SetEntity( 1, GrabbedEntity, GrabbedPos, true ); + } + + lastBeamPos = GrabbedEntity.Position + GrabbedEntity.Rotation * GrabbedPos; + + EndNoHit?.Destroy( false ); + EndNoHit = null; + + if ( GrabbedEntity is ModelEntity modelEnt ) + { + lastGrabbedEntity = modelEnt; + modelEnt.GlowState = GlowStates.GlowStateOn; + modelEnt.GlowDistanceStart = 0; + modelEnt.GlowDistanceEnd = 1000; + modelEnt.GlowColor = new Color( 0.1f, 1.0f, 1.0f, 1.0f ); + modelEnt.GlowActive = true; + + foreach ( var child in lastGrabbedEntity.Children.OfType() ) + { + if ( child is Player ) + continue; + + child.GlowState = GlowStates.GlowStateOn; + child.GlowDistanceStart = 0; + child.GlowDistanceEnd = 1000; + child.GlowColor = new Color( 0.1f, 1.0f, 1.0f, 1.0f ); + child.GlowActive = true; + } + } + } + else + { + lastBeamPos = tr.EndPos;// Vector3.Lerp( lastBeamPos, tr.EndPos, Time.Delta * 10 ); + Beam.SetPosition( 1, lastBeamPos ); + + if ( EndNoHit == null ) + EndNoHit = Particles.Create( "particles/physgun_end_nohit.vpcf", lastBeamPos ); + + EndNoHit.SetPosition( 0, lastBeamPos ); + } + } +} diff --git a/code/tools/PhysGun.cs b/code/tools/PhysGun.cs new file mode 100644 index 0000000..a874f36 --- /dev/null +++ b/code/tools/PhysGun.cs @@ -0,0 +1,419 @@ +using Sandbox; +using Sandbox.Joints; +using System; +using System.Linq; + +[Library( "physgun" )] +public partial class PhysGun : Carriable +{ + public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl"; + + protected PhysicsBody holdBody; + protected WeldJoint holdJoint; + + protected PhysicsBody heldBody; + protected Vector3 heldPos; + protected Rotation heldRot; + + protected float holdDistance; + protected bool grabbing; + + protected virtual float MinTargetDistance => 0.0f; + protected virtual float MaxTargetDistance => 10000.0f; + protected virtual float LinearFrequency => 20.0f; + protected virtual float LinearDampingRatio => 1.0f; + protected virtual float AngularFrequency => 20.0f; + protected virtual float AngularDampingRatio => 1.0f; + protected virtual float TargetDistanceSpeed => 50.0f; + protected virtual float RotateSpeed => 0.2f; + protected virtual float RotateSnapAt => 45.0f; + + [Net] public bool BeamActive { get; set; } + [Net] public Entity GrabbedEntity { get; set; } + [Net] public int GrabbedBone { get; set; } + [Net] public Vector3 GrabbedPos { get; set; } + + public PhysicsBody HeldBody => heldBody; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pistol/rust_pistol.vmdl" ); + + CollisionGroup = CollisionGroup.Weapon; + SetInteractsAs( CollisionLayer.Debris ); + } + + public override void Simulate( Client client ) + { + if ( Owner is not Player owner ) return; + + var eyePos = owner.EyePos; + var eyeDir = owner.EyeRot.Forward; + var eyeRot = Rotation.From( new Angles( 0.0f, owner.EyeRot.Angles().yaw, 0.0f ) ); + + if ( Input.Pressed( InputButton.Attack1 ) ) + { + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + + if ( !grabbing ) + grabbing = true; + } + + bool grabEnabled = grabbing && Input.Down( InputButton.Attack1 ); + bool wantsToFreeze = Input.Pressed( InputButton.Attack2 ); + + if ( GrabbedEntity.IsValid() && wantsToFreeze ) + { + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + } + + BeamActive = grabEnabled; + + if ( IsServer ) + { + using ( Prediction.Off() ) + { + if ( !holdBody.IsValid() ) + return; + + if ( grabEnabled ) + { + if ( heldBody.IsValid() ) + { + UpdateGrab( eyePos, eyeRot, eyeDir, wantsToFreeze ); + } + else + { + TryStartGrab( owner, eyePos, eyeRot, eyeDir ); + } + } + else if ( grabbing ) + { + GrabEnd(); + } + + if ( !grabbing && Input.Pressed( InputButton.Reload ) ) + { + TryUnfreezeAll( owner, eyePos, eyeRot, eyeDir ); + } + } + } + + if ( BeamActive ) + { + Input.MouseWheel = 0; + } + } + + private static bool IsBodyGrabbed( PhysicsBody body ) + { + // There for sure is a better way to deal with this + if ( All.OfType().Any( x => x?.HeldBody?.PhysicsGroup == body?.PhysicsGroup ) ) return true; + if ( All.OfType().Any( x => x?.HeldBody?.PhysicsGroup == body?.PhysicsGroup ) ) return true; + + return false; + } + + private void TryUnfreezeAll( Player owner, Vector3 eyePos, Rotation eyeRot, Vector3 eyeDir ) + { + var tr = Trace.Ray( eyePos, eyePos + eyeDir * MaxTargetDistance ) + .UseHitboxes() + .Ignore( owner, false ) + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() || tr.Entity.IsWorld ) return; + + var rootEnt = tr.Entity.Root; + if ( !rootEnt.IsValid() ) return; + + var physicsGroup = rootEnt.PhysicsGroup; + if ( physicsGroup == null ) return; + + bool unfrozen = false; + + for ( int i = 0; i < physicsGroup.BodyCount; ++i ) + { + var body = physicsGroup.GetBody( i ); + if ( !body.IsValid() ) continue; + + if ( body.BodyType == PhysicsBodyType.Static ) + { + body.BodyType = PhysicsBodyType.Dynamic; + unfrozen = true; + } + } + + if ( unfrozen ) + { + var freezeEffect = Particles.Create( "particles/physgun_freeze.vpcf" ); + freezeEffect.SetPosition( 0, tr.EndPos ); + } + } + + private void TryStartGrab( Player owner, Vector3 eyePos, Rotation eyeRot, Vector3 eyeDir ) + { + var tr = Trace.Ray( eyePos, eyePos + eyeDir * MaxTargetDistance ) + .UseHitboxes() + .Ignore( owner, false ) + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() || !tr.Body.IsValid() || tr.Entity.IsWorld ) return; + + var rootEnt = tr.Entity.Root; + var body = tr.Body; + + if ( tr.Entity.Parent.IsValid() ) + { + if ( rootEnt.IsValid() && rootEnt.PhysicsGroup != null ) + { + body = rootEnt.PhysicsGroup.GetBody( 0 ); + } + } + + if ( !body.IsValid() ) + return; + + // + // Don't move keyframed + // + if ( body.BodyType == PhysicsBodyType.Keyframed ) + return; + + // Unfreeze + if ( body.BodyType == PhysicsBodyType.Static ) + { + body.BodyType = PhysicsBodyType.Dynamic; + } + + if ( IsBodyGrabbed( body ) ) + return; + + GrabInit( body, eyePos, tr.EndPos, eyeRot ); + + GrabbedEntity = rootEnt; + GrabbedPos = body.Transform.PointToLocal( tr.EndPos ); + GrabbedBone = tr.Entity.PhysicsGroup.GetBodyIndex( body ); + + var client = GetClientOwner(); + if ( client != null ) + { + client.Pvs.Add( GrabbedEntity ); + } + } + + private void UpdateGrab( Vector3 eyePos, Rotation eyeRot, Vector3 eyeDir, bool wantsToFreeze ) + { + if ( wantsToFreeze ) + { + heldBody.BodyType = PhysicsBodyType.Static; + + if ( GrabbedEntity.IsValid() ) + { + var freezeEffect = Particles.Create( "particles/physgun_freeze.vpcf" ); + freezeEffect.SetPosition( 0, heldBody.Transform.PointToWorld( GrabbedPos ) ); + } + + GrabEnd(); + return; + } + + MoveTargetDistance( Input.MouseWheel * TargetDistanceSpeed ); + + bool rotating = Input.Down( InputButton.Use ); + bool snapping = false; + + if ( rotating ) + { + EnableAngularSpring( Input.Down( InputButton.Run ) ? 100.0f : 0.0f ); + DoRotate( eyeRot, Input.MouseDelta * RotateSpeed ); + + snapping = Input.Down( InputButton.Run ); + } + else + { + DisableAngularSpring(); + } + + GrabMove( eyePos, eyeDir, eyeRot, snapping ); + } + + private void EnableAngularSpring( float scale ) + { + if ( holdJoint.IsValid ) + { + holdJoint.AngularDampingRatio = AngularDampingRatio * scale; + holdJoint.AngularFrequency = AngularFrequency * scale; + } + } + + private void DisableAngularSpring() + { + if ( holdJoint.IsValid ) + { + holdJoint.AngularDampingRatio = 0.0f; + holdJoint.AngularFrequency = 0.0f; + } + } + + private void Activate() + { + if ( !IsServer ) + return; + + if ( !holdBody.IsValid() ) + { + holdBody = new PhysicsBody + { + BodyType = PhysicsBodyType.Keyframed + }; + } + } + + private void Deactivate() + { + if ( IsServer ) + { + GrabEnd(); + + holdBody?.Remove(); + holdBody = null; + } + + KillEffects(); + } + + public override void ActiveStart( Entity ent ) + { + base.ActiveStart( ent ); + + Activate(); + } + + public override void ActiveEnd( Entity ent, bool dropped ) + { + base.ActiveEnd( ent, dropped ); + + Deactivate(); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + Deactivate(); + } + + public override void OnCarryDrop( Entity dropper ) + { + } + + private void GrabInit( PhysicsBody body, Vector3 startPos, Vector3 grabPos, Rotation rot ) + { + if ( !body.IsValid() ) + return; + + GrabEnd(); + + grabbing = true; + heldBody = body; + holdDistance = Vector3.DistanceBetween( startPos, grabPos ); + holdDistance = holdDistance.Clamp( MinTargetDistance, MaxTargetDistance ); + heldPos = heldBody.Transform.PointToLocal( grabPos ); + heldRot = rot.Inverse * heldBody.Rotation; + + holdBody.Position = grabPos; + holdBody.Rotation = heldBody.Rotation; + + heldBody.Wake(); + heldBody.EnableAutoSleeping = false; + + holdJoint = PhysicsJoint.Weld + .From( holdBody ) + .To( heldBody, heldPos ) + .WithLinearSpring( LinearFrequency, LinearDampingRatio, 0.0f ) + .WithAngularSpring( 0.0f, 0.0f, 0.0f ) + .Create(); + } + + private void GrabEnd() + { + if ( holdJoint.IsValid ) + { + holdJoint.Remove(); + } + + if ( heldBody.IsValid() ) + { + heldBody.EnableAutoSleeping = true; + } + + var client = GetClientOwner(); + if ( client != null && GrabbedEntity.IsValid() ) + { + client.Pvs.Remove( GrabbedEntity ); + } + + heldBody = null; + GrabbedEntity = null; + grabbing = false; + } + + private void GrabMove( Vector3 startPos, Vector3 dir, Rotation rot, bool snapAngles ) + { + if ( !heldBody.IsValid() ) + return; + + holdBody.Position = startPos + dir * holdDistance; + holdBody.Rotation = rot * heldRot; + + if ( snapAngles ) + { + var angles = holdBody.Rotation.Angles(); + + holdBody.Rotation = Rotation.From( + MathF.Round( angles.pitch / RotateSnapAt ) * RotateSnapAt, + MathF.Round( angles.yaw / RotateSnapAt ) * RotateSnapAt, + MathF.Round( angles.roll / RotateSnapAt ) * RotateSnapAt + ); + } + } + + private void MoveTargetDistance( float distance ) + { + holdDistance += distance; + holdDistance = holdDistance.Clamp( MinTargetDistance, MaxTargetDistance ); + } + + protected virtual void DoRotate( Rotation eye, Vector3 input ) + { + var localRot = eye; + localRot *= Rotation.FromAxis( Vector3.Up, input.x ); + localRot *= Rotation.FromAxis( Vector3.Right, input.y ); + localRot = eye.Inverse * localRot; + + heldRot = localRot * heldRot; + } + + public override void BuildInput( InputBuilder owner ) + { + if ( !GrabbedEntity.IsValid() ) + return; + + if ( !owner.Down( InputButton.Attack1 ) ) + return; + + if ( owner.Down( InputButton.Use ) ) + { + owner.ViewAngles = owner.OriginalViewAngles; + } + } + + public override bool IsUsable( Entity user ) + { + return Owner == null || HeldBody.IsValid(); + } +} diff --git a/code/tools/Remover.cs b/code/tools/Remover.cs new file mode 100644 index 0000000..d44d618 --- /dev/null +++ b/code/tools/Remover.cs @@ -0,0 +1,42 @@ +namespace Sandbox.Tools +{ + [Library( "tool_remover", Title = "Remover", Description = "Remove entities", Group = "construction" )] + public partial class RemoverTool : BaseTool + { + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + if ( !Input.Pressed( InputButton.Attack1 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() ) + return; + + if ( tr.Entity is Player ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity.IsWorld ) + return; + + tr.Entity.Delete(); + + var particle = Particles.Create( "particles/physgun_freeze.vpcf" ); + particle.SetPosition( 0, tr.Entity.Position ); + } + } + } +} diff --git a/code/tools/Resizer.cs b/code/tools/Resizer.cs new file mode 100644 index 0000000..00040a9 --- /dev/null +++ b/code/tools/Resizer.cs @@ -0,0 +1,54 @@ +using System; + +namespace Sandbox.Tools +{ + [Library( "tool_resizer", Title = "Resizer", Description = "Change the scale of things", Group = "construction" )] + public partial class ResizerTool : BaseTool + { + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + int resizeDir = 0; + var reset = false; + + if ( Input.Down( InputButton.Attack1 ) ) resizeDir = 1; + else if ( Input.Down( InputButton.Attack2 ) ) resizeDir = -1; + else if ( Input.Pressed( InputButton.Reload ) ) reset = true; + else return; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .UseHitboxes() + .HitLayer( CollisionLayer.Debris ) + .Run(); + + if ( !tr.Hit || !tr.Entity.IsValid() || tr.Entity.PhysicsGroup == null ) + return; + + // Disable resizing lights for now + if ( tr.Entity is LightEntity || tr.Entity is LampEntity ) + return; + + var scale = reset ? 1.0f : Math.Clamp( tr.Entity.Scale + ((0.5f * Time.Delta) * resizeDir), 0.4f, 4.0f ); + + if ( tr.Entity.Scale != scale ) + { + tr.Entity.Scale = scale; + tr.Entity.PhysicsGroup.RebuildMass(); + tr.Entity.PhysicsGroup.Wake(); + } + + if ( Input.Pressed( InputButton.Attack1 ) || Input.Pressed( InputButton.Attack2 ) || reset ) + { + CreateHitEffects( tr.EndPos ); + } + } + } + } +} diff --git a/code/tools/Rope.cs b/code/tools/Rope.cs new file mode 100644 index 0000000..247c585 --- /dev/null +++ b/code/tools/Rope.cs @@ -0,0 +1,122 @@ +namespace Sandbox.Tools +{ + [Library( "tool_rope", Title = "Rope", Description = "Join two things together with a rope", Group = "construction" )] + public partial class RopeTool : BaseTool + { + private PhysicsBody targetBody; + private int targetBone; + private Vector3 localOrigin1; + private Vector3 globalOrigin1; + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + if ( !Input.Pressed( InputButton.Attack1 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit ) + return; + + if ( !tr.Body.IsValid() ) + return; + + if ( !tr.Entity.IsValid() ) + return; + + if ( tr.Entity is not ModelEntity ) + return; + + if ( !targetBody.IsValid() ) + { + targetBody = tr.Body; + targetBone = tr.Bone; + globalOrigin1 = tr.EndPos; + localOrigin1 = tr.Body.Transform.PointToLocal( globalOrigin1 ); + + CreateHitEffects( tr.EndPos ); + + return; + } + + if ( targetBody == tr.Body ) + return; + + var rope = Particles.Create( "particles/rope.vpcf" ); + + if ( targetBody.Entity.IsWorld ) + { + rope.SetPosition( 0, localOrigin1 ); + } + else + { + rope.SetEntityBone( 0, targetBody.Entity, targetBone, new Transform( localOrigin1 * (1.0f / targetBody.Entity.Scale) ) ); + } + + var localOrigin2 = tr.Body.Transform.PointToLocal( tr.EndPos ); + + if ( tr.Entity.IsWorld ) + { + rope.SetPosition( 1, localOrigin2 ); + } + else + { + rope.SetEntityBone( 1, tr.Entity, tr.Bone, new Transform( localOrigin2 * (1.0f / tr.Entity.Scale) ) ); + } + + var spring = PhysicsJoint.Spring + .From( targetBody, localOrigin1 ) + .To( tr.Body, localOrigin2 ) + .WithFrequency( 5.0f ) + .WithDampingRatio( 0.7f ) + .WithReferenceMass( targetBody.Mass ) + .WithMinRestLength( 0 ) + .WithMaxRestLength( tr.EndPos.Distance( globalOrigin1 ) ) + .WithCollisionsEnabled() + .Create(); + + spring.EnableAngularConstraint = false; + spring.OnBreak( () => + { + rope?.Destroy( true ); + spring.Remove(); + } ); + + CreateHitEffects( tr.EndPos ); + + Reset(); + } + } + + private void Reset() + { + targetBody = null; + targetBone = -1; + localOrigin1 = default; + } + + public override void Activate() + { + base.Activate(); + + Reset(); + } + + public override void Deactivate() + { + base.Deactivate(); + + Reset(); + } + } +} diff --git a/code/tools/Thruster.cs b/code/tools/Thruster.cs new file mode 100644 index 0000000..78f498e --- /dev/null +++ b/code/tools/Thruster.cs @@ -0,0 +1,89 @@ +namespace Sandbox.Tools +{ + [Library( "tool_thruster", Title = "Thruster", Description = "A rocket type thing that can push forwards and backward", Group = "construction" )] + public partial class ThrusterTool : BaseTool + { + PreviewEntity previewModel; + bool massless = true; + + public override void CreatePreviews() + { + if ( TryCreatePreview( ref previewModel, "models/thruster/thrusterprojector.vmdl" ) ) + { + previewModel.RotationOffset = Rotation.FromAxis( Vector3.Right, -90 ); + } + } + + protected override bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !base.IsPreviewTraceValid( tr ) ) + return false; + + if ( tr.Entity is ThrusterEntity ) + return false; + + return true; + } + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + if ( Input.Pressed( InputButton.Attack2 ) ) + { + massless = !massless; + } + + if ( !Input.Pressed( InputButton.Attack1 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit ) + return; + + if ( !tr.Entity.IsValid() ) + return; + + var attached = !tr.Entity.IsWorld && tr.Body.IsValid() && tr.Body.PhysicsGroup != null && tr.Body.Entity.IsValid(); + + if ( attached && tr.Entity is not Prop ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity is ThrusterEntity ) + { + // TODO: Set properties + + return; + } + + var ent = new ThrusterEntity + { + Position = tr.EndPos, + Rotation = Rotation.LookAt( tr.Normal, dir ) * Rotation.From( new Angles( 90, 0, 0 ) ), + PhysicsEnabled = !attached, + EnableSolidCollisions = !attached, + TargetBody = attached ? tr.Body : null, + Massless = massless + }; + + if ( attached ) + { + ent.SetParent( tr.Body.Entity, tr.Body.PhysicsGroup.GetBodyBoneName( tr.Body ) ); + } + + ent.SetModel( "models/thruster/thrusterprojector.vmdl" ); + } + } + } +} diff --git a/code/tools/Weld.cs b/code/tools/Weld.cs new file mode 100644 index 0000000..0b800d4 --- /dev/null +++ b/code/tools/Weld.cs @@ -0,0 +1,96 @@ +namespace Sandbox.Tools +{ + [Library( "tool_weld", Title = "Weld", Description = "Weld stuff together", Group = "construction" )] + public partial class WeldTool : BaseTool + { + private Prop target; + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit || !tr.Body.IsValid() || !tr.Entity.IsValid() || tr.Entity.IsWorld ) + return; + + if ( tr.Entity.PhysicsGroup == null || tr.Entity.PhysicsGroup.BodyCount > 1 ) + return; + + if ( tr.Entity is not Prop prop ) + return; + + if ( Input.Pressed( InputButton.Attack1 ) ) + { + if ( prop.Root is not Prop rootProp ) + { + return; + } + + if ( target == rootProp ) + return; + + if ( !target.IsValid() ) + { + target = rootProp; + } + else + { + target.Weld( rootProp ); + target = null; + } + } + else if ( Input.Pressed( InputButton.Attack2 ) ) + { + prop.Unweld( true ); + + Reset(); + } + else if ( Input.Pressed( InputButton.Reload ) ) + { + if ( prop.Root is not Prop rootProp ) + { + return; + } + + rootProp.Unweld(); + + Reset(); + } + else + { + return; + } + + CreateHitEffects( tr.EndPos ); + } + } + + private void Reset() + { + target = null; + } + + public override void Activate() + { + base.Activate(); + + Reset(); + } + + public override void Deactivate() + { + base.Deactivate(); + + Reset(); + } + } +} diff --git a/code/tools/Wheel.cs b/code/tools/Wheel.cs new file mode 100644 index 0000000..156d10b --- /dev/null +++ b/code/tools/Wheel.cs @@ -0,0 +1,83 @@ +namespace Sandbox.Tools +{ + [Library( "tool_wheel", Title = "Wheel", Description = "A wheel that you can turn on and off (but actually can't yet)", Group = "construction" )] + public partial class WheelTool : BaseTool + { + PreviewEntity previewModel; + + protected override bool IsPreviewTraceValid( TraceResult tr ) + { + if ( !base.IsPreviewTraceValid( tr ) ) + return false; + + if ( tr.Entity is WheelEntity ) + return false; + + return true; + } + + public override void CreatePreviews() + { + if ( TryCreatePreview( ref previewModel, "models/citizen_props/wheel01.vmdl" ) ) + { + previewModel.RotationOffset = Rotation.FromAxis( Vector3.Up, 90 ); + } + } + + public override void Simulate() + { + if ( !Host.IsServer ) + return; + + using ( Prediction.Off() ) + { + if ( !Input.Pressed( InputButton.Attack1 ) ) + return; + + var startPos = Owner.EyePos; + var dir = Owner.EyeRot.Forward; + + var tr = Trace.Ray( startPos, startPos + dir * MaxTraceDistance ) + .Ignore( Owner ) + .Run(); + + if ( !tr.Hit ) + return; + + if ( !tr.Entity.IsValid() ) + return; + + var attached = !tr.Entity.IsWorld && tr.Body.IsValid() && tr.Body.PhysicsGroup != null && tr.Body.Entity.IsValid(); + + if ( attached && tr.Entity is not Prop ) + return; + + CreateHitEffects( tr.EndPos ); + + if ( tr.Entity is WheelEntity ) + { + // TODO: Set properties + + return; + } + + var ent = new WheelEntity + { + Position = tr.EndPos, + Rotation = Rotation.LookAt( tr.Normal ) * Rotation.From( new Angles( 0, 90, 0 ) ), + }; + + ent.SetModel( "models/citizen_props/wheel01.vmdl" ); + + ent.PhysicsBody.Mass = tr.Body.Mass; + + ent.Joint = PhysicsJoint.Revolute + .From( ent.PhysicsBody ) + .To( tr.Body ) + .WithPivot( tr.EndPos ) + .WithBasis( Rotation.LookAt( tr.Normal ) * Rotation.From( new Angles( 90, 0, 0 ) ) ) + .Create(); + } + } + } +} diff --git a/code/ui/CurrentTool.cs b/code/ui/CurrentTool.cs new file mode 100644 index 0000000..1bb73c0 --- /dev/null +++ b/code/ui/CurrentTool.cs @@ -0,0 +1,41 @@ +using Sandbox; +using Sandbox.Tools; +using Sandbox.UI; +using Sandbox.UI.Construct; + +public class CurrentTool : Panel +{ + public Label Title; + public Label Description; + + public CurrentTool() + { + Title = Add.Label( "Tool", "title" ); + Description = Add.Label( "This is a tool", "description" ); + } + + public override void Tick() + { + var tool = GetCurrentTool(); + SetClass( "active", tool != null ); + + if ( tool != null ) + { + Title.SetText( tool.ClassInfo.Title ); + Description.SetText( tool.ClassInfo.Description ); + } + } + + BaseTool GetCurrentTool() + { + var player = Local.Pawn; + if ( player == null ) return null; + + var inventory = player.Inventory; + if ( inventory == null ) return null; + + if ( inventory.Active is not Tool tool ) return null; + + return tool?.CurrentTool; + } +} diff --git a/code/ui/Health.cs b/code/ui/Health.cs new file mode 100644 index 0000000..4f136c9 --- /dev/null +++ b/code/ui/Health.cs @@ -0,0 +1,21 @@ +using Sandbox; +using Sandbox.UI; +using Sandbox.UI.Construct; + +public class Health : Panel +{ + public Label Label; + + public Health() + { + Label = Add.Label( "100", "value" ); + } + + public override void Tick() + { + var player = Local.Pawn; + if ( player == null ) return; + + Label.Text = $"{player.Health.CeilToInt()}"; + } +} diff --git a/code/ui/InventoryBar.cs b/code/ui/InventoryBar.cs new file mode 100644 index 0000000..8888fcb --- /dev/null +++ b/code/ui/InventoryBar.cs @@ -0,0 +1,103 @@ +using Sandbox; +using Sandbox.UI; +using System.Collections.Generic; + +public class InventoryBar : Panel +{ + readonly List slots = new(); + + public InventoryBar() + { + for ( int i = 0; i < 9; i++ ) + { + var icon = new InventoryIcon( i + 1, this ); + slots.Add( icon ); + } + } + + public override void Tick() + { + base.Tick(); + + var player = Local.Pawn; + if ( player == null ) return; + if ( player.Inventory == null ) return; + + for ( int i = 0; i < slots.Count; i++ ) + { + UpdateIcon( player.Inventory.GetSlot( i ), slots[i], i ); + } + } + + private static void UpdateIcon( Entity ent, InventoryIcon inventoryIcon, int i ) + { + if ( ent == null ) + { + inventoryIcon.Clear(); + return; + } + + inventoryIcon.TargetEnt = ent; + inventoryIcon.Label.Text = ent.ClassInfo.Title; + inventoryIcon.SetClass( "active", ent.IsActiveChild() ); + } + + [Event( "buildinput" )] + public void ProcessClientInput( InputBuilder input ) + { + var player = Local.Pawn as Player; + if ( player == null ) + return; + + var inventory = player.Inventory; + if ( inventory == null ) + return; + + if ( player.ActiveChild is PhysGun physgun && physgun.BeamActive ) + { + return; + } + + if ( input.Pressed( InputButton.Slot1 ) ) SetActiveSlot( input, inventory, 0 ); + if ( input.Pressed( InputButton.Slot2 ) ) SetActiveSlot( input, inventory, 1 ); + if ( input.Pressed( InputButton.Slot3 ) ) SetActiveSlot( input, inventory, 2 ); + if ( input.Pressed( InputButton.Slot4 ) ) SetActiveSlot( input, inventory, 3 ); + if ( input.Pressed( InputButton.Slot5 ) ) SetActiveSlot( input, inventory, 4 ); + if ( input.Pressed( InputButton.Slot6 ) ) SetActiveSlot( input, inventory, 5 ); + if ( input.Pressed( InputButton.Slot7 ) ) SetActiveSlot( input, inventory, 6 ); + if ( input.Pressed( InputButton.Slot8 ) ) SetActiveSlot( input, inventory, 7 ); + if ( input.Pressed( InputButton.Slot9 ) ) SetActiveSlot( input, inventory, 8 ); + + if ( input.MouseWheel != 0 ) SwitchActiveSlot( input, inventory, -input.MouseWheel ); + } + + private static void SetActiveSlot( InputBuilder input, IBaseInventory inventory, int i ) + { + var player = Local.Pawn; + if ( player == null ) + return; + + var ent = inventory.GetSlot( i ); + if ( player.ActiveChild == ent ) + return; + + if ( ent == null ) + return; + + input.ActiveChild = ent; + } + + private static void SwitchActiveSlot( InputBuilder input, IBaseInventory inventory, int idelta ) + { + var count = inventory.Count(); + if ( count == 0 ) return; + + var slot = inventory.GetActiveSlot(); + var nextSlot = slot + idelta; + + while ( nextSlot < 0 ) nextSlot += count; + while ( nextSlot >= count ) nextSlot -= count; + + SetActiveSlot( input, inventory, nextSlot ); + } +} diff --git a/code/ui/InventoryIcon.cs b/code/ui/InventoryIcon.cs new file mode 100644 index 0000000..62746f5 --- /dev/null +++ b/code/ui/InventoryIcon.cs @@ -0,0 +1,24 @@ + +using Sandbox; +using Sandbox.UI; +using Sandbox.UI.Construct; + +public class InventoryIcon : Panel +{ + public Entity TargetEnt; + public Label Label; + public Label Number; + + public InventoryIcon( int i, Panel parent ) + { + Parent = parent; + Label = Add.Label( "empty", "item-name" ); + Number = Add.Label( $"{i}", "slot-number" ); + } + + public void Clear() + { + Label.Text = ""; + SetClass( "active", false ); + } +} diff --git a/code/ui/SandboxHud.cs b/code/ui/SandboxHud.cs new file mode 100644 index 0000000..86dfb51 --- /dev/null +++ b/code/ui/SandboxHud.cs @@ -0,0 +1,25 @@ +using Sandbox; +using Sandbox.UI; + +[Library] +public partial class SandboxHud : HudEntity +{ + public SandboxHud() + { + if ( !IsClient ) + return; + + RootPanel.StyleSheet.Load( "/ui/SandboxHud.scss" ); + + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild>(); + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild(); + RootPanel.AddChild(); + } +} diff --git a/code/ui/SandboxHud.scss b/code/ui/SandboxHud.scss new file mode 100644 index 0000000..6e0c570 --- /dev/null +++ b/code/ui/SandboxHud.scss @@ -0,0 +1,135 @@ +Health +{ + position: absolute; + background-color: rgba( black, 0.5 ); + right: 100px; + bottom: 48px; + font-size: 40px; + font-weight: bold; + color: white; + height: 80px; + padding: 0 20px; + align-items: center; +} + +CurrentTool +{ + position: absolute; + background: linear-gradient(to right, rgba(black, 0.5), rgba(black, 0.0)); + top: 100px; + left: 0px; + padding: 30px 70px; + flex-direction: column; + width: 560px; + opacity: 0.0; + + &.active + { + opacity: 1.0; + } + + .title + { + font-size: 50px; + font-weight: bold; + text-shadow: 4px 4px 10px rgba( black, 0.3 ); + color: white; + } + + .description + { + padding-top: 5px; + padding-left: 10px; + font-size: 18px; + font-weight: normal; + text-shadow: 2px 2px 5px rgba( black, 0.3 ); + color: white; + opacity: 0.8; + } +} + +rootpanel +{ + background-color: rgba( #333, 0 ); + transition: background-color 0.2s ease-in; + + &.spawnmenuopen + { + transition: background-color 0.3s ease-out; + background-color: rgba( #333, 0.9 ); + } + + + &.devcamera + { + display: none; + } +} + + +InventoryBar +{ + position: absolute; + width: 100%; + bottom: 48px; + justify-content: center; + align-items: flex-end; +} + +InventoryIcon +{ + width: 80px; + height: 80px; + background-color: rgba( #222, 0.3 ); + margin: 0px 2px; + position: relative; + transition: all 0.2s ease-in-out; + margin-top: 10px; + font-family: poppins; + + .item-name + { + padding: 5px; + color: white; + text-align: center; + left: 0; + right: 0; + font-size: 10px; + position: absolute; + bottom: 0; + opacity: 0.7; + transition: all 0.5s ease-in-out; + text-shadow: 1px 1px 5px black; + } + + .slot-number + { + font-size: 25px; + color: black; + font-weight: 900; + opacity: 0.3; + position: absolute; + top: 1px; + right: 5px; + } + + &.active + { + background-color: rgba( #0094ff, 0.5 ); + transition: all 0.1s ease-out; + width: 100px; + height: 100px; + + .slot-number + { + color: #275576; + } + + .item-name + { + opacity: 1; + transition: all 0.1s ease-out; + font-size: 13px; + } + } +} diff --git a/code/ui/SpawnMenu.cs b/code/ui/SpawnMenu.cs new file mode 100644 index 0000000..dde48a8 --- /dev/null +++ b/code/ui/SpawnMenu.cs @@ -0,0 +1,78 @@ + +using Sandbox; +using Sandbox.UI; +using Sandbox.UI.Construct; +using System; +using System.Reflection.Metadata; +using System.Threading.Tasks; + +[Library] +public partial class SpawnMenu : Panel +{ + public static SpawnMenu Instance; + + public SpawnMenu() + { + Instance = this; + + StyleSheet.Load( "/ui/SpawnMenu.scss" ); + + var left = Add.Panel( "left" ); + { + var tabs = left.AddChild(); + tabs.AddClass( "tabs" ); + + var body = left.Add.Panel( "body" ); + + { + var props = body.AddChild(); + tabs.SelectedButton = tabs.AddButtonActive( "Props", ( b ) => props.SetClass( "active", b ) ); + + var ents = body.AddChild(); + tabs.AddButtonActive( "Entities", ( b ) => ents.SetClass( "active", b ) ); + } + } + + var right = Add.Panel( "right" ); + { + var tabs = right.Add.Panel( "tabs" ); + { + tabs.Add.Button( "Tools" ).AddClass( "active" ); + tabs.Add.Button( "Utility" ); + } + var body = right.Add.Panel( "body" ); + { + var list = body.Add.Panel( "toollist" ); + { + foreach ( var entry in Library.GetAllAttributes() ) + { + if ( entry.Title == "BaseTool" ) + continue; + + var button = list.Add.Button( entry.Title ); + button.SetClass( "active", entry.Name == ConsoleSystem.GetValue( "tool_current" ) ); + + button.AddEventListener( "onclick", () => + { + ConsoleSystem.Run( "tool_current", entry.Name ); + ConsoleSystem.Run( "inventory_current", "weapon_tool" ); + + foreach ( var child in list.Children ) + child.SetClass( "active", child == button ); + } ); + } + } + body.Add.Panel( "inspector" ); + } + } + + } + + public override void Tick() + { + base.Tick(); + + Parent.SetClass( "spawnmenuopen", Input.Down( InputButton.Menu ) ); + } + +} diff --git a/code/ui/SpawnMenu.scss b/code/ui/SpawnMenu.scss new file mode 100644 index 0000000..233ec9c --- /dev/null +++ b/code/ui/SpawnMenu.scss @@ -0,0 +1,177 @@ +spawnmenu { + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + margin: 100px; + margin-bottom: 200px; + border-radius: 5px; + pointer-events: none; + transition: all 0.1s ease-out; + position: absolute; + font-family: poppins; + opacity: 0; + + .tabs { + font-size: 16px; + flex-shrink: 0; + margin-bottom: 4px; + padding-left: 10px; + + button { + padding: 6px 5px; + margin-right: 10px; + font-size: 13px; + align-items: center; + font-weight: 600; + opacity: 0.2; + color: #fff; + cursor: pointer; + //border-radius: 5px; + label { + text-shadow: 1px 1px 4px rgba( black, 0.2 ); + } + + &:hover { + opacity: 1; + } + + &.active { + transform: scale( 1 ); + opacity: 1; + color: #fff; + // background-color: #fff;//( #3273eb, 0.2 ); + //color: #3273eb; + } + } + } + + .left { + flex-grow: 1; + flex-direction: column; + position: relative; + left: -500px; + transition: all 0.3s ease-in; + + .body { + border-radius: 10px; + background-color: rgba( #111, 0.7 ); + flex-grow: 1; + } + } + + .right { + margin-left: 10px; + flex-direction: column; + min-width: 450px; + flex-shrink: 0; + left: 500px; + transition: all 0.3s ease-in; + + .body { + // background-color: rgba( #333, 0.95 ); + flex-grow: 1; + border-radius: 10px; + background-color: rgba( #111, 0.7 ); + padding: 10px; + + + .toollist { + flex-direction: column; + width: 150px; + color: #aaa; + + button { + padding: 3px 10px; + font-size: 11px; + transition: all 0.2s ease-out; + cursor: pointer; + + &:hover { + //border-left: 3px solid #3273eb; + color: white; + } + + &.active { + //border-left: 5px solid #3273eb; + color: white; + } + } + } + + .inspector { + //background-color: rgba( #111, 0.7 ); + flex-grow: 1; + margin-left: 5px; + border-radius: 0 3px 3px 0; + } + } + } +} + + +.spawnmenuopen spawnmenu { + transition: all 0.2s ease-out; + pointer-events: all; + opacity: 1; + transform: scale( 1 ); + + .left, .right { + left: 0; + transition: all 0.1s ease-out; + } +} + +.spawnpage { + display: none; + + &.active { + display: flex; + opacity: 1; + } + + .canvas { + overflow: scroll; + width: 100%; + flex-grow: 1; + flex-wrap: wrap; + justify-content: space-between; + margin: 10px; + border-radius: 5px; + + .cell { + padding: 5px; + } + + .icon { + border-radius: 16px; + color: rgba( #fff, 0.5 ); + font-size: 12px; + text-align: center; + cursor: pointer; + width: 100%; + height: 100%; + background-position: center; + background-size: cover; + background-color: rgba(black, 0.1); + background-image: url( /entity/spawnicon.png ); + + label { + font-size: 9px; + position: absolute; + bottom: -17px; + left: 0; + right: 0; + pointer-events: none; + } + + &:hover { + color: #fff; + background-color: rgba(black, 0.4); + } + + &:active { + } + } + } +} diff --git a/code/ui/left/EntityList.cs b/code/ui/left/EntityList.cs new file mode 100644 index 0000000..4a4d4a9 --- /dev/null +++ b/code/ui/left/EntityList.cs @@ -0,0 +1,38 @@ +using Sandbox; +using Sandbox.UI; +using Sandbox.UI.Construct; +using Sandbox.UI.Tests; +using System.Linq; + +[Library] +public partial class EntityList : Panel +{ + VirtualScrollPanel Canvas; + + public EntityList() + { + AddClass( "spawnpage" ); + AddChild( out Canvas, "canvas" ); + + Canvas.Layout.AutoColumns = true; + Canvas.Layout.ItemSize = new Vector2( 100, 100 ); + Canvas.OnCreateCell = ( cell, data ) => + { + var entry = (LibraryAttribute)data; + var btn = cell.Add.Button( entry.Title ); + btn.AddClass( "icon" ); + btn.AddEventListener( "onclick", () => ConsoleSystem.Run( "spawn_entity", entry.Name ) ); + btn.Style.Background = new PanelBackground + { + Texture = Texture.Load( $"/entity/{entry.Name}.png", false ) + }; + }; + + var ents = Library.GetAllAttributes().Where( x => x.Spawnable ).OrderBy( x => x.Title ).ToArray(); + + foreach ( var entry in ents ) + { + Canvas.AddItem( entry ); + } + } +} diff --git a/code/ui/left/SpawnList.cs b/code/ui/left/SpawnList.cs new file mode 100644 index 0000000..2fedf03 --- /dev/null +++ b/code/ui/left/SpawnList.cs @@ -0,0 +1,37 @@ +using Sandbox; +using Sandbox.UI; +using Sandbox.UI.Tests; + +[Library] +public partial class SpawnList : Panel +{ + VirtualScrollPanel Canvas; + + public SpawnList() + { + AddClass( "spawnpage" ); + AddChild( out Canvas, "canvas" ); + + Canvas.Layout.AutoColumns = true; + Canvas.Layout.ItemSize = new Vector2( 100, 100 ); + Canvas.OnCreateCell = ( cell, data ) => + { + var file = (string)data; + var panel = cell.Add.Panel( "icon" ); + panel.AddEventListener( "onclick", () => ConsoleSystem.Run( "spawn", "models/" + file ) ); + panel.Style.Background = new PanelBackground + { + Texture = Texture.Load( $"/models/{file}_c.png", false ) + }; + }; + + foreach ( var file in FileSystem.Mounted.FindFile( "models", "*.vmdl_c.png", true ) ) + { + if ( string.IsNullOrWhiteSpace( file ) ) continue; + if ( file.Contains( "_lod0" ) ) continue; + if ( file.Contains( "clothes" ) ) continue; + + Canvas.AddItem( file.Remove( file.Length - 6 ) ); + } + } +} diff --git a/code/weapons/Flashlight.cs b/code/weapons/Flashlight.cs new file mode 100644 index 0000000..a1b2b91 --- /dev/null +++ b/code/weapons/Flashlight.cs @@ -0,0 +1,211 @@ +using Sandbox; + +[Library( "weapon_flashlight", Title = "Flashlight", Spawnable = true )] +partial class Flashlight : Weapon +{ + public override string ViewModelPath => "weapons/rust_flashlight/v_rust_flashlight.vmdl"; + public override float SecondaryRate => 2.0f; + + protected virtual Vector3 LightOffset => Vector3.Forward * 10; + + private SpotLightEntity worldLight; + private SpotLightEntity viewLight; + + [Net, Local, Predicted] + private bool LightEnabled { get; set; } = true; + + TimeSince timeSinceLightToggled; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pistol/rust_pistol.vmdl" ); + + worldLight = CreateLight(); + worldLight.SetParent( this, "slide", new Transform( LightOffset ) ); + worldLight.EnableHideInFirstPerson = true; + worldLight.Enabled = false; + } + + public override void CreateViewModel() + { + base.CreateViewModel(); + + viewLight = CreateLight(); + viewLight.SetParent( ViewModelEntity, "light", new Transform( LightOffset ) ); + viewLight.EnableViewmodelRendering = true; + viewLight.Enabled = LightEnabled; + } + + private SpotLightEntity CreateLight() + { + var light = new SpotLightEntity + { + Enabled = true, + DynamicShadows = true, + Range = 512, + Falloff = 1.0f, + LinearAttenuation = 0.0f, + QuadraticAttenuation = 1.0f, + Brightness = 2, + Color = Color.White, + InnerConeAngle = 20, + OuterConeAngle = 40, + FogStength = 1.0f, + Owner = Owner, + }; + + light.UseFog(); + + return light; + } + + public override void Simulate( Client cl ) + { + if ( cl == null ) + return; + + base.Simulate( cl ); + + bool toggle = Input.Pressed( InputButton.Flashlight ) || Input.Pressed( InputButton.Attack1 ); + + if ( timeSinceLightToggled > 0.1f && toggle ) + { + LightEnabled = !LightEnabled; + + PlaySound( LightEnabled ? "flashlight-on" : "flashlight-off" ); + + if ( worldLight.IsValid() ) + { + worldLight.Enabled = LightEnabled; + } + + if ( viewLight.IsValid() ) + { + viewLight.Enabled = LightEnabled; + } + + timeSinceLightToggled = 0; + } + } + + public override bool CanReload() + { + return false; + } + + public override void AttackSecondary() + { + if ( MeleeAttack() ) + { + OnMeleeHit(); + } + else + { + OnMeleeMiss(); + } + + PlaySound( "rust_flashlight.attack" ); + } + + private bool MeleeAttack() + { + var forward = Owner.EyeRot.Forward; + forward = forward.Normal; + + bool hit = false; + + foreach ( var tr in TraceBullet( Owner.EyePos, Owner.EyePos + forward * 80, 20.0f ) ) + { + if ( !tr.Entity.IsValid() ) continue; + + tr.Surface.DoBulletImpact( tr ); + + hit = true; + + if ( !IsServer ) continue; + + using ( Prediction.Off() ) + { + var damageInfo = DamageInfo.FromBullet( tr.EndPos, forward * 100, 25 ) + .UsingTraceResult( tr ) + .WithAttacker( Owner ) + .WithWeapon( this ); + + tr.Entity.TakeDamage( damageInfo ); + } + } + + return hit; + } + + [ClientRpc] + private void OnMeleeMiss() + { + Host.AssertClient(); + + if ( IsLocalPawn ) + { + _ = new Sandbox.ScreenShake.Perlin(); + } + + ViewModelEntity?.SetAnimBool( "attack", true ); + } + + [ClientRpc] + private void OnMeleeHit() + { + Host.AssertClient(); + + if ( IsLocalPawn ) + { + _ = new Sandbox.ScreenShake.Perlin( 1.0f, 1.0f, 3.0f ); + } + + ViewModelEntity?.SetAnimBool( "attack_hit", true ); + } + + private void Activate() + { + if ( worldLight.IsValid() ) + { + worldLight.Enabled = LightEnabled; + } + } + + private void Deactivate() + { + if ( worldLight.IsValid() ) + { + worldLight.Enabled = false; + } + } + + public override void ActiveStart( Entity ent ) + { + base.ActiveStart( ent ); + + if ( IsServer ) + { + Activate(); + } + } + + public override void ActiveEnd( Entity ent, bool dropped ) + { + base.ActiveEnd( ent, dropped ); + + if ( IsServer ) + { + if ( dropped ) + { + Activate(); + } + else + { + Deactivate(); + } + } + } +} diff --git a/code/weapons/Pistol.cs b/code/weapons/Pistol.cs new file mode 100644 index 0000000..e7bbc0a --- /dev/null +++ b/code/weapons/Pistol.cs @@ -0,0 +1,62 @@ +using Sandbox; + +[Library( "weapon_pistol", Title = "Pistol", Spawnable = true )] +partial class Pistol : Weapon +{ + public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl"; + + public override float PrimaryRate => 15.0f; + public override float SecondaryRate => 1.0f; + + public TimeSince TimeSinceDischarge { get; set; } + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pistol/rust_pistol.vmdl" ); + } + + public override bool CanPrimaryAttack() + { + return base.CanPrimaryAttack() && Input.Pressed( InputButton.Attack1 ); + } + + public override void AttackPrimary() + { + TimeSincePrimaryAttack = 0; + TimeSinceSecondaryAttack = 0; + + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + + ShootEffects(); + PlaySound( "rust_pistol.shoot" ); + ShootBullet( 0.05f, 1.5f, 9.0f, 3.0f ); + } + + private void Discharge() + { + if ( TimeSinceDischarge < 0.5f ) + return; + + TimeSinceDischarge = 0; + + var muzzle = GetAttachment( "muzzle" ) ?? default; + var pos = muzzle.Position; + var rot = muzzle.Rotation; + + ShootEffects(); + PlaySound( "rust_pistol.shoot" ); + ShootBullet( pos, rot.Forward, 0.05f, 1.5f, 9.0f, 3.0f ); + + ApplyAbsoluteImpulse( rot.Backward * 200.0f ); + } + + protected override void OnPhysicsCollision( CollisionEventData eventData ) + { + if ( eventData.Speed > 500.0f ) + { + Discharge(); + } + } +} diff --git a/code/weapons/SMG.cs b/code/weapons/SMG.cs new file mode 100644 index 0000000..e469f01 --- /dev/null +++ b/code/weapons/SMG.cs @@ -0,0 +1,66 @@ +using Sandbox; + +[Library( "weapon_smg", Title = "SMG", Spawnable = true )] +partial class SMG : Weapon +{ + public override string ViewModelPath => "weapons/rust_smg/v_rust_smg.vmdl"; + + public override float PrimaryRate => 15.0f; + public override float SecondaryRate => 1.0f; + public override float ReloadTime => 5.0f; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_smg/rust_smg.vmdl" ); + } + + public override void AttackPrimary() + { + TimeSincePrimaryAttack = 0; + TimeSinceSecondaryAttack = 0; + + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + + // + // Tell the clients to play the shoot effects + // + ShootEffects(); + PlaySound( "rust_smg.shoot" ); + + // + // Shoot the bullets + // + ShootBullet( 0.1f, 1.5f, 5.0f, 3.0f ); + } + + public override void AttackSecondary() + { + // Grenade lob + } + + [ClientRpc] + protected override void ShootEffects() + { + Host.AssertClient(); + + Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" ); + Particles.Create( "particles/pistol_ejectbrass.vpcf", EffectEntity, "ejection_point" ); + + if ( Owner == Local.Pawn ) + { + new Sandbox.ScreenShake.Perlin( 0.5f, 4.0f, 1.0f, 0.5f ); + } + + ViewModelEntity?.SetAnimBool( "fire", true ); + CrosshairPanel?.CreateEvent( "fire" ); + } + + public override void SimulateAnimator( PawnAnimator anim ) + { + anim.SetParam( "holdtype", 2 ); // TODO this is shit + anim.SetParam( "aimat_weight", 1.0f ); + } + +} diff --git a/code/weapons/Shotgun.cs b/code/weapons/Shotgun.cs new file mode 100644 index 0000000..0de73d6 --- /dev/null +++ b/code/weapons/Shotgun.cs @@ -0,0 +1,111 @@ +using Sandbox; + +[Library( "weapon_shotgun", Title = "Shotgun", Spawnable = true )] +partial class Shotgun : Weapon +{ + public override string ViewModelPath => "weapons/rust_pumpshotgun/v_rust_pumpshotgun.vmdl"; + public override float PrimaryRate => 1; + public override float SecondaryRate => 1; + public override float ReloadTime => 0.5f; + + public override void Spawn() + { + base.Spawn(); + + SetModel( "weapons/rust_pumpshotgun/rust_pumpshotgun.vmdl" ); + } + + public override void AttackPrimary() + { + TimeSincePrimaryAttack = 0; + TimeSinceSecondaryAttack = 0; + + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + + // + // Tell the clients to play the shoot effects + // + ShootEffects(); + PlaySound( "rust_pumpshotgun.shoot" ); + + // + // Shoot the bullets + // + ShootBullets( 10, 0.1f, 10.0f, 9.0f, 3.0f ); + } + + public override void AttackSecondary() + { + TimeSincePrimaryAttack = -0.5f; + TimeSinceSecondaryAttack = -0.5f; + + (Owner as AnimEntity)?.SetAnimBool( "b_attack", true ); + + // + // Tell the clients to play the shoot effects + // + DoubleShootEffects(); + PlaySound( "rust_pumpshotgun.shootdouble" ); + + // + // Shoot the bullets + // + ShootBullets( 20, 0.4f, 20.0f, 8.0f, 3.0f ); + } + + [ClientRpc] + protected override void ShootEffects() + { + Host.AssertClient(); + + Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" ); + Particles.Create( "particles/pistol_ejectbrass.vpcf", EffectEntity, "ejection_point" ); + + ViewModelEntity?.SetAnimBool( "fire", true ); + + if ( IsLocalPawn ) + { + new Sandbox.ScreenShake.Perlin( 1.0f, 1.5f, 2.0f ); + } + + CrosshairPanel?.CreateEvent( "fire" ); + } + + [ClientRpc] + protected virtual void DoubleShootEffects() + { + Host.AssertClient(); + + Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" ); + + ViewModelEntity?.SetAnimBool( "fire_double", true ); + CrosshairPanel?.CreateEvent( "fire" ); + + if ( IsLocalPawn ) + { + new Sandbox.ScreenShake.Perlin( 3.0f, 3.0f, 3.0f ); + } + } + + public override void OnReloadFinish() + { + IsReloading = false; + + TimeSincePrimaryAttack = 0; + TimeSinceSecondaryAttack = 0; + + FinishReload(); + } + + [ClientRpc] + protected virtual void FinishReload() + { + ViewModelEntity?.SetAnimBool( "reload_finished", true ); + } + + public override void SimulateAnimator( PawnAnimator anim ) + { + anim.SetParam( "holdtype", 3 ); // TODO this is shit + anim.SetParam( "aimat_weight", 1.0f ); + } +}