diff options
Diffstat (limited to 'code')
35 files changed, 2409 insertions, 0 deletions
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<SpawnPoint>();
+ 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<Weapon>( 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<PlayerController>();
+ Components.Create<PlayerAnimator>();
+ Components.Create<PlayerInventory>();
+ 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<Player>, 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<Player>
+{
+ public int StepSize => 12;
+ public int GroundAngle => 45;
+ public int JumpSpeed => 300;
+ public float Gravity => 800f;
+ public float SpeedMultiplier { get; set; } = 1;
+
+ HashSet<string> 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<Player>
+{
+ 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<DroppedWeapon>())
+ {
+ entity.Delete();
+ }
+
+ var detectivesNeeded = 1;
+ var murderersNeeded = 1;
+
+ List<SpawnPoint> spawnpoints = Entity.All.OfType<SpawnPoint>().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<Entity, int> Blinded = new Dictionary<Entity, int>();
+ 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]
+<style>
+.hidden {
+ display: none;
+}
+</style>
+
+<root>
+ <BlindedOverlay/>
+ <RoleOverlay/>
+ <ChatBox/>
+ <VoiceList/>
+ <PhaseTimer/>
+ <Crosshair/>
+ <Reload/>
+ <PlayerInfo/>
+</root>
+
+@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
+
+<style>
+ playerinfo {
+ position: absolute;
+ left: 30px;
+ bottom: 30px;
+ background-color: rgba(0, 0, 0, 0.20);
+ backdrop-filter-blur: 8px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 10px;
+ padding: 10px;
+ }
+</style>
+
+<Health Colour="@GetTeamColour()"></Health>
+<TeamInfo Colour="@GetTeamColour()"></TeamInfo>
+
+@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
+
+<style>
+Health {
+ width: 400px;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.90);
+}
+.fill {
+ height: 100%;
+ transition: width 0.2s ease-out;
+}
+</style>
+<div class="fill" style="background-color: @(Colour); width: @(GetHealth())%;"></div>
+
+@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
+
+<root class="loadingpanel">
+
+ <div class="title">LOADING</div>
+ <div class="subtitle">@Progress.Title</div>
+
+ @if (Progress.Fraction > 0)
+ {
+ <div class="progress">
+ <div class="bar" style="width: @(Progress.Percent)%;"></div>
+ <div class="text-left">@(Progress.Percent.ToString("0") )%</div>
+ <div class="text-right"><span>@(Progress.Mbps.ToString("0"))</span><span class="unit">Mbps</span></div>
+ </div>
+ }
+
+ <div class="controls">
+ <div class="button" onclick="@Game.Menu.CancelLoading">
+ Cancel
+ </div>
+ </div>
+
+</root>
+
+@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
+
+<style>
+.wip-warning {
+ position: absolute;
+ z-index: 1000000;
+ background-color: #FF4136;
+ font-family: 'Roboto';
+ font-size: 40px;
+ width: 100%;
+ font-weight: 700;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+</style>
+<root class="gamemainmenu">
+
+
+ <div class="wip-warning">Work in progress! Expect bugs, missing features, and general weird-ness. Things *will* break.</div>
+
+ <div class="navigator-canvas" slot="navigator-canvas"></div>
+
+
+</root>
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
+
+<style>
+ @@keyframes fadeIn
+ {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+ blindedoverlay
+ {
+ position: absolute;
+ left: 0;
+ top: 0;
+ background-color: rgba(0, 0, 0, 0.80);
+ width: 100vw;
+ height: 100vh;
+ backdrop-filter-blur: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-direction: column;
+ gap: 100vh;
+ animation-name: fadeIn;
+ animation-duration: 1s;
+ animation-iteration-count: 1;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ }
+ .overlay-message
+ {
+ margin: 20vh;
+ color: white;
+ font-size: 35px;
+ font-family: "Roboto";
+ font-weight: 700;
+ }
+</style>
+
+<div class="overlay-message">
+You shot a bystander!
+</div>
+
+@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
+
+<style>
+ roleoverlay
+ {
+ position: absolute;
+ left: 0;
+ top: 0;
+ background-color: #000000;
+ width: 100vw;
+ height: 100vh;
+ z-index: 10000;
+ }
+ .container {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ max-width: 700px;
+ text-align: center;
+ gap: 50px;
+ margin: 0 auto;
+ top: 200px;
+ }
+ .name
+ {
+ font-size: 50;
+ font-weight: 700;
+ font-family: "Roboto";
+ }
+ .description
+ {
+ font-size: 30;
+ font-family: "Roboto";
+ }
+</style>
+
+<div class="container">
+ <div class="name" style="color: @(GetTeamColour())">
+ @GetTeamName()
+ </div>
+ <div class="description" style="color: @(GetTeamColour())">
+ @GetTeamDescription()
+ </div>
+</div>
+
+@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]
+
+<div>
+ @GetPhase().Title
+</div>
+
+@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
+
+<style>
+phasetimer {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ margin: 30px auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.box {
+ background-color: rgba(0, 0, 0, 0.20);
+ backdrop-filter-blur: 8px;
+ padding: 5px;
+ color: white;
+ font-weight: 700;
+ font-size: 30px;
+ font-family: "Roboto";
+}
+</style>
+
+@if (HasTime())
+{
+<div class="box">
+ @GetTime()
+</div>
+} else
+{
+<div class="box">
+ @GetPhase()
+</div>
+}
+
+@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
+
+<style>
+.team-info {
+ font-size: 35px;
+ font-weight: 700;
+ font-family: "Roboto";
+}
+</style>
+
+<div class="team-info" style="color: @(Colour)">
+@GetTeamName()
+</div>
+
+@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
+
+<style>
+crosshair {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+}
+.dot {
+ border-radius: 3px;
+ width: 6px;
+ height: 6px;
+ background-color: white;
+}
+</style>
+
+<div class="dot"></div>
+
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
+
+<style>
+@@keyframes blink {
+ 0% {
+ background-color: rgba(0, 0, 0, 0.20);
+ }
+ 49% {
+ background-color: rgba(0, 0, 0, 0.20);
+ }
+ 50% {
+ background-color: rgba(255, 0, 0, 0.20);
+ }
+ 100% {
+ background-color: rgba(255, 0, 0, 0.20);
+ }
+}
+reload {
+ width: 100vw;
+ height: 100vh;
+}
+.box {
+ position: absolute;
+ left: 47%;
+ top: 60%;
+ width: 6%;
+ backdrop-filter-blur: 8px;
+ background-color: rgba(0, 0, 0, 0.20);
+ padding: 5px;
+ color: white;
+ font-weight: 700;
+ font-size: 30px;
+ font-family: "Roboto";
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.blink {
+ animation-name: blink;
+ animation-duration: 0.33s;
+}
+</style>
+
+@if (ReloadNeeded) {
+<!--<div class="box @(@Blink ? "blink" : "")">-->
+<div class="box blink">
+ <div>Reload</div>
+</div>
+}
+
+@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<TraceResult> 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 );
+ }
+}
|
