1740 results

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Twitch Poop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "garry" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "garry.twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/6/2026 7:39:31 PM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.128.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.128.0")]
using Sandbox.UI;

namespace Sandbox;

public interface ICleanupEvents
{
	public void OnCleanup( int removedObjects, int restoredObjects );
}

/// <summary>
/// A system that tracks the baseline scene state and allows resetting the map to its original state.
/// Removes all spawned props and restores destroyed map objects while leaving players untouched.
/// </summary>
internal sealed class CleanupSystem : GameObjectSystem<CleanupSystem>, ISceneLoadingEvents
{
	/// <summary>
	/// Set of GameObjects that existed in the original scene baseline.
	/// </summary>
	private readonly HashSet<Guid> _baselineObjectIds = new();

	/// <summary>
	/// Serialized data of baseline objects so we can restore them if destroyed.
	/// </summary>
	private readonly Dictionary<Guid, string> _baselineObjectData = new();

	private static bool _restorePersistedBaseline;
	private static HashSet<Guid> _persistedBaselineIds;
	private static Dictionary<Guid, string> _persistedBaselineData;

	/// <summary>
	/// Whether a baseline has been captured.
	/// </summary>
	public bool HasBaseline => _baselineObjectIds.Count > 0;

	public CleanupSystem( Scene scene ) : base( scene )
	{
	}

	/// <summary>
	/// Call from SaveSystem before Game.ChangeScene() to snapshot the current baseline
	/// </summary>
	public static void PreserveBaselineForSaveLoad()
	{
		if ( Current is null || !Current.HasBaseline ) return;

		_restorePersistedBaseline = true;
		_persistedBaselineIds = new HashSet<Guid>( Current._baselineObjectIds );
		_persistedBaselineData = new Dictionary<Guid, string>( Current._baselineObjectData );
	}

	void ISceneLoadingEvents.BeforeLoad( Scene scene, SceneLoadOptions options )
	{
		// Clear any existing baseline when a new scene is loading
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();
	}

	async Task ISceneLoadingEvents.OnLoad( Scene scene, SceneLoadOptions options, LoadingContext context )
	{
		// We don't care if the game is not playing
		if ( !Game.IsPlaying ) return;

		// Wait for next frame to ensure all objects are spawned
		await Task.Yield();

		// Could be null if the scene was unloaded before this runs
		if ( !Scene.IsValid() ) return;

		// When loading a save, restore the baseline captured before the scene was destroyed
		if ( _restorePersistedBaseline && _persistedBaselineIds is not null )
		{
			_baselineObjectIds.UnionWith( _persistedBaselineIds );
			foreach ( var kvp in _persistedBaselineData )
				_baselineObjectData.TryAdd( kvp.Key, kvp.Value );

			_restorePersistedBaseline = false;
			Log.Info( $"CleanupSystem: Restored persisted baseline with {_baselineObjectIds.Count} objects." );
		}
		else
		{
			CaptureBaseline();
		}
	}

	/// <summary>
	/// Captures the current scene state as the baseline.
	/// All objects that exist at this point are considered part of the original map.
	/// </summary>
	public void CaptureBaseline()
	{
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();

		foreach ( var go in Scene.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( go );
		}

		Log.Info( $"CleanupSystem: Captured baseline with {_baselineObjectIds.Count} objects." );
	}

	private void CaptureObjectRecursive( GameObject go )
	{
		if ( !go.IsValid() )
			return;

		// Skip player objects
		if ( IsPlayerObject( go ) )
			return;

		if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
			return;

		_baselineObjectIds.Add( go.Id );

		var serialized = go.Serialize();
		if ( serialized is not null )
		{
			_baselineObjectData[go.Id] = serialized.ToJsonString();
		}

		foreach ( var child in go.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( child );
		}
	}

	/// <summary>
	/// Determines if a GameObject is a player or belongs to a player.
	/// </summary>
	private static bool IsPlayerObject( GameObject go )
	{
		if ( !go.IsValid() )
			return false;

		if ( go.Components.Get<Player>( true ) is not null )
			return true;

		if ( go.Components.Get<PlayerData>( true ) is not null )
			return true;

		var parent = go.Parent;
		while ( parent is not null && parent != go.Scene )
		{
			if ( parent.Components.Get<Player>( true ) is not null )
				return true;
			if ( parent.Components.Get<PlayerData>( true ) is not null )
				return true;
			parent = parent.Parent;
		}

		return false;
	}

	/// <summary>
	/// Cleans up the scene by removing all spawned objects and restoring destroyed baseline objects.
	/// Players and their belongings are preserved.
	/// </summary>
	public void Cleanup()
	{
		if ( !HasBaseline )
		{
			Log.Warning( "CleanupSystem: No baseline captured. Cannot cleanup." );
			return;
		}

		if ( !Networking.IsHost )
		{
			Log.Warning( "CleanupSystem: Only the host can perform cleanup." );
			return;
		}

		var removedCount = 0;
		var restoredCount = 0;
		var objectsToRemove = new List<GameObject>();
		var existingBaselineIds = new HashSet<Guid>();

		foreach ( var go in Scene.GetAllObjects( true ) )
		{
			if ( !go.IsValid() )
				continue;

			// Never remove player objects
			if ( IsPlayerObject( go ) )
				continue;

			if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
				continue;

			if ( _baselineObjectIds.Contains( go.Id ) )
			{
				existingBaselineIds.Add( go.Id );
			}
			else
			{
				if ( go.Parent == Scene )
				{
					objectsToRemove.Add( go );
				}
			}
		}

		// Remove spawned objects
		foreach ( var go in objectsToRemove )
		{
			if ( go.IsValid() )
			{
				go.Destroy();
				removedCount++;
			}
		}

		// Restore destroyed baseline objects
		foreach ( var kvp in _baselineObjectData )
		{
			var id = kvp.Key;

			// Skip if the object still exists
			if ( existingBaselineIds.Contains( id ) )
				continue;

			// Skip if we already processed the parent object
			var go = Scene.Directory.FindByGuid( id );
			if ( go.IsValid() )
				continue;

			try
			{
				var json = System.Text.Json.Nodes.JsonNode.Parse( kvp.Value );
				if ( json is System.Text.Json.Nodes.JsonObject jso )
				{
					var restored = new GameObject();
					restored.Deserialize( jso );
					restoredCount++;
				}
			}
			catch ( System.Exception ex )
			{
				Log.Warning( $"CleanupSystem: Failed to restore object {id}: {ex.Message}" );
			}
		}

		BroadcastCleanup( removedCount, restoredCount );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private static void BroadcastCleanup( int removedObjects, int restoredObjects )
	{
		Game.ActiveScene?.RunEvent<ICleanupEvents>( x => x.OnCleanup( removedObjects, restoredObjects ) );

		Log.Info( $"Cleanup complete. Removed {removedObjects} spawned objects, restored {restoredObjects} destroyed objects." );
	}

	/// <summary>
	/// Console command to cleanup the map.
	/// </summary>
	[ConCmd( "cleanup" )]
	public static void CleanupCommand( string targetName = null )
	{
		if ( !Networking.IsHost ) return;

		//
		// Targeted cleanup, doesn't use the same cleanup shit
		//
		if ( !string.IsNullOrEmpty( targetName ) )
		{
			var target = GameManager.FindPlayerWithName( targetName );
			if ( target is not null )
			{
				CleanupPlayer( target );
			}
			else
			{
				Notices.AddNotice( "cleaning_services", Color.Red, $"Can't find {targetName} to clean up" );
			}

			return;
		}

		if ( Current is null )
		{
			Log.Warning( "CleanupSystem: No active cleanup system." );
			return;
		}

		Current.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpMine()
	{
		CleanupPlayer( Rpc.Caller );
	}

	[Rpc.Host]
	public static void RpcCleanUpAll()
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		Current?.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpTarget( Connection target )
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		CleanupPlayer( target );
	}

	public static void CleanupPlayer( Connection caller )
	{
		Assert.True( Networking.IsHost, "Only the host may call this method!" );

		var removable = Game.ActiveScene.GetAllComponents<Ownable>()
			.Where( o => o.Owner == caller );

		var count = 0;
		foreach ( var ownable in removable.ToArray() )
		{
			ownable.GameObject.Destroy();
			count++;
		}

		Notices.SendNotice( caller, "cleaning_services", Color.Green, $"Cleaned up {count} objects" );
	}

}
[Alias( "dynamite" )]
public sealed class DynamiteEntity : Component, IPlayerControllable, Component.IDamageable
{
	[Property, Range( 1, 500 ), Step( 1 ), ClientEditable]
	public float Damage { get; set; } = 128;

	[Property, Range( 16, 4096 ), Step( 16 ), ClientEditable]
	public float Radius { get; set; } = 1024f;

	[Property, Range( 1, 100 ), Step( 1 ), ClientEditable]
	public float Force { get; set; } = 1;

	[Property, Sync, ClientEditable]
	public ClientInput Activate { get; set; }

	bool _isDead = false;

	[Rpc.Host]
	public void Explode()
	{
		_isDead = true;

		var explosionPrefab = ResourceLibrary.Get<PrefabFile>( "/prefabs/engine/explosion_med.prefab" );
		if ( explosionPrefab == null )
		{
			Log.Warning( "Can't find /prefabs/engine/explosion_med.prefab" );
			return;
		}

		var go = GameObject.Clone( explosionPrefab, new CloneConfig { Transform = WorldTransform.WithScale( 1 ), StartEnabled = false } );
		if ( !go.IsValid() ) return;

		go.RunEvent<RadiusDamage>( x =>
		{
			x.Radius = Radius;
			x.PhysicsForceScale = Force;
			x.DamageAmount = Damage;
			x.Attacker = go;
		}, FindMode.EverythingInSelfAndDescendants );

		go.Enabled = true;
		go.NetworkSpawn( true, null );

		GameObject.Destroy();
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		if ( _isDead ) return;
		if ( IsProxy ) return;

		Explode();
	}

	void IPlayerControllable.OnControl()
	{
		if ( Activate.Pressed() )
		{
			Explode();
		}
	}

	void IPlayerControllable.OnEndControl()
	{
		// nothing to do
	}

	void IPlayerControllable.OnStartControl()
	{
		// nothing to do
	}
}
/// <summary>
/// Whether the emitter fires while the input is held, or toggles on/off with a press.
/// </summary>
public enum EmitMode
{
	/// <summary>
	/// Press once to turn on, press again to turn off.
	/// </summary>
	Toggle,
	/// <summary>
	/// Emits only while the input is held down.
	/// </summary>
	Hold,
}

/// <summary>
/// A world-placed SENT that spawns and controls a particle/VFX emitter.
/// The emitter prefab is defined by a <see cref="ScriptedEmitter"/> resource.
/// </summary>
[Alias( "emitter" )]
public sealed class EmitterEntity : Component, IPlayerControllable
{
	/// <summary>
	/// The emitter definition points to a prefab containing a particle system.
	/// </summary>
	[Property, ClientEditable]
	public ScriptedEmitter Emitter { get; set; }

	/// <summary>
	/// Whether this emitter toggles on/off with a press, or emits only while held.
	/// </summary>
	[Property, ClientEditable]
	public EmitMode Mode { get; set; } = EmitMode.Toggle;

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Toggle"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput ToggleInput { get; set; }

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Hold"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput HoldInput { get; set; }

	/// <summary>
	/// Whether the emitter is currently active. Synced to all clients.
	/// </summary>
	[Sync] public bool IsEmitting { get; private set; }

	/// <summary>
	/// When enabled, forces the emitter on regardless of input or mode.
	/// Can be set from the editor or wired up externally.
	/// </summary>
	[Property, ClientEditable]
	public bool ManualOn
	{
		get => _manualOn;
		set { _manualOn = value; if ( !IsProxy ) UpdateEmitState(); }
	}
	private bool _manualOn;
	private bool _inputEmitting;

	private GameObject _particleInstance;
	private ScriptedEmitter _lastEmitter;

	protected override void OnStart() { }

	protected override void OnUpdate()
	{
		// Emitter resource changed — destroy existing instance so it gets recreated
		if ( _lastEmitter != Emitter && _particleInstance.IsValid() )
			DestroyParticle();

		_lastEmitter = Emitter;

		if ( IsEmitting && !_particleInstance.IsValid() )
			SpawnParticle();
		else if ( !IsEmitting && _particleInstance.IsValid() )
			DestroyParticle();
	}

	void IPlayerControllable.OnStartControl() { }
	void IPlayerControllable.OnEndControl()
	{
		if ( Mode == EmitMode.Hold )
		{
			_inputEmitting = false;
			UpdateEmitState();
		}
	}

	void IPlayerControllable.OnControl()
	{
		if ( Mode == EmitMode.Toggle )
		{
			if ( ToggleInput.Pressed() )
			{
				_inputEmitting = !_inputEmitting;
				UpdateEmitState();
			}
		}
		else
		{
			var held = HoldInput.Down();
			if ( held != _inputEmitting )
			{
				_inputEmitting = held;
				UpdateEmitState();
			}
		}
	}

	private void UpdateEmitState() => SetEmitting( _inputEmitting || _manualOn );

	[Rpc.Broadcast]
	private void SetEmitting( bool active )
	{
		IsEmitting = active;
	}

	private void SpawnParticle()
	{
		if ( !Emitter.IsValid() || Emitter.Prefab is null ) return;

		_particleInstance = GameObject.Clone( Emitter.Prefab, new CloneConfig
		{
			Parent = GameObject,
			Transform = new Transform( Vector3.Forward * 4f ),
			StartEnabled = true,
		} );
	}

	private void DestroyParticle()
	{
		_particleInstance.Destroy();
		_particleInstance = null;
	}
}


public sealed class SpotLightEntity : Component, IPlayerControllable
{
	[Property, ClientEditable, Group( "Light" )]
	public bool On { get; set { field = value; UpdateLight(); } } = true;

	[Property, ClientEditable, Group( "Light" )]
	public bool Shadows { get; set { field = value; UpdateLight(); } } = true;

	[Property, Range( 0, 1 ), ClientEditable, Group( "Light" )]
	public Color Color { get; set { field = value; UpdateLight(); } }

	[Property, Range( 0, 50 ), ClientEditable, Group( "Light" )]
	public float Brightness { get; set { field = value; UpdateLight(); } } = 2;

	[Property, Range( 0, 1000 ), ClientEditable, Group( "Light" )]
	public float Radius { get; set { field = value; UpdateLight(); } } = 500;

	[Property, Range( 0, 90 ), ClientEditable, Group( "Light" )]
	public float Angle { get; set { field = value; UpdateLight(); } } = 35;

	[Property, Range( 0, 16 ), ClientEditable, Group( "Light" )]
	public float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;


	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOn { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOff { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput Toggle { get; set; }

	[Property]
	public GameObject OnGameObject { get; set; }

	[Property]
	public GameObject OffGameObject { get; set; }

	void IPlayerControllable.OnControl()
	{

		if ( Toggle.Pressed() )
		{
			On = !On;
		}

		if ( TurnOn.Pressed() )
		{
			On = true;
		}

		if ( TurnOff.Pressed() )
		{
			On = false;
		}
	}

	void IPlayerControllable.OnEndControl()
	{

	}

	void IPlayerControllable.OnStartControl()
	{

	}

	void UpdateLight()
	{
		OnGameObject?.Enabled = On;
		OffGameObject?.Enabled = !On;

		if ( GetComponentInChildren<SpotLight>( true ) is not SpotLight light )
			return;

		light.Enabled = On;

		var color = Color;
		color.r *= Brightness;
		color.g *= Brightness;
		color.b *= Brightness;

		light.Shadows = Shadows;
		light.LightColor = color;
		light.Radius = Radius;
		light.Attenuation = Attenuation;
		light.ConeOuter = Angle;
		light.ConeInner = Angle * 0.5f;

		Network.Refresh();
	}
}
public partial class BaseBulletWeapon : BaseWeapon
{
	[Property]
	public SoundEvent ShootSound { get; set; }

	[Property, Group( "Bullet" )]
	public BulletConfiguration Bullet { get; set; } = new()
	{
		Damage = 12f,
		BulletRadius = 1f,
		Range = 4096f,
		AimConeBase = new Vector2( 0.5f, 0.25f ),
		AimConeSpread = new Vector2( 3f, 3f ),
		AimConeRecovery = 0.2f,
		RecoilPitch = new Vector2( -0.3f, -0.1f ),
		RecoilYaw = new Vector2( -0.1f, 0.1f ),
		CameraRecoilStrength = 1f,
		CameraRecoilFrequency = 1f,
	};

	[Property, Group( "Bullet" ), ClientEditable, Range( 0f, 500000f ), Step( 10f )]
	public float ShootForce { get; set; } = 100000f;

	protected TimeSince TimeSinceShoot = 0;

	/// <summary>
	/// Returns 0 for no aim spread, 1 for full aim cone, based on time since last shot.
	/// </summary>
	protected float GetAimConeAmount( float recovery )
	{
		return TimeSinceShoot.Relative.Remap( 0, recovery, 1, 0 );
	}

	/// <summary>
	/// Returns the aim cone amount using the configured recovery time
	/// </summary>
	protected float GetAimConeAmount()
	{
		return GetAimConeAmount( Bullet.AimConeRecovery );
	}

	/// <inheritdoc cref="ShootBullet(float, in BulletConfiguration)"/>
	protected void ShootBullet( float fireRate )
	{
		ShootBullet( fireRate, Bullet );
	}

	/// <summary>
	/// Shoot a bullet out of the front of the gun.
	/// When held by a player, fires from the player's eye with aim cone and recoil.
	/// When standalone (no owner), fires straight from the weapon's muzzle.
	/// </summary>
	protected void ShootBullet( float fireRate, in BulletConfiguration config )
	{
		if ( HasOwner && ( !HasAmmo() || IsReloading() ) )
		{
			TryAutoReload();
			return;
		}

		if ( TimeUntilNextShotAllowed > 0 )
			return;

		// Only consume ammo when held by a player
		if ( HasOwner && !TakeAmmo( 1 ) )
		{
			AddShootDelay( 0.2f );
			return;
		}

		AddShootDelay( fireRate );

		var aimConeAmount = GetAimConeAmount( config.AimConeRecovery );
		var forward = AimRay.Forward
			.WithAimCone(
				config.AimConeBase.x + aimConeAmount * config.AimConeSpread.x,
				config.AimConeBase.y + aimConeAmount * config.AimConeSpread.y
			);
		var traceRay = AimRay with { Forward = forward };

		var tr = Scene.Trace.Ray( traceRay, config.Range )
			.IgnoreGameObjectHierarchy( AimIgnoreRoot )
			.WithCollisionRules( "bullet" )
			.WithoutTags( "playercontroller" )
			.Radius( config.BulletRadius )
			.UseHitboxes()
			.Run();

		ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );
		TraceAttack( TraceAttackInfo.From( tr, config.Damage ) );
		TimeSinceShoot = 0;

		// Recoil only applies when held by a player
		if ( !HasOwner )
		{
			// Simulate physical recoil by pushing the weapon opposite to its fire direction
			if ( ShootForce > 0f && GetComponent<Rigidbody>( true ) is var rb )
			{
				var muzzle = WeaponModel?.MuzzleTransform?.WorldTransform ?? WorldTransform;
				rb.ApplyForce( muzzle.Rotation.Up * ShootForce );
			}
			return;
		}

		Owner.Controller.EyeAngles += new Angles(
			Random.Shared.Float( config.RecoilPitch.x, config.RecoilPitch.y ),
			Random.Shared.Float( config.RecoilYaw.x, config.RecoilYaw.y ),
			0
		);

		if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
		{
			_ = new Sandbox.CameraNoise.Recoil( config.CameraRecoilStrength, config.CameraRecoilFrequency );
		}
	}

	[Rpc.Broadcast]
	public void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject, Surface hitSurface, Vector3? origin = null, bool noEvents = false )
	{
		if ( Application.IsDedicatedServer ) return;
		if ( !hitSurface.IsValid() ) return;

		Owner?.Controller.Renderer.Set( "b_attack", true );

		if ( !noEvents )
		{
			if ( WeaponModel.IsValid() )
			{
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.OnAttack() );
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.CreateRangedEffects( this, hitpoint, origin ) );
			}

