ButtonMasherPlayerController.cs
using Sandbox;
using System;
using System.Linq;
using System.Threading.Tasks;
public sealed partial class ButtonMasherPlayerController : Component
{
public enum TeamType
{
[Icon( "group" )]
Player1,
[Icon( "group" )]
Player2
}
private static readonly Texture TeamLogoRed = Texture.Load( FileSystem.Mounted, "textures/red-icon.png", false );
private static readonly Texture TeamLogoBlue = Texture.Load( FileSystem.Mounted, "textures/blu-icon.png", false );
[Property]
public GameObject TracerPrefab { get; set; }
[Property]
public float TracerSpeed { get; set; } = 6000f;
[Property]
public int MagazineSize { get; set; } = 12;
[Property, Sync( SyncFlags.FromHost )]
public int AmmoInMagazine { get; set; } = 12;
[Property]
public float ReloadDuration { get; set; } = 1.5f;
[Property, Sync( SyncFlags.FromHost ), Change( nameof( OnReloadChanged ) )]
public bool b_reload { get; set; }
[Property, Sync( SyncFlags.FromHost ), Change( nameof( OnReloadingChanged ) )]
public bool b_reloading { get; set; }
private TimeUntil _reloadFinish;
private TimeUntil _reloadAnimReset;
[Property, Sync( SyncFlags.FromHost )]
public string Name { get; set; } = "Player";
[Property, Sync( SyncFlags.FromHost )]
public TeamType Team { get; set; }
[Property, Sync( SyncFlags.FromHost )]
public float MaxHealth { get; set; } = 100f;
[Property, Sync( SyncFlags.FromHost ), Change( nameof( OnHealthChanged ) )]
public float Health { get; set; } = 100f;
[Property, Sync( SyncFlags.FromHost ), Change( nameof( OnIsDeadChanged ) )]
public bool IsDead { get; set; }
[Property, Sync( SyncFlags.FromHost )]
public GameObject OriginalSpawn { get; set; }
[Property, Sync( SyncFlags.FromHost ), Change( nameof( OnAttackChanged ) )]
public bool b_attack { get; set; }
[Property]
public float RespawnDelay { get; set; } = 3f;
[Property]
public float InteractRange { get; set; } = 150f;
[Property]
public float Attack1Range { get; set; } = 3000f;
[Property]
public float AttackDamage { get; set; } = 10f;
[Property]
public SoundEvent GunshotSound { get; set; }
[Property]
public SoundEvent ReloadSound { get; set; }
[Property]
public GameObject MuzzleFlashPrefab { get; set; }
public float HealthFraction => MaxHealth <= 0f ? 0f : Health / MaxHealth;
private TimeUntil _timeUntilRespawn;
private TimeUntil _attackReset;
private Vector3? _pendingRespawnPosition;
private Rotation? _pendingRespawnRotation;
protected override void OnStart()
{
if ( Networking.IsHost )
{
Health = MaxHealth;
IsDead = false;
b_attack = false;
b_reload = false;
b_reloading = false;
AmmoInMagazine = MagazineSize;
foreach ( var button in GameObject.Scene.GetAllComponents<Score_Button>() )
{
button.SyncScoreToClient( button.Count );
}
}
var modelRenderer = GameObject.Components.Get<SkinnedModelRenderer>();
if ( modelRenderer != null && GameObject.Components.Get<ModelPhysics>() == null )
{
var ragdoll = AddComponent<ModelPhysics>();
ragdoll.Renderer = modelRenderer;
ragdoll.Enabled = true;
ragdoll.MotionEnabled = false;
}
UpdateBodyMorph();
}
protected override void OnUpdate()
{
ApplyPendingRespawnTransform();
if ( Networking.IsHost )
{
if ( IsDead && _timeUntilRespawn <= 0f )
{
RespawnOnHost();
}
if ( b_attack && _attackReset <= 0f )
{
b_attack = false;
}
if ( b_reload && _reloadAnimReset <= 0f )
{
b_reload = false;
}
if ( b_reloading && _reloadFinish <= 0f )
{
FinishReloadOnHost();
}
}
if ( !IsLocalOwner() )
return;
DrawHud();
if ( IsDead )
{
UpdateDeathCamera();
return;
}
UpdateViewModel();
if ( Input.Pressed( "use" ) )
{
PerformInteraction();
}
if ( Input.Pressed( "reload" ) )
{
RequestReloadOnHost();
}
if ( Input.Pressed( "attack1" ) )
{
if ( b_reloading )
return;
if ( AmmoInMagazine <= 0 )
{
RequestReloadOnHost();
return;
}
var traceResult = PerformWeaponTrace( Attack1Range );
var burst = FindInHierarchy( GameObject, "Burst" );
if ( burst == null )
return;
var startPos = burst.WorldPosition;
var endPos = traceResult.EndPosition;
PlayGunshotSound();
PlayMuzzleFlash();
PlayTracerBetweenPoints( startPos, endPos );
using ( Rpc.FilterExclude( Connection.Local ) )
{
BroadcastAttackEffects( startPos, endPos );
}
TriggerAttackOnHost();
RequestDamageFromAttack( traceResult );
RequestConsumeAmmoOnHost();
}
}
private async void PlayTracerBetweenPoints( Vector3 startPos, Vector3 endPos )
{
if ( TracerPrefab == null )
return;
if ( startPos.AlmostEqual( endPos ) )
return;
var direction = (endPos - startPos).Normal;
var distance = startPos.Distance( endPos );
var lifetime = distance / TracerSpeed;
var tracer = TracerPrefab.Clone( startPos );
tracer.WorldPosition = startPos;
tracer.WorldRotation = Rotation.LookAt( direction );
// If your particle component exposes a way to restart / set parameters, do it here.
// The prefab itself should already have forward motion configured at TracerSpeed.
await Task.DelaySeconds( lifetime );
if ( tracer.IsValid() )
tracer.Destroy();
}
[Rpc.Host]
private void RequestConsumeAmmoOnHost()
{
if ( IsDead || b_reloading )
return;
if ( AmmoInMagazine <= 0 )
return;
AmmoInMagazine = Math.Max( AmmoInMagazine - 1, 0 );
}
[Rpc.Host]
private void RequestReloadOnHost()
{
if ( IsDead )
return;
if ( b_reloading )
return;
if ( AmmoInMagazine >= MagazineSize )
return;
b_reload = false;
b_reload = true;
b_reloading = true;
_reloadAnimReset = 0.05f;
_reloadFinish = ReloadDuration;
BroadcastReloadSound();
}
private void FinishReloadOnHost()
{
if ( !Networking.IsHost )
return;
AmmoInMagazine = MagazineSize;
b_reloading = false;
b_reload = false;
}
private void RequestDamageFromAttack( SceneTraceResult traceResult )
{
if ( !traceResult.Hit )
return;
var scaledDamage = AttackDamage * GetDamageMultiplier( traceResult );
RequestDamageOnHost( traceResult.GameObject, scaledDamage );
}
[Rpc.Broadcast]
private void BroadcastAttackEffects( Vector3 startPos, Vector3 endPos )
{
PlayGunshotSound();
PlayMuzzleFlash();
PlayTracerBetweenPoints( startPos, endPos );
}
private float GetDamageMultiplier( SceneTraceResult trace )
{
var hitObject = trace.GameObject;
if ( hitObject == null || !hitObject.IsValid() )
return 1.0f;
var name = hitObject.Name.ToLowerInvariant();
if ( name.Contains( "head" ) || name.Contains( "neck" ) )
return 2.0f;
if ( name.Contains( "arm" ) || name.Contains( "hand" ) ||
name.Contains( "leg" ) || name.Contains( "foot" ) )
return 0.8f;
return 1.0f;
}
[Rpc.Host]
private void RequestDamageOnHost( GameObject hitObject, float damage )
{
if ( hitObject == null || !hitObject.IsValid() )
return;
var targetPlayer = hitObject.Components.Get<ButtonMasherPlayerController>( FindMode.EverythingInSelfAndAncestors );
if ( targetPlayer == null || targetPlayer.IsDead )
return;
targetPlayer.Health = (targetPlayer.Health - damage).Clamp( 0f, targetPlayer.MaxHealth );
}
private bool IsLocalOwner()
{
return !IsProxy && GameObject.Scene.Camera is not null;
}
private void PlayGunshotSound()
{
if ( GunshotSound == null )
return;
var vmWeapon = GameObject.Children.FirstOrDefault( x => x.Name == "vm_weapon" );
if ( vmWeapon != null )
{
vmWeapon.PlaySound( GunshotSound );
return;
}
GameObject.PlaySound( GunshotSound );
}
private void PlayReloadSound()
{
if ( ReloadSound == null )
return;
var vmWeapon = GameObject.Children.FirstOrDefault( x => x.Name == "vm_weapon" );
if ( vmWeapon != null )
{
vmWeapon.PlaySound( ReloadSound );
return;
}
GameObject.PlaySound( ReloadSound );
}
[Rpc.Broadcast]
private void BroadcastReloadSound()
{
PlayReloadSound();
}
private GameObject FindInHierarchy( GameObject root, string name )
{
if ( root.Name == name )
return root;
foreach ( var child in root.Children )
{
var found = FindInHierarchy( child, name );
if ( found != null )
return found;
}
return null;
}
private async void PlayMuzzleFlash()
{
if ( MuzzleFlashPrefab == null )
return;
var burst = FindInHierarchy( GameObject, "Burst" );
if ( burst == null )
return;
var flash = MuzzleFlashPrefab.Clone( burst.WorldPosition );
flash.WorldRotation = burst.WorldRotation;
flash.SetParent( burst );
await Task.DelaySeconds( 0.15f );
if ( flash.IsValid() )
{
flash.Destroy();
}
}
private void UpdateDeathCamera()
{
var camera = GameObject.Scene.Camera;
if ( camera == null )
return;
var bodyChild = GameObject.Children.FirstOrDefault( x => x.Name == "Body" );
if ( bodyChild == null )
return;
var renderer = bodyChild.Components.Get<SkinnedModelRenderer>();
if ( renderer != null )
{
var headAttachment =
renderer.GetAttachment( "eyes" ) ??
renderer.GetAttachment( "eye" ) ??
renderer.GetAttachment( "head" ) ??
renderer.GetAttachment( "camera" );
if ( headAttachment.HasValue )
{
var tx = headAttachment.Value;
camera.WorldPosition = tx.Position;
camera.WorldRotation = tx.Rotation;
return;
}
}
camera.WorldPosition = bodyChild.WorldPosition + bodyChild.WorldRotation.Up * 64f;
camera.WorldRotation = bodyChild.WorldRotation;
}
private void DrawHud()
{
var camera = GameObject.Scene.Camera;
if ( camera == null )
return;
var hud = camera.Hud;
var screenCenter = new Vector2( Screen.Width * 0.5f, Screen.Height * 0.5f );
DrawTeamLogo( hud );
// Score tracking connected to button presses
long team1Score = GetTeamButtonCount( TeamType.Player1 );
long team2Score = GetTeamButtonCount( TeamType.Player2 );
// Red score
var scope1 = new TextRendering.Scope( $"Red Score:\n{team1Score}", Color.Red, 24 );
var size1 = scope1.Measure();
hud.DrawText( scope1, new Vector2( Screen.Width * 0.30f - size1.x * 0.5f, 40 ), TextFlag.Center );
// Blue score
var scope2 = new TextRendering.Scope( $"Blue Score:\n{team2Score}", Color.Blue, 24 );
var size2 = scope2.Measure();
hud.DrawText( scope2, new Vector2( Screen.Width * 0.75f - size2.x * 0.5f, 40 ), TextFlag.Center );
var scope3 = new TextRendering.Scope( $"WORK IN PROGRESS", Color.Green, 18 );
var size3 = scope3.Measure();
hud.DrawText( scope3, new Vector2( Screen.Width * 1f - size3.x * .5f, 40 ), TextFlag.Center );
// Crosshair
hud.DrawRect( new Rect( screenCenter.x - 1, screenCenter.y - 1, 2, 2 ), new Color( 1f, 1f, 1f, 0.7f ) );
hud.DrawLine( screenCenter - new Vector2( 5, 0 ), screenCenter - new Vector2( 15, 0 ), 1, new Color( 1f, 1f, 1f, 0.5f ) );
hud.DrawLine( screenCenter + new Vector2( 15, 0 ), screenCenter + new Vector2( 5, 0 ), 1, new Color( 1f, 1f, 1f, 0.5f ) );
hud.DrawLine( screenCenter - new Vector2( 0, 15 ), screenCenter - new Vector2( 0, 5 ), 1, new Color( 1f, 1f, 1f, 0.5f ) );
hud.DrawLine( screenCenter + new Vector2( 0, 15 ), screenCenter + new Vector2( 0, 5 ), 1, new Color( 1f, 1f, 1f, 0.5f ) );
// Ammo counter
var ammoScope = new TextRendering.Scope( $"{AmmoInMagazine}/{MagazineSize}", Color.White, 28 );
var ammoSize = ammoScope.Measure();
hud.DrawText(
ammoScope,
new Vector2( Screen.Width - ammoSize.x - 40, Screen.Height - 80 ),
TextFlag.LeftTop
);
}
private void ApplyPendingRespawnTransform()
{
if ( !_pendingRespawnPosition.HasValue || !_pendingRespawnRotation.HasValue )
return;
GameObject.WorldPosition = _pendingRespawnPosition.Value;
GameObject.WorldRotation = _pendingRespawnRotation.Value;
_pendingRespawnPosition = null;
_pendingRespawnRotation = null;
}
private void UpdateViewModel()
{
var vmWeapon = GameObject.Children.FirstOrDefault( c => c.Name == "vm_weapon" );
if ( vmWeapon == null )
return;
var camera = GameObject.Scene.Camera;
if ( camera == null )
return;
vmWeapon.WorldPosition =
camera.WorldPosition +
camera.WorldRotation.Forward * 0.5f +
camera.WorldRotation.Right * 0.3f +
camera.WorldRotation.Down * 0.3f;
vmWeapon.WorldRotation = camera.WorldRotation;
}
private long GetTeamButtonCount( TeamType team )
{
long total = 0;
foreach ( var button in GameObject.Scene.GetAllComponents<Score_Button>() )
{
if ( button.Team == team )
{
total += button.Count;
}
}
return total;
}
private void DrawTeamLogo( Sandbox.Rendering.HudPainter hud )
{
var texture = Team == TeamType.Player1 ? TeamLogoRed : TeamLogoBlue;
var teamColor = Team == TeamType.Player1 ? Color.Red : Color.Blue;
if ( texture == null )
{
hud.DrawRect( new Rect( 20, 20, 64, 64 ), teamColor );
return;
}
float scale = 0.10f;
float width = texture.Width * scale;
float height = texture.Height * scale;
hud.DrawTexture( texture, new Rect( 20f, 20f, width, height ), Color.White );
}
[Rpc.Host]
private void TriggerAttackOnHost()
{
if ( IsDead || b_reloading )
return;
b_attack = false;
b_attack = true;
_attackReset = 0.02f;
}
private void OnAttackChanged( bool oldValue, bool newValue )
{
UpdateBodyMorph();
}
private void OnReloadChanged( bool oldValue, bool newValue )
{
UpdateBodyMorph();
}
private void OnReloadingChanged( bool oldValue, bool newValue )
{
UpdateBodyMorph();
}
private void UpdateBodyMorph()
{
var bodyChild = GameObject.Children.FirstOrDefault( x => x.Name == "Body" );
if ( bodyChild == null )
return;
var skinnedRenderer = bodyChild.Components.Get<SkinnedModelRenderer>();
if ( skinnedRenderer == null )
return;
skinnedRenderer.Set( "b_attack", b_attack );
skinnedRenderer.Set( "b_reload", b_reload );
skinnedRenderer.Set( "b_reloading", b_reloading );
}
private void OnHealthChanged( float oldValue, float newValue )
{
if ( Networking.IsHost && !IsDead && newValue <= 0f )
{
KillOnHost();
}
}
private void OnIsDeadChanged( bool oldValue, bool newValue )
{
if ( !newValue && IsLocalOwner() )
{
var camera = GameObject.Scene.Camera;
if ( camera != null )
{
camera.WorldPosition = GameObject.WorldPosition;
camera.WorldRotation = GameObject.WorldRotation;
}
}
}
private void KillOnHost()
{
if ( !Networking.IsHost || IsDead )
return;
b_attack = false;
b_reload = false;
b_reloading = false;
IsDead = true;
Health = 0f;
_reloadFinish = 0f;
_reloadAnimReset = 0f;
_timeUntilRespawn = RespawnDelay;
RagdollBroadcast();
}
[Rpc.Broadcast]
private void RagdollBroadcast()
{
var parentRagdoll = GameObject.Components.Get<ModelPhysics>();
if ( parentRagdoll != null )
{
parentRagdoll.MotionEnabled = true;
}
var bodyChild = GameObject.Children.FirstOrDefault( x => x.Name == "Body" );
var bodyRagdoll = bodyChild?.Components.Get<ModelPhysics>();
if ( bodyRagdoll != null )
{
bodyRagdoll.MotionEnabled = true;
}
}
[Rpc.Broadcast]
private void RespawnOnHost()
{
AmmoInMagazine = MagazineSize;
b_reload = false;
b_reloading = false;
Health = MaxHealth;
IsDead = false;
b_attack = false;
_reloadFinish = 0f;
_reloadAnimReset = 0f;
var parentRagdoll = GameObject.Components.Get<ModelPhysics>();
if ( parentRagdoll != null )
{
parentRagdoll.MotionEnabled = false;
}
var bodyChild = GameObject.Children.FirstOrDefault( x => x.Name == "Body" );
var bodyRagdoll = bodyChild?.Components.Get<ModelPhysics>();
if ( bodyRagdoll != null )
{
bodyRagdoll.MotionEnabled = false;
}
if ( OriginalSpawn is not null && OriginalSpawn.IsValid() )
{
TeleportToSpawn( OriginalSpawn.WorldPosition, OriginalSpawn.WorldRotation );
}
}
[Rpc.Broadcast]
private void TeleportToSpawn( Vector3 position, Rotation rotation )
{
_pendingRespawnPosition = position;
_pendingRespawnRotation = rotation;
}
private void PerformInteraction()
{
var traceResult = PerformTrace( InteractRange );
if ( !traceResult.Hit )
return;
RequestButtonIncrementOnHost( traceResult.GameObject );
}
private SceneTraceResult PerformWeaponTrace( float range )
{
var camera = GameObject.Scene.Camera;
var burst = FindInHierarchy( GameObject, "Burst" );
if ( camera == null || burst == null )
return default;
// 1) Camera aim from crosshair
var aimStart = camera.WorldPosition;
var aimEnd = aimStart + camera.WorldRotation.Forward * range;
var aimTrace = GameObject.Scene.Trace
.Ray( aimStart, aimEnd )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
var targetPoint = aimTrace.Hit ? aimTrace.EndPosition : aimEnd;
// 2) Real shot from muzzle toward target point
var shotStart = burst.WorldPosition;
var shotDirection = (targetPoint - shotStart).Normal;
var shotEnd = shotStart + shotDirection * range;
return GameObject.Scene.Trace
.Ray( shotStart, shotEnd )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
}
[Rpc.Host]
private void RequestButtonIncrementOnHost( GameObject hitObject )
{
if ( hitObject == null || !hitObject.IsValid() )
return;
var scoreButton = hitObject.Components.Get<Score_Button>( FindMode.EverythingInSelfAndAncestors );
if ( scoreButton == null )
return;
if ( scoreButton.Team != Team )
return;
scoreButton.CountIncrement();
}
private SceneTraceResult PerformTrace( float range )
{
var camera = GameObject.Scene.Camera;
if ( camera == null )
return default;
var startPos = camera.WorldPosition;
var endPos = startPos + camera.WorldRotation.Forward * range;
Gizmo.Draw.Line( startPos, endPos );
DebugOverlay.Line( startPos, endPos, Color.Cyan, 3f );
return GameObject.Scene.Trace
.Ray( startPos, endPos )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
}
}