From 1e5c6393a6b29eb00dbb8fb137d86647cb0c356b Mon Sep 17 00:00:00 2001 From: Leonardo Bishop Date: Fri, 28 Jul 2023 22:06:03 +0100 Subject: Add TryUnstuck and death overlay --- .sbproj | 12 +- code/Game.cs | 1 + code/pawn/Player.Use.cs | 117 +++++++++++++++ code/pawn/Player.cs | 62 ++++++-- code/pawn/PlayerAnimator.cs | 21 --- code/pawn/PlayerController.cs | 177 ---------------------- code/pawn/PlayerInventory.cs | 126 ---------------- code/pawn/PlayerSpectator.cs | 48 ------ code/pawn/component/PlayerAnimator.cs | 21 +++ code/pawn/component/PlayerInventory.cs | 126 ++++++++++++++++ code/pawn/component/PlayerSpectator.cs | 48 ++++++ code/pawn/component/movement/BaseController.cs | 9 ++ code/pawn/component/movement/PlayerController.cs | 182 +++++++++++++++++++++++ code/phase/AssignPhase.cs | 7 +- code/phase/PlayPhase.cs | 8 +- code/phase/WaitPhase.cs | 31 ++++ code/ui/Hud.razor | 1 + code/ui/overlay/DeathOverlay.Network.cs | 18 +++ code/ui/overlay/DeathOverlay.razor | 89 +++++++++++ code/weapon/Weapon.cs | 2 + 20 files changed, 714 insertions(+), 392 deletions(-) create mode 100644 code/pawn/Player.Use.cs delete mode 100644 code/pawn/PlayerAnimator.cs delete mode 100644 code/pawn/PlayerController.cs delete mode 100644 code/pawn/PlayerInventory.cs delete mode 100644 code/pawn/PlayerSpectator.cs create mode 100644 code/pawn/component/PlayerAnimator.cs create mode 100644 code/pawn/component/PlayerInventory.cs create mode 100644 code/pawn/component/PlayerSpectator.cs create mode 100644 code/pawn/component/movement/BaseController.cs create mode 100644 code/pawn/component/movement/PlayerController.cs create mode 100644 code/ui/overlay/DeathOverlay.Network.cs create mode 100644 code/ui/overlay/DeathOverlay.razor diff --git a/.sbproj b/.sbproj index d5b5ec0..6172332 100644 --- a/.sbproj +++ b/.sbproj @@ -34,14 +34,14 @@ "TickRate": 50, "LaunchConfigs": [ { - "Name": "Murder", + "Name": "Murder (Clue)", "GameIdent": "leonardobishop.murder#local", "MapName": "thieves.clue", - "MaxPlayers": 3, + "MaxPlayers": 6, "GameSettings": {}, "Addons": "", - "PreLaunchCommand": "sv_lan 1;", - "PostLaunchCommand": "sv_lan 1;" + "PreLaunchCommand": "sv_lan 1; sv_cheats 1;", + "PostLaunchCommand": "sv_lan 1; sv_cheats 1;" }, { "Name": "Murder (Square)", @@ -50,8 +50,8 @@ "MaxPlayers": 3, "GameSettings": {}, "Addons": "", - "PreLaunchCommand": "sv_lan 1;", - "PostLaunchCommand": "sv_lan 1;" + "PreLaunchCommand": "sv_lan 1; sv_cheats 1;", + "PostLaunchCommand": "sv_lan 1; sv_cheats 1;" } ], "ControlModes": {}, diff --git a/code/Game.cs b/code/Game.cs index 24fd79d..ad04fc8 100644 --- a/code/Game.cs +++ b/code/Game.cs @@ -56,6 +56,7 @@ public partial class MurderGame : Sandbox.GameManager // Create a pawn for this client to play with var pawn = new Player(); client.Pawn = pawn; + pawn.Spawn(); var spawnpoints = Entity.All.OfType(); var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault(); diff --git a/code/pawn/Player.Use.cs b/code/pawn/Player.Use.cs new file mode 100644 index 0000000..3f35052 --- /dev/null +++ b/code/pawn/Player.Use.cs @@ -0,0 +1,117 @@ +using Sandbox; + +namespace MurderGame; + +public partial class Player +{ + public Entity Using { get; protected set; } + + protected virtual void TickPlayerUse() + { + // This is serverside only + if ( !Game.IsServer ) return; + + // Turn prediction off + using ( Prediction.Off() ) + { + if ( Input.Pressed( "use" ) ) + { + Using = FindUsable(); + + if ( Using == null ) + { + UseFail(); + return; + } + } + + if ( !Input.Down( "use" ) ) + { + StopUsing(); + return; + } + + if ( !Using.IsValid() ) + return; + + // If we move too far away or something we should probably ClearUse()? + + // + // If use returns true then we can keep using it + // + if ( Using is IUse use && use.OnUse( this ) ) + return; + + StopUsing(); + } + } + + /// + /// Player tried to use something but there was nothing there. + /// Tradition is to give a disappointed boop. + /// + protected virtual void UseFail() + { + PlaySound( "player_use_fail" ); + } + + /// + /// If we're using an entity, stop using it + /// + protected virtual void StopUsing() + { + Using = null; + } + + /// + /// Returns if the entity is a valid usable entity + /// + protected bool IsValidUseEntity( Entity e ) + { + if ( e == null ) return false; + if ( e is not IUse use ) return false; + if ( !use.IsUsable( this ) ) return false; + + return true; + } + + /// + /// Find a usable entity for this player to use + /// + protected virtual Entity FindUsable() + { + // First try a direct 0 width line + var tr = Trace.Ray( EyePosition, EyePosition + EyeRotation.Forward * 85 ) + .Ignore( this ) + .Run(); + + // See if any of the parent entities are usable if we ain't. + var ent = tr.Entity; + while ( ent.IsValid() && !IsValidUseEntity( ent ) ) + { + ent = ent.Parent; + } + + // Nothing found, try a wider search + if ( !IsValidUseEntity( ent ) ) + { + tr = Trace.Ray( EyePosition, EyePosition + EyeRotation.Forward * 85 ) + .Radius( 2 ) + .Ignore( this ) + .Run(); + + // See if any of the parent entities are usable if we ain't. + ent = tr.Entity; + while ( ent.IsValid() && !IsValidUseEntity( ent ) ) + { + ent = ent.Parent; + } + } + + // Still no good? Bail. + if ( !IsValidUseEntity( ent ) ) return null; + + return ent; + } +} + diff --git a/code/pawn/Player.cs b/code/pawn/Player.cs index 68d383d..cd2f83a 100644 --- a/code/pawn/Player.cs +++ b/code/pawn/Player.cs @@ -36,8 +36,8 @@ public partial class Player : AnimatedEntity { get => new ( - new Vector3( -12, -12, 0 ), - new Vector3( 12, 12, 64 ) + new Vector3( -16, -16, 0 ), + new Vector3( 16, 16, 72 ) ); } @@ -49,6 +49,7 @@ public partial class Player : AnimatedEntity [BindComponent] public PlayerInventory Inventory { get; } [BindComponent] public PlayerSpectator Spectator { get; } + [Net] public Ragdoll PlayerRagdoll { get; set; } public ClothingContainer PlayerClothingContainer { get; set; } @@ -57,6 +58,9 @@ public partial class Player : AnimatedEntity public override Ray AimRay => new Ray( EyePosition, EyeRotation.Forward ); + [Net, Predicted] + public TimeSince TimeSinceDeath { get; set; } = 0; + public override void Spawn() { SetModel( "models/citizen/citizen.vmdl" ); @@ -66,21 +70,29 @@ public partial class Player : AnimatedEntity EnableDrawing = false; EnableHideInFirstPerson = true; EnableShadowInFirstPerson = true; + SetupPhysicsFromAABB( PhysicsMotionType.Keyframed, Hull.Mins, Hull.Maxs ); EnableSolidCollisions = false; + + Health = 0f; + LifeState = LifeState.Dead; } public void Respawn() { + DeleteRagdoll(); Tags.Add( "livingplayer" ); + EnableAllCollisions = true; EnableDrawing = true; - Components.Remove( Spectator ); + + Components.RemoveAll(); Components.Create(); Components.Create(); Components.Create(); + Health = 100f; - DeleteRagdoll(); + LifeState = LifeState.Alive; } public void Cleanup() @@ -101,19 +113,27 @@ public partial class Player : AnimatedEntity public void DisablePlayer() { - EnableAllCollisions = false; - LifeState = LifeState.Dead; Tags.Remove( "livingplayer" ); + + EnableAllCollisions = false; + EnableDrawing = false; + Inventory?.Clear(); Components.RemoveAll(); - EnableDrawing = false; + + LifeState = LifeState.Dead; } public override void OnKilled() { + TimeSinceDeath = 0; + Inventory?.SpillContents(EyePosition, new Vector3(0,0,0)); + DisablePlayer(); + Event.Run( MurderEvent.Kill, LastAttacker, this ); + var ragdoll = new Ragdoll(); ragdoll.Position = Position; ragdoll.Rotation = Rotation; @@ -121,7 +141,8 @@ public partial class Player : AnimatedEntity ragdoll.PhysicsGroup.AddVelocity(LastAttackForce / 100); PlayerClothingContainer.DressEntity( ragdoll ); PlayerRagdoll = ragdoll; - Components.Create(); + + DeathOverlay.Show( To.Single( Client ) ); } public override void TakeDamage( DamageInfo info ) @@ -151,11 +172,22 @@ public partial class Player : AnimatedEntity public override void Simulate( IClient cl ) { SimulateRotation(); - Controller?.Simulate( cl ); + TickPlayerUse(); + + Controller?.Simulate( this ); Animator?.Simulate(); Inventory?.Simulate( cl ); Spectator?.Simulate(); + EyeLocalPosition = Vector3.Up * (64f * Scale); + + if (Game.IsServer && Spectator == null && LifeState == LifeState.Dead && TimeSinceDeath > 3) + { + Log.Info( "Spectator created" ); + DeathOverlay.Hide( To.Single( Client ) ); + Components.Create(); + } + } public override void BuildInput() @@ -186,13 +218,23 @@ public partial class Player : AnimatedEntity Spectator.FrameSimulate(this); return; } + else if (Controller != null) + { + //TOOD move below logic to controller + } SimulateRotation(); Camera.Rotation = ViewAngles.ToRotation(); Camera.FieldOfView = Screen.CreateVerticalFieldOfView( Game.Preferences.FieldOfView ); Camera.FirstPersonViewer = this; - Camera.Position = EyePosition; + if (PlayerRagdoll != null && PlayerRagdoll.IsValid) + { + Camera.Position = PlayerRagdoll.Position; + } else + { + Camera.Position = EyePosition; + } } public TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f ) diff --git a/code/pawn/PlayerAnimator.cs b/code/pawn/PlayerAnimator.cs deleted file mode 100644 index 4cd4e3f..0000000 --- a/code/pawn/PlayerAnimator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Sandbox; -using System; - -namespace MurderGame; - -public class PlayerAnimator : EntityComponent, ISingletonComponent -{ - public void Simulate() - { - var helper = new CitizenAnimationHelper( Entity ); - helper.WithVelocity( Entity.Velocity ); - helper.WithLookAt( Entity.EyePosition + Entity.EyeRotation.Forward * 100 ); - helper.HoldType = CitizenAnimationHelper.HoldTypes.None; - helper.IsGrounded = Entity.GroundEntity.IsValid(); - - if ( Entity.Controller.HasEvent( "jump" ) ) - { - helper.TriggerJump(); - } - } -} diff --git a/code/pawn/PlayerController.cs b/code/pawn/PlayerController.cs deleted file mode 100644 index d638bb0..0000000 --- a/code/pawn/PlayerController.cs +++ /dev/null @@ -1,177 +0,0 @@ -using Sandbox; -using System; -using System.Collections.Generic; - -namespace MurderGame; - -public class PlayerController : EntityComponent -{ - public int StepSize => 12; - public int GroundAngle => 45; - public int JumpSpeed => 300; - public float Gravity => 800f; - public float SpeedMultiplier { get; set; } = 1; - - HashSet ControllerEvents = new( StringComparer.OrdinalIgnoreCase ); - - bool Grounded => Entity.GroundEntity.IsValid(); - - public void Simulate( IClient cl ) - { - ControllerEvents.Clear(); - - var movement = Entity.InputDirection.Normal; - var angles = Entity.ViewAngles.WithPitch( 0 ); - var moveVector = Rotation.From( angles ) * movement * 320f; - var groundEntity = CheckForGround(); - var team = Entity.CurrentTeam; - - if ( groundEntity.IsValid() ) - { - if ( !Grounded ) - { - Entity.Velocity = Entity.Velocity.WithZ( 0 ); - AddEvent( "grounded" ); - } - var sprintMultiplier = TeamOperations.CanSprint( team ) ? (Input.Down( "run" ) ? 2.5f : 1f) : 1f; - - Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, SpeedMultiplier * 200.0f * sprintMultiplier, 7.5f ); - Entity.Velocity = ApplyFriction( Entity.Velocity, 4.0f ); - } - else - { - Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, SpeedMultiplier * 100, 20f ); - Entity.Velocity += Vector3.Down * Gravity * Time.Delta * (1/SpeedMultiplier); - } - - if ( Input.Pressed( "jump" ) ) - { - DoJump(); - } - - var mh = new MoveHelper( Entity.Position, Entity.Velocity ); - mh.Trace = mh.Trace.Size( Entity.Hull ).Ignore( Entity ); - - if ( mh.TryMoveWithStep( Time.Delta, StepSize ) > 0 ) - { - if ( Grounded ) - { - mh.Position = StayOnGround( mh.Position ); - } - Entity.Position = mh.Position; - Entity.Velocity = mh.Velocity; - } - - Entity.GroundEntity = groundEntity; - } - - void DoJump() - { - if ( Grounded ) - { - Entity.Velocity = ApplyJump( Entity.Velocity, "jump" ); - } - } - - Entity CheckForGround() - { - if ( Entity.Velocity.z > 100f ) - return null; - - var trace = Entity.TraceBBox( Entity.Position, Entity.Position + Vector3.Down, 2f ); - - if ( !trace.Hit ) - return null; - - if ( trace.Normal.Angle( Vector3.Up ) > GroundAngle ) - return null; - - return trace.Entity; - } - - Vector3 ApplyFriction( Vector3 input, float frictionAmount ) - { - float StopSpeed = 100.0f; - - var speed = input.Length; - if ( speed < 0.1f ) return input; - - // Bleed off some speed, but if we have less than the bleed - // threshold, bleed the threshold amount. - float control = (speed < StopSpeed) ? StopSpeed : speed; - - // Add the amount to the drop amount. - var drop = control * Time.Delta * frictionAmount; - - // scale the velocity - float newspeed = speed - drop; - if ( newspeed < 0 ) newspeed = 0; - if ( newspeed == speed ) return input; - - newspeed /= speed; - input *= newspeed; - - return input; - } - - Vector3 Accelerate( Vector3 input, Vector3 wishdir, float wishspeed, float speedLimit, float acceleration ) - { - if ( speedLimit > 0 && wishspeed > speedLimit ) - wishspeed = speedLimit; - - var currentspeed = input.Dot( wishdir ); - var addspeed = wishspeed - currentspeed; - - if ( addspeed <= 0 ) - return input; - - var accelspeed = acceleration * Time.Delta * wishspeed; - - if ( accelspeed > addspeed ) - accelspeed = addspeed; - - input += wishdir * accelspeed; - - return input; - } - - Vector3 ApplyJump( Vector3 input, string jumpType ) - { - AddEvent( jumpType ); - - return input + Vector3.Up * JumpSpeed; - } - - Vector3 StayOnGround( Vector3 position ) - { - var start = position + Vector3.Up * 2; - var end = position + Vector3.Down * StepSize; - - // See how far up we can go without getting stuck - var trace = Entity.TraceBBox( position, start ); - start = trace.EndPosition; - - // Now trace down from a known safe position - trace = Entity.TraceBBox( start, end ); - - if ( trace.Fraction <= 0 ) return position; - if ( trace.Fraction >= 1 ) return position; - if ( trace.StartedSolid ) return position; - if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return position; - - return trace.EndPosition; - } - - public bool HasEvent( string eventName ) - { - return ControllerEvents.Contains( eventName ); - } - - void AddEvent( string eventName ) - { - if ( HasEvent( eventName ) ) - return; - - ControllerEvents.Add( eventName ); - } -} diff --git a/code/pawn/PlayerInventory.cs b/code/pawn/PlayerInventory.cs deleted file mode 100644 index 55fa3ed..0000000 --- a/code/pawn/PlayerInventory.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Sandbox; -using System; - -namespace MurderGame; - -public partial class PlayerInventory : EntityComponent -{ - const int MIN_SLOT = 1; - const int MAX_SLOT = 2; - - const int UNARMED_SLOT = 1; - const int PRIMARY_SLOT = 2; - - [Net] - public Weapon PrimaryWeapon { get; private set; } - - [Net] - public int ActiveSlot { get; set; } - - [Net] - public bool AllowPickup { get; set; } = true; - - public Weapon GetCurrentWeapon() - { - return ActiveSlot switch - { - PRIMARY_SLOT => PrimaryWeapon, - _ => null, - }; - } - - public void SetPrimaryWeapon( Weapon weapon ) - { - PrimaryWeapon?.OnHolster(); - PrimaryWeapon?.Delete(); - PrimaryWeapon = weapon; - if (weapon != null) - { - weapon.ChangeOwner( Entity ); - } - if (ActiveSlot == PRIMARY_SLOT) - { - weapon?.OnEquip( Entity ); - } - } - - private void PrevSlot() - { - if (ActiveSlot > MIN_SLOT) - { - --ActiveSlot; - } - else - { - ActiveSlot = MAX_SLOT; - } - } - private void NextSlot() - { - if (ActiveSlot < MAX_SLOT) - { - ++ActiveSlot; - } - else - { - ActiveSlot = MIN_SLOT; - } - } - - public void Simulate(IClient cl) - { - var currentWeapon = GetCurrentWeapon(); - var currentSlot = ActiveSlot; - - if (Input.Released("SlotPrev")) - { - PrevSlot(); - } - else if (Input.Released("SlotNext")) - { - NextSlot(); - } - else if (Input.Down("Slot1")) - { - ActiveSlot = 1; - } - else if (Input.Down("Slot2")) - { - ActiveSlot = 2; - } - - if (ActiveSlot != currentSlot) - { - currentWeapon?.OnHolster(); - GetCurrentWeapon()?.OnEquip( Entity ); - } - GetCurrentWeapon()?.Simulate( cl ); - } - - public void Holster() - { - Weapon weapon = GetCurrentWeapon(); - weapon?.OnHolster(); - } - - public void Clear() - { - Holster(); - SetPrimaryWeapon( null ); - } - - public void SpillContents(Vector3 location, Vector3 velocity) - { - Holster(); - if (PrimaryWeapon is not null and Pistol ) - { - PrimaryWeapon.ChangeOwner( null ); - DroppedWeapon droppedWeapon = new( (Pistol)PrimaryWeapon ); - droppedWeapon.CopyFrom( PrimaryWeapon ); - droppedWeapon.Position = location; - droppedWeapon.Velocity = velocity; - } - Clear(); - } - -} diff --git a/code/pawn/PlayerSpectator.cs b/code/pawn/PlayerSpectator.cs deleted file mode 100644 index c468de0..0000000 --- a/code/pawn/PlayerSpectator.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Sandbox; -using System.Collections.Generic; -using System.Linq; - -namespace MurderGame; - -public class PlayerSpectator : EntityComponent -{ - public Player Target { get; set; } - - public void Simulate() - { - if (Target == null || !Target.IsValid() || Target.LifeState == LifeState.Dead) - { - var targets = GetTargets(); - if ( targets.Count == 0 ) - { - Target = null; - return; - } - var nextTarget = targets.First(); - Target = (Player)nextTarget.Pawn; - } - } - - public void FrameSimulate( Player player ) - { - if ( Target == null || !Target.IsValid() || Target.LifeState == LifeState.Dead ) return; - - // SimulateRotation(player); - Camera.Rotation = Target.EyeRotation; - Camera.FieldOfView = Screen.CreateVerticalFieldOfView( Game.Preferences.FieldOfView ); - - Camera.FirstPersonViewer = Target; - Camera.Position = Target.EyePosition; - } - - protected void SimulateRotation(Player player) - { - player.EyeRotation = Target.ViewAngles.ToRotation(); - player.Rotation = Target.ViewAngles.WithPitch( 0f ).ToRotation(); - } - - public List GetTargets() - { - return Game.Clients.Where(c => c.Pawn is Player player && player.CurrentTeam != Team.Spectator && player.LifeState == LifeState.Alive).ToList(); - } -} diff --git a/code/pawn/component/PlayerAnimator.cs b/code/pawn/component/PlayerAnimator.cs new file mode 100644 index 0000000..4cd4e3f --- /dev/null +++ b/code/pawn/component/PlayerAnimator.cs @@ -0,0 +1,21 @@ +using Sandbox; +using System; + +namespace MurderGame; + +public class PlayerAnimator : EntityComponent, ISingletonComponent +{ + public void Simulate() + { + var helper = new CitizenAnimationHelper( Entity ); + helper.WithVelocity( Entity.Velocity ); + helper.WithLookAt( Entity.EyePosition + Entity.EyeRotation.Forward * 100 ); + helper.HoldType = CitizenAnimationHelper.HoldTypes.None; + helper.IsGrounded = Entity.GroundEntity.IsValid(); + + if ( Entity.Controller.HasEvent( "jump" ) ) + { + helper.TriggerJump(); + } + } +} diff --git a/code/pawn/component/PlayerInventory.cs b/code/pawn/component/PlayerInventory.cs new file mode 100644 index 0000000..55fa3ed --- /dev/null +++ b/code/pawn/component/PlayerInventory.cs @@ -0,0 +1,126 @@ +using Sandbox; +using System; + +namespace MurderGame; + +public partial class PlayerInventory : EntityComponent +{ + const int MIN_SLOT = 1; + const int MAX_SLOT = 2; + + const int UNARMED_SLOT = 1; + const int PRIMARY_SLOT = 2; + + [Net] + public Weapon PrimaryWeapon { get; private set; } + + [Net] + public int ActiveSlot { get; set; } + + [Net] + public bool AllowPickup { get; set; } = true; + + public Weapon GetCurrentWeapon() + { + return ActiveSlot switch + { + PRIMARY_SLOT => PrimaryWeapon, + _ => null, + }; + } + + public void SetPrimaryWeapon( Weapon weapon ) + { + PrimaryWeapon?.OnHolster(); + PrimaryWeapon?.Delete(); + PrimaryWeapon = weapon; + if (weapon != null) + { + weapon.ChangeOwner( Entity ); + } + if (ActiveSlot == PRIMARY_SLOT) + { + weapon?.OnEquip( Entity ); + } + } + + private void PrevSlot() + { + if (ActiveSlot > MIN_SLOT) + { + --ActiveSlot; + } + else + { + ActiveSlot = MAX_SLOT; + } + } + private void NextSlot() + { + if (ActiveSlot < MAX_SLOT) + { + ++ActiveSlot; + } + else + { + ActiveSlot = MIN_SLOT; + } + } + + public void Simulate(IClient cl) + { + var currentWeapon = GetCurrentWeapon(); + var currentSlot = ActiveSlot; + + if (Input.Released("SlotPrev")) + { + PrevSlot(); + } + else if (Input.Released("SlotNext")) + { + NextSlot(); + } + else if (Input.Down("Slot1")) + { + ActiveSlot = 1; + } + else if (Input.Down("Slot2")) + { + ActiveSlot = 2; + } + + if (ActiveSlot != currentSlot) + { + currentWeapon?.OnHolster(); + GetCurrentWeapon()?.OnEquip( Entity ); + } + GetCurrentWeapon()?.Simulate( cl ); + } + + public void Holster() + { + Weapon weapon = GetCurrentWeapon(); + weapon?.OnHolster(); + } + + public void Clear() + { + Holster(); + SetPrimaryWeapon( null ); + } + + public void SpillContents(Vector3 location, Vector3 velocity) + { + Holster(); + if (PrimaryWeapon is not null and Pistol ) + { + PrimaryWeapon.ChangeOwner( null ); + DroppedWeapon droppedWeapon = new( (Pistol)PrimaryWeapon ); + droppedWeapon.CopyFrom( PrimaryWeapon ); + droppedWeapon.Position = location; + droppedWeapon.Velocity = velocity; + } + Clear(); + } + +} diff --git a/code/pawn/component/PlayerSpectator.cs b/code/pawn/component/PlayerSpectator.cs new file mode 100644 index 0000000..c468de0 --- /dev/null +++ b/code/pawn/component/PlayerSpectator.cs @@ -0,0 +1,48 @@ +using Sandbox; +using System.Collections.Generic; +using System.Linq; + +namespace MurderGame; + +public class PlayerSpectator : EntityComponent +{ + public Player Target { get; set; } + + public void Simulate() + { + if (Target == null || !Target.IsValid() || Target.LifeState == LifeState.Dead) + { + var targets = GetTargets(); + if ( targets.Count == 0 ) + { + Target = null; + return; + } + var nextTarget = targets.First(); + Target = (Player)nextTarget.Pawn; + } + } + + public void FrameSimulate( Player player ) + { + if ( Target == null || !Target.IsValid() || Target.LifeState == LifeState.Dead ) return; + + // SimulateRotation(player); + Camera.Rotation = Target.EyeRotation; + Camera.FieldOfView = Screen.CreateVerticalFieldOfView( Game.Preferences.FieldOfView ); + + Camera.FirstPersonViewer = Target; + Camera.Position = Target.EyePosition; + } + + protected void SimulateRotation(Player player) + { + player.EyeRotation = Target.ViewAngles.ToRotation(); + player.Rotation = Target.ViewAngles.WithPitch( 0f ).ToRotation(); + } + + public List GetTargets() + { + return Game.Clients.Where(c => c.Pawn is Player player && player.CurrentTeam != Team.Spectator && player.LifeState == LifeState.Alive).ToList(); + } +} diff --git a/code/pawn/component/movement/BaseController.cs b/code/pawn/component/movement/BaseController.cs new file mode 100644 index 0000000..0b92c75 --- /dev/null +++ b/code/pawn/component/movement/BaseController.cs @@ -0,0 +1,9 @@ +using Sandbox; + +namespace MurderGame; + +//TODO make spectatro a controller +public class BaseController : EntityComponent +{ + public Player Player { get; set; } +} diff --git a/code/pawn/component/movement/PlayerController.cs b/code/pawn/component/movement/PlayerController.cs new file mode 100644 index 0000000..5df9a33 --- /dev/null +++ b/code/pawn/component/movement/PlayerController.cs @@ -0,0 +1,182 @@ +using Sandbox; +using System; +using System.Collections.Generic; + +namespace MurderGame; + +public class PlayerController : EntityComponent +{ + public int StepSize => 24; + public int GroundAngle => 45; + public int JumpSpeed => 300; + public float Gravity => 800f; + public float SpeedMultiplier { get; set; } = 1; + + HashSet ControllerEvents = new( StringComparer.OrdinalIgnoreCase ); + + bool Grounded => Entity.GroundEntity.IsValid(); + + public void Simulate( Player player ) + { + ControllerEvents.Clear(); + + var movement = Entity.InputDirection.Normal; + var angles = Entity.ViewAngles.WithPitch( 0 ); + var moveVector = Rotation.From( angles ) * movement * 320f; + var groundEntity = CheckForGround(); + var team = Entity.CurrentTeam; + + if ( groundEntity.IsValid() ) + { + if ( !Grounded ) + { + Entity.Velocity = Entity.Velocity.WithZ( 0 ); + AddEvent( "grounded" ); + } + var sprintMultiplier = TeamOperations.CanSprint( team ) ? (Input.Down( "run" ) ? 2.5f : 1f) : 1f; + + Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, SpeedMultiplier * 200.0f * sprintMultiplier, 7.5f ); + Entity.Velocity = ApplyFriction( Entity.Velocity, 4.0f ); + } + else + { + Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, SpeedMultiplier * 100, 20f ); + Entity.Velocity += Vector3.Down * Gravity * Time.Delta * (1/SpeedMultiplier); + } + + if ( Input.Pressed( "jump" ) ) + { + DoJump(); + } + + var mh = new MoveHelper( Entity.Position, Entity.Velocity ); + mh.Trace = mh.Trace.Size( Entity.Hull ).Ignore( Entity ); + + if (mh.TryUnstuck()) + { + Entity.Position = mh.Position; + Entity.Velocity = mh.Velocity; + } + + if ( mh.TryMoveWithStep( Time.Delta, StepSize ) > 0 ) + { + if ( Grounded ) + { + mh.Position = StayOnGround( mh.Position ); + } + Entity.Position = mh.Position; + Entity.Velocity = mh.Velocity; + } + Entity.GroundEntity = groundEntity; + } + + void DoJump() + { + if ( Grounded ) + { + Entity.Velocity = ApplyJump( Entity.Velocity, "jump" ); + } + } + + Entity CheckForGround() + { + if ( Entity.Velocity.z > 100f ) + return null; + + var trace = Entity.TraceBBox( Entity.Position, Entity.Position + Vector3.Down, 2f ); + + if ( !trace.Hit ) + return null; + + if ( trace.Normal.Angle( Vector3.Up ) > GroundAngle ) + return null; + + return trace.Entity; + } + + Vector3 ApplyFriction( Vector3 input, float frictionAmount ) + { + float StopSpeed = 100.0f; + + var speed = input.Length; + if ( speed < 0.1f ) return input; + + // Bleed off some speed, but if we have less than the bleed + // threshold, bleed the threshold amount. + float control = (speed < StopSpeed) ? StopSpeed : speed; + + // Add the amount to the drop amount. + var drop = control * Time.Delta * frictionAmount; + + // scale the velocity + float newspeed = speed - drop; + if ( newspeed < 0 ) newspeed = 0; + if ( newspeed == speed ) return input; + + newspeed /= speed; + input *= newspeed; + + return input; + } + + Vector3 Accelerate( Vector3 input, Vector3 wishdir, float wishspeed, float speedLimit, float acceleration ) + { + if ( speedLimit > 0 && wishspeed > speedLimit ) + wishspeed = speedLimit; + + var currentspeed = input.Dot( wishdir ); + var addspeed = wishspeed - currentspeed; + + if ( addspeed <= 0 ) + return input; + + var accelspeed = acceleration * Time.Delta * wishspeed; + + if ( accelspeed > addspeed ) + accelspeed = addspeed; + + input += wishdir * accelspeed; + + return input; + } + + Vector3 ApplyJump( Vector3 input, string jumpType ) + { + AddEvent( jumpType ); + + return input + Vector3.Up * JumpSpeed; + } + + Vector3 StayOnGround( Vector3 position ) + { + var start = position + Vector3.Up * 2; + var end = position + Vector3.Down * StepSize; + + // See how far up we can go without getting stuck + var trace = Entity.TraceBBox( position, start ); + start = trace.EndPosition; + + // Now trace down from a known safe position + trace = Entity.TraceBBox( start, end ); + + if ( trace.Fraction <= 0 ) return position; + if ( trace.Fraction >= 1 ) return position; + if ( trace.StartedSolid ) return position; + if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return position; + + return trace.EndPosition; + } + + public bool HasEvent( string eventName ) + { + return ControllerEvents.Contains( eventName ); + } + + void AddEvent( string eventName ) + { + if ( HasEvent( eventName ) ) + return; + + ControllerEvents.Add( eventName ); + } +} diff --git a/code/phase/AssignPhase.cs b/code/phase/AssignPhase.cs index 26c9fc1..0ae6bd2 100644 --- a/code/phase/AssignPhase.cs +++ b/code/phase/AssignPhase.cs @@ -21,9 +21,10 @@ public class AssignPhase : BasePhase var detectivesNeeded = 1; var murderersNeeded = 1; - List spawnpoints = Entity.All.OfType().OrderBy( x => Guid.NewGuid() ).ToList(); + Random random = new(); + List spawnpoints = Entity.All.OfType().OrderBy( _ => random.Next() ).ToList(); var clients = Game.Clients.ToList(); - foreach ( int i in Enumerable.Range( 0, clients.Count ).OrderBy( x => Guid.NewGuid() ) ) + foreach ( int i in Enumerable.Range( 0, clients.Count ).OrderBy( _ => random.Next() ) ) { var client = clients[i]; if (client.Pawn != null) @@ -60,7 +61,7 @@ public class AssignPhase : BasePhase var spawnpoint = spawnpoints[0]; spawnpoints.RemoveAt( 0 ); var tx = spawnpoint.Transform; - tx.Position = tx.Position + Vector3.Up * 50.0f; + tx.Position = tx.Position + Vector3.Up * 10.0f; pawn.Transform = tx; RoleOverlay.Show( To.Single( client ) ); diff --git a/code/phase/PlayPhase.cs b/code/phase/PlayPhase.cs index 118529e..b6210b3 100644 --- a/code/phase/PlayPhase.cs +++ b/code/phase/PlayPhase.cs @@ -47,6 +47,7 @@ public class PlayPhase : BasePhase { Log.Info( "Removing blind from " + entity.Name ); BlindedOverlay.Hide( To.Single( entity ) ); + DeathOverlay.Hide( To.Single( entity ) ); if (entity is Player pawn && pawn.IsValid() ) { if (pawn.Controller != null) pawn.Controller.SpeedMultiplier = 1; @@ -103,20 +104,25 @@ public class PlayPhase : BasePhase } Player victimPlayer = (Player)victim; Player killerPlayer = (Player)killer; + Team victimTeam = victimPlayer.CurrentTeam; Team killerTeam = killerPlayer.CurrentTeam; + victimPlayer.CurrentTeam = Team.Spectator; Log.Info( victimPlayer + " died to " + killerPlayer ); + if (victimTeam != Team.Murderer && killerTeam != Team.Murderer) { Log.Info( killerPlayer + " shot a bystander"); + ChatBox.Say( killerPlayer.Client.Name + " killed an innocent bystander" ); + BlindedOverlay.Show( To.Single( killer ) ); + if (killerPlayer.Controller != null) killerPlayer.Controller.SpeedMultiplier = 0.3f; if (killerPlayer.Inventory != null) { - Log.Info( killerPlayer + "bonk"); killerPlayer.Inventory.AllowPickup = false; killerPlayer.Inventory.SpillContents(killerPlayer.EyePosition, killerPlayer.AimRay.Forward); } diff --git a/code/phase/WaitPhase.cs b/code/phase/WaitPhase.cs index 4f2e876..9eab094 100644 --- a/code/phase/WaitPhase.cs +++ b/code/phase/WaitPhase.cs @@ -23,6 +23,7 @@ public class WaitPhase : BasePhase { base.NextPhase = new AssignPhase(); base.IsFinished = true; + return; } else if (CountIn && !_isCountDown) { @@ -34,6 +35,36 @@ public class WaitPhase : BasePhase _isCountDown = false; base.TimeLeft = -1; } + + foreach (var client in Game.Clients) + { + if (client.Pawn == null) + { + var pawn = new Player(); + client.Pawn = pawn; + + var spawnpoints = Entity.All.OfType(); + var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault(); + if ( randomSpawnPoint != null ) + { + var tx = randomSpawnPoint.Transform; + tx.Position = tx.Position + Vector3.Up * 50.0f; + pawn.Transform = tx; + } + + pawn.CurrentTeam = Team.Bystander; + pawn.Spawn(); + pawn.Respawn(); + } else + { + var pawn = (Player)client.Pawn; + if (pawn.LifeState == LifeState.Dead) + { + pawn.CurrentTeam = Team.Bystander; + pawn.Respawn(); + } + } + } } public override void HandleClientJoin( ClientJoinedEvent e ) diff --git a/code/ui/Hud.razor b/code/ui/Hud.razor index cacf26b..9fcdfdc 100644 --- a/code/ui/Hud.razor +++ b/code/ui/Hud.razor @@ -13,6 +13,7 @@ + diff --git a/code/ui/overlay/DeathOverlay.Network.cs b/code/ui/overlay/DeathOverlay.Network.cs new file mode 100644 index 0000000..f1f3bb2 --- /dev/null +++ b/code/ui/overlay/DeathOverlay.Network.cs @@ -0,0 +1,18 @@ +using Sandbox; + +namespace MurderGame; + +public partial class DeathOverlay +{ + [ClientRpc] + public static void Show( ) + { + if (Instance != null) Instance.ShowOverlay = true; + } + + [ClientRpc] + public static void Hide() + { + if (Instance != null) Instance.ShowOverlay = false; + } +} diff --git a/code/ui/overlay/DeathOverlay.razor b/code/ui/overlay/DeathOverlay.razor new file mode 100644 index 0000000..15848f6 --- /dev/null +++ b/code/ui/overlay/DeathOverlay.razor @@ -0,0 +1,89 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +@if (ShowOverlay) +{ +
+
+
+
+ You have died +
+
+} + +@code +{ + public static DeathOverlay Instance { get; private set; } + + public DeathOverlay() + { + Instance = this; + } + + public bool ShowOverlay { get; set; } + + protected override int BuildHash() + { + var clientPawn = Game.LocalPawn; + if (clientPawn is Player player) + { + ShowOverlay = player.LifeState == LifeState.Dead && player.Spectator == null; + } + return ShowOverlay.GetHashCode(); + } + +} \ No newline at end of file diff --git a/code/weapon/Weapon.cs b/code/weapon/Weapon.cs index 4519adc..6d6fc20 100644 --- a/code/weapon/Weapon.cs +++ b/code/weapon/Weapon.cs @@ -195,11 +195,13 @@ public partial class Weapon : AnimatedEntity [ClientRpc] public void CreateViewModel() { + DestroyViewModel(); if ( ViewModelPath == null ) return; var vm = new WeaponViewModel( this ); vm.Model = Model.Load( ViewModelPath ); vm.Owner = Owner; + vm.Parent = Game.LocalPawn; ViewModelEntity = vm; if (!string.IsNullOrEmpty(HandsModelPath)) { -- cgit v1.2.3-70-g09d2