			if ( ShootSound.IsValid() )
			{
				var snd = GameObject.PlaySound( ShootSound );

				// If we're shooting, the sound should not be spatialized
				if ( HasOwner && Owner.IsLocalPlayer && snd.IsValid() )
				{
					snd.SpacialBlend = 0;
				}
			}
		}

		if ( !hit || !hitObject.IsValid() )
			return;

		var baseSurface = hitSurface.GetBaseSurface();
		var bulletSound = hitSurface.SoundCollection.Bullet ?? baseSurface?.SoundCollection.Bullet;
		if ( bulletSound.IsValid() )
		{
			Sound.Play( bulletSound, hitpoint );
		}

		var prefab = hitSurface.PrefabCollection.BulletImpact ?? baseSurface?.PrefabCollection.BulletImpact;

		// Still null?
		if ( prefab is null )
			return;

		var fwd = Rotation.LookAt( normal * -1.0f, Vector3.Random );

		var impact = prefab.Clone();
		impact.WorldPosition = hitpoint;
		impact.WorldRotation = fwd;
		impact.SetParent( hitObject, true );

		if ( hitObject.GetComponentInChildren<SkinnedModelRenderer>() is not { CreateBoneObjects: true } skinned )
			return;

		// find closest bone
		var bones = skinned.GetBoneTransforms( true );

		var closestDist = float.MaxValue;

		for ( var i = 0; i < bones.Length; i++ )
		{
			var bone = bones[i];
			var dist = bone.Position.Distance( hitpoint );
			if ( dist < closestDist )
			{
				closestDist = dist;
				impact.SetParent( skinned.GetBoneObject( i ), true );
			}
		}
	}

	public record struct BulletConfiguration
	{
		public float Damage { get; set; }
		public float BulletRadius { get; set; }
		public Vector2 AimConeBase { get; set; }
		public Vector2 AimConeSpread { get; set; }
		public float AimConeRecovery { get; set; }
		public Vector2 RecoilPitch { get; set; }
		public Vector2 RecoilYaw { get; set; }
		public float CameraRecoilStrength { get; set; }
		public float CameraRecoilFrequency { get; set; }
		public float Range { get; set; }
	}
}
using System.Threading;

public partial class BaseWeapon
{
	/// <summary>
	/// Should we consume 1 bullet per reload instead of filling the clip?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool IncrementalReloading { get; set; } = false;

	/// <summary>
	/// Extra delay after the first shell reload before subsequent shells begin (e.g. longer carrier insertion animation).
	/// Only used with incremental reloading. If zero, no extra delay is added.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float FirstShellReloadTime { get; set; } = 0f;

	/// <summary>
	/// Delay before the first shell is inserted during incremental reload.
	/// If zero, uses <see cref="ReloadTime"/>.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float ReloadStartTime { get; set; } = 0f;

	/// <summary>
	/// Can we cancel reloads?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool CanCancelReload { get; set; } = true;

	private CancellationTokenSource reloadToken;
	private bool isReloading;

	public bool CanReload()
	{
		if ( !UsesClips ) return false;
		if ( ClipContents >= ClipMaxSize ) return false;
		if ( isReloading ) return false;
		if ( !WeaponConVars.InfiniteReserves && ReserveAmmo <= 0 ) return false;

		return true;
	}

	public bool IsReloading() => isReloading;

	public virtual void CancelReload()
	{
		if ( reloadToken?.IsCancellationRequested == false )
		{
			reloadToken?.Cancel();
			isReloading = false;

			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadCancel() );
		}
	}

	public virtual async void OnReloadStart()
	{
		if ( !CanReload() )
			return;

		CancelReload();

		var cts = new CancellationTokenSource();
		reloadToken = cts;
		isReloading = true;

		try
		{
			await ReloadAsync( cts.Token );
		}
		finally
		{
			// Only clean up our own reload
			if ( reloadToken == cts )
			{
				isReloading = false;
				reloadToken = null;
			}
			cts.Dispose();
		}
	}

	[Rpc.Broadcast]
	private void BroadcastReload()
	{
		if ( !HasOwner ) return;

		Assert.True( Owner.Controller.IsValid(), "BaseWeapon::BroadcastReload - Player Controller is invalid!" );
		Assert.True( Owner.Controller.Renderer.IsValid(), "BaseWeapon::BroadcastReload - Renderer is invalid!" );

		Owner.Controller.Renderer.Set( "b_reload", true );
	}

	protected virtual async Task ReloadAsync( CancellationToken ct )
	{
		// Capture so we can tell if a newer reload has replaced us by the time finally runs.
		var mySource = reloadToken;
		var isFirstShell = ClipContents == 0;

		try
		{
			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadStart() );

			BroadcastReload();

			var firstIteration = true;

			while ( ClipContents < ClipMaxSize && !ct.IsCancellationRequested )
				{
					var delay = (firstIteration && IncrementalReloading && ReloadStartTime > 0f) ? ReloadStartTime : ReloadTime;
					firstIteration = false;
					await Task.DelaySeconds( delay, ct );

					var needed = IncrementalReloading ? 1 : (ClipMaxSize - ClipContents);

					if ( WeaponConVars.InfiniteReserves )
					{
						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );
						ClipContents += needed;
					}
					else
					{
						var available = Math.Min( needed, ReserveAmmo );

						if ( available <= 0 )
							break;

						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );

						ReserveAmmo -= available;
						ClipContents += available;
					}

					// After the first shell, wait longer before the next one starts
					if ( isFirstShell && FirstShellReloadTime > 0f )
					{
						await Task.DelaySeconds( FirstShellReloadTime, ct );
					}

					isFirstShell = false;
				}
		}
		finally
		{
			if ( reloadToken == mySource )
			{
				ViewModel?.RunEvent<ViewModel>( x => x.OnReloadFinish() );
			}
		}
	}
}
/// <summary>
/// The local user's preferences in Deathmatch
/// </summary>
internal static class GamePreferences
{
	/// <summary>
	/// Enables automatic switching to better weapons on item pickup
	/// </summary>
	[ConVar( "sb.autoswitch", ConVarFlags.UserInfo | ConVarFlags.Saved )]
	public static bool AutoSwitch { get; set; } = true;

	/// <summary>
	/// Enables fast switching between inventory weapons
	/// </summary>
	[ConVar( "sb.fastswitch", ConVarFlags.Saved )]
	public static bool FastSwitch { get; set; } = false;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.viewbob", ConVarFlags.Saved )]
	[Group( "Camera" )]
	public static bool ViewBobbing { get; set; } = true;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.screenshake", ConVarFlags.Saved )]
	[Range( 0.1f, 2f ), Step( 0.1f ), Group( "Camera" )]
	public static float Screenshake { get; set; } = 0.3f;
}
namespace Sandbox.Npcs;

/// <summary>
/// Console variables that control NPC AI behaviour globally.
/// </summary>
public static class NpcConVars
{
	/// <summary>
	/// When disabled, all NPC AI thinking is paused — they just stand idle.
	/// </summary>
	[ConVar( "sb.ai.enabled", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "Enable or disable NPC AI thinking." )]
	public static bool Enabled { get; set; } = true;

	/// <summary>
	/// When enabled, NPCs cannot target players.
	/// </summary>
	[ConVar( "sb.ai.notarget", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "When enabled, NPCs cannot target players." )]
	public static bool NoTarget { get; set; } = false;
}
using Sandbox.Npcs.Layers;
using Sandbox.Npcs.Tasks;

namespace Sandbox.Npcs.Schedules;

/// <summary>
/// Panic flee — scream while sprinting away from the source.
/// </summary>
public sealed class ScientistFleeSchedule : ScheduleBase
{
	private static readonly string[] PanicLines =
	[
		"AHHH!",
		"Don't hurt me!",
		"Help! HELP!",
		"Stay away from me!",
		"I'm just a scientist!",
		"Please, no!",
		"Somebody help!",
		"Oh god oh god oh god!",
		"What did I do?!",
		"Leave me alone!",
	];

	public GameObject Source { get; set; }

	/// <summary>
	/// 0–1 panic intensity. Higher values mean faster speed and longer flee distance.
	/// </summary>
	public float PanicLevel { get; set; } = 0.5f;

	protected override void OnStart()
	{
		if ( !Source.IsValid() ) return;

		// Sprint speed scales with panic (200–350)
		Npc.Navigation.WishSpeed = 200f + 150f * PanicLevel;

		// Don't stare at the player — look where we're running
		Npc.Animation.ClearLookTarget();

		// Scream immediately — but only if not already mid-speech
		if ( Npc.Speech.CanSpeak )
		{
			var line = PanicLines[Game.Random.Int( 0, PanicLines.Length - 1 )];
			Npc.Speech.Say( line, 2f );
		}

		// Flee direction — away from the attacker with some randomness
		var awayDir = (GameObject.WorldPosition - Source.WorldPosition).WithZ( 0 ).Normal;
		var randomAngle = Game.Random.Float( -40f, 40f );
		awayDir = Rotation.FromAxis( Vector3.Up, randomAngle ) * awayDir;

		// Distance scales with panic (200–500)
		var fleeDist = 512f + 1024f * PanicLevel;
		var fleeTarget = GameObject.WorldPosition + awayDir * fleeDist;

		// Snap to navmesh
		if ( Npc.Scene.NavMesh.GetClosestPoint( fleeTarget ) is { } navPoint )
		{
			AddTask( new MoveTo( navPoint, 15f ) );
		}
		else
		{
			AddTask( new MoveTo( fleeTarget, 15f ) );
		}
	}

	protected override void OnEnd()
	{
		// Reset to normal walk speed
		// TODO: this is shit, can we scope these somehow so the IDisposable handles all this ?
		Npc.Navigation.WishSpeed = 100f;
	}

	protected override bool ShouldCancel()
	{
		return !Source.IsValid();
	}
}
/// <summary>
/// Apply fall damage to the player
/// </summary>
public class PlayerFallDamage : Component, Local.IPlayerEvents
{
	[RequireComponent] public Player Player { get; set; }

	/// <summary>
	/// Fatal fall speed, you will die if you fall at or above this speed
	/// </summary>
	[Property] public float FatalFallSpeed { get; set; } = 1536.0f;

	/// <summary>
	/// Maximum safe fall speed, you won't take damage at or below this speed
	/// </summary>
	[Property] public float MaxSafeFallSpeed { get; set; } = 512.0f;

	/// <summary>
	/// Multiply damage amount by this much
	/// </summary>
	[Property] public float DamageMultiplier { get; set; } = 1.0f;

	/// <summary>
	/// Fall damage sound
	/// </summary>
	[Property] public SoundEvent FallSound { get; set; }

	[Rpc.Owner]
	private void PlayFallSound()
	{
		GameObject.PlaySound( FallSound );
	}

	void Local.IPlayerEvents.OnLand( float distance, Vector3 velocity )
	{
		var fallSpeed = Math.Abs( velocity.z );

		if ( fallSpeed <= MaxSafeFallSpeed )
			return;

		var damageAmount = MathX.Remap( fallSpeed, MaxSafeFallSpeed, FatalFallSpeed, 0f, 100f ) * DamageMultiplier;
		if ( damageAmount < 1 ) return;

		if ( Networking.IsHost && damageAmount >= Player.Health )
			Player.PlayerData?.AddStat( "player.fall.death" );

		TakeFallDamage( damageAmount );
	}


	[Rpc.Broadcast]
	public void TakeFallDamage( float amount )
	{
		if ( !Networking.IsHost ) return;


		if ( Player is IDamageable damage )
		{
			var dmg = new DamageInfo( amount.CeilToInt(), Player.GameObject, null );
			dmg.Tags.Add( DamageTags.Fall );
			damage.OnDamage( dmg );

			PlayFallSound();
		}
	}
}
/// <summary>
/// Manages loadout persistence, presets, and restoration for a player.
/// Lives on the Player GameObject alongside PlayerInventory.
/// Listens to inventory events to auto-save, and handles all loadout RPCs directly.
/// </summary>
public sealed class PlayerLoadout : Component, Local.IPlayerEvents, Global.IPlayerEvents, Global.ISaveEvents
{
	[RequireComponent] public Player Player { get; set; }
	[RequireComponent] public PlayerInventory Inventory { get; set; }

	private bool _isRestoringLoadout;

	/// <summary>
	/// One entry in a serialized loadout: the prefab resource path and the slot it occupies.
	/// </summary>
	public struct LoadoutEntry
	{
		public string PrefabPath { get; set; }
		public int Slot { get; set; }
		public string SpawnerDataPayload { get; set; }
	}

	public struct SavedPreset
	{
		public string Name { get; set; }
		public string LoadoutJson { get; set; }
	}

	public static IReadOnlyList<SavedPreset> GetLoadoutPresets()
	{
		return LocalData.Get<List<SavedPreset>>( "presets", new() );
	}

