From 71db52c5443a7bf82d9a23a770994a42b043be04 Mon Sep 17 00:00:00 2001 From: Leonardo Bishop Date: Thu, 27 Jul 2023 22:11:31 +0100 Subject: Initial commit --- code/Game.cs | 75 +++++++++ code/entity/DroppedWeapon.cs | 39 +++++ code/event/MurderEvent.cs | 13 ++ code/pawn/Player.cs | 218 +++++++++++++++++++++++++ code/pawn/PlayerAnimator.cs | 21 +++ code/pawn/PlayerController.cs | 177 ++++++++++++++++++++ code/pawn/PlayerInventory.cs | 126 +++++++++++++++ code/pawn/Ragdoll.cs | 13 ++ code/phase/AssignPhase.cs | 89 +++++++++++ code/phase/BasePhase.cs | 28 ++++ code/phase/EndPhase.cs | 26 +++ code/phase/PlayPhase.cs | 133 ++++++++++++++++ code/phase/WaitPhase.cs | 43 +++++ code/team/Team.cs | 91 +++++++++++ code/ui/Hud.razor | 26 +++ code/ui/Hud.razor.scss | 21 +++ code/ui/PlayerInfo.razor | 41 +++++ code/ui/health/Health.razor | 38 +++++ code/ui/mainmenu/LoadingScreen.razor | 41 +++++ code/ui/mainmenu/LoadingScreen.razor.scss | 101 ++++++++++++ code/ui/mainmenu/MainMenu.razor | 30 ++++ code/ui/mainmenu/MainMenu.razor.scss | 257 ++++++++++++++++++++++++++++++ code/ui/overlay/BlindedOverlay.Network.cs | 20 +++ code/ui/overlay/BlindedOverlay.razor | 65 ++++++++ code/ui/overlay/RoleOverlay.cs | 20 +++ code/ui/overlay/RoleOverlay.razor | 94 +++++++++++ code/ui/phase/PhaseInfo.razor | 23 +++ code/ui/phase/PhaseTimer.razor | 61 +++++++ code/ui/team/TeamInfo.razor | 37 +++++ code/ui/weapon/Crosshair.razor | 23 +++ code/ui/weapon/Reload.razor | 82 ++++++++++ code/weapon/Knife.cs | 37 +++++ code/weapon/Pistol.cs | 52 ++++++ code/weapon/Weapon.cs | 226 ++++++++++++++++++++++++++ code/weapon/WeaponViewModel.cs | 22 +++ 35 files changed, 2409 insertions(+) create mode 100644 code/Game.cs create mode 100644 code/entity/DroppedWeapon.cs create mode 100644 code/event/MurderEvent.cs create mode 100644 code/pawn/Player.cs create mode 100644 code/pawn/PlayerAnimator.cs create mode 100644 code/pawn/PlayerController.cs create mode 100644 code/pawn/PlayerInventory.cs create mode 100644 code/pawn/Ragdoll.cs create mode 100644 code/phase/AssignPhase.cs create mode 100644 code/phase/BasePhase.cs create mode 100644 code/phase/EndPhase.cs create mode 100644 code/phase/PlayPhase.cs create mode 100644 code/phase/WaitPhase.cs create mode 100644 code/team/Team.cs create mode 100644 code/ui/Hud.razor create mode 100644 code/ui/Hud.razor.scss create mode 100644 code/ui/PlayerInfo.razor create mode 100644 code/ui/health/Health.razor create mode 100644 code/ui/mainmenu/LoadingScreen.razor create mode 100644 code/ui/mainmenu/LoadingScreen.razor.scss create mode 100644 code/ui/mainmenu/MainMenu.razor create mode 100644 code/ui/mainmenu/MainMenu.razor.scss create mode 100644 code/ui/overlay/BlindedOverlay.Network.cs create mode 100644 code/ui/overlay/BlindedOverlay.razor create mode 100644 code/ui/overlay/RoleOverlay.cs create mode 100644 code/ui/overlay/RoleOverlay.razor create mode 100644 code/ui/phase/PhaseInfo.razor create mode 100644 code/ui/phase/PhaseTimer.razor create mode 100644 code/ui/team/TeamInfo.razor create mode 100644 code/ui/weapon/Crosshair.razor create mode 100644 code/ui/weapon/Reload.razor create mode 100644 code/weapon/Knife.cs create mode 100644 code/weapon/Pistol.cs create mode 100644 code/weapon/Weapon.cs create mode 100644 code/weapon/WeaponViewModel.cs (limited to 'code') diff --git a/code/Game.cs b/code/Game.cs new file mode 100644 index 0000000..e603fda --- /dev/null +++ b/code/Game.cs @@ -0,0 +1,75 @@ + +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MurderGame; + +public partial class MurderGame : Sandbox.GameManager +{ + public MurderGame() + { + if ( Game.IsClient ) + { + Game.RootPanel = new Hud(); + } + // if ( Game.IsServer ) + // { + // // The packageName should always match org.ident + // // In this case, we ask to download gvar.citizen_zombie -> https://asset.party/gvar/citizen_zombie + // DownloadAsset( "gvar.citizen_zombie" ); + // } + } + + public static MurderGame Instance + { + get => Current as MurderGame; + } + + + [ConVar.Server( "mm_min_players", Help = "The minimum number of players required to start a round." )] + public static int MinPlayers { get; set; } = 2; + + [ConVar.Server( "mm_allow_suicide", Help = "Allow players to kill themselves during a round." )] + public static bool AllowSuicide { get; set; } = true; + + [ConVar.Server( "mm_round_time", Help = "The amount of time in a round." )] + public static int RoundTime { get; set; } = 600; + + [Net] + public BasePhase CurrentPhase { get; set; } = new WaitPhase() { CountIn = true }; + + [GameEvent.Tick.Server] + public void TickServer() + { + CurrentPhase.Tick(); + + if (CurrentPhase.NextPhase != null && CurrentPhase.IsFinished) + { + CurrentPhase.Deactivate(); + CurrentPhase = CurrentPhase.NextPhase; + Log.Info("Advancing phase to " + CurrentPhase.ToString()); + CurrentPhase.Activate(); + } + } + + public override void ClientJoined( IClient client ) + { + base.ClientJoined( client ); + + // Create a pawn for this client to play with + 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; + } + } +} + diff --git a/code/entity/DroppedWeapon.cs b/code/entity/DroppedWeapon.cs new file mode 100644 index 0000000..f0cad67 --- /dev/null +++ b/code/entity/DroppedWeapon.cs @@ -0,0 +1,39 @@ +using MurderGame; +using Sandbox; +using System; + +public partial class DroppedWeapon : AnimatedEntity +{ + public int Ammo { get; set; } + public Type WeaponType { get; set; } + + public DroppedWeapon(Weapon weapon) : this() + { + Ammo = weapon.Ammo; + WeaponType = weapon.GetType(); + } + + public DroppedWeapon() + { + Tags.Add("droppedweapon"); + PhysicsEnabled = true; + UsePhysicsCollision = true; + EnableSelfCollisions = true; + EnableSolidCollisions = true; + } + + public override void StartTouch( Entity other ) + { + if ( !Game.IsServer ) return; + if ( !other.Tags.Has( "livingplayer" ) ) return; + + MurderGame.Player player = (MurderGame.Player)other; + + if ( player.Inventory == null || player.Inventory.PrimaryWeapon != null || !player.Inventory.AllowPickup) return; + + Weapon instance = TypeLibrary.Create( WeaponType ); + instance.Ammo = Ammo; + player.Inventory.SetPrimaryWeapon( instance ); + Delete(); + } +} diff --git a/code/event/MurderEvent.cs b/code/event/MurderEvent.cs new file mode 100644 index 0000000..aed2a80 --- /dev/null +++ b/code/event/MurderEvent.cs @@ -0,0 +1,13 @@ +using Sandbox; + +namespace MurderGame; + +public static class MurderEvent +{ + public const string Kill = "kill"; + + public class KillAttribute : EventAttribute + { + public KillAttribute() : base( Kill ) { } + } +} diff --git a/code/pawn/Player.cs b/code/pawn/Player.cs new file mode 100644 index 0000000..a1b126a --- /dev/null +++ b/code/pawn/Player.cs @@ -0,0 +1,218 @@ +using Sandbox; +using Sandbox.UI; +using System.ComponentModel; + +namespace MurderGame; + +public partial class Player : AnimatedEntity +{ + [ClientInput] + public Vector3 InputDirection { get; set; } + + [ClientInput] + public Angles ViewAngles { get; set; } + + [Browsable( false )] + public Vector3 EyePosition + { + get => Transform.PointToWorld( EyeLocalPosition ); + set => EyeLocalPosition = Transform.PointToLocal( value ); + } + + [Net, Predicted, Browsable( false )] + public Vector3 EyeLocalPosition { get; set; } + + [Browsable( false )] + public Rotation EyeRotation + { + get => Transform.RotationToWorld( EyeLocalRotation ); + set => EyeLocalRotation = Transform.RotationToLocal( value ); + } + + [Net, Predicted, Browsable( false )] + public Rotation EyeLocalRotation { get; set; } + + public BBox Hull + { + get => new + ( + new Vector3( -12, -12, 0 ), + new Vector3( 12, 12, 64 ) + ); + } + + [Net] + public Team CurrentTeam { get; set; } + + [BindComponent] public PlayerController Controller { get; } + [BindComponent] public PlayerAnimator Animator { get; } + [BindComponent] public PlayerInventory Inventory { get; } + + public Ragdoll PlayerRagdoll { get; set; } + public ClothingContainer PlayerClothingContainer { get; set; } + + public Vector3 LastAttackForce { get; set; } + public int LastHitBone { get; set; } + + public override Ray AimRay => new Ray( EyePosition, EyeRotation.Forward ); + + public override void Spawn() + { + SetModel( "models/citizen/citizen.vmdl" ); + + Tags.Add( "player" ); + EnableHitboxes = true; + EnableDrawing = false; + EnableHideInFirstPerson = true; + EnableShadowInFirstPerson = true; + SetupPhysicsFromAABB( PhysicsMotionType.Keyframed, Hull.Mins, Hull.Maxs ); + EnableSolidCollisions = false; + } + + public void Respawn() + { + Tags.Add( "livingplayer" ); + EnableAllCollisions = true; + EnableDrawing = true; + Components.Create(); + Components.Create(); + Components.Create(); + Health = 100f; + DeleteRagdoll(); + } + + public void Cleanup() + { + DisablePlayer(); + DeleteRagdoll(); + } + + public void DeleteRagdoll() + { + if (PlayerRagdoll != null) + { + PlayerRagdoll.Delete(); + PlayerRagdoll = null; + } + } + + public void DisablePlayer() + { + EnableAllCollisions = false; + LifeState = LifeState.Dead; + Tags.Remove( "livingplayer" ); + Inventory?.Clear(); + Components.RemoveAll(); + EnableDrawing = false; + } + + public override void OnKilled() + { + 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; + ragdoll.CopyFrom(this); + ragdoll.PhysicsGroup.AddVelocity(LastAttackForce / 100); + PlayerClothingContainer.DressEntity( ragdoll ); + PlayerRagdoll = ragdoll; + } + + public override void TakeDamage( DamageInfo info ) + { + LastAttacker = info.Attacker; + LastAttackerWeapon = info.Weapon; + LastAttackForce = info.Force; + LastHitBone = info.BoneIndex; + if (Game.IsServer && Health > 0f && LifeState == LifeState.Alive) + { + Health -= info.Damage; + if (Health <= 0f) + { + Health = 0f; + OnKilled(); + } + } + } + + public void DressFromClient( IClient cl ) + { + PlayerClothingContainer = new ClothingContainer(); + PlayerClothingContainer.LoadFromClient( cl ); + PlayerClothingContainer.DressEntity( this ); + } + + public override void Simulate( IClient cl ) + { + SimulateRotation(); + Controller?.Simulate( cl ); + Animator?.Simulate(); + Inventory?.Simulate( cl ); + EyeLocalPosition = Vector3.Up * (64f * Scale); + } + + public override void BuildInput() + { + InputDirection = Input.AnalogMove; + + if ( Input.StopProcessing ) + return; + + var look = Input.AnalogLook; + + if ( ViewAngles.pitch > 90f || ViewAngles.pitch < -90f ) + { + look = look.WithYaw( look.yaw * -1f ); + } + + var viewAngles = ViewAngles; + viewAngles += look; + viewAngles.pitch = viewAngles.pitch.Clamp( -89f, 89f ); + viewAngles.roll = 0f; + ViewAngles = viewAngles.Normal; + } + + bool IsThirdPerson { get; set; } = false; + + public override void FrameSimulate( IClient cl ) + { + SimulateRotation(); + + Camera.Rotation = ViewAngles.ToRotation(); + Camera.FieldOfView = Screen.CreateVerticalFieldOfView( Game.Preferences.FieldOfView ); + + Camera.FirstPersonViewer = this; + Camera.Position = EyePosition; + } + + public TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f ) + { + return TraceBBox( start, end, Hull.Mins, Hull.Maxs, liftFeet ); + } + + public TraceResult TraceBBox( Vector3 start, Vector3 end, Vector3 mins, Vector3 maxs, float liftFeet = 0.0f ) + { + if ( liftFeet > 0 ) + { + start += Vector3.Up * liftFeet; + maxs = maxs.WithZ( maxs.z - liftFeet ); + } + + var tr = Trace.Ray( start, end ) + .Size( mins, maxs ) + .WithAnyTags( "solid", "player", "passbullets" ) + .Ignore( this ) + .Run(); + + return tr; + } + + protected void SimulateRotation() + { + EyeRotation = ViewAngles.ToRotation(); + Rotation = ViewAngles.WithPitch( 0f ).ToRotation(); + } + +} diff --git a/code/pawn/PlayerAnimator.cs b/code/pawn/PlayerAnimator.cs new file mode 100644 index 0000000..4cd4e3f --- /dev/null +++ b/code/pawn/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/PlayerController.cs b/code/pawn/PlayerController.cs new file mode 100644 index 0000000..d638bb0 --- /dev/null +++ b/code/pawn/PlayerController.cs @@ -0,0 +1,177 @@ +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 new file mode 100644 index 0000000..55fa3ed --- /dev/null +++ b/code/pawn/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/Ragdoll.cs b/code/pawn/Ragdoll.cs new file mode 100644 index 0000000..9948946 --- /dev/null +++ b/code/pawn/Ragdoll.cs @@ -0,0 +1,13 @@ +using Sandbox; + +public partial class Ragdoll : AnimatedEntity +{ + public Ragdoll() + { + Tags.Add( "ragdoll" ); + PhysicsEnabled = true; + UsePhysicsCollision = true; + EnableSelfCollisions = true; + EnableSolidCollisions = true; + } +} diff --git a/code/phase/AssignPhase.cs b/code/phase/AssignPhase.cs new file mode 100644 index 0000000..26c9fc1 --- /dev/null +++ b/code/phase/AssignPhase.cs @@ -0,0 +1,89 @@ +using Sandbox; +using Sandbox.UI; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MurderGame; + +public class AssignPhase : BasePhase +{ + public override string Title => "Assigning teams"; + public int TicksElapsed; + + public override void Activate() + { + foreach (var entity in Entity.All.OfType()) + { + entity.Delete(); + } + + var detectivesNeeded = 1; + var murderersNeeded = 1; + + List spawnpoints = Entity.All.OfType().OrderBy( x => Guid.NewGuid() ).ToList(); + var clients = Game.Clients.ToList(); + foreach ( int i in Enumerable.Range( 0, clients.Count ).OrderBy( x => Guid.NewGuid() ) ) + { + var client = clients[i]; + if (client.Pawn != null) + { + ((Player) client.Pawn).Cleanup(); + client.Pawn.Delete(); + } + Player pawn = new(); + client.Pawn = pawn; + if (spawnpoints.Count == 0) + { + ChatBox.Say( "Could not spawn " + client.Name + " as there are not enough spawn points." ); + pawn.CurrentTeam = Team.Spectator; + continue; + } + pawn.DressFromClient( client ); + + if (murderersNeeded > 0) + { + pawn.CurrentTeam = Team.Murderer; + --murderersNeeded; + } + else if (detectivesNeeded > 0) + { + pawn.CurrentTeam = Team.Detective; + --detectivesNeeded; + } + else + { + pawn.CurrentTeam = Team.Bystander; + } + Log.Info( "Assigning " + client.Name + " to team " + TeamOperations.GetTeamName( pawn.CurrentTeam ) ); + + var spawnpoint = spawnpoints[0]; + spawnpoints.RemoveAt( 0 ); + var tx = spawnpoint.Transform; + tx.Position = tx.Position + Vector3.Up * 50.0f; + pawn.Transform = tx; + + RoleOverlay.Show( To.Single( client ) ); + } + base.TimeLeft = 5; + } + + public override void Deactivate() + { + foreach (var client in Game.Clients) + { + RoleOverlay.Hide( To.Single( client ) ); + } + } + + public override void Tick() + { + ++TicksElapsed; + if ( base.TimeLeft != -1 && TicksElapsed % Game.TickRate == 0 && --base.TimeLeft == 0 ) + { + base.IsFinished = true; + base.NextPhase = new PlayPhase(); + } + } + +} diff --git a/code/phase/BasePhase.cs b/code/phase/BasePhase.cs new file mode 100644 index 0000000..8f3d044 --- /dev/null +++ b/code/phase/BasePhase.cs @@ -0,0 +1,28 @@ +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MurderGame; + +public abstract partial class BasePhase : BaseNetworkable +{ + public virtual string Title => "Name"; + + [Net] + public int TimeLeft { get; set; } = -1; + + public BasePhase NextPhase { get; set;} + + public bool IsFinished { get; set; } + + public abstract void Tick(); + + public virtual void Activate() { } + + public virtual void Deactivate() { } + + public virtual void HandleClientJoin(ClientJoinedEvent e) { } +} diff --git a/code/phase/EndPhase.cs b/code/phase/EndPhase.cs new file mode 100644 index 0000000..904dc81 --- /dev/null +++ b/code/phase/EndPhase.cs @@ -0,0 +1,26 @@ +using Sandbox; +using System.Linq; + +namespace MurderGame; + +public class EndPhase : BasePhase +{ + public override string Title => "Game over"; + public int TicksElapsed; + + public override void Activate() + { + base.TimeLeft = 7; + } + + public override void Tick() + { + ++TicksElapsed; + if (base.TimeLeft != -1 && TicksElapsed % Game.TickRate == 0 && --base.TimeLeft == 0) + { + base.NextPhase = new WaitPhase() { CountIn = false }; + base.IsFinished = true; + return; + } + } +} diff --git a/code/phase/PlayPhase.cs b/code/phase/PlayPhase.cs new file mode 100644 index 0000000..118529e --- /dev/null +++ b/code/phase/PlayPhase.cs @@ -0,0 +1,133 @@ +using Sandbox; +using Sandbox.UI; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; + +namespace MurderGame; + +public class PlayPhase : BasePhase +{ + public override string Title => "Play"; + public IDictionary Blinded = new Dictionary(); + public int TicksElapsed; + private string MurdererNames { get; set; } + + public override void Activate() + { + base.TimeLeft = MurderGame.RoundTime; + Event.Register(this); + foreach ( var client in Game.Clients ) + { + if ( client.Pawn is Player pawn ) + { + if (pawn.CurrentTeam != Team.Spectator) + { + pawn.Respawn(); + TeamOperations.GiveLoadouts( pawn ); + } + } + } + MurdererNames = string.Join( ',', Game.Clients.Where( c => ((Player)c.Pawn).CurrentTeam == Team.Murderer ).Select(c => c.Name)); + } + + public override void Deactivate() + { + base.TimeLeft = MurderGame.RoundTime; + Event.Unregister(this); + foreach(var item in Blinded) + { + ClearDebuffs( item.Key ); + } + Blinded.Clear(); + } + + public void ClearDebuffs(Entity entity ) + { + Log.Info( "Removing blind from " + entity.Name ); + BlindedOverlay.Hide( To.Single( entity ) ); + if (entity is Player pawn && pawn.IsValid() ) + { + if (pawn.Controller != null) pawn.Controller.SpeedMultiplier = 1; + if (pawn.Inventory != null) pawn.Inventory.AllowPickup = true; + } + + } + + public override void Tick() + { + ++TicksElapsed; + if (base.TimeLeft != -1 && TicksElapsed % Game.TickRate == 0 && --base.TimeLeft == 0) + { + TriggerEndOfGame(); + return; + } + bool bystandersAlive = Game.Clients.Any(c =>((Player)c.Pawn).CurrentTeam == Team.Bystander || ((Player)c.Pawn).CurrentTeam == Team.Detective); + bool murderersAlive = Game.Clients.Any(c =>((Player)c.Pawn).CurrentTeam == Team.Murderer); + if (!bystandersAlive || !murderersAlive) + { + TriggerEndOfGame(); + } + + foreach(var item in Blinded) + { + var blindLeft = item.Value - 1; + if (blindLeft < 0) + { + Blinded.Remove( item.Key ); + ClearDebuffs( item.Key ); + Log.Info( "Removing blind from " + item.Key.Name ); + } + else + { + Blinded[item.Key] = blindLeft; + } + } + } + + public void TriggerEndOfGame() + { + bool bystandersWin = Game.Clients.Any(c =>((Player)c.Pawn).CurrentTeam == Team.Bystander || ((Player)c.Pawn).CurrentTeam == Team.Detective); + ChatBox.Say( (bystandersWin ? "Bystanders" : "Murderers") +" win! The murderers were: " + MurdererNames ); + base.NextPhase = new EndPhase(); + base.IsFinished = true; + } + + [MurderEvent.Kill] + public void OnKill(Entity killer, Entity victim) + { + if (killer == null || killer is not Player || victim == null || victim is not Player ) + { + return; + } + 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); + } + + Blinded[killer] = 20 * Game.TickRate; + } + else if (victimTeam == Team.Murderer ) + { + Log.Info( killerPlayer + " killed a murderer"); + ChatBox.Say( killerPlayer.Client.Name + " killed a murderer" ); + } + } + +} diff --git a/code/phase/WaitPhase.cs b/code/phase/WaitPhase.cs new file mode 100644 index 0000000..4f2e876 --- /dev/null +++ b/code/phase/WaitPhase.cs @@ -0,0 +1,43 @@ +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MurderGame; + +public class WaitPhase : BasePhase +{ + public override string Title => "Waiting for players"; + public bool CountIn { get; set; } + public int TicksElapsed { get; set; } + + private bool _isCountDown { get; set; } + + public override void Tick() + { + if (Game.Clients.Count >= MurderGame.MinPlayers) + { + if (!CountIn || (_isCountDown && ++TicksElapsed % Game.TickRate == 0 && --base.TimeLeft == 0)) + { + base.NextPhase = new AssignPhase(); + base.IsFinished = true; + } + else if (CountIn && !_isCountDown) + { + _isCountDown = true; + base.TimeLeft = 10; + } + } else if (CountIn && _isCountDown) + { + _isCountDown = false; + base.TimeLeft = -1; + } + } + + public override void HandleClientJoin( ClientJoinedEvent e ) + { + + } +} diff --git a/code/team/Team.cs b/code/team/Team.cs new file mode 100644 index 0000000..e5cb8dd --- /dev/null +++ b/code/team/Team.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MurderGame; + +public enum Team : ushort +{ + Spectator = 0, + Murderer = 1, + Detective = 2, + Bystander = 3 +} + +// why are c# enums so bad +public static class TeamOperations +{ + + public static string GetTeamName(Team team) + { + return team switch + { + Team.Detective => "Detective", + Team.Murderer => "Murderer", + Team.Bystander => "Bystander", + Team.Spectator => "Spectator", + _ => "None", + }; + } + + public static string GetTeamColour(Team team) + { + return team switch + { + Team.Detective => "#33A0FF", + Team.Murderer => "#FF4136", + Team.Bystander => "#33A0FF", + _ => "#AAAAAA", + }; + } + + public static string GetTeamDescription(Team team) + { + return team switch + { + Team.Detective => "There is a murderer on the loose! Find out who they are and shoot them before they kill everybody else.", + Team.Murderer => "Kill everybody else in time and avoid detection. At least one other player is armed.", + Team.Bystander => "There is a murderer on the loose! Avoid getting killed and work with others to establish who the murderer is.", + _ => "None", + }; + } + + public static bool CanSprint(Team team) + { + return team switch + { + Team.Murderer => true, + _ => false, + }; + } + + public static void GiveLoadouts(Player pawn) + { + pawn.Inventory.Clear(); + + switch (pawn.CurrentTeam) + { + case Team.Detective: + GiveDetectiveWeapon(pawn); break; + case Team.Murderer: + GiveMurdererWeapon(pawn); break; + default: break; + } + } + + private static void GiveDetectiveWeapon( Player pawn ) + { + Pistol pistol = new() + { + Ammo = 1 + }; + pawn.Inventory.SetPrimaryWeapon( pistol ); + } + + private static void GiveMurdererWeapon(Player pawn) + { + pawn.Inventory.SetPrimaryWeapon( new Knife() ); + } +} diff --git a/code/ui/Hud.razor b/code/ui/Hud.razor new file mode 100644 index 0000000..6ea6103 --- /dev/null +++ b/code/ui/Hud.razor @@ -0,0 +1,26 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits RootPanel +@attribute [StyleSheet] + + + + + + + + + + + + + +@code +{ +} \ No newline at end of file diff --git a/code/ui/Hud.razor.scss b/code/ui/Hud.razor.scss new file mode 100644 index 0000000..fe07ac5 --- /dev/null +++ b/code/ui/Hud.razor.scss @@ -0,0 +1,21 @@ +Hud +{ + .header + { + left: 128px; + top: 128px; + flex-direction: column; + + label + { + font-family: Roboto; + color: white; + font-size: 32px; + + &.subtitle + { + font-size: 16px; + } + } + } +} diff --git a/code/ui/PlayerInfo.razor b/code/ui/PlayerInfo.razor new file mode 100644 index 0000000..53a71df --- /dev/null +++ b/code/ui/PlayerInfo.razor @@ -0,0 +1,41 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + + + + +@code +{ + public string GetTeamColour() + { + var ClientPawn = Game.LocalPawn; + if (ClientPawn is Player) + { + return TeamOperations.GetTeamColour(((Player)ClientPawn).CurrentTeam); + } + return ""; + } + + protected override int BuildHash() + { + return GetTeamColour().GetHashCode(); + } +} diff --git a/code/ui/health/Health.razor b/code/ui/health/Health.razor new file mode 100644 index 0000000..9d7037b --- /dev/null +++ b/code/ui/health/Health.razor @@ -0,0 +1,38 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + +
+ +@code +{ + public string Colour { get; set; } + + public int GetHealth() + { + var ClientPawn = Game.LocalPawn; + if (ClientPawn is Player) + { + return ClientPawn.Health.CeilToInt(); + } + return 0; + } + + protected override int BuildHash() + { + return GetHealth().GetHashCode(); + } +} diff --git a/code/ui/mainmenu/LoadingScreen.razor b/code/ui/mainmenu/LoadingScreen.razor new file mode 100644 index 0000000..9e3e5ae --- /dev/null +++ b/code/ui/mainmenu/LoadingScreen.razor @@ -0,0 +1,41 @@ +@using System +@using Sandbox; +@using Sandbox.UI; +@using Sandbox.Menu; +@attribute [StyleSheet] +@inherits RootPanel +@namespace DmMenu +@implements Sandbox.Menu.ILoadingScreenPanel + + + +
LOADING
+
@Progress.Title
+ + @if (Progress.Fraction > 0) + { +
+
+
@(Progress.Percent.ToString("0") )%
+
@(Progress.Mbps.ToString("0"))Mbps
+
+ } + +
+
+ Cancel +
+
+ +
+ +@code +{ + public LoadingProgress Progress; + + public void OnLoadingProgress( LoadingProgress progress ) + { + Progress = progress; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/code/ui/mainmenu/LoadingScreen.razor.scss b/code/ui/mainmenu/LoadingScreen.razor.scss new file mode 100644 index 0000000..894d259 --- /dev/null +++ b/code/ui/mainmenu/LoadingScreen.razor.scss @@ -0,0 +1,101 @@ +.loadingpanel +{ + background-image: url( "menu/boxes.webm" ); + background-position: center; + background-size: cover; + background-tint: #555; + width: 100%; + height: 100%; + color: white; + padding: 100px; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: Poppins; + pointer-events: all; + color: #ccc; + + > .title + { + font-size: 80px; + font-weight: bolder; + } + + > .subtitle + { + font-size: 30px; + color: #aaa; + font-weight: 200; + } + + > .controls + { + position: absolute; + bottom: 100px; + right: 200px; + + .button + { + font-size: 20px; + padding: 10px 50px; + background-color: #666; + color: #111; + font-weight: bold; + cursor: pointer; + + &:hover + { + background-color: #888; + } + } + } + + > .progress + { + margin: 50px 0px; + background-color: #0003; + width: 500px; + border-radius: 5px; + overflow: hidden; + + .bar + { + background-color: #fff1; + position: absolute; + height: 100%; + } + + .text-left + { + flex-grow: 1; + flex-shrink: 0; + } + + .text-right + { + flex-shrink: 0; + } + + .text-left, .text-right + { + padding: 5px 20px; + white-space: nowrap; + font-weight: bold; + opacity: 0.2; + font-size: 20px; + align-items: flex-end; + + .unit + { + font-size: 15px; + opacity: 0.5; + } + } + } +} + +choosemappage.navigator-body +{ + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/code/ui/mainmenu/MainMenu.razor b/code/ui/mainmenu/MainMenu.razor new file mode 100644 index 0000000..ffca318 --- /dev/null +++ b/code/ui/mainmenu/MainMenu.razor @@ -0,0 +1,30 @@ +@using System +@using Sandbox; +@using Sandbox.MenuSystem; +@using Sandbox.UI; +@attribute [StyleSheet] +@inherits Sandbox.UI.GameMenu.DefaultGameMenu + + + + + +
Work in progress! Expect bugs, missing features, and general weird-ness. Things *will* break.
+ + + + +
diff --git a/code/ui/mainmenu/MainMenu.razor.scss b/code/ui/mainmenu/MainMenu.razor.scss new file mode 100644 index 0000000..cbedff5 --- /dev/null +++ b/code/ui/mainmenu/MainMenu.razor.scss @@ -0,0 +1,257 @@ + +.gamemainmenu +{ + background-image: url( "menu/boxes.webm" ); + background-position: center; + background-size: cover; + opacity: 1; + flex-direction: column; + font-size: 25px; + width: 100%; + height: 100%; + position: absolute; + transition: all 0.3s ease-out; + color: white; + + &:intro + { + opacity: 0; + transform: scaleX( 1.1 ); + } + + &.ingame + { + background-color: #151313ee; + background-image: none; + } + +} + +.button, .block +{ + padding: 4px 16px; + + opacity: 0.8; + + font-size: 28px; + font-family: Poppins; + font-weight: 700; + flex-shrink: 0; +} + +.button +{ + background-color: #000a; + cursor: pointer; + + &:hover + { + opacity: 1; + } + + &:active + { + left: 2px; + top: 3px; + } +} + + +.gamemainmenu .navigator-canvas +{ + height: 100%; + flex-grow: 1; + flex-shrink: 0; + backdrop-filter: blur( 20px ); + padding: 0px 100px; + + .navigator-body + { + width: 100%; + height: 100%; + flex-direction: column; + padding: 100px 0px; + } +} + +section +{ + flex-direction: column; + flex-grow: 1; + flex-shrink: 0; + + &.nogrow + { + flex-grow: 0; + } + + &.box + { + background-color: rgba( black, 0.5 ); + + } +} + +.scroll +{ + overflow-y: scroll; + flex-shrink: 1; + flex-grow: 0; +} + +h2 +{ + font-family: poppins; + font-weight: 400; + opacity: 0.2; + margin-bottom: 16px; + flex-shrink: 0; +} + +.member-list +{ + overflow-x: scroll; + padding: 20px; + gap: 8px; +} + + + +.hidden +{ + display: none; +} + +.inset +{ + overflow: hidden; +} + +.layout +{ + flex-direction: column; + + > * + { + flex-shrink: 0; + } + + > .body + { + flex-grow: 1; + flex-shrink: 0; + flex-direction: column; + + &.columned + { + flex-direction: row; + flex-grow: 1; + flex-shrink: 1; + justify-content: space-around; + align-items: center; + + > .left + { + flex-grow: 0; + flex-shrink: 0; + overflow-y: scroll; + flex-direction: column; + } + + > .right + { + flex-grow: 0; + flex-shrink: 0; + flex-direction: column; + } + } + } +} + +.navbar +{ + padding: 32px 0; + flex-shrink: 0; + + .right, .left + { + flex-grow: 0; + flex-shrink: 0; + gap: 10px; + } + + .left + { + flex-grow: 1; + } +} + +$form-row-height: 48px; + +.form +{ + flex-direction: column; + flex-shrink: 0; + flex-grow: 0; + gap: 2px; + margin-bottom: 50px; + + > .form-group + { + flex-direction: column; + flex-shrink: 0; + margin-bottom: 20px; + + > .form-label + { + opacity: 0.5; + height: $form-row-height; + font-size: 20px; + white-space: nowrap; + } + } +} + +.form .form-control +{ + flex-grow: 1; + + SliderControl, > DropDown, > textentry, SliderControl textentry + { + flex-grow: 1; + font-size: 20px; + height: $form-row-height; + } + + > textentry, SliderControl textentry + { + flex-grow: 1; + background-color: #ffffff05; + height: $form-row-height; + color: #aaa; + width: 600px; + border-radius: 0; + padding: 5px; + + &:hover + { + background-color: #ffffff11; + } + + &:focus + { + background-color: #ffffff22; + color: #fff; + } + } + + SliderControl + { + + } +} + +choosemappage.navigator-body +{ + padding: 0; +} \ No newline at end of file diff --git a/code/ui/overlay/BlindedOverlay.Network.cs b/code/ui/overlay/BlindedOverlay.Network.cs new file mode 100644 index 0000000..c81751e --- /dev/null +++ b/code/ui/overlay/BlindedOverlay.Network.cs @@ -0,0 +1,20 @@ +using Sandbox; + +namespace MurderGame; + +public partial class BlindedOverlay +{ + [ClientRpc] + public static void Show( ) + { + Instance.SetClass( "hidden", false ); + Instance.ShowOverlay = true; + } + + [ClientRpc] + public static void Hide() + { + Instance.SetClass( "hidden", true ); + Instance.ShowOverlay = false; + } +} diff --git a/code/ui/overlay/BlindedOverlay.razor b/code/ui/overlay/BlindedOverlay.razor new file mode 100644 index 0000000..024c31a --- /dev/null +++ b/code/ui/overlay/BlindedOverlay.razor @@ -0,0 +1,65 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +
+You shot a bystander! +
+ +@code +{ + public static BlindedOverlay Instance { get; private set; } + + public bool ShowOverlay { get; set; } = false; + + public BlindedOverlay() + { + SetClass( "hidden", true ); + + Instance = this; + } + + protected override int BuildHash() + { + return ShowOverlay.GetHashCode(); + } + +} \ No newline at end of file diff --git a/code/ui/overlay/RoleOverlay.cs b/code/ui/overlay/RoleOverlay.cs new file mode 100644 index 0000000..84758b7 --- /dev/null +++ b/code/ui/overlay/RoleOverlay.cs @@ -0,0 +1,20 @@ +using Sandbox; + +namespace MurderGame; + +public partial class RoleOverlay +{ + [ClientRpc] + public static void Show( ) + { + Instance.SetClass( "hidden", false ); + Instance.ShowOverlay = true; + } + + [ClientRpc] + public static void Hide() + { + Instance.SetClass( "hidden", true ); + Instance.ShowOverlay = false; + } +} diff --git a/code/ui/overlay/RoleOverlay.razor b/code/ui/overlay/RoleOverlay.razor new file mode 100644 index 0000000..c41e359 --- /dev/null +++ b/code/ui/overlay/RoleOverlay.razor @@ -0,0 +1,94 @@ + +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +
+
+ @GetTeamName() +
+
+ @GetTeamDescription() +
+
+ +@code +{ + public string GetTeamName() + { + if (Game.LocalPawn is Player player) + { + return TeamOperations.GetTeamName(player.CurrentTeam); + } + return ""; + } + public string GetTeamDescription() + { + if (Game.LocalPawn is Player player) + { + return TeamOperations.GetTeamDescription(player.CurrentTeam); + } + return ""; + } + public string GetTeamColour() + { + if (Game.LocalPawn is Player player) + { + return TeamOperations.GetTeamColour(player.CurrentTeam); + } + return ""; + } + + public static RoleOverlay Instance { get; private set; } + + public bool ShowOverlay { get; set; } = false; + + public RoleOverlay() + { + SetClass( "hidden", true ); + + Instance = this; + } + + protected override int BuildHash() + { + return ShowOverlay.GetHashCode(); + } + +} diff --git a/code/ui/phase/PhaseInfo.razor b/code/ui/phase/PhaseInfo.razor new file mode 100644 index 0000000..74bf237 --- /dev/null +++ b/code/ui/phase/PhaseInfo.razor @@ -0,0 +1,23 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel +@attribute [StyleSheet] + +
+ @GetPhase().Title +
+ +@code +{ + public BasePhase GetPhase() + { + return MurderGame.Instance.CurrentPhase; + } + + protected override int BuildHash() + { + return GetPhase().GetHashCode(); + } +} diff --git a/code/ui/phase/PhaseTimer.razor b/code/ui/phase/PhaseTimer.razor new file mode 100644 index 0000000..1fb0baa --- /dev/null +++ b/code/ui/phase/PhaseTimer.razor @@ -0,0 +1,61 @@ +@using Sandbox; +@using System +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +@if (HasTime()) +{ +
+ @GetTime() +
+} else +{ +
+ @GetPhase() +
+} + +@code +{ + public bool HasTime() + { + return MurderGame.Instance.CurrentPhase.TimeLeft >= 0; + } + public string GetTime() + { + TimeSpan timeSpan = TimeSpan.FromSeconds(MurderGame.Instance.CurrentPhase.TimeLeft); + return timeSpan.ToString(@"mm\:ss"); + } + public string GetPhase() + { + return MurderGame.Instance.CurrentPhase.Title; + } + + protected override int BuildHash() + { + return HashCode.Combine(MurderGame.Instance.CurrentPhase.TimeLeft.GetHashCode(), MurderGame.Instance.CurrentPhase.Title.GetHashCode()); + } +} diff --git a/code/ui/team/TeamInfo.razor b/code/ui/team/TeamInfo.razor new file mode 100644 index 0000000..18e1f7f --- /dev/null +++ b/code/ui/team/TeamInfo.razor @@ -0,0 +1,37 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +
+@GetTeamName() +
+ +@code +{ + public string Colour { get; set; } + + public string GetTeamName() + { + var ClientPawn = Game.LocalPawn; + if (ClientPawn is Player) + { + return TeamOperations.GetTeamName(((Player)ClientPawn).CurrentTeam); + } + return ""; + } + + protected override int BuildHash() + { + return GetTeamName().GetHashCode(); + } +} diff --git a/code/ui/weapon/Crosshair.razor b/code/ui/weapon/Crosshair.razor new file mode 100644 index 0000000..2dc5db1 --- /dev/null +++ b/code/ui/weapon/Crosshair.razor @@ -0,0 +1,23 @@ +@using Sandbox; +@using Sandbox.UI; + +@namespace MurderGame +@inherits Panel + + + +
+ diff --git a/code/ui/weapon/Reload.razor b/code/ui/weapon/Reload.razor new file mode 100644 index 0000000..d325ade --- /dev/null +++ b/code/ui/weapon/Reload.razor @@ -0,0 +1,82 @@ +@using Sandbox; +@using Sandbox.UI; +@using System; + +@namespace MurderGame +@inherits Panel + + + +@if (ReloadNeeded) { + + +} + +@code +{ + public bool ReloadNeeded { get; set; } + public bool Blink { get; set; } + + protected override int BuildHash() + { + var localPawn = Game.LocalPawn; + if (localPawn is Player player) + { + var inventory = player.Inventory; + if (inventory != null && inventory.GetCurrentWeapon() != null) + { + var weapon = inventory.GetCurrentWeapon(); + var ammo = weapon.Ammo; + ReloadNeeded = ammo == 0 && !weapon.Reloading; + Blink = !weapon.Reloading; + return HashCode.Combine(ReloadNeeded.GetHashCode(), Blink.GetHashCode()); + } + } + if (ReloadNeeded) + { + ReloadNeeded = false; + } + return ReloadNeeded.GetHashCode(); + } +} diff --git a/code/weapon/Knife.cs b/code/weapon/Knife.cs new file mode 100644 index 0000000..1bc74ed --- /dev/null +++ b/code/weapon/Knife.cs @@ -0,0 +1,37 @@ +using Sandbox; + +namespace MurderGame; + +public partial class Knife : Weapon +{ + public override string ModelPath => "weapons/swb/melee/bayonet/w_bayonet.vmdl"; + public override string ViewModelPath => "weapons/swb/melee/bayonet/v_bayonet.vmdl"; + public override string HandsModelPath => "weapons/swb/hands/rebel/v_hands_rebel.vmdl"; + public override float PrimaryRate => 1f; + + public Knife() + { + Ammo = -1; + MaxAmmo = -1; + } + + [ClientRpc] + protected virtual void ShootEffects(bool hit) + { + Game.AssertClient(); + + Pawn.SetAnimParameter( "b_attack", true ); + ViewModelEntity?.SetAnimParameter( hit ? "swing" : "swing_miss" , true ); + } + + public override void PrimaryAttack() + { + Pawn.PlaySound( "bayonet.slash" ); + ShootEffects( Melee( 100, 100 ) ); + } + + protected override void Animate() + { + Pawn.SetAnimParameter( "holdtype", (int)CitizenAnimationHelper.HoldTypes.Swing ); + } +} diff --git a/code/weapon/Pistol.cs b/code/weapon/Pistol.cs new file mode 100644 index 0000000..567eb3e --- /dev/null +++ b/code/weapon/Pistol.cs @@ -0,0 +1,52 @@ +using Sandbox; + +namespace MurderGame; + +public partial class Pistol : Weapon +{ + public override string ModelPath => "weapons/rust_pistol/rust_pistol.vmdl"; + public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl"; + + public Pistol() + { + MaxAmmo = 1; + } + + [ClientRpc] + protected virtual void ShootEffects() + { + Game.AssertClient(); + + Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" ); + + Pawn?.SetAnimParameter( "b_attack", true ); + ViewModelEntity?.SetAnimParameter( "fire", true ); + } + + public override void PrimaryAttack() + { + if (Ammo > 0) + { + --Ammo; + ShootEffects(); + Pawn?.PlaySound( "rust_pistol.shoot" ); + ShootBullet( 100, 100, 1 ); + } + } + + public override void Reload() + { + ReloadEffects(); + } + + [ClientRpc] + protected virtual void ReloadEffects() + { + ViewModelEntity?.SetAnimParameter( "reload", true ); + } + + protected override void Animate() + { + Pawn?.SetAnimParameter( "holdtype", (int)CitizenAnimationHelper.HoldTypes.Pistol ); + } +} diff --git a/code/weapon/Weapon.cs b/code/weapon/Weapon.cs new file mode 100644 index 0000000..2af8ba6 --- /dev/null +++ b/code/weapon/Weapon.cs @@ -0,0 +1,226 @@ +using Sandbox; +using System.Collections.Generic; + +namespace MurderGame; + +public partial class Weapon : AnimatedEntity +{ + public WeaponViewModel ViewModelEntity { get; protected set; } + public BaseViewModel HandModelEntity { get; protected set; } + + public Player Pawn => Owner as Player; + + public AnimatedEntity EffectEntity => Camera.FirstPersonViewer == Owner ? ViewModelEntity : this; + + public virtual string ViewModelPath => null; + public virtual string ModelPath => null; + public virtual string HandsModelPath => null; + + public virtual float PrimaryRate => 5f; + public virtual float ReloadTime => 3.5f; + + [Net, Predicted] public TimeSince TimeSincePrimaryAttack { get; set; } + + [Net, Predicted] public TimeSince TimeSinceReload { get; set; } + [Net, Predicted] public TimeUntil TimeUntilReloadComplete { get; set; } + [Net, Predicted] public bool Reloading { get; set; } + + [Net, Predicted] public int Ammo { get; set; } + [Net, Predicted] public int MaxAmmo { get; set; } + + public override void Spawn() + { + EnableHideInFirstPerson = true; + EnableShadowInFirstPerson = true; + EnableDrawing = false; + + if ( ModelPath != null ) + { + SetModel( ModelPath ); + } + } + + public void ChangeOwner( Player pawn ) + { + Owner = pawn; + SetParent( pawn, true ); + } + + public void OnEquip( Player pawn ) + { + if (Owner == null) + { + Owner = pawn; + SetParent( pawn, true ); + } + EnableDrawing = true; + CreateViewModel( To.Single( pawn ) ); + } + + public void OnHolster() + { + Reloading = false; + EnableDrawing = false; + DestroyViewModel( To.Single( Owner ) ); + Owner = null; + } + + public override void Simulate( IClient player ) + { + Animate(); + if (Reloading && TimeUntilReloadComplete) + { + Reloading = false; + Ammo = MaxAmmo; + } + + if ( CanPrimaryAttack() ) + { + using ( LagCompensation() ) + { + TimeSincePrimaryAttack = 0; + PrimaryAttack(); + } + } + else if (Input.Down("reload") && !Reloading && Ammo != MaxAmmo) + { + Reload(); + Reloading = true; + TimeUntilReloadComplete = ReloadTime; + } + } + + public virtual bool CanPrimaryAttack() + { + if ( !Owner.IsValid() || !Input.Down( "attack1" ) ) return false; + + var rate = PrimaryRate; + if ( rate <= 0 ) return true; + + return !Reloading && TimeSincePrimaryAttack > (1 / rate); + } + + public virtual void PrimaryAttack() + { + } + + public virtual void Reload() + { + } + + protected virtual void Animate() + { + } + + public virtual IEnumerable TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f ) + { + bool underWater = Trace.TestPoint( start, "water" ); + + var trace = Trace.Ray( start, end ) + .UseHitboxes() + .WithAnyTags( "solid", "livingplayer", "npc" ) + .Ignore( this ) + .Size( radius ); + + if ( !underWater ) + trace = trace.WithAnyTags( "water" ); + + var tr = trace.Run(); + + if ( tr.Hit ) + yield return tr; + } + + 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; + + foreach ( var tr in TraceBullet( pos, pos + forward * 5000, bulletSize ) ) + { + tr.Surface.DoBulletImpact( tr ); + + if ( !Game.IsServer ) continue; + if ( !tr.Entity.IsValid() || !tr.Entity.Tags.Has("player") ) continue; + + using ( Prediction.Off() ) + { + var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward * 100 * force, damage ) + .UsingTraceResult( tr ) + .WithAttacker( Owner ) + .WithWeapon( this ); + + tr.Entity.TakeDamage( damageInfo ); + } + } + } + + public virtual void ShootBullet( float force, float damage, float bulletSize ) + { + Game.SetRandomSeed( Time.Tick ); + + var ray = Owner.AimRay; + ShootBullet( ray.Position, ray.Forward, 0, force, damage, bulletSize ); + } + + public virtual bool Melee( float force, float damage ) + { + var ray = Owner.AimRay; + var forward = ray.Forward.Normal; + var pos = ray.Position; + bool hit = false; + + foreach (var tr in TraceBullet(pos, pos + forward * 50, 20)) + { + tr.Surface.DoBulletImpact(tr); + hit = true; + + if ( !Game.IsServer ) continue; + if ( !tr.Entity.IsValid() || !tr.Entity.Tags.Has("player") ) continue; + + using (Prediction.Off()) + { + var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward.Normal * 100 * force, damage ) + .UsingTraceResult(tr) + .WithAttacker(Owner) + .WithWeapon(this); + + tr.Entity.TakeDamage(damageInfo); + } + } + return hit; + } + + [ClientRpc] + public void CreateViewModel() + { + if ( ViewModelPath == null ) return; + + var vm = new WeaponViewModel( this ); + vm.Model = Model.Load( ViewModelPath ); + vm.Owner = Owner; + ViewModelEntity = vm; + if (!string.IsNullOrEmpty(HandsModelPath)) + { + HandModelEntity = new BaseViewModel(); + HandModelEntity.Owner = Owner; + HandModelEntity.EnableViewmodelRendering = true; + HandModelEntity.SetModel(HandsModelPath); + HandModelEntity.SetParent(ViewModelEntity, true); + } + } + + [ClientRpc] + public void DestroyViewModel() + { + if ( ViewModelEntity.IsValid() ) + { + ViewModelEntity.Delete(); + } + if ( HandModelEntity.IsValid() ) + { + HandModelEntity.Delete(); + } + } +} diff --git a/code/weapon/WeaponViewModel.cs b/code/weapon/WeaponViewModel.cs new file mode 100644 index 0000000..97004fe --- /dev/null +++ b/code/weapon/WeaponViewModel.cs @@ -0,0 +1,22 @@ +using Sandbox; + +namespace MurderGame; + +public partial class WeaponViewModel : BaseViewModel +{ + protected Weapon Weapon { get; init; } + + public WeaponViewModel( Weapon weapon ) + { + Weapon = weapon; + EnableShadowCasting = false; + EnableViewmodelRendering = true; + } + + public override void PlaceViewmodel() + { + base.PlaceViewmodel(); + + Camera.Main.SetViewModelCamera( 80f, 1, 500 ); + } +} -- cgit v1.2.3-70-g09d2