	public static void SaveLoadoutPreset( string name, string loadoutJson )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		var idx = presets.FindIndex( p => p.Name == name );
		var entry = new SavedPreset { Name = name, LoadoutJson = loadoutJson };
		if ( idx >= 0 )
			presets[idx] = entry;
		else
			presets.Add( entry );
		LocalData.Set( "presets", presets );
	}

	public static void DeleteLoadoutPreset( string name )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		presets.RemoveAll( p => p.Name == name );
		LocalData.Set( "presets", presets );
	}

	public string SerializeLoadout()
	{
		var entries = Inventory.Weapons
			.Where( w => !string.IsNullOrEmpty( w.GameObject.PrefabInstanceSource ) )
			.Select( w => new LoadoutEntry
			{
				PrefabPath = w.GameObject.PrefabInstanceSource,
				Slot = w.InventorySlot,
				SpawnerDataPayload = (w as SpawnerWeapon)?.SpawnerData
			} )
			.ToList();

		return entries.Count > 0 ? Json.Serialize( entries ) : null;
	}

	public void SaveLoadout()
	{
		if ( _isRestoringLoadout ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		if ( Player.IsLocalPlayer )
		{
			LocalData.Set( "hotbar", json );
		}
		else
		{
			PushLoadoutToClient( json );
		}
	}

	public void GiveLoadoutWeapons( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		_isRestoringLoadout = true;
		try
		{
			foreach ( var entry in entries )
			{
				if ( !Inventory.Pickup( entry.PrefabPath, entry.Slot, false ) )
					continue;

				if ( !string.IsNullOrEmpty( entry.SpawnerDataPayload ) && Inventory.GetSlot( entry.Slot ) is SpawnerWeapon spawnerWeapon )
				{
					spawnerWeapon.RestoreSpawnerData( entry.SpawnerDataPayload );
				}
			}
		}
		finally
		{
			_isRestoringLoadout = false;
		}
	}

	private static async Task EnsureMountedAsync( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		var needsMounts = entries.Any( e => !string.IsNullOrEmpty( e.SpawnerDataPayload )
			&& e.SpawnerDataPayload.EndsWith( ".vmdl", StringComparison.OrdinalIgnoreCase ) );

		if ( !needsMounts ) return;

		foreach ( var entry in Sandbox.Mounting.Directory.GetAll().Where( e => e.Available ) )
			await Sandbox.Mounting.Directory.Mount( entry.Ident );
	}

	public void SwitchToPreset( string loadoutJson )
	{
		if ( !Networking.IsHost )
		{
			HostSwitchToPreset( loadoutJson );
			return;
		}
		_ = SwitchToPresetAsync( loadoutJson );
	}

	public void ResetToDefault()
	{
		if ( !Networking.IsHost )
		{
			HostResetToDefault();
			return;
		}
		_ = ResetToDefaultAsync();
	}

	[Rpc.Host]
	private void HostSwitchToPreset( string loadoutJson )
	{
		_ = SwitchToPresetAsync( loadoutJson );
	}

	[Rpc.Host]
	private void HostResetToDefault()
	{
		_ = ResetToDefaultAsync();
	}

	private async Task SwitchToPresetAsync( string loadoutJson )
	{
		var previousSlot = Inventory.ActiveWeapon?.InventorySlot ?? 0;

		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( loadoutJson );
		GiveLoadoutWeapons( loadoutJson );

		var toEquip = Inventory.GetSlot( previousSlot ) ?? Inventory.GetBestWeapon();
		if ( toEquip.IsValid() )
			Inventory.SwitchWeapon( toEquip );

		SaveLoadout();
	}

	private async Task ResetToDefaultAsync()
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		Inventory.GiveDefaultWeapons();
		Inventory.SwitchWeapon( Inventory.GetBestWeapon() );
		SaveLoadout();
	}

	[Rpc.Owner]
	private void PushLoadoutToClient( string loadoutJson )
	{
		LocalData.Set( "hotbar", loadoutJson );
	}

	[Rpc.Owner]
	private void RequestClientLoadout()
	{
		var json = LocalData.Get<string>( "hotbar" );
		if ( !string.IsNullOrEmpty( json ) )
			HostRestoreLoadoutFromClient( json );
	}

	/// <summary>
	/// Clears the current inventory, waits a frame, then gives the loadout from JSON and equips the best weapon.
	/// </summary>
	private async Task ReplaceLoadoutAsync( string json )
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( json );
		GiveLoadoutWeapons( json );

		var best = Inventory.GetBestWeapon();
		if ( best.IsValid() )
			Inventory.SwitchWeapon( best );
	}

	[Rpc.Host]
	private async void HostRestoreLoadoutFromClient( string loadoutJson )
	{
		await ReplaceLoadoutAsync( loadoutJson );
	}

	void Global.IPlayerEvents.OnPlayerSpawned( Player player )
	{
		if ( player != Player ) return;
		if ( !Networking.IsHost ) return;

		_ = RestoreOnSpawnAsync();
	}

	private async Task RestoreOnSpawnAsync()
	{
		if ( Player.IsLocalPlayer )
		{
			var json = LocalData.Get<string>( "hotbar" );
			if ( !string.IsNullOrEmpty( json ) )
			{
				await ReplaceLoadoutAsync( json );
				return;
			}
		}
		else
		{
			RequestClientLoadout();
			return;
		}

		Inventory.GiveDefaultWeapons();
		var bestWeapon = Inventory.GetBestWeapon();
		if ( bestWeapon.IsValid() )
			Inventory.SwitchWeapon( bestWeapon );
	}

	void Local.IPlayerEvents.OnDied( PlayerDiedParams args )
	{
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnPickup( PlayerPickupEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnDrop( PlayerDropEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnRemoveWeapon( PlayerRemoveWeaponEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnMoveSlot( PlayerMoveSlotEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	private async Task SaveLoadoutAfterYield()
	{
		await Task.Yield();
		SaveLoadout();
	}

	void Global.ISaveEvents.BeforeSave( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		SaveSystem.Current?.SetMetadata( $"Loadout_{steamId}", json );
	}

	void Global.ISaveEvents.AfterLoad( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SaveSystem.Current?.GetMetadata( $"Loadout_{steamId}" );
		if ( string.IsNullOrEmpty( json ) ) return;

		_ = RestoreLoadoutFromSaveAsync( json );
	}

	private async Task RestoreLoadoutFromSaveAsync( string json )
	{
		await ReplaceLoadoutAsync( json );
	}
}
/// <summary>
/// Dead players become these. They try to observe their last corpse. 
/// </summary>
internal sealed class PlayerObserver : Component
{
	Angles EyeAngles;
	TimeSince timeSinceStarted;
	DeathCameraTarget _cachedCorpse;
	float currentDistance;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EyeAngles = Scene.Camera.WorldRotation;
		timeSinceStarted = 0;
		currentDistance = 32;

		_cachedCorpse = Scene.GetAllComponents<DeathCameraTarget>()
					.Where( x => x.Connection == Network.Owner )
					.OrderByDescending( x => x.Created )
					.FirstOrDefault();
	}

	protected override void OnUpdate()
	{
		// Don't allow immediate respawn
		if ( timeSinceStarted < 1 )
			return;

		// If pressed a button, or has been too long
		if ( Input.Pressed( "attack1" ) || Input.Pressed( "jump" ) || timeSinceStarted > 4f )
		{
			GameManager.Current?.RequestRespawn();
			GameObject.Destroy();
		}
	}

	protected override void OnPreRender()
	{
		if ( IsProxy ) return;

		if ( _cachedCorpse.IsValid() )
		{
			RotateAround( _cachedCorpse );
		}
	}

	private void RotateAround( Component target )
	{
		// Find the corpse eyes
		if ( target.Components.Get<SkinnedModelRenderer>().TryGetBoneTransform( "pelvis", out var tx ) )
		{
			tx.Position += Vector3.Up * 25;
		}

		var e = EyeAngles;
		e += Input.AnalogLook;
		e.pitch = e.pitch.Clamp( -90, 90 );
		e.roll = 0.0f;
		EyeAngles = e;

		currentDistance = currentDistance.LerpTo( 150, Time.Delta * 5 );

		var center = tx.Position;
		var targetPos = center - EyeAngles.Forward * currentDistance;

		var tr = Scene.Trace.FromTo( center, targetPos ).Radius( 1.0f ).WithoutTags( "ragdoll", "effect" ).Run();

		Scene.Camera.WorldPosition = tr.EndPosition;
		Scene.Camera.WorldRotation = EyeAngles;
	}
}

namespace Sandbox.UI;


public sealed class ResourceSelectAttribute : System.Attribute
{
	public string Extension { get; set; }
	public bool AllowPackages { get; set; }
}
public interface ISpawnMenuTab
{

}

namespace Sandbox.UI;

public class NoticePanel : Panel
{
	bool initialized;
	Vector3.SpringDamped _springy;

	public RealTimeUntil TimeUntilDie;

	/// <summary>
	/// If true, the notice won't auto-dismiss. Call <see cref="Dismiss"/> to remove it.
	/// </summary>
	public bool Manual { get; set; }

	public bool IsDead => !Manual && TimeUntilDie < 0;
	public bool wasDead = false;

	/// <summary>
	/// Dismiss a manual notice, causing it to slide out and be deleted.
	/// </summary>
	public void Dismiss()
	{
		Manual = false;
		TimeUntilDie = 0;
	}

	internal void UpdatePosition( Vector2 vector2 )
	{
		if ( initialized == false )
		{
			_springy = new Vector3.SpringDamped( new Vector3( Screen.Width + 50, vector2.y + Random.Shared.Float( -10, 10 ), 0 ), 0.0f );
			_springy.Velocity = Vector3.Random * 1000;
			initialized = true;
		}

		if ( !Manual && TimeUntilDie < 0.4f )
		{
			vector2.x -= 50;
		}

		// we're dead, push us out to rhe right
		if ( IsDead )
		{
			vector2.x = Screen.Width + 50;

			// we've been dead for 2 seconds, get rid of us
			if ( TimeUntilDie < -2 )
			{
				Delete();
				return;
			}

			wasDead = true;
		}

		_springy.Target = new Vector3( vector2.x, vector2.y, 0 );
		_springy.Frequency = 4;
		_springy.Damping = 0.5f;
		_springy.Update( RealTime.Delta * 1.0f );

		Style.Left = _springy.Current.x * ScaleFromScreen;
		Style.Top = _springy.Current.y * ScaleFromScreen;
	}
}
public static class Extensions
{
	public static Vector3 WithAimCone( this Vector3 direction, float degrees )
	{
		var angle = Rotation.LookAt( direction );
		angle *= new Angles( Game.Random.Float( -degrees / 2.0f, degrees / 2.0f ), Game.Random.Float( -degrees / 2.0f, degrees / 2.0f ), 0 );
		return angle.Forward;
	}

	public static Vector3 WithAimCone( this Vector3 direction, float horizontalDegrees, float verticalDegrees )
	{
		var angle = Rotation.LookAt( direction );
		angle *= new Angles( Game.Random.Float( -verticalDegrees / 2.0f, verticalDegrees / 2.0f ), Game.Random.Float( -horizontalDegrees / 2.0f, horizontalDegrees / 2.0f ), 0 );
		return angle.Forward;
	}
}
using Sandbox.Rendering;

public sealed class CameraWeapon : BaseWeapon
{
	float fov;
	float roll = 0;

	bool focusing;

	[Property] SoundEvent CameraShoot { get; set; }

	/// <summary>
	/// The RT camera's resolution 
	/// </summary>
	private static int _cameraResolution = 512;

	/// <summary>
	/// The render target texture produced by this camera. Read by <see cref="TVEntity"/>.
	/// </summary>
	public Texture RenderTexture => _renderTexture;

	private Texture _renderTexture;
	private CameraComponent _rtCamera;

	public override bool WantsHideHud => true;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EnsureRTCamera();
		EnsureRenderTexture();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnDestroy()
	{
		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnPreRender()
	{
		if ( !_rtCamera.IsValid() ) return;

		EnsureRenderTexture();

		if ( HasOwner && Scene.Camera.IsValid() )
		{
			// When held, mirror the player's camera so the TV shows their POV.
			// TODO: network some props to the TV so they show up in the RT camera when held by a player other than the host.
			_rtCamera.WorldPosition = Scene.Camera.WorldPosition;
			_rtCamera.WorldRotation = Scene.Camera.WorldRotation;
			_rtCamera.FieldOfView = Scene.Camera.FieldOfView;

			if ( !_rtCamera.RenderExcludeTags.Has( "viewer" ) )
				_rtCamera.RenderExcludeTags.Add( "viewer" );
		}
		else
		{
			_rtCamera.RenderExcludeTags.Remove( "viewer" );
			_rtCamera.FieldOfView = 40f;
		}
	}

	/// <summary>
	/// We want to control the camera fov when held by a player.
	/// </summary>
	public override void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
	{
		if ( !player.Network.IsOwner || !Network.IsOwner ) return;

		if ( fov > 0 )
			camera.FieldOfView = fov;

		camera.WorldRotation = camera.WorldRotation * new Angles( 0, 0, roll );
	}

	public override void OnCameraMove( Player player, ref Angles angles )
	{
		if ( Input.Down( "attack2" ) )
		{
			angles = default;
		}

		var currentFov = fov > 0 ? fov : Scene.Camera.FieldOfView;
		float sensitivity = currentFov.Remap( 1, 70, 0.01f, 1 );
		angles *= sensitivity;
	}

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "reload" ) )
		{
			fov = 0;
			roll = 0;
		}

		if ( Input.Down( "attack2" ) )
		{
			fov = ((fov > 0 ? fov : Scene.Camera.FieldOfView) + Input.AnalogLook.pitch).Clamp( 1, 150 );
			roll -= Input.AnalogLook.yaw;
		}

		if ( focusing && Input.Released( "attack1" ) )
		{
			Game.TakeScreenshot();
			Sandbox.Services.Stats.Increment( "photos", 1 );

			GameObject?.PlaySound( CameraShoot );
		}

		focusing = Input.Down( "attack1" );
	}

	private void EnsureRTCamera()
	{
		_rtCamera = GetComponentInChildren<CameraComponent>( true );

		if ( _rtCamera is null )
		{
			var go = new GameObject( GameObject, true, "rt_camera" );
			_rtCamera = go.AddComponent<CameraComponent>();
		}

		_rtCamera.IsMainCamera = false;
		_rtCamera.BackgroundColor = Color.Black;
		_rtCamera.ClearFlags = ClearFlags.Color | ClearFlags.Depth | ClearFlags.Stencil;
		_rtCamera.FieldOfView = Scene.Camera.FieldOfView;
		_rtCamera.RenderExcludeTags.Add( "viewmodel" );
	}

	private void EnsureRenderTexture()
	{
		if ( _renderTexture.IsValid() && _renderTexture.Width == _cameraResolution && _renderTexture.Height == _cameraResolution )
			return;

		CleanupRenderTexture();

		_renderTexture = Texture.CreateRenderTarget()
			.WithSize( _cameraResolution, _cameraResolution )
			.Create();

		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = _renderTexture;
		}
	}

	private void CleanupRenderTexture()
	{
		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = null;
		}

		_renderTexture?.Dispose();
		_renderTexture = null;
	}

	public override void DrawHud( HudPainter painter, Vector2 crosshair )
	{
		// nothing!
	}
}
using Sandbox.Rendering;
using Sandbox.Utility;

public sealed class RpgWeapon : BaseWeapon
{
	[Property] public float TimeBetweenShots { get; set; } = 2f;
	[Property] public GameObject ProjectilePrefab { get; set; }
	[Property] public SoundEvent ShootSound { get; set; }
	[Property] public float ProjectileSpeed { get; set; } = 1024f;

	/// <summary>
	/// When enabled, fired rockets will continuously track toward the player's crosshair.
	/// Toggle with right-click (player) or SecondaryInput (standalone/seat).
	/// </summary>
	[Property, Sync, ClientEditable] public bool IsTrackedAim { get; set; } = false;

	public override bool IsTargetedAim => IsTrackedAim;

	[Sync( SyncFlags.FromHost )] RpgProjectile Projectile { get; set; }

	TimeSince TimeSinceShoot;
	private bool _hasFired;
	private bool _waitingForReload;

	/// <summary>
	/// Whether a live rocket is currently being guided toward the crosshair.
	/// </summary>
	public bool IsGuiding => IsTrackedAim && Projectile.IsValid();

	protected override float GetPrimaryFireRate() => TimeBetweenShots;

	public override bool CanSecondaryAttack() => false;

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "attack2" ) )
			ToggleTrackedAim();

		// Auto-reload after firing
		if ( _hasFired && Input.Released( "attack1" ) )
		{
			_hasFired = false;

			if ( IsGuiding )
				_waitingForReload = true;
			else if ( CanReload() )
				OnReloadStart();
		}

		if ( IsGuiding )
		{
			var target = GetAimTarget();
			Projectile.UpdateWithTarget( target, ProjectileSpeed );
		}
		else if ( _waitingForReload )
		{
			_waitingForReload = false;
			if ( CanReload() )
				OnReloadStart();
		}
	}

	/// <summary>
	/// Standalone / seat control — uses SecondaryInput to toggle tracking.
	/// </summary>
	public override void OnControl()
	{
		base.OnControl();

		if ( HasOwner || IsProxy ) return;

		if ( SecondaryInput.Pressed() )
			ToggleTrackedAim();

		if ( IsGuiding )
		{
			var target = GetAimTarget();
			Projectile.UpdateWithTarget( target, ProjectileSpeed );
		}
	}

	[Rpc.Host]
	private void ToggleTrackedAim()
	{
		IsTrackedAim = !IsTrackedAim;
	}

	/// <summary>
	/// Traces from AimRay and returns the world-space point the player is looking at.
	/// </summary>
	private Vector3 GetAimTarget()
	{
		var ray = AimRay;
		var tr = Scene.Trace.Ray( ray, 16384f )
			.IgnoreGameObjectHierarchy( AimIgnoreRoot )
			.WithoutTags( "trigger", "projectile" )
			.Run();

		return tr.Hit ? tr.HitPosition : ray.Position + ray.Forward * 16384f;
	}

	public override void PrimaryAttack()
	{
		if ( HasOwner && !TakeAmmo( 1 ) )
		{
			TryAutoReload();
			return;
		}

		TimeSinceShoot = 0;
		AddShootDelay( TimeBetweenShots );

		if ( ViewModel.IsValid() )
			ViewModel.RunEvent<ViewModel>( x => x.OnAttack() );
		else if ( WorldModel.IsValid() )
			WorldModel.RunEvent<WorldModel>( x => x.OnAttack() );

		if ( ShootSound.IsValid() )
			GameObject.PlaySound( ShootSound );

		var ray = AimRay;
		var muzzlePos = MuzzleTransform.WorldTransform.Position;
		var spawnPos = muzzlePos + ray.Forward * 64f;

		if ( HasOwner )
		{
			spawnPos = CheckThrowPosition( Owner, muzzlePos, spawnPos );

			Owner.Controller.EyeAngles += new Angles( Random.Shared.Float( -0.2f, -0.3f ), Random.Shared.Float( -0.1f, 0.1f ), 0 );

			if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
			{
				new Sandbox.CameraNoise.Punch( new Vector3( Random.Shared.Float( 45, 35 ), Random.Shared.Float( -10, -5 ), 0 ), 1.5f, 2, 0.5f );
				new Sandbox.CameraNoise.Shake( 1f, 0.6f );

				_hasFired = true;
			}
		}

		CreateProjectile( spawnPos, ray.Forward, ProjectileSpeed );
	}

	private Vector3 CheckThrowPosition( Player player, Vector3 eyePosition, Vector3 grenadePosition )
	{
		var tr = Scene.Trace.Box( BBox.FromPositionAndSize( Vector3.Zero, 8.0f ), eyePosition, grenadePosition )
			.WithoutTags( "trigger", "ragdoll", "player", "effect" )
			.IgnoreGameObjectHierarchy( player.GameObject )
			.Run();

		if ( tr.Hit )
			return tr.EndPosition;

		return grenadePosition;
	}

	/// <summary>
	/// Creates the projectile with the host's permission
	/// </summary>
	[Rpc.Host]
	void CreateProjectile( Vector3 start, Vector3 direction, float speed )
	{
		var go = ProjectilePrefab?.Clone( start );

		var projectile = go.GetComponent<RpgProjectile>();
		Assert.True( projectile.IsValid(), "RpgProjectile not on projectile prefab" );

		if ( Owner.IsValid() )
			projectile.Instigator = Owner;
		else if ( ClientInput.Current.IsValid() )
			projectile.Instigator = ClientInput.Current;

		go.NetworkSpawn();

		Projectile = projectile;
		projectile.UpdateDirection( direction, speed );
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var tss = TimeSinceShoot.Relative.Remap( 0, 0.2f, 1, 0 );
		var w = 2;

		hud.SetBlendMode( BlendMode.Lighten );

		if ( IsTrackedAim )
		{
			// Diamond crosshair when in tracked aim mode
			Color guideColor = IsGuiding ? new Color( 1f, 0.5f, 0.1f ) : CrosshairCanShoot;
			var size = 32f;

			hud.DrawLine( center + new Vector2( 0, -size ), center + new Vector2( size, 0 ), w, guideColor );
			hud.DrawLine( center + new Vector2( size, 0 ), center + new Vector2( 0, size ), w, guideColor );
			hud.DrawLine( center + new Vector2( 0, size ), center + new Vector2( -size, 0 ), w, guideColor );
			hud.DrawLine( center + new Vector2( -size, 0 ), center + new Vector2( 0, -size ), w, guideColor );

			return;
		}

		Color color = !CanPrimaryAttack() ? CrosshairNoShoot : CrosshairCanShoot;

		var squareSize = 64f;

		hud.DrawLine( center + new Vector2( -squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, -squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, squareSize / 2 ), w, color );
		hud.DrawLine( center + new Vector2( -squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, -squareSize / 2 ), w, color );
	}
}
using System.Text.Json.Nodes;

/// <summary>
/// Holds a bunch of GameObject json, a bounding box, and some preview models for a
/// duplication. This is what gets serialized to a string and stored in the Duplicator tool.
/// The objects and the bounds are created in selection space. Where the user right clicked to 
/// select is 0,0,0, and the player's view yaw is the rotation identity.
/// </summary>
public class DuplicationData
{
	/// <summary>
	/// An array of JsonObject objects, which are serialzed GameObjects
	/// </summary>
	public JsonArray Objects { get; set; }

	/// <summary>
	/// The bounds are used to work out where to place the duplication, so it
	/// doesn't clip through the floor.
	/// </summary>
	public BBox Bounds { get; set; }

	/// <summary>
	/// Describes where to draw a model for the preview
	/// </summary>
	public record struct PreviewModel( Model Model, Transform Transform, Transform[] Bones, BBox Bounds );

	/// <summary>
	/// A list of preview models to help visualze where the duplication will be placed
	/// </summary>
	public List<PreviewModel> PreviewModels { get; set; }

	/// <summary>
	/// Packages used in this
	/// </summary>
	public List<string> Packages { get; set; }

	/// <summary>
	/// Create DuplicationData from a bunch of objects.
	/// center is the transform to use as the origin for the duplication.
	/// The rotation of center should be the player's view yaw when they made the selection.
	/// </summary>
	public static DuplicationData CreateFromObjects( IEnumerable<GameObject> objects, Transform center )
	{
		var dupe = new DuplicationData();
		dupe.Objects = new JsonArray();
		dupe.Bounds = BBox.FromPositionAndSize( 0, 0.01f );
		dupe.PreviewModels = new();

		List<BBox> worldBounds = new List<BBox>();

		foreach ( var obj in objects )
		{
			var entry = obj.Serialize();
			worldBounds.Add( GetWorldBounds( obj ) );

			var localized = center.ToLocal( obj.WorldTransform );
			entry["Position"] = JsonValue.Create( localized.Position );
			entry["Rotation"] = JsonValue.Create( localized.Rotation );
			entry["Scale"] = JsonValue.Create( localized.Scale );

			dupe.Objects.Add( entry );

			foreach ( var renderer in obj.GetComponentsInChildren<ModelRenderer>() )
			{
				var model = renderer.Model ?? Model.Cube;

				if ( model.IsError ) continue;

				Transform[] bones = null;

				if ( renderer is SkinnedModelRenderer skinned )
				{
					bones = skinned.GetBoneTransforms( false );
				}

				var modelTx = center.ToLocal( renderer.WorldTransform );
				dupe.PreviewModels.Add( new DuplicationData.PreviewModel( model, modelTx, bones, model.Bounds ) );
			}
		}

		if ( worldBounds.Count > 0 )
		{
			var txi = new Transform( -center.Position, center.Rotation.Inverse );

			dupe.Bounds = BBox.FromBoxes( worldBounds.Select( x => x.Transform( txi ) ) );
		}

		var packages = Cloud.ResolvePrimaryAssetsFromJson( dupe.Objects );
		dupe.Packages = packages.Select( x => x.FullIdent ).ToList();


		return dupe;
	}

	public static BBox GetWorldBounds( GameObject go )
	{
		BBox box = BBox.FromPositionAndSize( 0, 0.01f );

		var rb = go.GetComponentsInChildren<Collider>( false, true ).ToArray();
		if ( rb.Length > 0 )
		{
			box = rb[0].GetWorldBounds();

			foreach ( var b in rb )
			{
				box = box.AddBBox( b.GetWorldBounds() );
			}
		}

		return box;
	}
}

[Icon( "🔗" )]
[Title( "#tool.name.linker" )]
[ClassName( "linker" )]
[Group( "#tool.group.constraints" )]
public sealed class LinkerTool : BaseConstraintToolMode
{
	public override string Description => Stage == 1 ? "#tool.hint.linker.stage1" : "#tool.hint.linker.stage0";
	public override string PrimaryAction => Stage == 1 ? "#tool.hint.linker.finish" : "#tool.hint.linker.source";
	public override string ReloadAction => "#tool.hint.linker.remove";

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var link in linked.GetComponentsInChildren<ManualLink>( true ) )
			if ( linked == target || link.Body?.Root == target )
				yield return link.GameObject;
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		var go1 = new GameObject( point1.GameObject, false, "link" );
		var go2 = new GameObject( point2.GameObject, false, "link" );

		var link1 = go1.AddComponent<ManualLink>();
		var link2 = go2.AddComponent<ManualLink>();

		link1.Body = go2;
		link2.Body = go1;

		go2.NetworkSpawn();
		go1.NetworkSpawn();

		Track( go1, go2 );

		var undo = Player.Undo.Create();
		undo.Name = "Link";
		undo.Add( go1 );
	}
}


[Icon( "➖" )]
[Title( "#tool.name.slider" )]
[ClassName( "slider" )]
[Group( "#tool.group.constraints" )]
public sealed class SliderTool : BaseConstraintToolMode
{
	public override string Description => Stage == 1 ? "#tool.hint.slider.stage1" : "#tool.hint.slider.stage0";
	public override string PrimaryAction => Stage == 1 ? "#tool.hint.slider.finish" : "#tool.hint.slider.source";
	public override string SecondaryAction => Stage == 1 ? "#tool.hint.slider.secondary.stage1" : "#tool.hint.slider.secondary";
	public override string ReloadAction => "#tool.hint.slider.remove";

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var joint in linked.GetComponentsInChildren<SliderJoint>( true ) )
			if ( linked == target || joint.Body?.Root == target )
				yield return joint.GameObject;
	}

	protected override SelectionPoint? GetSecondaryPoint( SelectionPoint select )
	{
		return TraceFromRay( select.WorldTransform().ForwardRay, 4096, select.GameObject );
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		if ( point1.GameObject == point2.GameObject )
			return;

		var axis = Rotation.LookAt( Vector3.Direction( point1.WorldPosition(), point2.WorldPosition() ) );

		var go1 = new GameObject( false, "slider" );
		go1.Parent = point1.GameObject;
		go1.LocalTransform = point1.LocalTransform;
		go1.WorldRotation = axis;

		var go2 = new GameObject( false, "slider" );
		go2.Parent = point2.GameObject;
		go2.LocalTransform = point2.LocalTransform;
		go2.WorldRotation = axis;

		var cleanup = go1.AddComponent<ConstraintCleanup>();
		cleanup.Attachment = go2;

		var len = point1.WorldPosition().Distance( point2.WorldPosition() );

		var joint = go1.AddComponent<SliderJoint>();
		joint.Body = go2;
		joint.MinLength = 0;
		joint.MaxLength = len;
		joint.EnableCollision = true;

		var lineRenderer = go1.AddComponent<LineRenderer>();
		lineRenderer.Points = [go1, go2];
		lineRenderer.Width = 0.5f;
		lineRenderer.Color = Color.Black;
		lineRenderer.Lighting = true;
		lineRenderer.CastShadows = true;

		go2.NetworkSpawn();
		go1.NetworkSpawn();

		Track( go1, go2 );

		var undo = Player.Undo.Create();
		undo.Name = "Slider";
		undo.Add( go1 );
		undo.Add( go2 );
	}
}
public abstract partial class ToolMode
{
	[Rpc.Broadcast]
	public virtual void ShootEffects( SelectionPoint target )
	{
		if ( !Toolgun.IsValid() ) return;

		var player = Toolgun.Owner;
		if ( !player.IsValid() ) return;

		if ( !target.IsValid() )
		{
			Log.Warning( "ShootEffects: Unknown object" );
			return;
		}

		Toolgun.SpinCoil();

		var muzzle = Toolgun.MuzzleTransform;

		if ( Toolgun.SuccessImpactEffect is GameObject impactPrefab )
		{
			var wt = target.WorldTransform();
			wt.Rotation = wt.Rotation * new Angles( 90, 0, 0 );

			var impact = impactPrefab.Clone( wt, null, false );
			impact.Enabled = true;
		}

		if ( Toolgun.SuccessBeamEffect is GameObject beamEffect )
		{
			var wt = target.WorldTransform();

			var go = beamEffect.Clone( new Transform( muzzle.WorldTransform.Position ), null, false );

			foreach ( var beam in go.GetComponentsInChildren<BeamEffect>( true ) )
			{
				beam.TargetPosition = wt.Position;
			}

			go.Enabled = true;
		}

		Toolgun.ViewModel?.GetComponentInChildren<SkinnedModelRenderer>().Set( "b_attack", true );
	}

	public virtual void ShootFailEffects( SelectionPoint target )
	{

	}

}

using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothVector2
{
    public Vector2 Current;
    public Vector2 Target;
    public Vector2 Velocity;
    public float SmoothTime;

    public SmoothVector2(Vector2 initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        SmoothTime = smoothTime;
    }

    public void Update(float dt)
    {
        float vx = Velocity.x, vy = Velocity.y;
        Current = new Vector2(
            MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
            MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
        Velocity = new Vector2(vx, vy);
    }

    public bool IsSettled =>
        MathF.Abs(Target.x - Current.x) < 0.0001f &&
        MathF.Abs(Target.y - Current.y) < 0.0001f &&
        MathF.Abs(Velocity.x) < 0.0001f &&
        MathF.Abs(Velocity.y) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

// Style helpers hoisted so generated Blob facades share them. Keep the early-return form: an engine-type ternary that resolves bare null via an implicit string operator silently produces magenta (Color.Parse fallback) instead of the intended absent-property. See engine-fact memories.

internal static class StyleAccumulator
{
    static StyleList Rent(StyleList current)
        => ReferenceEquals(current, StyleList.Empty)
            ? BuildContext.Current.RentStyleList()
            : current;

    public static StyleList Add(StyleList current, StyleField field, Length? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromLength(value.Value));
        return list;
    }

    public static StyleList Add<TEnum>(StyleList current, StyleField field, TEnum? value, System.Func<TEnum, StyleValue> wrap) where TEnum : struct
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, wrap(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, Color? value, System.Func<Color, StyleValue> wrap)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, wrap(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, string? value)
    {
        if (value is null) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromString(value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, float? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromSingle(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, bool? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromBoolean(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, int? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromInt32(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, Texture? value)
    {
        if (value is null) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromTexture(value));
        return list;
    }
}
using System;
using Sandbox;

namespace Goo.FpsUI;

// ============================================================================
//  ALL demo / self-preview behaviour for the FPS UI pack lives in this ONE file.
//
//  DELETE THIS FILE to remove demo functionality.
//  deleting this file makes every Demo inspector toggle disappear
// ============================================================================
static class FpsDemo
{
    static readonly string[] Names = { "Vex", "Korr", "Juno", "Rhys", "Mara", "Dane", "Iko", "Sol" };
    static readonly (int Amount, string Action)[] Grants =
    {
        (100, "Kill"), (125, "Headshot"), (50, "Assist"), (200, "Double Kill"), (25, "Hitmarker"),
    };

    public static void Health( HealthModel m, ref float t, float dt )  // bleed in chunks, big heal when low
    {
        t += dt;
        if ( t < 2.4f ) return;
        t = 0f;
        if ( m.Health <= m.MaxHealth * 0.3f ) m.Heal( m.MaxHealth * 0.65f );
        else m.Damage( m.MaxHealth * 0.22f );
    }

    public static void Stamina( StaminaModel m, ref float t, float dt )  // sprint on/off cycle
    {
        t += dt;
        if ( t >= 3.1f ) { t = 0f; m.SetSprinting( !m.Sprinting ); }
        if ( m.Stamina <= 0f ) m.SetSprinting( false );
    }

    public static void Ammo( AmmoModel m, ref float t, float dt )  // steady fire, auto-reload on empty
    {
        t += dt;
        if ( m.Reloading ) return;
        if ( m.Mag <= 0 ) { m.Reload(); return; }
        if ( t >= 0.18f ) { t = 0f; m.Fire(); }
    }

    public static void Crosshair( CrosshairModel m, ref float t, float dt )  // steady fire cadence
    {
        t += dt;
        if ( t >= 0.18f ) { t = 0f; m.Fire(); }
    }

    public static void Hitmarker( HitmarkerModel m, ref float t, float dt )  // periodic pop
    {
        t += dt;
        if ( t >= 0.5f ) { t = 0f; m.Pop( false ); }
    }

    public static void Killfeed( KillfeedModel m, ref float t, ref int pick, float dt )  // periodic fake kills
    {
        t += dt;
        if ( t < 2.2f ) return;
        t = 0f;
        string a = Names[pick % Names.Length];
        string v = Names[(pick + 3) % Names.Length];
        // Alternate teams, occasionally make the local player the killer so the preview shows both.
        bool youKill = pick % 3 == 0;
        KillTeam at = youKill || pick % 2 == 0 ? KillTeam.Friendly : KillTeam.Enemy;
        KillTeam vt = at == KillTeam.Friendly ? KillTeam.Enemy : KillTeam.Friendly;
        pick++;
        m.Add( a, at, v, vt, attackerLocal: youKill );
    }

    public static void Xp( XpModel m, ref float t, ref int pick, float dt )  // periodic grants
    {
        t += dt;
        if ( t < 1.6f ) return;
        t = 0f;
        var g = Grants[pick % Grants.Length];
        pick++;
        m.Add( g.Amount, g.Action );
    }

    public static void Scoreboard( ScoreboardModel m, ref float t, float dt )  // loop the clock, nudge scores, cycle variant
    {
        if ( m.TimeRemaining <= 0f ) m.TimeRemaining = 600f;
        t += dt;
        if ( t < 4f ) return;
        t = 0f;
        m.Mode = m.Mode switch
        {
            ScoreboardMode.Tdm        => ScoreboardMode.Domination,
            ScoreboardMode.Domination => ScoreboardMode.Ffa,
            _                         => ScoreboardMode.Tdm,
        };
        m.FriendlyScore = (m.FriendlyScore + 7) % (m.ScoreLimit + 1);
        m.EnemyScore    = (m.EnemyScore + 5) % (m.ScoreLimit + 1);
        m.PlayerScore  += 150;
        m.LeaderScore   = Math.Max( m.LeaderScore, m.PlayerScore ) + 50;
        for ( int i = 0; i < m.Points.Length; i++ )  // rotate cap ownership
            m.Points[i] = (CapOwner)(((int)m.Points[i] + 1) % 3);
    }
}

// ---- per-widget demo hooks (the [Property] Demo toggle + timers + StepDemo body) ----

public sealed partial class HealthWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Health( _m, ref _demoT, dt );
    }
}

public sealed partial class StaminaWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Stamina( _m, ref _demoT, dt );
    }
}

public sealed partial class AmmoWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-fire in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Ammo( _m, ref _demoT, dt );
    }
}

public sealed partial class CrosshairWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-fire in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Crosshair( _m, ref _demoT, dt );
    }
}

public sealed partial class HitmarkerWidget
{
    [Property] public bool Demo { get; set; } = true;  // periodic pop in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Hitmarker( _m, ref _demoT, dt );
    }
}

public sealed partial class KillfeedWidget
{
    [Property] public bool Demo { get; set; } = true;  // fake kills in editor
    float _demoT;
    int _demoPick;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Killfeed( _m, ref _demoT, ref _demoPick, dt );
    }
}

public sealed partial class XpWidget
{
    [Property] public bool Demo { get; set; } = true;  // fake grants in editor
    float _demoT;
    int _demoPick;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Xp( _m, ref _demoT, ref _demoPick, dt );
    }
}

public sealed partial class ScoreboardWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Scoreboard( _m, ref _demoT, dt );
    }
}

// ---- the assembled HUD: one coordinated synthetic firefight ----

public sealed partial class FpsHud
{
    [Property, Group( "General" )] public bool Demo { get; set; } = true;  // self-run a synthetic firefight in editor

    readonly Random _demoRng = new();
    float _hpT, _stT, _kfT, _xpT, _sbT;  // per-subsystem demo timers
    int _kfPick, _xpPick;                // demo name / grant cursors
    float _triggerT, _modeT;             // synthetic trigger square-wave + fire-mode cycle

    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;

        // Cycle the fire mode every few seconds, then drive a synthetic trigger square-wave (held ~1s,
        // released ~0.4s) through the real fire control so each mode visibly fires differently.
        _modeT -= dt;
        if ( _modeT <= 0f ) { _modeT = 3.0f; CycleFireMode(); }
        _fire.Mode = FireMode; _fire.BurstCount = BurstCount; _fire.Rpm = RoundsPerMinute;
        _triggerT += dt;
        bool trigger = _triggerT % 1.4f < 1.0f;
        if ( !_ammo.Reloading )
        {
            if ( _ammo.Mag <= 0 ) _ammo.Reload();
            else if ( _fire.Tick( trigger, dt ) )
            {
                _ammo.Fire(); _crosshair.Fire();
                if ( _demoRng.NextSingle() < 0.5f ) _hitmarker.Pop( _demoRng.NextSingle() < 0.15f );
            }
        }

        FpsDemo.Health( _health, ref _hpT, dt );
        FpsDemo.Stamina( _stamina, ref _stT, dt );
        FpsDemo.Killfeed( _killfeed, ref _kfT, ref _kfPick, dt );
        FpsDemo.Xp( _xp, ref _xpT, ref _xpPick, dt );
        if ( ShowScoreboard ) FpsDemo.Scoreboard( _scoreboard, ref _sbT, dt );
    }
}
using System;
using Sandbox;

namespace Goo.FpsUI;

// A drop-in shootable target for the FPS pack. Attach to any GameObject that has a Collider; the HUD's
// hitscan damages it through the engine's Component.IDamageable interface (the same path TriggerHurt uses).
// Exposes Health / IsDead / Name so FpsHud can drive the hitmarker, killfeed, and XP on a hit or kill.
// Engine-side component, not a Goo panel.
[Title( "FPS Target" ), Category( "FPS UI" ), Icon( "my_location" )]
public sealed class FpsTarget : Component, Component.IDamageable
{
    [Property] public string Name { get; set; } = "Target";                          // shown as the victim in the killfeed
    [Property, Range( 1f, 1000f )] public float MaxHealth { get; set; } = 100f;       // health restored on enable / respawn
    [Property] public bool RespawnOnDeath { get; set; } = true;                       // refill health a moment after dying
    [Property, Range( 0.5f, 10f )] public float RespawnDelay { get; set; } = 3f;      // seconds dead before respawning

    public float Health { get; private set; }
    public bool IsDead => Health <= 0f;

    float _respawnAt;

    protected override void OnEnabled() => Health = MaxHealth;

    // Engine damage entry point: TriggerHurt volumes and the HUD's hitscan both call this.
    public void OnDamage( in DamageInfo damage )
    {
        if ( IsDead ) return;                                       // already down, ignore extra hits
        Health = Math.Max( 0f, Health - damage.Damage );
        if ( IsDead && RespawnOnDeath ) _respawnAt = Time.Now + RespawnDelay;
    }

    protected override void OnUpdate()
    {
        if ( IsDead && RespawnOnDeath && Time.Now >= _respawnAt ) Health = MaxHealth;
    }
}
using System;

namespace Goo.FpsUI;

// Compass logic: a 0..360 heading plus the shortest signed angular delta the view uses to
// place cardinal ticks. Engine-free.
public sealed class CompassModel
{
    public float Heading { get; private set; }  // current yaw, normalized 0..360

    public void SetHeading( float yawDeg )       // set heading from game code (any range)
        => Heading = ((yawDeg % 360f) + 360f) % 360f;

    // Signed delta from `from` to `to`, in (-180, 180]. Used to position ticks around center.
    public static float ShortestDelta( float from, float to ) => ((to - from + 540f) % 360f) - 180f;

    public bool Tick( float dt ) => false;  // heading changes drive rebuilds, not the damper
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform;   // rule 18: ambiguous with Sandbox.UI otherwise

namespace Goo.FpsUI;

// Shared, stateless view helpers. Each returns a fresh Container (never cache a Container).
static class Parts
{
    public enum Corner { TopLeft, TopCenter, TopRight, BottomLeft, BottomRight, Center }

    // Absolute fill spanning `frac` (0..1) of its Relative parent's width, full height.
    public static Container FillRect( string key, float frac, Color color, float radius ) => new()
    {
        Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
        Height = Length.Percent( 100 ),
        Width = Length.Percent( Math.Clamp( frac, 0f, 1f ) * 100f ),
        BorderRadius = radius, BackgroundColor = color,
    };

    // Absolute full-cover tint at a given opacity (flashes, warnings).
    public static Container Overlay( string key, Color color, float opacity, float radius ) => new()
    {
        Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
        Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
        BorderRadius = radius, BackgroundColor = color, Opacity = opacity,
    };

    // Pin `child` to a screen corner/center inside a Relative, full-size root. `m` is the edge inset.
    // Container properties are init-only (record struct), so we use `with` expressions per corner.
    public static Container Anchor( string key, Corner c, float m, Container child )
    {
        var box = new Container
        {
            Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
            Children = { child },
        };
        return c switch
        {
            Corner.TopLeft     => box with { Top = m, Left = m },
            Corner.TopRight    => box with { Top = m, Right = m },
            Corner.BottomLeft  => box with { Bottom = m, Left = m },
            Corner.BottomRight => box with { Bottom = m, Right = m },
            Corner.TopCenter   => box with
            {
                Top = m, Left = Length.Percent( 50 ),
                Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( 0 ) ?? default ),
            },
            _ => box with  // Center
            {
                Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
                Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
            },
        };
    }

    // Pin `child` horizontally centered, offset `dy` px from the screen center (positive = down, negative = up).
    public static Container CenterOffset( string key, float dy, Container child ) => new()
    {
        Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
        Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
        Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Px.Of( dy ) ),
        Children = { child },
    };

    // Pin `child`'s top-left corner at a pixel offset from the screen center (positive dx = right, positive dy = down).
    public static Container Offset( string key, float dx, float dy, Container child ) => new()
    {
        Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
        Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
        Transform = PanelTransform.Translate( Px.Of( dx ), Px.Of( dy ) ),
        Children = { child },
    };

    // Semi-opaque rounded backing behind a HUD readout, so text/bars stay legible over the world.
    // Wraps one child and sizes to it, all card styling comes from the theme.
    public static Container Panel( string key, FpsTheme t, Container child ) => new()
    {
        Key = key, Position = PositionMode.Relative,
        Padding = t.PanelPad, BorderRadius = t.PanelRadius,
        BackgroundColor = t.BackingBg,
        Children = { child },
    };

    // A full-size, pointer-through, Relative root used by every standalone widget and the FpsHud.
    public static Container Root( string key ) => new()
    {
        Key = key, Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
        Position = PositionMode.Relative, PointerEvents = PointerEvents.None,
    };
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Goo.FpsUI;

// Stateless presenter: the secondary (stamina/armor) bar.
static class StaminaView
{
    public const float TrackW = 320f, TrackH = 10f;

    public static Container Build( StaminaModel m, FpsTheme t )
    {
        float frac = m.ShownFraction;
        Color fill = Color.Lerp( t.Warn, t.Good, frac );

        var track = new Container
        {
            Key = "stTrack", Position = PositionMode.Relative,
            Width = TrackW, Height = TrackH, BackgroundColor = t.TrackBg,
            BorderRadius = t.Radius, Overflow = OverflowMode.Hidden,
            Children = { Parts.FillRect( "fill", frac, fill, t.Radius ) },
        };
        if ( m.Flash > 0.001f ) track.Children.Add( Parts.Overlay( "flash", Color.White, m.Flash * 0.8f, t.Radius ) );
        return track;
    }
}

// Standalone stamina bar. Call Sprinting(bool) from your movement code each frame.
public sealed partial class StaminaWidget : GooPanel<Container>
{
    [Property, Range( 1f, 1000f )] public float MaxStamina { get; set; } = 100f; // full-bar value
    [Property, Range( 0.05f, 2f )] public float DrainRate { get; set; } = 0.55f;  // fraction of max drained per second while sprinting
    [Property] public PlayerController? Player { get; set; }                      // player to gate sprint on when stamina is empty (null = skip gating)

    readonly StaminaModel _m = new();
    readonly FpsTheme _t = new();
    bool _booted;
    bool _sprintBlocked;
    float _cachedRunSpeed;

    void Boot() { _m.MaxStamina = MaxStamina; _m.Reset(); _booted = true; }

    public void Sprinting( bool on ) => _m.SetSprinting( on ); // toggle sprint drain

    // Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
    partial void StepDemo( float dt, ref bool active );

    protected override bool Tick( float dt )
    {
        if ( !_booted ) Boot();
        _m.DrainRate = DrainRate;  // live-tunable
        bool demo = false;
        StepDemo( dt, ref demo );
        if ( !demo )
        {
            Sprinting( Sandbox.Input.Down( "run" ) );
            FpsInput.ApplySprintGate( Player, _m.Stamina, ref _sprintBlocked, ref _cachedRunSpeed );
        }
        bool moving = _m.Tick( dt );
        return demo || moving;
    }

    protected override Container Build()
    {
        if ( !_booted ) Boot();
        var root = Parts.Root( "fpsStamina" );
        // Sits just above where the health bar would be (health height + gap).
        root.Children.Add( Parts.Anchor( "a", Parts.Corner.BottomLeft, _t.Margin + HealthView.TrackH + 10f, Parts.Panel( "bg", _t, StaminaView.Build( _m, _t ) ) ) );
        return root;
    }
}
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulShapePanel : Panel, IStatefulEventHost
{
    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    // Apply a baked alpha texture as BackgroundImage. The shape color comes from
    // BackgroundColor flowing through the engine's BgTint multiplier; this method
    // is only responsible for the alpha mask.
    public void ApplyShape(BlobKind kind, in ShapeParams shape)
    {
        Style.BackgroundImage = ShapeTextureCache.GetOrBake(kind, in shape);
        Style.BackgroundSizeX = Length.Percent(100);
        Style.BackgroundSizeY = Length.Percent(100);
    }

    public void ApplyPolygon(Vector2[] points)
    {
        Style.BackgroundImage = ShapeTextureCache.GetOrBakePolygon(points);
        Style.BackgroundSizeX = Length.Percent(100);
        Style.BackgroundSizeY = Length.Percent(100);
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
using System;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulSvgPanel : Sandbox.UI.SvgPanel, IStatefulEventHost
{
    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
using Sandbox.UI;

/// <summary>
/// Holds a banlist, can ban users
/// </summary>
public sealed class BanSystem : GameObjectSystem<BanSystem>, Component.INetworkListener
{
	public record struct BanEntry( string DisplayName, string Reason );

	private Dictionary<long, BanEntry> _bans = new();

	public BanSystem( Scene scene ) : base( scene )
	{
		_bans = LocalData.Get<Dictionary<long, BanEntry>>( "bans", new() ) ?? new();
	}

	bool Component.INetworkListener.AcceptConnection( Connection connection, ref string reason )
	{
		if ( !_bans.TryGetValue( connection.SteamId, out var entry ) )
			return true;

		reason = $"You're banned from this server: {entry.Reason}";
		return false;
	}

	/// <summary>
	/// Bans a connected player and kicks them immediately
	/// </summary>
	public void Ban( Connection connection, string reason )
	{
		Assert.True( Networking.IsHost, "Only the host may ban players." );

		_bans[connection.SteamId] = new BanEntry( connection.DisplayName, reason );
		Save();

		GameManager.Current.Notify( $"🔨 {connection.DisplayName} was banned: {reason}" );

		connection.Kick( $"Banned: {reason}" );
	}

	/// <summary>
	/// Bans a Steam ID by value. Use for pre-banning or banning players who are not currently connected.
	/// Display name falls back to the Steam ID string.
	/// </summary>
	public void Ban( SteamId steamId, string reason )
	{
		Assert.True( Networking.IsHost, "Only the host may ban players." );

		_bans[steamId] = new BanEntry( steamId.ToString(), reason );
		Save();
	}

	/// <summary>
	/// Removes the ban for the given Steam ID.
	/// </summary>
	public void Unban( SteamId steamId )
	{
		Assert.True( Networking.IsHost, "Only the host may unban players." );

		if ( _bans.Remove( steamId ) )
			Save();
	}

	/// <summary>
	/// Returns true if the given Steam ID is currently banned
	/// </summary>
	public bool IsBanned( SteamId steamId ) => _bans.ContainsKey( steamId );

	/// <summary>
	/// Returns a read-only view of all active bans
	/// </summary>
	public IReadOnlyDictionary<SteamId, BanEntry> GetBannedList() => _bans.ToDictionary( x => (SteamId)x.Key, x => x.Value );

	private void Save() => LocalData.Set( "bans", _bans );

	/// <summary>
	/// RPC to ban a connected player. Caller must be host or have admin permission.
	/// </summary>
	[Rpc.Host]
	internal static void RpcBanPlayer( Connection target, string reason = "Banned" )
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		Current.Ban( target, reason );
	}

	/// <summary>
	/// Bans a player by name or Steam ID. Optionally provide a reason.
	/// Usage: ban [name|steamid] [reason]
	/// </summary>
	[ConCmd( "ban" )]
	internal static void BanCommand( string target, string reason = "Banned" )
	{
		if ( !Networking.IsHost ) return;

		// Try parsing as a Steam ID (64-bit integer) first
		if ( ulong.TryParse( target, out var steamIdValue ) )
		{
			var steamId = steamIdValue;
			var connection = Connection.All.FirstOrDefault( c => c.SteamId == steamId );

			if ( connection is not null )
				Current.Ban( connection, reason );
			else
				Current.Ban( steamId, reason );

			Log.Info( $"Banned {steamId}: {reason}" );
			return;
		}

		// Fall back to partial name match
		var conn = GameManager.FindPlayerWithName( target );
		if ( conn is not null )
		{
			Current.Ban( conn, reason );
			Log.Info( $"Banned {conn.DisplayName}: {reason}" );
		}
		else
		{
			Log.Warning( $"Could not find player '{target}'" );
		}
	}
}


using Sandbox.Utility;

public struct ClientInput
{
	readonly record struct State( Connection connection, Player player );

	static State _currentState;

	static Connection Connection => _currentState.connection;

	public readonly bool IsEnabled => !string.IsNullOrWhiteSpace( Action );

	public string Action { get; set; }

	/// <summary>
	/// Returns an analog value between 0 and 1 representing how much the input is pressed
	/// </summary>
	public readonly float GetAnalog()
	{
		if ( !IsEnabled ) return 0;
		return Down() ? 1 : 0;
	}

	/// <summary>
	/// Returns true if button is currently held down
	/// </summary>
	public readonly bool Down()
	{
		if ( !IsEnabled ) return false;

		return Connection?.Down( Action ) ?? false;
	}

	/// <summary>
	/// Returns true if button was released
	/// </summary>
	public readonly bool Released()
	{
		if ( !IsEnabled ) return false;

		return Connection?.Released( Action ) ?? false;
	}

	/// <summary>
	/// Returns true if button was pressed
	/// </summary>
	public readonly bool Pressed()
	{
		if ( !IsEnabled ) return false;

		return Connection?.Pressed( Action ) ?? false;
	}

	internal static IDisposable PushScope( Player player )
	{
		var previousState = _currentState;
		_currentState = new State( player?.Network?.Owner, player );

		return DisposeAction.Create( () => _currentState = previousState );
	}

	/// <summary>
	/// The player currently running an <see cref="IPlayerControllable.OnControl"/> tick,
	/// or null when not inside a control scope (e.g. during regular player input).
	/// </summary>
	public static Player Current => _currentState.player;
}


public sealed class SpotLightEntity : Component, IPlayerControllable
{
	[Property, ClientEditable, Group( "Light" )]
	public bool On { get; set { field = value; UpdateLight(); } } = true;

	[Property, ClientEditable, Group( "Light" )]
	public bool Shadows { get; set { field = value; UpdateLight(); } } = true;

	[Property, Range( 0, 1 ), ClientEditable, Group( "Light" )]
	public Color Color { get; set { field = value; UpdateLight(); } }

	[Property, Range( 0, 50 ), ClientEditable, Group( "Light" )]
	public float Brightness { get; set { field = value; UpdateLight(); } } = 2;

	[Property, Range( 0, 1000 ), ClientEditable, Group( "Light" )]
	public float Radius { get; set { field = value; UpdateLight(); } } = 500;

	[Property, Range( 0, 90 ), ClientEditable, Group( "Light" )]
	public float Angle { get; set { field = value; UpdateLight(); } } = 35;

	[Property, Range( 0, 16 ), ClientEditable, Group( "Light" )]
	public float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;


	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOn { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOff { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput Toggle { get; set; }

	[Property]
	public GameObject OnGameObject { get; set; }

	[Property]
	public GameObject OffGameObject { get; set; }

	void IPlayerControllable.OnControl()
	{

		if ( Toggle.Pressed() )
		{
			On = !On;
		}

		if ( TurnOn.Pressed() )
		{
			On = true;
		}

		if ( TurnOff.Pressed() )
		{
			On = false;
		}
	}

	void IPlayerControllable.OnEndControl()
	{

	}

	void IPlayerControllable.OnStartControl()
	{

	}

	void UpdateLight()
	{
		OnGameObject?.Enabled = On;
		OffGameObject?.Enabled = !On;

		if ( GetComponentInChildren<SpotLight>( true ) is not SpotLight light )
			return;

		light.Enabled = On;

		var color = Color;
		color.r *= Brightness;
		color.g *= Brightness;
		color.b *= Brightness;

		light.Shadows = Shadows;
		light.LightColor = color;
		light.Radius = Radius;
		light.Attenuation = Attenuation;
		light.ConeOuter = Angle;
		light.ConeInner = Angle * 0.5f;

		Network.Refresh();
	}
}
/// <summary>
/// Implement on any component that can appear as an attacker in the kill feed.
/// Examples: Player, Npc, explosive barrel, turret, whatever the fuck.
/// </summary>
public interface IKillSource
{
	/// <summary>
	/// Display name
	/// </summary>
	string DisplayName { get; }

	/// <summary>
	/// Steam ID for the local "is-me" highlight. Defaults to 0 (not a player).
	/// </summary>
	long SteamId => default;

	/// <summary>
	/// Entity-type tag passed as <c>attackerTags</c>.
	/// Return an empty string for plain player kills. Examples: "npc"
	/// </summary>
	string Tags => "";

	/// <summary>
	/// Called on the host when this source kills something.
	/// Credit kills, update stats, etc. Default is no-op.
	/// </summary>
	void OnKill( GameObject victim ) { }
}
/// <summary>
/// Stores shared ammo pools on a player, keyed by <see cref="AmmoResource"/>.
/// Add this component to the player prefab alongside <see cref="PlayerInventory"/>.
/// </summary>
public sealed class AmmoInventory : Component
{
	/// <summary>
	/// Ammo pool: resource path → current count.
	/// Host-authoritative so server-side pickups replicate correctly to the owning client.
	/// </summary>
	[Sync( SyncFlags.FromHost )] public NetDictionary<string, int> Pool { get; set; } = new();

	/// <summary>
	/// Returns the current ammo count for the given resource.
	/// </summary>
	public int GetAmmo( AmmoResource resource )
	{
		if ( resource is null ) return 0;
		return Pool.TryGetValue( resource.ResourcePath, out var count ) ? count : 0;
	}

	/// <summary>
	/// Sets the ammo count for the given resource directly, clamped to [0, max].
	/// Routes through the host when called from a client.
	/// </summary>
	public void SetAmmo( AmmoResource resource, int value )
	{
		if ( resource is null ) return;
		if ( !Networking.IsHost ) { SetAmmoRpc( resource, Math.Clamp( value, 0, resource.MaxReserve ) ); return; }
		Pool[resource.ResourcePath] = Math.Clamp( value, 0, resource.MaxReserve );
	}

	/// <summary>
	/// Adds ammo to the pool for the given resource (clamped to max).
	/// Returns the actual amount added (optimistic when called from a client).
	/// </summary>
	public int AddAmmo( AmmoResource resource, int count )
	{
		if ( resource is null ) return 0;
		if ( !Networking.IsHost ) { AddAmmoRpc( resource, count ); return count; }
		var current = GetAmmo( resource );
		var space = resource.MaxReserve - current;
		var toAdd = Math.Min( count, space );
		if ( toAdd <= 0 ) return 0;
		Pool[resource.ResourcePath] = current + toAdd;
		return toAdd;
	}

	/// <summary>
	/// Attempts to consume <paramref name="count"/> ammo from the pool.
	/// Returns <c>true</c> and deducts the ammo if successful (optimistic when called from a client).
	/// </summary>
	public bool TakeAmmo( AmmoResource resource, int count )
	{
		if ( resource is null ) return false;
		if ( !Networking.IsHost ) { TakeAmmoRpc( resource, count ); return GetAmmo( resource ) >= count; }
		var current = GetAmmo( resource );
		if ( current < count ) return false;
		Pool[resource.ResourcePath] = current - count;
		return true;
	}

	/// <summary>
	/// Returns true if there is at least <paramref name="count"/> ammo in the pool.
	/// </summary>
	public bool HasAmmo( AmmoResource resource, int count = 1 )
	{
		return GetAmmo( resource ) >= count;
	}

	[Rpc.Host]
	private void SetAmmoRpc( AmmoResource resource, int value )
	{
		Pool[resource.ResourcePath] = value;
	}

	[Rpc.Host]
	private void AddAmmoRpc( AmmoResource resource, int count )
	{
		var current = Pool.TryGetValue( resource.ResourcePath, out var c ) ? c : 0;
		var toAdd = Math.Min( count, resource.MaxReserve - current );
		if ( toAdd > 0 )
			Pool[resource.ResourcePath] = current + toAdd;
	}

	[Rpc.Host]
	private void TakeAmmoRpc( AmmoResource resource, int count )
	{
		var current = Pool.TryGetValue( resource.ResourcePath, out var c ) ? c : 0;
		if ( current >= count )
			Pool[resource.ResourcePath] = current - count;
	}
}
namespace Sandbox;

/// <summary>
/// Console variables that control weapon behaviour globally.
/// </summary>
public static class WeaponConVars
{
	/// <summary>
	/// When enabled, weapons have unlimited ammo — no ammo is consumed when firing.
	/// </summary>
	[ConVar( "sb.weapon.unlimitedammo", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "When enabled, weapons have unlimited ammo." )]
	public static bool UnlimitedAmmo { get; set; } = false;

	/// <summary>
	/// When enabled, reserve ammo never depletes — clip ammo is still consumed normally, but you can always reload.
	/// </summary>
	[ConVar( "sb.weapon.infinitereserves", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "When enabled, reserve ammo is infinite — clip ammo is still consumed." )]
	public static bool InfiniteReserves { get; set; } = false;
}
/// <summary>
/// The local user's preferences in Deathmatch
/// </summary>
internal static class GamePreferences
{
	/// <summary>
	/// Enables automatic switching to better weapons on item pickup
	/// </summary>
	[ConVar( "sb.autoswitch", ConVarFlags.UserInfo | ConVarFlags.Saved )]
	public static bool AutoSwitch { get; set; } = true;

	/// <summary>
	/// Enables fast switching between inventory weapons
	/// </summary>
	[ConVar( "sb.fastswitch", ConVarFlags.Saved )]
	public static bool FastSwitch { get; set; } = false;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.viewbob", ConVarFlags.Saved )]
	[Group( "Camera" )]
	public static bool ViewBobbing { get; set; } = true;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.screenshake", ConVarFlags.Saved )]
	[Range( 0.1f, 2f ), Step( 0.1f ), Group( "Camera" )]
	public static float Screenshake { get; set; } = 0.3f;
}
using Sandbox.Npcs.Schedules;

namespace Sandbox.Npcs.CombatNpc;

/// <summary>
/// A combat NPC that searches for players, advances on them, fires in bursts, and repositions.
/// When friendly, follows players and engages hostile NPCs instead.
/// </summary>
public class CombatNpc : Npc, Component.IDamageable
{
	private static readonly string[] PainLines =
	{
		"Argh!",
		"They got me!",
		"I'm hit!",
		"Taking fire!",
		"Ugh!",
	};

	private static readonly string[] DeathLines =
	{
		"Tell them... I fought...",
		"Not like this...",
		"I can't...",
	};

	/// <summary>
	/// When true, this NPC is friendly to players and will follow them, engaging hostile NPCs.
	/// When false, this NPC targets players and friendly NPCs.
	/// </summary>
	[Property, ClientEditable, Sync]
	public bool Friendly { get; set; } = false;

	[Property, ClientEditable, Range( 1, 250 ), Sync]
	public float Health { get; set; } = 100f;

	/// <summary>
	/// The weapon this NPC uses to attack.
	/// </summary>
	[Property]
	public BaseWeapon Weapon { get; set; }

	[Property, Group( "Balance" ), Range( 512, 4096 ), Step( 1 ), ClientEditable, Sync]
	public float AttackRange { get; set; } = 1024f;

	[Property, Group( "Balance" ), Range( 90, 250f ), Step( 1 ), ClientEditable, Sync]
	public float EngageSpeed { get; set; } = 180f;

	/// <summary>
	/// How long after losing sight of a player to keep searching their last known position.
	/// </summary>
	[Property, Group( "Balance" )]
	public float SearchTimeout { get; set; } = 8f;

	[Property, Group( "Balance" )]
	public float PatrolRadius { get; set; } = 400f;

	[Property, Group( "Balance" )]
	public float BurstDuration { get; set; } = 1.5f;

	[Property, Group( "Balance" )]
	public float BurstPause { get; set; } = 0.8f;

	/// <summary>
	/// How far a friendly NPC will follow a player before stopping.
	/// </summary>
	[Property, Group( "Balance" )]
	public float FollowDistance { get; set; } = 150f;

	private Vector3? _lastKnownPosition;
	private TimeSince _timeSinceLastSeen;

	protected override void OnStart()
	{
		base.OnStart();

		if ( !IsProxy )
		{
			Senses.ScanTags = new TagSet { "player", "friendly_npc", "hostile_npc" };

			if ( Friendly )
			{
				GameObject.Tags.Add( "friendly_npc" );
				Senses.TargetTags = new TagSet { "hostile_npc" };
			}
			else
			{
				GameObject.Tags.Add( "hostile_npc" );
				Senses.TargetTags = new TagSet { "player", "friendly_npc" };
			}
		}

		if ( Weapon.IsValid() && Renderer.IsValid() )
		{
			Weapon.CreateWorldModel( Renderer );

			if ( !IsProxy )
				Animation.SetHoldType( Weapon.HoldType );
		}
	}

	public override ScheduleBase GetSchedule()
	{
		var visible = Senses.GetNearestVisible();

		if ( visible.IsValid() )
		{
			_lastKnownPosition = visible.WorldPosition;
			_timeSinceLastSeen = 0;

			var engage = GetSchedule<CombatEngageSchedule>();
			engage.Target = visible;
			engage.Weapon = Weapon;
			engage.AttackRange = AttackRange;
			engage.EngageSpeed = EngageSpeed;
			engage.BurstDuration = BurstDuration;
			engage.BurstPause = BurstPause;
			return engage;
		}

		// Search last known position if recent enough
		if ( _lastKnownPosition.HasValue && _timeSinceLastSeen < SearchTimeout )
		{
			var search = GetSchedule<ScientistSearchSchedule>();
			search.Target = _lastKnownPosition.Value;
			return search;
		}

		// Friendly NPCs follow the nearest player when idle
		if ( Friendly )
		{
			var follow = GetSchedule<CombatFollowSchedule>();
			follow.FollowDistance = FollowDistance;
			return follow;
		}

		// No intel — patrol
		var patrol = GetSchedule<CombatPatrolSchedule>();
		patrol.PatrolRadius = PatrolRadius;
		return patrol;
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		if ( IsProxy )
			return;

		Health -= damage.Damage;

		// If we can hear the attacker, treat their position as the last known location
		if ( damage.Attacker.IsValid() )
		{
			var dist = WorldPosition.Distance( damage.Attacker.WorldPosition );
			if ( dist <= Senses.HearingRange )
			{
				_lastKnownPosition = damage.Attacker.WorldPosition;
				_timeSinceLastSeen = 0;
			}
		}

		if ( Health < 1f )
		{
			if ( Speech.CanSpeak )
				Speech.Say( Game.Random.FromArray( DeathLines ), 2f );

			Die( damage );
			return;
		}

		if ( Speech.CanSpeak && Game.Random.Float() < 0.5f )
			Speech.Say( Game.Random.FromArray( PainLines ), 1.5f );

		// Interrupt current schedule so we react immediately
		EndCurrentSchedule();
	}
}
using Sandbox.Npcs.Layers;

namespace Sandbox.Npcs.Tasks;

/// <summary>
/// Task that commands the NavigationLayer to move to a target position or GameObject.
/// When tracking a GameObject, re-evaluates the path periodically.
/// Does not override the NPC's look target — but will rotate the body to face the
/// movement direction when the angle would otherwise cause silly walking
/// </summary>
public class MoveTo : TaskBase
{
	public Vector3? TargetPosition { get; set; }
	public GameObject TargetObject { get; set; }
	public float StopDistance { get; set; } = 10f;
	public float ReevaluateInterval { get; set; } = 0.5f;
	public float LateralThreshold { get; set; } = 60f;

	private TimeSince _lastReevaluate;

	public MoveTo( Vector3 targetPosition, float stopDistance = 10f )
	{
		TargetPosition = targetPosition;
		StopDistance = stopDistance;
	}

	public MoveTo( GameObject targetObject, float stopDistance = 10f )
	{
		TargetObject = targetObject;
		StopDistance = stopDistance;
	}

	protected override void OnStart()
	{
		var pos = GetTargetPosition();
		if ( !pos.HasValue ) return;

		Npc.Navigation.MoveTo( pos.Value, StopDistance );
		_lastReevaluate = 0;
	}

	protected override TaskStatus OnUpdate()
	{
		// Target object destroyed mid-move
		if ( TargetObject is not null && !TargetObject.IsValid() )
			return TaskStatus.Failed;

		// Re-evaluate path for moving targets
		if ( TargetObject.IsValid() && _lastReevaluate > ReevaluateInterval )
		{
			var pos = GetTargetPosition();
			if ( pos.HasValue )
				Npc.Navigation.MoveTo( pos.Value, StopDistance );
			_lastReevaluate = 0;
		}

		var agent = Npc.Navigation.Agent;
		if ( agent.IsValid() && agent.Velocity.WithZ( 0 ).Length > 1f )
		{
			var moveDir = agent.Velocity.WithZ( 0 ).Normal;
			var fwd = Npc.WorldRotation.Forward.WithZ( 0 ).Normal;
			var angle = Vector3.GetAngle( fwd, moveDir );

			if ( angle > LateralThreshold && !Npc.Animation.LookTarget.HasValue )
			{
				// No look target — face the movement direction
				var targetRot = Rotation.LookAt( moveDir, Vector3.Up );
				Npc.GameObject.WorldRotation = Rotation.Lerp(
					Npc.WorldRotation, targetRot, Npc.Animation.LookSpeed * Time.Delta );
			}
		}

		return Npc.Navigation.GetStatus();
	}

	private Vector3? GetTargetPosition()
	{
		if ( TargetObject.IsValid() )
		{
			// Navigate to the closest point on the object's bounds, not its origin.
			// This prevents the NPC from trying to walk inside large props.
			var bounds = TargetObject.GetBounds();
			return bounds.ClosestPoint( Npc.WorldPosition );
		}

		return TargetPosition;
	}
}
public sealed partial class Player
{
	/// <summary>
	/// Kill yourself
	/// </summary>
	[ConCmd( "kill" )]
	public static void KillSelf( Connection source )
	{
		var player = Player.FindForConnection( source );
		if ( player is null ) return;

		player.KillSelf();
	}

	[Rpc.Host]
	internal void KillSelf()
	{
		if ( Rpc.Caller != Network.Owner ) return;

		this.OnDamage( new DamageInfo( float.MaxValue, GameObject, null ) );
	}

	[ConCmd( "god", ConVarFlags.Server | ConVarFlags.Cheat, Help = "Toggle invulnerability" )]
	public static void God( Connection source )
	{
		var player = PlayerData.For( source );
		if ( !player.IsValid() )
			return;

		player.IsGodMode = !player.IsGodMode;
		source.SendLog( LogLevel.Info, player.IsGodMode ? "Godmode enabled" : "Godmode disabled" );
	}

	/// <summary>
	/// Switch to another map
	/// </summary>
	[ConCmd( "map", ConVarFlags.Admin )]
	public static void ChangeMap( string mapName )
	{
		LaunchArguments.Map = mapName;
		Game.Load( Game.Ident, true );
	}

	/// <summary>
	/// Undo the last action for the calling player
	/// </summary>
	[ConCmd( "undo", ConVarFlags.Server )]
	public static void RunUndo( Connection source )
	{
		var player = Player.FindForConnection( source );
		if ( !player.IsValid() )
			return;

		player.Undo.Undo();
	}
}
public sealed partial class Player : Component, Component.IDamageable, PlayerController.IEvents, Global.ISaveEvents, IKillSource
{
	private static Player LocalPlayer { get; set; }
	public static Player FindLocalPlayer() => LocalPlayer;
	public static T FindLocalWeapon<T>() where T : BaseCarryable => FindLocalPlayer()?.GetComponentInChildren<T>( true );
	public static T FindLocalToolMode<T>() where T : ToolMode => FindLocalPlayer()?.GetComponentInChildren<T>( true );

	/// <summary>
	/// Find a player for this connection
	/// </summary>
	public static Player FindForConnection( Connection c )
	{
		return Game.ActiveScene.GetAll<Player>().FirstOrDefault( x => x.Network.Owner == c );
	}

	/// <summary>
	/// Get player from a connection id
	/// </summary>
	public static Player For( Guid playerId )
	{
		return Game.ActiveScene.GetAll<Player>().FirstOrDefault( x => x.Network.Owner?.Id == playerId );
	}
}
public struct PlayerDiedParams
{
	public GameObject Attacker { get; set; }
}

public struct PlayerDamageParams
{
	public float Damage { get; set; }
	public GameObject Attacker { get; set; }
	public GameObject Weapon { get; set; }
	public TagSet Tags { get; set; }
	public Vector3 Position { get; set; }
	public Vector3 Origin { get; set; }
}

/// <summary>
/// Data passed to pickup events. Set <see cref="Cancelled"/> to true to prevent the pickup.
/// </summary>
public class PlayerPickupEvent
{
	public Player Player { get; init; }
	public BaseCarryable Weapon { get; init; }
	public int Slot { get; init; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Data passed to drop events. Set <see cref="Cancelled"/> to true to prevent the drop.
/// </summary>
public class PlayerDropEvent
{
	public Player Player { get; init; }
	public BaseCarryable Weapon { get; init; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Data passed to switch weapon events. Set <see cref="Cancelled"/> to true to prevent the switch.
/// </summary>
public class PlayerSwitchWeaponEvent
{
	public Player Player { get; init; }
	public BaseCarryable From { get; init; }
	public BaseCarryable To { get; init; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Data passed to remove weapon events. Set <see cref="Cancelled"/> to true to prevent the removal.
/// </summary>
public class PlayerRemoveWeaponEvent
{
	public Player Player { get; init; }
	public BaseCarryable Weapon { get; init; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Data passed to slot move events. Set <see cref="Cancelled"/> to true to prevent the move.
/// </summary>
public class PlayerMoveSlotEvent
{
	public Player Player { get; init; }
	public int FromSlot { get; init; }
	public int ToSlot { get; init; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Pre-damage event. Fired before damage is applied. Listeners can modify <see cref="Damage"/>
/// or set <see cref="Cancelled"/> to block damage entirely.
/// </summary>
public class PlayerDamageEvent
{
	public Player Player { get; init; }
	public DamageInfo DamageInfo { get; init; }
	public float Damage { get; set; }
	public bool Cancelled { get; set; }
}

/// <summary>
/// Pre-respawn event. Fired before the player is spawned. Listeners can modify
/// <see cref="SpawnLocation"/> to control where the player appears.
/// </summary>
public class PlayerRespawnEvent
{
	public PlayerData PlayerData { get; init; }
	public Transform SpawnLocation { get; set; }
}

/// <summary>
/// Fired when a player kills another player or NPC.
/// </summary>
public class PlayerKillEvent
{
	public Player Player { get; init; }
	public GameObject Victim { get; init; }
	public DamageInfo DamageInfo { get; init; }
}

/// <summary>
/// Events fired only to the Player's own GameObject hierarchy.
/// </summary>
public static partial class Local
{
	public interface IPlayerEvents : ISceneEvent<IPlayerEvents>
	{
		void OnSpawned() { }
		void OnDied( PlayerDiedParams args ) { }
		void OnDamage( PlayerDamageParams args ) { }
		void OnJump() { }
		void OnLand( float distance, Vector3 velocity ) { }
		void OnSuicide() { }
		void OnPickup( PlayerPickupEvent e ) { }
		void OnDrop( PlayerDropEvent e ) { }
		void OnSwitchWeapon( PlayerSwitchWeaponEvent e ) { }
		void OnRemoveWeapon( PlayerRemoveWeaponEvent e ) { }
		void OnMoveSlot( PlayerMoveSlotEvent e ) { }
		void OnDamaging( PlayerDamageEvent e ) { }
		void OnKill( PlayerKillEvent e ) { }
		void OnCameraMove( ref Angles angles ) { }
		void OnCameraSetup( CameraComponent camera ) { }
		void OnCameraPostSetup( CameraComponent camera ) { }
	}
}

/// <summary>
/// Events broadcasted to the entire scene for any player action.
/// </summary>
public static partial class Global
{
	public interface IPlayerEvents : ISceneEvent<IPlayerEvents>
	{
		void OnPlayerSpawned( Player player ) { }
		void OnPlayerDied( Player player, PlayerDiedParams args ) { }
		void OnPlayerDamage( Player player, PlayerDamageParams args ) { }
		void OnPlayerJumped( Player player ) { }
		void OnPlayerLanded( Player player, float distance, Vector3 velocity ) { }
		void OnPlayerSuicide( Player player ) { }
		void OnPlayerPickup( PlayerPickupEvent e ) { }
		void OnPlayerDrop( PlayerDropEvent e ) { }
		void OnPlayerSwitchWeapon( PlayerSwitchWeaponEvent e ) { }
		void OnPlayerRemoveWeapon( PlayerRemoveWeaponEvent e ) { }
		void OnPlayerMoveSlot( PlayerMoveSlotEvent e ) { }
		void OnPlayerDamaging( PlayerDamageEvent e ) { }
		void OnPlayerRespawning( PlayerRespawnEvent e ) { }
		void OnPlayerKill( PlayerKillEvent e ) { }
	}
}
using Sandbox.Citizen;

public sealed class PlayerInventory : Component, Local.IPlayerEvents
{
	[Property] public int MaxSlots { get; private set; } = 6;

	[RequireComponent] public Player Player { get; set; }

	/// <summary>
	/// All weapons currently in the inventory, ordered by slot.
	/// </summary>
	public IEnumerable<BaseCarryable> Weapons => 
		GetComponentsInChildren<BaseCarryable>( true ).OrderBy( x => x.InventorySlot );

	[Sync( SyncFlags.FromHost ), Change] public BaseCarryable ActiveWeapon { get; private set; }

	internal void OnActiveWeaponChanged( BaseCarryable oldWeapon, BaseCarryable newWeapon )
	{
		if ( oldWeapon.IsValid() )
			oldWeapon.GameObject.Enabled = false;

		if ( newWeapon.IsValid() )
		{
			newWeapon.GameObject.Enabled = true;
			newWeapon.SetDropped( false );
		}
	}

	/// <summary>
	/// Returns the weapon in the given slot, or null if the slot is empty.
	/// </summary>
	public BaseCarryable GetSlot( int slot )
	{
		if ( slot < 0 || slot >= MaxSlots ) return null;
		foreach ( var w in Weapons )
		{
			if ( w.InventorySlot == slot ) return w;
		}
		return null;
	}

	/// <summary>
	/// Returns whether the given item could be inserted into the inventory.
	/// Checks for existing weapons that can receive ammo, and empty slots.
	/// </summary>
	public bool CanTake( BaseCarryable item )
	{
		if ( !item.IsValid() )
			return false;

		var existing = Weapons.FirstOrDefault( x => x.GetType() == item.GetType() );
		if ( existing.IsValid() )
		{
			// We already have this weapon — only allow if it can receive ammo
			if ( existing is BaseWeapon existingWeapon && existingWeapon.UsesAmmo )
				return existingWeapon.ReserveAmmo < existingWeapon.MaxReserveAmmo;

			return false;
		}

		return FindEmptySlot() >= 0;
	}

	/// <summary>
	/// Returns the first empty slot index, or -1 if the inventory is full.
	/// </summary>
	public int FindEmptySlot()
	{
		var weapons = Weapons;
		for ( int i = 0; i < MaxSlots; i++ )
		{
			bool occupied = false;
			foreach ( var w in weapons )
			{
				if ( w.InventorySlot == i ) { occupied = true; break; }
			}
			if ( !occupied ) return i;
		}

		return -1;
	}

	internal void GiveDefaultWeapons()
	{
		Pickup( "weapons/physgun/physgun.prefab", false );
		Pickup( "weapons/toolgun/toolgun.prefab", false );
		Pickup( "weapons/camera/camera.prefab", 8, false );
	}

	/// <summary>
	/// Activates the named tool mode, giving and equipping the toolgun first if the player doesn't have one.
	/// </summary>
	public void SetToolMode( string name )
	{
		if ( !Networking.IsHost )
		{
			HostSetToolMode( name );
			return;
		}

		if ( !HasWeapon<Toolgun>() )
		{
			Pickup( "weapons/toolgun/toolgun.prefab", false );
		}

		var toolGun = GetWeapon<Toolgun>();
		if ( !toolGun.IsValid() ) 
			return;

		SwitchWeapon( toolGun );
		toolGun.SetToolMode( name );
	}

	[Rpc.Host]
	private void HostSetToolMode( string toolModeName )
	{
		SetToolMode( toolModeName );
	}

	/// <summary>
	/// If we already own a weapon matching this prefab, try to give it ammo.
	/// Returns true if handled (caller should stop). False means no existing weapon found.
	/// </summary>
	private bool TryGiveAmmoToExisting( GameObject prefab, bool notice )
	{
		var baseCarry = prefab.Components.Get<BaseCarryable>( true );
		if ( !baseCarry.IsValid() )
			return false;

		var existing = Weapons.FirstOrDefault( x => x.GameObject.Name == prefab.Name );
		if ( !existing.IsValid() )
			return false;

		if ( existing is BaseWeapon existingWeapon && baseCarry is BaseWeapon pickupWeapon && existingWeapon.UsesAmmo )
		{
			if ( existingWeapon.ReserveAmmo >= existingWeapon.MaxReserveAmmo )
				return true;

			var ammoToGive = pickupWeapon.UsesClips ? pickupWeapon.ClipContents : pickupWeapon.StartingAmmo;
			existingWeapon.AddReserveAmmo( ammoToGive );

			if ( notice )
				OnClientPickup( existing, true );
		}

		return true;
	}

	public bool Pickup( string prefabName, bool notice = true )
	{
		if ( !Networking.IsHost )
			return false;

		var prefab = GameObject.GetPrefab( prefabName );
		if ( prefab is null )
		{
			Log.Warning( $"Prefab not found: {prefabName}" );
			return false;
		}

		if ( TryGiveAmmoToExisting( prefab, notice ) )
			return true;

		var slot = FindEmptySlot();
		if ( slot < 0 )
			return false;

		return Pickup( prefabName, slot, notice );
	}

	public bool HasWeapon( GameObject prefab )
	{
		var baseCarry = prefab.GetComponent<BaseCarryable>( true );
		if ( !baseCarry.IsValid() )
			return false;

		return Weapons.Where( x => x.GetType() == baseCarry.GetType() )
			.FirstOrDefault()
			.IsValid();
	}

	public bool HasWeapon<T>() where T : BaseCarryable
	{
		return GetWeapon<T>().IsValid();
	}

	public T GetWeapon<T>() where T : BaseCarryable
	{
		return Weapons.OfType<T>().FirstOrDefault();
	}

	public bool Pickup( GameObject prefab, bool notice = true )
	{
		if ( TryGiveAmmoToExisting( prefab, notice ) )
			return true;

		var slot = FindEmptySlot();
		if ( slot < 0 )
			return false;

		return Pickup( prefab, slot, notice );
	}

	public bool Pickup( string prefabName, int targetSlot, bool notice = true )
	{
		if ( !Networking.IsHost )
			return false;

		var prefab = GameObject.GetPrefab( prefabName );
		if ( prefab is null )
		{
			Log.Warning( $"Prefab not found: {prefabName}" );
			return false;
		}

		if ( !Pickup( prefab, targetSlot, notice ) )
			return false;

		return true;
	}

	public bool Pickup( GameObject prefab, int targetSlot, bool notice = true )
	{
		if ( !Networking.IsHost )
			return false;

		if ( targetSlot < 0 || targetSlot >= MaxSlots )
			return false;

		var baseCarry = prefab.Components.Get<BaseCarryable>( true );
		if ( !baseCarry.IsValid() )
			return false;

		if ( TryGiveAmmoToExisting( prefab, notice ) )
			return true;

		// Reject if the target slot is already occupied
		var occupant = GetSlot( targetSlot );
		if ( occupant.IsValid() )
			return false;

		var clone = prefab.Clone( new CloneConfig { Parent = GameObject, StartEnabled = false } );
		clone.NetworkSpawn( false, Network.Owner );

		//
		// Dropped variant components
		//
		{
			var cloneCarryable = clone.GetComponent<BaseCarryable>( true );
			cloneCarryable?.SetDropped( false );
		}

		var weapon = clone.GetComponent<BaseCarryable>( true );
		Assert.NotNull( weapon );

		weapon.InventorySlot = targetSlot;
		weapon.OnAdded( Player );

		var pickupEvent = new PlayerPickupEvent { Player = Player, Weapon = weapon, Slot = targetSlot };
		Local.IPlayerEvents.PostToGameObject( Player.GameObject, e => e.OnPickup( pickupEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerPickup( pickupEvent ) );

		if ( pickupEvent.Cancelled )
		{
			weapon.DestroyGameObject();
			return false;
		}

		if ( notice )
			OnClientPickup( weapon );

		return true;
	}

	/// <summary>
	/// If we already own a weapon of the same type as this live item, try to transfer its ammo.
	/// Returns true if handled (caller should stop). False means no existing weapon found.
	/// </summary>
	private bool TryGiveAmmoFromItem( BaseCarryable item, bool notice )
	{
		var existing = Weapons.FirstOrDefault( x => x.GetType() == item.GetType() );
		if ( !existing.IsValid() )
			return false;

		if ( existing is BaseWeapon existingWeapon && item is BaseWeapon pickupWeapon && existingWeapon.UsesAmmo )
		{
			if ( existingWeapon.ReserveAmmo >= existingWeapon.MaxReserveAmmo )
			{
				item.DestroyGameObject();
				return true;
			}

			var ammoToGive = pickupWeapon.UsesClips ? pickupWeapon.ClipContents : pickupWeapon.StartingAmmo;
			existingWeapon.AddReserveAmmo( ammoToGive );

			if ( notice )
				OnClientPickup( existing, true );

			item.DestroyGameObject();
			return true;
		}

		return true;
	}

	public bool Take( BaseCarryable item, bool includeNotices )
	{
		if ( !CanTake( item ) )
			return false;

		if ( TryGiveAmmoFromItem( item, includeNotices ) )
			return true;

		var slot = FindEmptySlot();
		item.GameObject.SetParent( GameObject, false );
		item.LocalTransform = global::Transform.Zero;
		item.InventorySlot = slot;
		item.GameObject.Enabled = false;

		// Remove from undo stacks so the weapon can't be undone out of our hands
		UndoSystem.Current.Remove( item.GameObject );

		if ( Network.Owner is not null )
			item.Network.AssignOwnership( Network.Owner );
		else
			item.Network.DropOwnership();

		item.OnAdded( Player );

		var pickupEvent = new PlayerPickupEvent { Player = Player, Weapon = item, Slot = slot };
		Local.IPlayerEvents.PostToGameObject( GameObject, e => e.OnPickup( pickupEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerPickup( pickupEvent ) );

		if ( pickupEvent.Cancelled )
		{
			item.DestroyGameObject();
			return false;
		}

		OnClientPickup( item );
		return true;
	}

	/// <summary>
	/// Spawns a dropped item into the world from a prefab, assigns ownership, and applies velocity.
	/// </summary>
	private void SpawnDroppedItem( GameObject prefab, Vector3 position, Vector3 velocity )
	{
		var pickup = prefab.Clone( new CloneConfig
		{
			Transform = new Transform( position ),
			StartEnabled = true
		} );

		Ownable.Set( pickup, Player.Network.Owner );
		pickup.Tags.Add( "removable" );
		pickup.NetworkSpawn();

		if ( pickup.GetComponent<Rigidbody>() is { } rb )
		{
			rb.Velocity = Player.Controller.Velocity + velocity;
			rb.AngularVelocity = Vector3.Random * 8.0f;
		}
	}

	/// <summary>
	/// Drops the given weapon from the inventory.
	/// </summary>
	public bool Drop( BaseCarryable weapon )
	{
		if ( !Networking.IsHost )
		{
			HostDrop( weapon );
			return true;
		}

		if ( !weapon.IsValid() ) return false;
		if ( weapon.Owner != Player ) return false;

		var dropEvent = new PlayerDropEvent { Player = Player, Weapon = weapon };
		Local.IPlayerEvents.PostToGameObject( Player.GameObject, e => e.OnDrop( dropEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerDrop( dropEvent ) );

		if ( dropEvent.Cancelled )
			return false;

		var dropPosition = Player.EyeTransform.Position + Player.EyeTransform.Forward * 48f;
		var dropVelocity = Player.EyeTransform.Forward * 200f + Vector3.Up * 100f;

		// If this is the active weapon, holster first
		if ( ActiveWeapon == weapon )
		{
			SwitchWeapon( null, true );
		}

		// Weapons with a DroppedWeapon component: spawn a fresh prefab clone as server.
		// This avoids all ownership/state issues from the inventory copy.
		var droppedWeapon = weapon.GetComponent<DroppedWeapon>( true );
		if ( droppedWeapon.IsValid() )
		{
			var prefabSource = weapon.GameObject.PrefabInstanceSource;
			if ( !string.IsNullOrEmpty( prefabSource ) )
			{
				var prefab = GameObject.GetPrefab( prefabSource );
				if ( prefab.IsValid() )
				{
					SpawnDroppedItem( prefab, dropPosition, dropVelocity );
				}
			}

			weapon.DestroyGameObject();
		}
		else
		{
			if ( !weapon.ItemPrefab.IsValid() )
			{
				weapon.DestroyGameObject();
				_ = FinishDropAsync();
				return true;
			}

			SpawnDroppedItem( weapon.ItemPrefab, dropPosition, dropVelocity );
			weapon.DestroyGameObject();
		}

		_ = FinishDropAsync();

		return true;
	}

	private async Task FinishDropAsync()
	{
		await Task.Yield();
		var best = GetBestWeapon();
		if ( best.IsValid() )
		{
			SwitchWeapon( best );
		}
	}

	private static SoundEvent AmmoPickupSound = ResourceLibrary.Get<SoundEvent>( "sounds/weapons/ammo_pickup.sound" );
	private static SoundEvent GunPickupSound = ResourceLibrary.Get<SoundEvent>( "sounds/weapons/ammo_pickup.sound" );

	[Rpc.Owner]
	private void OnClientPickup( BaseCarryable weapon, bool justAmmo = false )
	{
		if ( !weapon.IsValid() ) return;

		if ( ShouldAutoswitchTo( weapon ) )
		{
			SwitchWeapon( weapon );
		}

		if ( Player.IsLocalPlayer )
		{
			GameObject.PlaySound( justAmmo ? AmmoPickupSound : GunPickupSound );
			Global.IPlayerEvents.Post( e => e.OnPlayerPickup( new PlayerPickupEvent { Player = Player, Weapon = weapon, Slot = weapon.InventorySlot } ) );
		}
	}

	private bool ShouldAutoswitchTo( BaseCarryable item )
	{
		Assert.True( item.IsValid(), "item invalid" );

		if ( !ActiveWeapon.IsValid() )
			return true;

		if ( !GamePreferences.AutoSwitch )
			return false;

		if ( ActiveWeapon.IsInUse() )
			return false;

		if ( item is BaseWeapon weapon && weapon.UsesAmmo )
		{
			if ( !weapon.HasAmmo() && !weapon.CanReload() )
			{
				return false;
			}
		}

		return item.Value > ActiveWeapon.Value;
	}

	/// <summary>
	/// Moves the item in <paramref name="fromSlot"/> to <paramref name="toSlot"/>.
	/// If both slots are occupied the items are swapped; if <paramref name="toSlot"/> is
	/// empty the item is simply relocated.
	/// </summary>
	public void MoveSlot( int fromSlot, int toSlot )
	{
		if ( !Networking.IsHost )
		{
			HostMoveSlot( fromSlot, toSlot );
			return;
		}

		if ( fromSlot == toSlot ) return;
		if ( fromSlot < 0 || fromSlot >= MaxSlots ) return;
		if ( toSlot < 0 || toSlot >= MaxSlots ) return;

		var fromWeapon = GetSlot( fromSlot );
		if ( !fromWeapon.IsValid() ) return;

		var moveEvent = new PlayerMoveSlotEvent { Player = Player, FromSlot = fromSlot, ToSlot = toSlot };
		Local.IPlayerEvents.PostToGameObject( Player.GameObject, e => e.OnMoveSlot( moveEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerMoveSlot( moveEvent ) );

		if ( moveEvent.Cancelled )
			return;

		var toWeapon = GetSlot( toSlot );

		fromWeapon.InventorySlot = toSlot;
		if ( toWeapon.IsValid() )
			toWeapon.InventorySlot = fromSlot;
	}

	[Rpc.Host]
	private void HostMoveSlot( int fromSlot, int toSlot )
	{
		MoveSlot( fromSlot, toSlot );
	}

	public BaseCarryable GetBestWeapon()
	{
		return Weapons.OrderByDescending( x => x.Value ).FirstOrDefault();
	}

	public void SwitchWeapon( BaseCarryable weapon, bool allowHolster = false )
	{
		if ( !Networking.IsHost )
		{
			HostSwitchWeapon( weapon, allowHolster );
			return;
		}

		if ( weapon == ActiveWeapon )
		{
			if ( allowHolster )
			{
				ActiveWeapon = null;
			}
			return;
		}

		var switchEvent = new PlayerSwitchWeaponEvent { Player = Player, From = ActiveWeapon, To = weapon };
		Local.IPlayerEvents.PostToGameObject( Player.GameObject, e => e.OnSwitchWeapon( switchEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerSwitchWeapon( switchEvent ) );

		if ( switchEvent.Cancelled )
			return;

		ActiveWeapon = weapon;
	}

	[Rpc.Host]
	private void HostSwitchWeapon( BaseCarryable weapon, bool allowHolster = false )
	{
		SwitchWeapon( weapon, allowHolster );
	}

	protected override void OnUpdate()
	{
		var renderer = Player?.Controller?.Renderer;

		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.OnFrameUpdate( Player );

			if ( renderer.IsValid() )
			{
				renderer.Set( "holdtype", (int)ActiveWeapon.HoldType );
			}
		}
		else
		{
			if ( renderer.IsValid() )
			{
				renderer.Set( "holdtype", (int)CitizenAnimationHelper.HoldTypes.None );
			}
		}
	}

	public void OnControl()
	{
		if ( Input.Pressed( "drop" ) )
		{
			if ( ActiveWeapon.IsValid() )
				DropActiveWeapon();

			return;
		}

		if ( ActiveWeapon.IsValid() && !ActiveWeapon.IsProxy )
			ActiveWeapon.OnPlayerUpdate( Player );
	}

	/// <summary>
	/// Called by the owning client to drop their currently held weapon.
	/// </summary>
	[Rpc.Host]
	private void DropActiveWeapon()
	{
		if ( !ActiveWeapon.IsValid() ) return;
		Drop( ActiveWeapon );
	}

	[Rpc.Host]
	private void HostDrop( BaseCarryable weapon )
	{
		Drop( weapon );
	}

	/// <summary>
	/// Removes a weapon from the inventory and destroys it without dropping it into the world.
	/// </summary>
	public void Remove( BaseCarryable weapon )
	{
		if ( !Networking.IsHost )
		{
			HostRemove( weapon );
			return;
		}
		_ = RemoveAsync( weapon );
	}

	private async Task RemoveAsync( BaseCarryable weapon )
	{
		if ( !weapon.IsValid() ) return;
		if ( weapon.Owner != Player ) return;

		var removeEvent = new PlayerRemoveWeaponEvent { Player = Player, Weapon = weapon };
		Local.IPlayerEvents.PostToGameObject( Player.GameObject, e => e.OnRemoveWeapon( removeEvent ) );
		Global.IPlayerEvents.Post( e => e.OnPlayerRemoveWeapon( removeEvent ) );

		if ( removeEvent.Cancelled )
			return;

		if ( ActiveWeapon == weapon )
			SwitchWeapon( null, true );

		weapon.DestroyGameObject();
		await Task.Yield();

		var best = GetBestWeapon();
		if ( best.IsValid() )
			SwitchWeapon( best );
	}

	[Rpc.Host]
	private void HostRemove( BaseCarryable weapon )
	{
		Remove( weapon );
	}

	void Local.IPlayerEvents.OnDied( PlayerDiedParams args )
	{
		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.OnPlayerDeath( args );
		}
	}

	void Local.IPlayerEvents.OnCameraMove( ref Angles angles )
	{
		if ( !ActiveWeapon.IsValid() ) return;

		ActiveWeapon.OnCameraMove( Player, ref angles );
	}

	void Local.IPlayerEvents.OnCameraPostSetup( Sandbox.CameraComponent camera )
	{
		if ( !ActiveWeapon.IsValid() ) return;

		ActiveWeapon.OnCameraSetup( Player, camera );
	}
}
using Sandbox.UI;
namespace Sandbox;

public partial class SpawnlistFooter : Panel
{
	protected override int BuildHash() => HashCode.Combine( CanCreate() );

	bool CanCreate()
	{
		return true;
	}

	void CreatePopup()
	{
		var popup = new SpawnlistCreatePopup();
		popup.Parent = FindPopupPanel();
		popup.OnCreated = () => Ancestors.OfType<SpawnlistsPage>().FirstOrDefault()?.RefreshList();
	}

	void Refresh()
	{
		Ancestors.OfType<SpawnlistsPage>().FirstOrDefault()?.RefreshList();
	}
}


/// <summary>
/// Quick data folder file storage, good for saving local data
/// </summary>
internal static class LocalData
{
	/// <summary>
	/// Serialize <paramref name="value"/> and write it to <c>{key}.json</c> under <see cref="FileSystem.Data"/>.
	/// The directory hierarchy is created automatically.
	/// </summary>
	public static void Set<T>( string key, T value )
	{
		var path = KeyToPath( key );
		var dir = System.IO.Path.GetDirectoryName( path );

		if ( !string.IsNullOrEmpty( dir ) && !FileSystem.Data.DirectoryExists( dir ) )
			FileSystem.Data.CreateDirectory( dir );

		FileSystem.Data.WriteJson( path, value );
	}

	/// <summary>
	/// Read and deserialize the value stored at <paramref name="key"/>.
	/// Returns <paramref name="fallback"/> if the file doesn't exist or deserialization fails.
	/// </summary>
	public static T Get<T>( string key, T fallback = default )
	{
		var path = KeyToPath( key );

		if ( !FileSystem.Data.FileExists( path ) )
			return fallback;

		try
		{
			return FileSystem.Data.ReadJson<T>( path );
		}
		catch ( Exception ex )
		{
			Log.Warning( ex, $"[LocalData] Failed to read '{path}'" );
			return fallback;
		}
	}

	/// <summary>
	/// Returns true if a value has been stored at <paramref name="key"/>.
	/// </summary>
	public static bool Has( string key ) => FileSystem.Data.FileExists( KeyToPath( key ) );

	/// <summary>
	/// Delete the value stored at <paramref name="key"/>. No-op if it doesn't exist.
	/// </summary>
	public static void Delete( string key )
	{
		var path = KeyToPath( key );
		if ( FileSystem.Data.FileExists( path ) )
			FileSystem.Data.DeleteFile( path );
	}

	static string KeyToPath( string key ) => $"{key}.json";
}
using Sandbox.Rendering;

public sealed class CameraWeapon : BaseWeapon
{
	float fov = 50;
	float roll = 0;

	DepthOfField dof;
	bool focusing;
	Vector3 focusPoint;

	[Property] SoundEvent CameraShoot { get; set; }

	/// <summary>
	/// The RT camera's resolution 
	/// </summary>
	private static int _cameraResolution = 512;

	/// <summary>
	/// The render target texture produced by this camera. Read by <see cref="TVEntity"/>.
	/// </summary>
	public Texture RenderTexture => _renderTexture;

	private Texture _renderTexture;
	private CameraComponent _rtCamera;

	public override bool WantsHideHud => true;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EnsureRTCamera();
		EnsureRenderTexture();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		DestroyDepthOfField();
		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnDestroy()
	{
		DestroyDepthOfField();
		CleanupRenderTexture();
		_rtCamera = null;
	}

	protected override void OnPreRender()
	{
		if ( !_rtCamera.IsValid() ) return;

		EnsureRenderTexture();

		if ( HasOwner && Scene.Camera.IsValid() )
		{
			// When held, mirror the player's camera so the TV shows their POV.
			// TODO: network some props to the TV so they show up in the RT camera when held by a player other than the host.
			_rtCamera.WorldPosition = Scene.Camera.WorldPosition;
			_rtCamera.WorldRotation = Scene.Camera.WorldRotation;
			_rtCamera.FieldOfView = Scene.Camera.FieldOfView;

			if ( !_rtCamera.RenderExcludeTags.Has( "viewer" ) )
				_rtCamera.RenderExcludeTags.Add( "viewer" );
		}
		else
		{
			_rtCamera.RenderExcludeTags.Remove( "viewer" );
			_rtCamera.FieldOfView = 40f;
		}
	}

	/// <summary>
	/// We want to control the camera fov when held by a player.
	/// </summary>
	public override void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
	{
		if ( !player.Network.IsOwner || !Network.IsOwner ) return;

		camera.FieldOfView = fov;
		camera.WorldRotation = camera.WorldRotation * new Angles( 0, 0, roll );
	}

	public override void OnCameraMove( Player player, ref Angles angles )
	{
		if ( Input.Down( "attack2" ) )
		{
			angles = default;
		}

		float sensitivity = fov.Remap( 1, 70, 0.01f, 1 );
		angles *= sensitivity;
	}

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		if ( Input.Pressed( "reload" ) )
		{
			fov = 50;
			roll = 0;
		}

		if ( Input.Down( "attack2" ) )
		{
			fov += Input.AnalogLook.pitch;
			fov = fov.Clamp( 1, 150 );
			roll -= Input.AnalogLook.yaw;
		}

		EnsureDepthOfField();

		if ( dof.IsValid() )
		{
			UpdateDepthOfField( dof );
		}

		if ( focusing && Input.Released( "attack1" ) )
		{
			Game.TakeScreenshot();
			Sandbox.Services.Stats.Increment( "photos", 1 );

			GameObject?.PlaySound( CameraShoot );
		}

		focusing = Input.Down( "attack1" );
	}

	private void EnsureDepthOfField()
	{
		if ( dof.IsValid() ) return;

		dof = Scene.Camera.GetOrAddComponent<DepthOfField>();
		dof.Flags |= ComponentFlags.NotNetworked;
		focusing = false;
	}

	private void DestroyDepthOfField()
	{
		dof?.Destroy();
		dof = default;
	}

	private void UpdateDepthOfField( DepthOfField dof )
	{
		if ( !focusing )
		{
			dof.BlurSize = MathF.Pow( Scene.Camera.FieldOfView.Remap( 1, 55, 1, 0 ), 4 ) * 16;
			dof.FocusRange = 512;
			dof.FrontBlur = false;

			var tr = Scene.Trace.Ray( Scene.Camera.Transform.World.ForwardRay, 5000 )
								.Radius( 4 )
								.IgnoreGameObjectHierarchy( GameObject.Root )
								.Run();

			focusPoint = tr.EndPosition;
		}

		var target = Scene.Camera.WorldPosition.Distance( focusPoint ) + 64;

		dof.FocalDistance = dof.FocalDistance.LerpTo( target, Time.Delta * 2.0f );
	}

	private void EnsureRTCamera()
	{
		_rtCamera = GetComponentInChildren<CameraComponent>( true );

		if ( _rtCamera is null )
		{
			var go = new GameObject( GameObject, true, "rt_camera" );
			_rtCamera = go.AddComponent<CameraComponent>();
		}

		_rtCamera.IsMainCamera = false;
		_rtCamera.BackgroundColor = Color.Black;
		_rtCamera.ClearFlags = ClearFlags.Color | ClearFlags.Depth | ClearFlags.Stencil;
		_rtCamera.FieldOfView = fov;
		_rtCamera.RenderExcludeTags.Add( "viewmodel" );
	}

	private void EnsureRenderTexture()
	{
		if ( _renderTexture.IsValid() && _renderTexture.Width == _cameraResolution && _renderTexture.Height == _cameraResolution )
			return;

		CleanupRenderTexture();

		_renderTexture = Texture.CreateRenderTarget()
			.WithSize( _cameraResolution, _cameraResolution )
			.Create();

		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = _renderTexture;
		}
	}

	private void CleanupRenderTexture()
	{
		if ( _rtCamera.IsValid() )
		{
			_rtCamera.RenderTarget = null;
		}

		_renderTexture?.Dispose();
		_renderTexture = null;
	}

	public override void DrawHud( HudPainter painter, Vector2 crosshair )
	{
		// nothing!
	}
}
using Sandbox.Rendering;
using Sandbox.Utility;

public partial class Physgun : ScreenWeapon, IPlayerControllable
{
	[Property] public LineRenderer BeamRenderer { get; set; }
	[Property] public GameObject EndPointEffectPrefab { get; set; }
	[Property] public GameObject FreezeEffectPrefab { get; set; }
	[Property] public GameObject UnFreezeEffectPrefab { get; set; }
	[Property] public GameObject GrabEffectPrefab { get; set; }

	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput ShootInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput SecondaryInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput ExtendInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput RetractInput { get; set; }

	public void OnStartControl() { }
	public void OnEndControl() { }

	[Property, Group( "Screen" )] public float PowerMinDistance { get; set; } = 64f;
	[Property, Group( "Screen" )] public float PowerMaxDistance { get; set; } = 512f;
	[Property, Group( "Screen" )] public float PowerMinFraction { get; set; } = 0.5f;
	[Property, Group( "Screen" )] public float PowerMaxFraction { get; set; } = 1f;

	protected override string ScreenMaterialName => "v_physgun_display";
	protected override string ScreenMaterialPath => "weapons/physgun/physgun-screen.vmat";
	protected override float ScreenRefreshInterval => 0.1f;
	protected override Vector2Int ScreenTextureSize => new Vector2Int( 80, 80 );

	Vector3.SpringDamped middleSpring = new Vector3.SpringDamped( 0, 0 );

	float _prevBeamDistance;
	GameObject _endPointEffect;
	GameObject _grabEffect;

	public bool BeamActive => BeamRenderer?.Active == true || _state.Pulling || _stateHovered.Pulling;
	public bool PullActive => _state.Pulling || _stateHovered.Pulling;

	void UpdateBeam( Transform source, Vector3 end, Vector3 endNormal, bool grabbed )
	{
		if ( !BeamRenderer.IsValid() ) return;

		var endTx = new Transform( end, Rotation.LookAt( endNormal ) );

		if ( grabbed )
		{
			if ( _endPointEffect != null )
			{
				ITemporaryEffect.DisableLoopingEffects( _endPointEffect );
				_endPointEffect = null;
			}


			if ( !_grabEffect.IsValid() )
			{
				_grabEffect = GrabEffectPrefab.Clone( endTx );
			}

			if ( _grabEffect.IsValid() )
			{
				_grabEffect.WorldTransform = endTx;
			}

		}
		else
		{
			if ( _grabEffect != null )
			{
				_grabEffect.Destroy();
				_grabEffect = null;
			}

			if ( !_endPointEffect.IsValid() )
			{
				_endPointEffect = EndPointEffectPrefab.Clone( endTx );
			}

			if ( _endPointEffect.IsValid() )
			{
				_endPointEffect.WorldTransform = endTx;
			}
		}

		// obj
		if ( _state.GameObject.IsValid() )
		{
			//	BeamHighlight.Enabled = true;
			//	BeamHighlight.OverrideTargets = true;
			//	BeamHighlight.Targets.Clear();
			//	BeamHighlight.Targets.AddRange( _state.GameObject.GetComponents<Renderer>() );
			//	BeamHighlight.Width = 0.1f + Noise.Fbm( 3, Time.Now * 100.0f ) * 0.1f;
			//	BeamHighlight.Color = Color.Lerp( Color.Cyan, Color.White, Noise.Fbm( 3, Time.Now * 40.0f ) * 0.5f ) * 200.0f;
		}

		bool justEnabled = !BeamRenderer.GameObject.Enabled;

			if ( BeamRenderer.VectorPoints == null || BeamRenderer.VectorPoints.Count != 4 )
			BeamRenderer.VectorPoints = new List<Vector3>( [0, 0, 0, 0] );

		var distance = source.Position.Distance( end );
		var targetMiddle = source.Position + source.Forward * distance * 0.33f;
		targetMiddle = targetMiddle + Noise.FbmVector( 2, Time.Now * 400.0f, Time.Now * 100.0f ) * 1.0f;

		if ( !justEnabled )
		{
			// If the beam halved or more in a single frame, snap the spring to the new position to avoid shakiness
			if ( _prevBeamDistance > 1f && distance / _prevBeamDistance < 0.5f )
			{
				middleSpring = new Vector3.SpringDamped( targetMiddle, targetMiddle, 4, 0.2f );
			}

			// Ensure the middle point is never behind the first one
			var alongFwd = Vector3.Dot( middleSpring.Current - source.Position, source.Forward );
			if ( alongFwd < 0 )
			{
				var clamped = middleSpring.Current - source.Forward * alongFwd;
				middleSpring = new Vector3.SpringDamped( clamped, targetMiddle, 4, 0.2f );
			}
		}
		_prevBeamDistance = distance;

		BeamRenderer.VectorPoints[0] = source.Position;

		BeamRenderer.VectorPoints[1] = middleSpring.Current;
		middleSpring.Target = targetMiddle;
		middleSpring.Update( Time.Delta );

		BeamRenderer.VectorPoints[2] = Vector3.Lerp( (end + endNormal * 10), BeamRenderer.VectorPoints[1], 0.3f + MathF.Sin( Time.Now * 10.0f ) * 0.2f );
		BeamRenderer.VectorPoints[3] = end;

		if ( justEnabled )
		{
			BeamRenderer.GameObject.Enabled = true;
			_prevBeamDistance = distance;
			BeamRenderer.VectorPoints[1] = targetMiddle;
			middleSpring = new Vector3.SpringDamped( targetMiddle, targetMiddle, 4, 0.2f );
		}


	}

	void CloseBeam()
	{
		if ( _stateHovered.GameObject.IsValid() )
		{
			//	BeamHighlight.Enabled = true;
			//	BeamHighlight.OverrideTargets = true;
			//	BeamHighlight.Targets.Clear();
			//	BeamHighlight.Targets.AddRange( _stateHovered.GameObject.GetComponents<Renderer>() );
			//	BeamHighlight.Width = 0.2f;
			//	BeamHighlight.Color = new Color( 0.5f, 1, 1, 0.3f );
		}
		else
		{
			BeamHighlight.Enabled = false;
		}

		if ( !BeamRenderer.IsValid() ) return;

		BeamRenderer.GameObject.Enabled = false;

		if ( _endPointEffect.IsValid() )
		{
			ITemporaryEffect.DisableLoopingEffects( _endPointEffect );
			_endPointEffect = null;
		}

		if ( _grabEffect.IsValid() )
		{
			_grabEffect.Destroy();
			_grabEffect = null;
		}
	}

	private const int GraphSamples = 128;
	private float[] _graph1 = new float[GraphSamples];
	private float[] _graph2 = new float[GraphSamples];
	private float[] _graph3 = new float[GraphSamples];
	private int _graphCursor;
	private float _graphTimer;
	private const float GraphInterval = 0.02f;

	private float _plotValue1;
	private float _plotValue2;
	private float _plotValue3;

	private Texture _graphTexture;
	private byte[] _graphPixels = new byte[GraphSamples * 4]; // RGBA8

	protected override void DrawScreenContent( Rect rect, HudPainter paint )
	{
		paint.SetBlendMode( BlendMode.Lighten );

		var w = rect.Width;
		var h = rect.Height;
		var padX = w * 0.05f;
		var padY = h * 0.15f;

		var barWidthFraction = 0.55f;
		var barHeightFraction = 0.1f;

		var barW = w * barWidthFraction;
		var barH = h * barHeightFraction;
		var barX = rect.Left + padX;
		var barY = rect.Top + padY;

		var borderColor = new Color( 0.5f, 0.5f, 0.5f );

		// Fill bar
		var fillFraction = MathF.Max( _plotValue1, _plotValue2 );
		var normalized = ((fillFraction - 0.1f) / (0.8f - 0.1f)).Clamp( 0f, 1f );
		var fillWidth = barW * normalized;
		if ( fillWidth > 0f )
		{
			paint.DrawRect( new Rect( barX, barY, fillWidth, barH ), new Color( 1, 1, 1, 0.8f ) );
		}

		// Bar outline
		paint.DrawLine( new Vector2( barX, barY ), new Vector2( barX + barW, barY ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX, barY + barH ), new Vector2( barX + barW, barY + barH ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX, barY ), new Vector2( barX, barY + barH ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX + barW, barY ), new Vector2( barX + barW, barY + barH ), 1f, borderColor );

		// Percentage label
		var percent = (int)(normalized * 100f);
		var percentLabel = new TextRendering.Scope( $"{percent}", Color.White, h * 0.135f );
		percentLabel.FontName = "Consolas";
		percentLabel.TextColor = Color.White;
		percentLabel.FontWeight = 100;
		percentLabel.FilterMode = FilterMode.Point;
		paint.DrawText( percentLabel, new Rect( barX + barW + padX, barY, w - barW - padX * 3f, barH ), TextFlag.LeftCenter );

		// Channel / voltage row
		var rowY = barY + barH + padY;

		var ch2 = new TextRendering.Scope( "Ch2", Color.White, h * 0.14f );
		ch2.FontName = "Consolas";
		ch2.TextColor = new Color( 0f, 1f, 0f );
		ch2.FontWeight = 400;
		ch2.FilterMode = FilterMode.Point;
		paint.DrawText( ch2, new Rect( barX, rowY, w * 0.45f, 0 ), TextFlag.LeftCenter );

		var voltage = new TextRendering.Scope( "731v", Color.White, h * 0.14f );
		voltage.FontName = "Consolas";
		voltage.TextColor = new Color( 0f, 1f, 0f );
		voltage.FontWeight = 400;
		voltage.FilterMode = FilterMode.Point;
		paint.DrawText( voltage, new Rect( barX + w * 0.45f, rowY, w * 0.45f, 0 ), TextFlag.LeftCenter );
	}

	private float _spinIntensity;

	private TimeSince _lastGraphUpdate;

	private void UpdateScreenGraph()
	{
		var active1 = _state.Active && !_state.Pulling;
		var active2 = Input.Down( "attack2" ) && !_preventReselect || _state.Pulling;
		var active3 = _isSpinning;

		var distancePower = 1f;
		if ( active1 )
		{
			var range = PowerMaxDistance - PowerMinDistance;
			var fraction = PowerMaxFraction - PowerMinFraction;
			distancePower = ((_state.GrabDistance - PowerMinDistance) / range * fraction + PowerMinFraction).Clamp( PowerMinFraction, PowerMaxFraction );
		}

		// Track rotation intensity from analog look input
		if ( active3 )
		{
			var look = Input.AnalogLook;
			var rotationMagnitude = MathF.Sqrt( look.pitch * look.pitch + look.yaw * look.yaw + look.roll * look.roll );
			var rotationPower = (rotationMagnitude / 5f).Clamp( 0f, 1f );
			_spinIntensity = _spinIntensity.LerpTo( 0.2f + rotationPower * 0.6f, Time.Delta * 15f );
		}
		else
		{
			_spinIntensity = _spinIntensity.LerpTo( 0f, Time.Delta * 10f );
		}

		var target1 = active1 ? (0.8f * distancePower) + Random.Shared.Float( -0.05f, 0.05f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );

		// Held object velocity increases graph power on channel 1
		if ( active1 && _state.Body.IsValid() )
		{
			var velocityPower = (_state.Body.Velocity.Length / 500f).Clamp( 0f, 0.5f );
			target1 += velocityPower;
		}
		var target2 = active2 ? 0.8f + Random.Shared.Float( -0.05f, 0.05f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );
		var target3 = active3 ? _spinIntensity + Random.Shared.Float( -0.03f, 0.03f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );
		_plotValue1 = _plotValue1.LerpTo( target1, Time.Delta * 10f );
		_plotValue2 = _plotValue2.LerpTo( target2, Time.Delta * 10f );
		_plotValue3 = _plotValue3.LerpTo( target3, Time.Delta * 10f );

		_graphTimer += Time.Delta;
		while ( _graphTimer >= GraphInterval )
		{
			_graphTimer -= GraphInterval;
			_graph1[_graphCursor % GraphSamples] = _plotValue1;
			_graph2[_graphCursor % GraphSamples] = _plotValue2;
			_graph3[_graphCursor % GraphSamples] = _plotValue3;
			_graphCursor++;
		}

		if ( _lastGraphUpdate < ScreenRefreshInterval )
			return;

		_lastGraphUpdate = 0;

		var count = Math.Min( _graphCursor, GraphSamples );
		for ( var i = 0; i < GraphSamples; i++ )
		{
			float r, g, b;
			if ( i < count )
			{
				var idx = (_graphCursor - 1 - i + GraphSamples) % GraphSamples;
				r = _graph1[idx];
				g = _graph2[idx];
				b = _graph3[idx];
			}
			else
			{
				r = 0.1f;
				g = 0.1f;
				b = 0.1f;
			}

			var offset = i * 4;
			_graphPixels[offset + 0] = (byte)(r * 255f);
			_graphPixels[offset + 1] = (byte)(g * 255f);
			_graphPixels[offset + 2] = (byte)(b * 255f);
			_graphPixels[offset + 3] = 255;
		}

		_graphTexture ??= Texture.Create( GraphSamples, 1 ).WithDynamicUsage().Finish();
		_graphTexture.Update( _graphPixels );

		if ( !ViewModel.IsValid() ) return;

		var renderer = ViewModel.GetComponentInChildren<SkinnedModelRenderer>();
		if ( !renderer.IsValid() ) return;

		var so = renderer.SceneObject;
		so.Attributes.Set( "GraphData", _graphTexture );

		so.Attributes.Set( "Grid", new Vector4( 16f, 16f, 0.1f, 1.0f ) );
		so.Attributes.Set( "GraphInfo", new Vector4( GraphSamples, 0f, 0f, 0f ) );
		so.Attributes.Set( "Ch1Color", new Vector4( 0f, 1f, 1f, 1f ) );
		so.Attributes.Set( "Ch2Color", new Vector4( 1f, 1f, 0f, 1f ) );
		so.Attributes.Set( "Ch3Color", new Vector4( 1f, 0f, 0f, 0.5f ) );
		so.Attributes.Set( "Band1", new Vector4( 0.5f, 0.3f, 0f, 0f ) );
		so.Attributes.Set( "Band2", new Vector4( 0.48f, 0.28f, 0f, 0f ) );
		so.Attributes.Set( "Band3", new Vector4( 0.52f, 0.32f, 0f, 0f ) );
	}
}
/// <summary>
/// Sniper viewmodel helper. Moves the weapon down during scope transitions.
/// </summary>
public sealed class SniperViewModel : Component, ICameraSetup
{
	[Property] public float LowerAmount { get; set; } = 1.5f;
	[Property] public float LowerSpeed { get; set; } = 10f;

	private float _offset;

	void ICameraSetup.PostSetup( CameraComponent cc )
	{
		var weapon = GetComponentInParent<SniperWeapon>();
		if ( !weapon.IsValid() ) return;

		// Move the gun down while transitioning in/out of scope
		var target = weapon.IsScoped ? LowerAmount : 0f;
		_offset = _offset.LerpTo( target, Time.Delta * LowerSpeed );

		if ( _offset > 0.01f )
		{
			WorldPosition += cc.WorldRotation.Down * _offset;
		}
	}
}