3092 results

using Sandbox;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace SpriteTools;

[Category( "2D" )]
[Title( "2D Tileset" )]
[Icon( "calendar_view_month" )]
[Tint( EditorTint.Yellow )]
public partial class TilesetComponent : Component, Component.ExecuteInEditor
{
	/// <summary>
	/// The Layers within the TilesetComponent
	/// </summary>
	[Property, Group( "Layers" )]
	public List<Layer> Layers
	{
		get => _layers;
		set
		{
			_layers = value;
			foreach ( var layer in _layers )
			{
				layer.TilesetComponent = this;
			}
		}
	}
	List<Layer> _layers;

	[Property, WideMode( HasLabel = false )]
	ComponentControls InternalControls { get; set; }

	/// <summary>
	/// Whether or not the component should generate a collider based on the specified Collision Layer
	/// </summary>
	[Property, FeatureEnabled( "Collision" )]
	public bool HasCollider
	{
		get => _hasCollider;
		set
		{
			if ( value == _hasCollider ) return;
			_hasCollider = value;
			if ( value ) CreateCollider();
			else DestroyCollider();
		}
	}
	bool _hasCollider;

	/// <inheritdoc cref="Collider.Static" />
	[Property, Feature( "Collision" )]
	public bool Static
	{
		get => _static;
		set
		{
			if ( value == _static ) return;
			_static = value;
			if ( Collider.IsValid() ) Collider.Static = value;
		}
	}
	private bool _static = true;

	/// <inheritdoc cref="Collider.IsTrigger" />
	[Property, Feature( "Collision" )]
	public bool IsTrigger
	{
		get => _isTrigger;
		set
		{
			if ( value == _isTrigger ) return;
			_isTrigger = value;
			if ( Collider.IsValid() ) Collider.IsTrigger = value;
		}
	}
	private bool _isTrigger = false;

	/// <summary>
	/// The width of the generated collider
	/// </summary>
	[Property, Feature( "Collision" )]
	public float ColliderWidth
	{
		get => _colliderWidth;
		set
		{
			if ( value < 0f ) _colliderWidth = 0f;
			else if ( value == _colliderWidth ) return;
			_colliderWidth = value;
			Collider?.RebuildMesh();
		}
	}
	float _colliderWidth;

	/// <inheritdoc cref="Collider.Friction" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	[Range( 0f, 1f, true, true ), Step( 0.01f )]
	public float? Friction
	{
		get => _friction;
		set
		{
			if ( value == _friction ) return;
			_friction = value;
			if ( Collider.IsValid() ) Collider.Friction = value;
		}
	}
	private float? _friction;

	/// <inheritdoc cref="Collider.Surface" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	public Surface Surface
	{
		get => _surface;
		set
		{
			if ( value == _surface ) return;
			_surface = value;
			if ( Collider.IsValid() ) Collider.Surface = value;
		}
	}
	private Surface _surface;

	/// <inheritdoc cref="Collider.SurfaceVelocity" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	public Vector3 SurfaceVelocity
	{
		get => _surfaceVelocity;
		set
		{
			if ( value == _surfaceVelocity ) return;
			_surfaceVelocity = value;
			if ( Collider.IsValid() ) Collider.SurfaceVelocity = value;
		}
	}
	private Vector3 _surfaceVelocity;

	[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
	public Action<Collider> OnTriggerEnter { get; set; }

	[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
	public Action<Collider> OnTriggerExit { get; set; }

	/// <summary>
	/// Whether or not the associated Collider is dirty. Setting this to true will rebuild the Collider on the next frame.
	/// </summary>
	public bool IsDirty
	{
		get => Collider?.IsDirty ?? false;
		set
		{
			if ( !Collider.IsValid() ) return;
			Collider.IsDirty = value;
		}
	}
	TilesetCollider Collider;
	internal List<TilesetSceneObject> _sos = new();

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

		CreateCollider();

		if ( Layers is null ) return;
		foreach ( var layer in Layers )
		{
			layer.TilesetComponent = this;
		}
	}

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

		DestroyCollider();

		foreach ( var _so in _sos )
		{
			_so.Delete();
		}
		_sos.Clear();
	}

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

		_sos ??= new();
		Layers ??= new();
		var _newSos = new List<TilesetSceneObject>();
		foreach ( var sos in _sos )
		{
			if ( sos is not null || sos.IsValid() )
			{
				_newSos.Add( sos );
			}
			else
			{
				sos?.Delete();
			}
		}
		_sos = _newSos;
		if ( Layers.Count != _sos.Count )
		{
			RebuildSceneObjects();
		}
	}

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

		foreach ( var _so in _sos )
			_so?.Tags.SetFrom( Tags );
	}

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

		if ( Layers is null ) return;
		if ( Layers.Count == 0 )
		{
			return;
		}

		foreach ( var _so in _sos )
		{
			if ( !_so.IsValid() ) continue;
			_so.RenderingEnabled = true;
			_so.Transform = Transform.World;
			_so.Flags.CastShadows = false;
			_so.Flags.IsOpaque = false;
			_so.Flags.IsTranslucent = true;
		}
	}

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

		var bounds = GetBounds();
		Gizmo.Hitbox.BBox( bounds );

		if ( !Gizmo.IsSelected ) return;

		using ( Gizmo.Scope( "tileset", new Transform( 0, WorldRotation.Inverse, 1 ) ) )
		{
			Gizmo.Draw.Color = Color.Yellow;
			Gizmo.Draw.LineThickness = 1f;
			Gizmo.Draw.LineBBox( bounds );
		}
	}

	public BBox GetBounds ()
	{
		var bounds = BBox.FromPositionAndSize( 0, 0 );
		foreach ( var _so in _sos )
		{
			if ( !_so.IsValid() ) continue;

			var boundSize = _so.Bounds.Size;
			if ( ( boundSize.x + boundSize.y + boundSize.z ) > ( bounds.Size.x + bounds.Size.y + bounds.Size.z ) )
			{
				bounds = _so.Bounds.Translate( -_so.Position );
			}
		}

		return bounds;
	}

	void RebuildSceneObjects ()
	{
		foreach ( var _so in _sos )
		{
			_so.Delete();
		}

		_sos = new List<TilesetSceneObject>();
		for ( int i = 0; i < Layers.Count; i++ )
		{
			_sos.Add( new TilesetSceneObject( this, Scene.SceneWorld, i ) );
		}
	}

	void CreateCollider ()
	{
		if ( !HasCollider ) return;
		if ( Collider.IsValid() ) return;
		Collider = AddComponent<TilesetCollider>();
		Collider.Flags |= ComponentFlags.Hidden | ComponentFlags.NotSaved;
		Collider.Tileset = this;
		Collider.Static = Static;
		Collider.IsTrigger = IsTrigger;
		Collider.Friction = Friction;
		Collider.Surface = Surface;
		Collider.SurfaceVelocity = SurfaceVelocity;
		Collider.OnTriggerEnter += OnTriggerEnter;
		Collider.OnTriggerExit += OnTriggerExit;
	}

	void DestroyCollider ()
	{
		if ( Collider.IsValid() )
			Collider.Destroy();
		Collider = null;
	}

	/// <summary>
	/// Returns the Layer with the specified name
	/// </summary>
	/// <param name="name"></param>
	/// <returns></returns>
	public Layer GetLayerFromName ( string name )
	{
		return Layers.FirstOrDefault( x => x.Name == name );
	}

	/// <summary>
	/// Returns the Layer at the specified index
	/// </summary>
	/// <param name="index"></param>
	/// <returns></returns>
	public Layer GetLayerFromIndex ( int index )
	{
		if ( index < 0 || index >= Layers.Count ) return null;
		return Layers[index];
	}

	public class Layer
	{
		/// <summary>
		/// The name of the Layer
		/// </summary>
		public string Name { get; set; }

		/// <summary>
		/// Whether or not this Layer is currently being rendered
		/// </summary>
		public bool IsVisible { get; set; }

		/// <summary>
		/// Whether or not this Layer is locked. Locked Layers will ignore any attempted changes
		/// </summary>
		public bool IsLocked { get; set; }

		/// <summary>
		/// The Tileset that this Layer uses
		/// </summary>
		[Property, Group( "Selected Layer" )] public TilesetResource TilesetResource { get; set; }

		/// <summary>
		/// The height of the Layer
		/// </summary>
		[Property, Group( "Selected Layer" )] public float? Height { get; set; } = null;

		/// <summary>
		/// Whether or not this Layer dictates the collision mesh
		/// </summary>
		[Group( "Selected Layer" ), Title( "Has Collisions" )] public bool IsCollisionLayer { get; set; }

		/// <summary>
		/// A dictionary of all Tiles in the layer by their position.
		/// </summary>
		public Dictionary<Vector2Int, Tile> Tiles { get; set; }

		/// <summary>
		/// A dictionary containing a list of positions for each Autotile Brush by their ID.
		/// </summary>
		public Dictionary<Guid, List<AutotilePosition>> Autotiles { get; set; }

		/// <summary>
		/// The TilesetComponent that this Layer belongs to
		/// </summary>
		[JsonIgnore, Hide] public TilesetComponent TilesetComponent { get; set; }

		public Layer ( string name = "Untitled Layer" )
		{
			Name = name;
			IsVisible = true;
			IsLocked = false;
			Tiles = new();
		}

		/// <summary>
		/// Returns an exact copy of the Layer
		/// </summary>
		/// <returns></returns>
		public Layer Copy ()
		{
			var layer = new Layer( Name )
			{
				IsVisible = IsVisible,
				IsLocked = IsLocked,
				Tiles = new(),
				IsCollisionLayer = false,
				TilesetComponent = TilesetComponent,
			};

			foreach ( var tile in Tiles )
			{
				layer.Tiles[tile.Key] = tile.Value.Copy();
			}

			return layer;
		}

		/// <summary>
		/// Set a tile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="position"></param>
		/// <param name="tileId"></param>
		/// <param name="cellPosition"></param>
		/// <param name="angle"></param>
		/// <param name="flipX"></param>
		/// <param name="flipY"></param>
		/// <param name="rebuild"></param>
		public void SetTile ( Vector2Int position, Guid tileId, Vector2Int cellPosition = default, int angle = 0, bool flipX = false, bool flipY = false, bool rebuild = true, bool removeAutotile = true )
		{
			if ( IsLocked ) return;
			var tile = new Tile( tileId, cellPosition, angle, flipX, flipY );
			Tiles[position] = tile;
			if ( rebuild && TilesetComponent.IsValid() )
				TilesetComponent.IsDirty = true;

			if ( removeAutotile && Autotiles is not null )
			{
				foreach ( var group in Autotiles )
				{
					foreach ( var autotile in group.Value )
					{
						if ( autotile.Position == position )
						{
							Autotiles[group.Key].Remove( autotile );
							break;
						}
					}
				}
			}
		}

		/// <summary>
		/// Get the Tile at the specified position
		/// </summary>
		/// <param name="position"></param>
		/// <returns></returns>
		public Tile GetTile ( Vector2Int position )
		{
			return Tiles[position];
		}

		/// <summary>
		/// Get the Tile at the specified position
		/// </summary>
		/// <param name="position"></param>
		/// <returns></returns>
		public Tile GetTile ( Vector3 position )
		{
			return Tiles[new Vector2Int( (int)position.x, (int)position.y )];
		}

		/// <summary>
		/// Remove the Tile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="position"></param>
		public void RemoveTile ( Vector2Int position )
		{
			if ( IsLocked ) return;
			Tiles.Remove( position );

			if ( Autotiles is not null )
			{
				foreach ( var group in Autotiles )
				{
					foreach ( var autotile in group.Value )
					{
						if ( autotile.Position == position )
						{
							Autotiles[group.Key].Remove( autotile );
							break;
						}
					}
				}
			}
		}

		/// <summary>
		/// Set an Autotile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="autotileBrush"></param>
		/// <param name="position"></param>
		/// <param name="enabled"></param>
		///	<param name="update"></param>
		/// <param name="isMerging"></param>
		public void SetAutotile ( AutotileBrush autotileBrush, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
		{
			SetAutotile( autotileBrush.Id, position, enabled, update, isMerging );
		}

		/// <summary>
		/// Set an Autotile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="autotileId"></param>
		/// <param name="position"></param>
		/// <param name="enabled"></param>
		/// <param name="update"></param>
		/// <param name="isMerging"></param>
		public void SetAutotile ( Guid autotileId, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
		{
			if ( IsLocked ) return;
			Autotiles ??= new();

			foreach ( var group in Autotiles )
			{
				if ( group.Key == autotileId ) continue;
				foreach ( var autotile in group.Value )
				{
					if ( autotile.Position == position )
					{
						Autotiles[group.Key].Remove( autotile );
						break;
					}
				}
			}

			if ( !Autotiles.ContainsKey( autotileId ) )
				Autotiles[autotileId] = new List<AutotilePosition>();

			bool shouldUpdate = false;
			if ( enabled )
			{
				if ( !Autotiles[autotileId].Any( x => x.Position == position ) )
				{
					Autotiles[autotileId].Add( new( position, isMerging ) );
					shouldUpdate = true;
				}
			}
			else
			{
				var foundPos = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
				if ( foundPos is not null )
				{
					Tiles.Remove( position );
					Autotiles[autotileId].Remove( foundPos );
					shouldUpdate = true;
				}
				else
				{
					RemoveTile( position );
				}
			}

			if ( update && shouldUpdate )
			{
				UpdateAutotile( autotileId, position, !enabled, shouldMerge: isMerging );
			}
		}

		/// <summary>
		/// Update the Autotile at the specified position. Used when manually modifying the placed autotiles.
		/// </summary>
		/// <param name="autotileId"></param>
		/// <param name="position"></param>
		/// <param name="checkErased"></param>
		/// <param name="updateSurrounding"></param>
		/// <param name="shouldMerge"></param>
		public void UpdateAutotile ( Guid autotileId, Vector2Int position, bool checkErased, bool updateSurrounding = true, bool shouldMerge = false )
		{
			if ( !Autotiles.ContainsKey( autotileId ) ) return;

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
			var autotile = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
			if ( autotile is not null )
			{
				if ( shouldMerge ) autotile.ShouldMerge = true;
				if ( autotile.ShouldMerge ) shouldMerge = true;

				var bitmask = GetAutotileBitmask( autotileId, position, shouldMerge );
				if ( bitmask == -1 )
				{
					if ( checkErased ) RemoveTile( position );
				}
				else
				{
					if ( brush is not null )
					{
						var tile = brush.GetTileFromBitmask( bitmask );
						if ( tile is not null )
						{
							SetTile( position, tile.Id, Vector2Int.Zero, 0, false, false, false, removeAutotile: false );
						}
						else
						{
							Log.Warning( $"Tile not found for bitmask {bitmask} in AutotileBrush {brush.Name}" );
						}
					}
				}
			}

			if ( updateSurrounding )
			{
				var up = position.WithY( position.y + 1 );
				var down = position.WithY( position.y - 1 );
				var left = position.WithX( position.x - 1 );
				var right = position.WithX( position.x + 1 );
				var upLeft = up.WithX( left.x );
				var upRight = up.WithX( right.x );
				var downLeft = down.WithX( left.x );
				var downRight = down.WithX( right.x );

				if ( brush is not null && brush.AutotileType == AutotileType.Bitmask2x2Edge )
				{
					ClearInvalidAutotile( autotileId, up );
					ClearInvalidAutotile( autotileId, down );
					ClearInvalidAutotile( autotileId, left );
					ClearInvalidAutotile( autotileId, right );
					ClearInvalidAutotile( autotileId, upLeft );
					ClearInvalidAutotile( autotileId, upRight );
					ClearInvalidAutotile( autotileId, downLeft );
					ClearInvalidAutotile( autotileId, downRight );
				}

				UpdateAutotile( autotileId, up, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, down, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, left, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, right, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, upLeft, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, upRight, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, downLeft, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, downRight, checkErased, false, shouldMerge );
			}
		}

		void ClearInvalidAutotile ( Guid autotileId, Vector2Int position )
		{
			if ( !Tiles.TryGetValue( position, out var tile ) ) return;

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );

			if ( brush is null ) return;
			if ( brush.AutotileType != AutotileType.Bitmask2x2Edge ) return;
			if ( !brush.Tiles.Any( x => x.Tiles.Any( y => y.Id == tile.TileId ) ) ) return;
			if ( GetAutotileBitmask( autotileId, position ) != -1 ) return;

			RemoveTile( position );
		}


		public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, bool mergeAll = false )
		{
			if ( Autotiles is null || ( !mergeAll && !Autotiles.ContainsKey( autotileId ) ) ) return -1;

			List<AutotilePosition> positions = new();
			if ( mergeAll )
			{
				foreach ( var kvp in Autotiles )
				{
					positions.AddRange( kvp.Value );
				}
			}
			else
			{
				positions = Autotiles[autotileId];
			}
			int value = 0;

			var up = position.WithY( position.y + 1 );
			var down = position.WithY( position.y - 1 );
			var left = position.WithX( position.x - 1 );
			var right = position.WithX( position.x + 1 );

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
			if ( brush is null ) return 0;

			bool is2x2 = brush.AutotileType == AutotileType.Bitmask2x2Edge;
			if ( is2x2 )
			{
				foreach ( var pos in positions )
				{
					if ( pos.Position == up ) value += 1;
					if ( pos.Position == left ) value += 2;
					if ( pos.Position == right ) value += 4;
					if ( pos.Position == down ) value += 8;
				}
				switch ( value )
				{
					case 0:
					case 1:
					case 2:
					case 4:
					case 8:
					case 9:
					case 6:
						return -1;
				}
				value = 0;
			}

			var upLeft = up.WithX( left.x );
			var upRight = up.WithX( right.x );
			var downLeft = down.WithX( left.x );
			var downRight = down.WithX( right.x );

			foreach ( var thing in positions )
			{
				var pos = thing.Position;
				if ( pos == upLeft ) value += 1;
				if ( pos == up ) value += 2;
				if ( pos == upRight ) value += 4;
				if ( pos == left ) value += 8;
				if ( pos == right ) value += 16;
				if ( pos == downLeft ) value += 32;
				if ( pos == down ) value += 64;
				if ( pos == downRight ) value += 128;
			}

			if ( is2x2 )
			{
				switch ( value )
				{
					case 46:
					case 116:
					case 147:
					case 201:
						return -1;
				}
			}

			return value;
		}

		public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, Dictionary<Vector2Int, bool> overrides, bool mergeAll = false )
		{
			if ( Autotiles is null ) return -1;

			var positions = new List<Vector2Int>();
			foreach ( var thing in Autotiles )
			{
				if ( !mergeAll && thing.Key != autotileId ) continue;
				foreach ( var pos in thing.Value )
				{
					if ( !positions.Contains( pos.Position ) )
						positions.Add( pos.Position );
				}
			}
			int value = 0;

			foreach ( var ride in overrides )
			{
				if ( ride.Value )
				{
					if ( !positions.Contains( ride.Key ) )
					{
						positions.Add( ride.Key );
					}
				}
				else
				{
					if ( positions.Contains( ride.Key ) )
					{
						positions.Remove( ride.Key );
					}
				}
			}

			var up = position.WithY( position.y + 1 );
			var down = position.WithY( position.y - 1 );
			var left = position.WithX( position.x - 1 );
			var right = position.WithX( position.x + 1 );
			var upLeft = up.WithX( left.x );
			var upRight = up.WithX( right.x );
			var downLeft = down.WithX( left.x );
			var downRight = down.WithX( right.x );

			foreach ( var pos in positions )
			{
				if ( pos == upLeft ) value += 1;
				if ( pos == up ) value += 2;
				if ( pos == upRight ) value += 4;
				if ( pos == left ) value += 8;
				if ( pos == right ) value += 16;
				if ( pos == downLeft ) value += 32;
				if ( pos == down ) value += 64;
				if ( pos == downRight ) value += 128;
			}

			return value;
		}

		public class AutotilePosition
		{
			public Vector2Int Position { get; set; }
			public bool ShouldMerge { get; set; } = false;

			public AutotilePosition ( Vector2Int position, bool shouldMerge = false )
			{
				Position = position;
				ShouldMerge = shouldMerge;
			}
		}
	}

	public class Tile
	{
		public Guid TileId { get; set; } = Guid.NewGuid();
		public Vector2Int CellPosition { get; set; }
		public bool HorizontalFlip { get; set; }
		public bool VerticalFlip { get; set; }
		public int Rotation { get; set; }
		public Vector2Int BakedPosition { get; set; }

		public Tile () { }

		public Tile ( Guid tileId, Vector2Int cellPosition, int rotation, bool flipX, bool flipY )
		{
			TileId = tileId;
			CellPosition = cellPosition;
			HorizontalFlip = flipX;
			VerticalFlip = flipY;
			Rotation = rotation;
		}

		public Tile Copy ()
		{
			return new Tile( TileId, CellPosition, Rotation, HorizontalFlip, VerticalFlip );
		}
	}

	public class ComponentControls { }

}

internal sealed class TilesetSceneObject : SceneCustomObject
{
	TilesetComponent Component;
	Dictionary<TilesetResource, (TileAtlas, Material)> Materials = new();
	Material MissingMaterial;
	int LayerIndex;

	public TilesetSceneObject ( TilesetComponent component, SceneWorld world, int layerIndex ) : base( world )
	{
		Component = component;
		LayerIndex = layerIndex;

		MissingMaterial = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
		MissingMaterial.Set( "Texture", Texture.Load( "images/missing-tile.png" ) );
		Tags.SetFrom( Component.Tags );
	}

	public override void RenderSceneObject ()
	{
		if ( Component?.Layers is null ) return;
		var Layer = Component.Layers.ElementAtOrDefault( LayerIndex );
		if ( Layer is null )
		{
			return;
		}

		var layers = Component.Layers.ToList();
		layers.Reverse();
		if ( layers.Count == 0 ) return;

		Dictionary<Vector2Int, TilesetComponent.Tile> missingTiles = new();

		if ( Layer?.IsVisible != true ) return;

		int i = 0;
		int layerIndex = layers.IndexOf( Layer );

		{
			var tileset = Layer.TilesetResource;
			if ( tileset is null ) return;
			var tilemap = tileset.TileMap;

			var combo = GetMaterial( tileset );
			if ( combo.Item1 is null || combo.Item2 is null ) return;

			var tiling = combo.Item1.GetTiling();
			var totalTiles = Layer.Tiles.Where( x => x.Value.TileId == default || tilemap.ContainsKey( x.Value.TileId ) );
			var vertex = ArrayPool<Vertex>.Shared.Rent( totalTiles.Count() * 6 );

			var minPosition = new Vector3( int.MaxValue, int.MaxValue, int.MaxValue );
			var maxPosition = new Vector3( int.MinValue, int.MinValue, int.MinValue );

			foreach ( var tile in Layer.Tiles )
			{
				var pos = tile.Key;
				Vector2Int offsetPos = Vector2Int.Zero;
				if ( tile.Value.TileId == default ) offsetPos = tile.Value.BakedPosition;
				else
				{
					if ( !tilemap.ContainsKey( tile.Value.TileId ) )
					{
						missingTiles[pos] = tile.Value;
						continue;
					}
					offsetPos = tilemap[tile.Value.TileId].Position;
				}
				var offset = combo.Item1.GetOffset( offsetPos + tile.Value.CellPosition );
				if ( tile.Value.HorizontalFlip )
					offset.x = -offset.x - tiling.x;
				if ( !tile.Value.VerticalFlip )
					offset.y = -offset.y - tiling.y;


				var size = tileset.GetTileSize();
				var position = new Vector3( pos.x, pos.y, Layer.Height ?? ( Component.Layers.Count - Component.Layers.IndexOf( Layer ) ) ) * new Vector3( size.x, size.y, 1 );

				minPosition = Vector3.Min( minPosition, position );
				maxPosition = Vector3.Max( maxPosition, position );

				var topLeft = new Vector3( position.x, position.y, position.z );
				var topRight = new Vector3( position.x + size.x, position.y, position.z );
				var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
				var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );

				var uvTopLeft = new Vector2( offset.x, offset.y );
				var uvTopRight = new Vector2( offset.x + tiling.x, offset.y );
				var uvBottomRight = new Vector2( offset.x + tiling.x, offset.y + tiling.y );
				var uvBottomLeft = new Vector2( offset.x, offset.y + tiling.y );

				if ( tile.Value.Rotation == 90 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvBottomLeft;
					uvBottomLeft = uvBottomRight;
					uvBottomRight = uvTopRight;
					uvTopRight = tempUv;
				}
				else if ( tile.Value.Rotation == 180 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvBottomRight;
					uvBottomRight = tempUv;
					tempUv = uvTopRight;
					uvTopRight = uvBottomLeft;
					uvBottomLeft = tempUv;
				}
				else if ( tile.Value.Rotation == 270 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvTopRight;
					uvTopRight = uvBottomRight;
					uvBottomRight = uvBottomLeft;
					uvBottomLeft = tempUv;
				}

				vertex[i] = new Vertex( topLeft );
				vertex[i].TexCoord0 = uvTopLeft;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( topRight );
				vertex[i].TexCoord0 = uvTopRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomRight );
				vertex[i].TexCoord0 = uvBottomRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( topLeft );
				vertex[i].TexCoord0 = uvTopLeft;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomRight );
				vertex[i].TexCoord0 = uvBottomRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomLeft );
				vertex[i].TexCoord0 = uvBottomLeft;
				vertex[i].Normal = Vector3.Up;
				i++;
			}

			Graphics.Draw( vertex, totalTiles.Count() * 6, combo.Item2, Attributes );
			ArrayPool<Vertex>.Shared.Return( vertex );

			var siz = tileset.GetTileSize();
			maxPosition += new Vector3( siz.x, siz.y, 0 );
			Bounds = new BBox( minPosition, maxPosition + Vector3.Down * 0.01f ).Rotate( Rotation ).Translate( Position );


		}

		if ( missingTiles.Count > 0 )
		{
			var uvTopLeft = new Vector2( 0, 0 );
			var uvTopRight = new Vector2( 1, 0 );
			var uvBottomRight = new Vector2( 1, 1 );
			var uvBottomLeft = new Vector2( 0, 1 );

			foreach ( var tile in missingTiles )
			{
				var material = MissingMaterial;
				var pos = tile.Key;
				var size = Component.Layers[0].TilesetResource.TileSize;
				var position = new Vector3( pos.x, pos.y, 0 ) * new Vector3( size.x, size.y, 1 );

				var topLeft = new Vector3( position.x, position.y, position.z );
				var topRight = new Vector3( position.x + size.x, position.y, position.z );
				var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
				var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );

				var vertex = new Vertex[]
				{
				new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
				new Vertex(topRight) { TexCoord0 = uvTopRight, Normal = Vector3.Up },
				new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
				new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
				new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
				new Vertex(bottomLeft) { TexCoord0 = uvBottomLeft, Normal = Vector3.Up },
				};

				Graphics.Draw( vertex, 6, material, Attributes );
			}
		}
	}

	(TileAtlas, Material) GetMaterial ( TilesetResource resource )
	{
		var texture = TileAtlas.FromTileset( resource );

		if ( Materials.TryGetValue( resource, out var combo ) )
		{
			combo.Item1 = texture;
			combo.Item2.Set( "Texture", texture );
		}
		else
		{
			var material = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
			material.Set( "Texture", texture );
			combo.Item1 = texture;
			combo.Item2 = material;
			Materials.Add( resource, combo );
		}

		return combo;
	}
}
namespace SpriteTools.Converters
{
    using System;
    using System.Collections.Generic;
    using System.Text.Json;
    using System.Text.Json.Serialization;

    /// <summary>
    /// Json collection converter.
    /// </summary>
    /// <typeparam name="TDatatype">Type of item to convert.</typeparam>
    /// <typeparam name="TConverterType">Converter to use for individual items.</typeparam>
    public class JsonCollectionItemConverter<TDatatype, TConverterType> : JsonConverter<IEnumerable<TDatatype>>
        where TConverterType : JsonConverter
    {
        /// <summary>
        /// Reads a json string and deserializes it into an object.
        /// </summary>
        /// <param name="reader">Json reader.</param>
        /// <param name="typeToConvert">Type to convert.</param>
        /// <param name="options">Serializer options.</param>
        /// <returns>Created object.</returns>
        public override IEnumerable<TDatatype> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.Null)
            {
                return default(IEnumerable<TDatatype>);
            }

            JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
            jsonSerializerOptions.Converters.Clear();
            jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());

            List<TDatatype> returnValue = new List<TDatatype>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                if (reader.TokenType != JsonTokenType.StartArray)
                {
                    returnValue.Add((TDatatype)JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions));
                }

                reader.Read();
            }

            return returnValue;
        }

        /// <summary>
        /// Writes a json string.
        /// </summary>
        /// <param name="writer">Json writer.</param>
        /// <param name="value">Value to write.</param>
        /// <param name="options">Serializer options.</param>
        public override void Write(Utf8JsonWriter writer, IEnumerable<TDatatype> value, JsonSerializerOptions options)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }

            JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
            jsonSerializerOptions.Converters.Clear();
            jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());

            writer.WriteStartArray();

            foreach (TDatatype data in value)
            {
                JsonSerializer.Serialize(writer, data, jsonSerializerOptions);
            }

            writer.WriteEndArray();
        }
    }
}
using Sandbox;

public class Camera2D : Component
{
	[Property] public CameraComponent Camera { get; set; }

	public Vector2 TargetPos { get; set; }
	public Vector2 CameraPos { get; set; }

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

		//Vector2 newPos = Vector2.Lerp( (Vector2)WorldPosition, TargetPos, Time.Delta * 3f );
		CameraPos = Utils.DynamicEaseTo( CameraPos, TargetPos, 0.2f, Time.Delta );

		var newPos = CameraPos + Utils.GetRandomVector() * Manager.Instance.Player.CamShakeAmount;

		float bounds_zoom = 1f + Manager.Instance.Player.Stats[PlayerStat.ZoomAmount] * 0.44f;
		var XDIST = 10.75f / bounds_zoom;
		var Y_MIN = -8.8f / bounds_zoom;
		var Y_MAX = 8.9f / bounds_zoom;
		newPos = new Vector2( MathX.Clamp( newPos.x, -XDIST, XDIST ), MathX.Clamp( newPos.y, Y_MIN, Y_MAX ) );

		WorldPosition = ((Vector3)newPos).WithZ( WorldPosition.z );
	}

	public void SetPos( Vector2 pos )
	{
		CameraPos = pos;
		TargetPos = pos;
		WorldPosition = ((Vector3)pos).WithZ( WorldPosition.z );
	}
}
using Sandbox;

public class FloatingDamageNumber : Component
{
	private RealTimeSince _timeSince;

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

		_timeSince = 0f;
	}

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

		if ( _timeSince > 1f )
			GameObject.Destroy();
	}
}
using Sandbox.UI;
using SpriteTools;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using static Manager;

public class LavaPuddle : Component
{
	[Property] public SpriteRendererLayer Sprite { get; set; }

	public TimeSince TimeSinceSpawn { get; set; }
	public float Lifetime { get; set; }
	
	public float Radius { get; set; }
	public float FullRadius { get; set; }

	private float _timeOffset;
	private TimeSince _timeSinceDamagePlayer;
	private const float DAMAGE_INTERVAL = 0.25f;

	private float _expandTime;

	public float DamageToPlayer { get; set; }

	public Color ColorA { get; set; }
	public Color ColorB { get; set; }

	public Vector2 Position2D
	{
		get { return (Vector2)WorldPosition; }
		set { WorldPosition = new Vector3( value.x, value.y, WorldPosition.z ); }
	}

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

		if ( Game.Random.Float( 0f, 1f ) < 0.5f )
			Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;

		Radius = 0f;
		LocalScale = new Vector3( Radius * 1f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;

		//LocalScale = new Vector3( Game.Random.Float( 0.4f, 0.6f ), Game.Random.Float( 0.15f, 0.25f ), 1f ) * Globals.SPRITE_SCALE;
		//LocalScale = new Vector3( Game.Random.Float( 0.33f, 0.48f ), Game.Random.Float( 0.4f, 0.63f ), 1f ) * 5f * Globals.SPRITE_SCALE;

		_timeOffset = Game.Random.Float( 0f, 99f );

		TimeSinceSpawn = 0f;
		_expandTime = Game.Random.Float( 0.4f, 0.5f );

		DamageToPlayer = 2f + Math.Min(Manager.Instance.Difficulty, 5);
	}

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

		float FADE_IN_TIME = 0.3f;
		var color = Color.Lerp( ColorA, ColorB, 0.5f + Utils.FastSin( TimeSinceSpawn * 16f ) * 0.5f );
		var opacity = 0.3f 
			* Utils.Map( TimeSinceSpawn, 0f, FADE_IN_TIME, 0f, 1f, EasingType.SineOut ) 
			* Utils.Map( TimeSinceSpawn, Lifetime - 1f, Lifetime, 1f, 0f, EasingType.SineIn ) 
			* ( 0.85f + Utils.FastSin( _timeOffset + Time.Now * 16f ) * 0.15f );
		Sprite.Tint = color.WithAlpha( opacity );

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );
		//Gizmo.Draw.LineSphere( WorldPosition, Radius, 20 );

		if (TimeSinceSpawn < _expandTime )
		{
			Radius = Utils.Map( TimeSinceSpawn, 0f, _expandTime, FullRadius * 0.05f, FullRadius, EasingType.QuadOut );
			LocalScale = new Vector3( Radius * 0.98f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;
		}

		if ( TimeSinceSpawn > Lifetime )
		{
			Manager.Instance.RemoveLavaPuddle( this );
			GameObject.Destroy();
			return;
		}

		if( TimeSinceSpawn > FADE_IN_TIME && TimeSinceSpawn < Lifetime - 0.5f ) 
		{
			var player = Manager.Instance.Player;
			float distSqr = (player.Position2D - Position2D).LengthSquared;

			if( distSqr < MathF.Pow(Radius, 1.9f) && _timeSinceDamagePlayer > DAMAGE_INTERVAL && player.TimeSinceHurtLava > DAMAGE_INTERVAL && !player.IsDead )
			{
				float currDmg = Utils.Map( TimeSinceSpawn, FADE_IN_TIME, FADE_IN_TIME * 2f, 1f, DamageToPlayer );
				float dmg = player.CheckDamageAmount( currDmg, DamageType.LavaPuddle );

				if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
				{
					player.Damage( dmg );

					_timeSinceDamagePlayer = 0f;
					player.TimeSinceHurtLava = 0f;

					Manager.Instance.PlaySfxNearby( "lava_puddle_03", player.Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1f, maxDist: 4f );
				}
			}
		}
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 5, 0, 1f, 0, false )]
public class BulletLifetimeDamageStatus : Status
{
	public BulletLifetimeDamageStatus()
	{
		Title = "Growing Bullets";
		IconPath = "textures/icons/bullet_lifetime_damage.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.BulletDamageGrow, GetAddForLevel( Level ), ModifierType.Add );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Bullets grow their damage by {0} per second", GetPrintAmountForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Bullets grow their damage by {0}→{1} per second", GetPrintAmountForLevel( newLevel - 1 ), GetPrintAmountForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetAddForLevel( int level )
	{
		return 0.2f + 1.5f * level + (level == 7 ? 1.5f : 0f);
	}

	public string GetPrintAmountForLevel( int level )
	{
		return string.Format( "{0:0.0}", GetAddForLevel( level ) );
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 7, 0, 1f, 0, false )]
public class CritChanceStatus : Status
{
	public CritChanceStatus()
	{
		Title = "Sharp Bullets";
		IconPath = "textures/icons/crit_chance.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.CritChance, GetAddForLevel( Level ), ModifierType.Add );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Increase critical chance from 5%→{0}%", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Increase critical chance from {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetAddForLevel( int level )
	{
		return 0.05f + 0.08f * level + (level == 7 ? 0.04f : 0f);
	}

	public float GetPercentForLevel( int level )
	{
		return 5 + 8 * level + (level == 7 ? 4 : 0);
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status(4, 0, 0.5f, 0, true)]
public class CurseAimDir : Status
{
	public CurseAimDir()
    {
		Title = "Random Aim";
		IconPath = "textures/icons/curse_shoot_random_dir.png";
	}

	public override void Init(Player player)
	{
		base.Init(player);
	}

	public override void Refresh()
    {
		Description = GetDescription(Level);

		Player.Modify(this, PlayerStat.ShootRandomDirChance, GetChanceForLevel(Level), ModifierType.Add);
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "{0}% chance to shoot in a random direction", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "{0}%→{1}% chance to shoot in a random direction", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetChanceForLevel( int level )
	{
		return 0.25f * level;
	}

	public float GetPercentForLevel( int level )
	{
		return 25 * level;
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 7, 0, 1f, 0, true )]
public class CurseAttackSpeedStatus : Status
{
	public CurseAttackSpeedStatus()
	{
		Title = "Lazy Shooting";
		IconPath = "textures/icons/curse_attack_speed.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.AttackSpeed, GetMultForLevel( Level ), ModifierType.Mult );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Decrease attack speed by {0}%", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Decrease attack speed by {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetMultForLevel( int level )
	{
		return 1f - 0.20f * level;
	}

	public float GetPercentForLevel( int level )
	{
		return 20 * level;
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 4, 0, 1f, 0, false )]
public class DashStrengthStatus : Status
{
	public DashStrengthStatus()
	{
		Title = "Leg Day";
		IconPath = "textures/icons/dash_strength.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.DashStrength, GetMultForLevel( Level ), ModifierType.Mult );
		Player.Modify( this, PlayerStat.DashInvulnTime, GetMultForLevel( Level ), ModifierType.Mult );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "You dash {0}% longer", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "You dash {0}%→{1}% longer", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetMultForLevel( int level )
	{
		return 1f + 0.18f * level + (level == 4 ? 0.03f : 0f);
	}

	public float GetPercentForLevel( int level )
	{
		return 18 * level + (level == 4 ? 3 : 0);
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 7, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]
public class FearDropGrenadeStatus : Status
{
	public FearDropGrenadeStatus()
	{
		Title = "Bomb Curse";
		IconPath = "textures/icons/fear_drop_grenade.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.FearDropGrenadeChance, GetAddForLevel( Level ), ModifierType.Add ); ;
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Enemies you scare have a {0}% chance to drop a grenade on death", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Enemies you scare have a {0}%→{1}% chance to drop a grenade on death", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetAddForLevel( int level )
	{
		return level * 0.07f;
	}

	public float GetPercentForLevel( int level )
	{
		return level * 7;
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status( 5, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]
public class FearPainStatus : Status
{
	public FearPainStatus()
	{
		Title = "Killer Stress";
		IconPath = "textures/icons/fear_pain.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.FearPainPercent, GetAddForLevel( Level ), ModifierType.Add );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Enemies you scare lose {0}% of their remaining HP each second", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Enemies you scare lose {0}%→{1}% of their remaining HP each second", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetAddForLevel( int level )
	{
		return 0.02f + level * 0.07f + (level == 5 ? 0.01f : 0f);
	}

	public float GetPercentForLevel( int level )
	{
		return 2 + level * 7 + (level == 5 ? 1 : 0);
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status(4, 0, 1f, 0, false, typeof(GrenadeShootReloadStatus), typeof(FearDropGrenadeStatus))]
public class GrenadeFearStatus : Status
{
    public GrenadeFearStatus()
    {
        Title = "Terrorism";
        IconPath = "textures/icons/grenade_fear.png";
    }

    public override void Init(Player player)
    {
        base.Init(player);
    }

    public override void Refresh()
    {
        Description = GetDescription(Level);

        Player.Modify(this, PlayerStat.GrenadeFearChance, GetAddForLevel(Level), ModifierType.Add); ;
    }

    public override string GetDescription(int newLevel)
    {
        return string.Format("Your grenades have a {0}% chance to scare enemies they hurt", GetPercentForLevel(Level));
    }

    public override string GetUpgradeDescription(int newLevel)
    {
        return newLevel > 1 ? string.Format("Your grenades have a {0}%→{1}% chance to scare enemies they hurt", GetPercentForLevel(newLevel - 1), GetPercentForLevel(newLevel)) : GetDescription(newLevel);
    }

    public float GetAddForLevel(int level)
    {
        return level == 4 ? 1f : 0.3f * level;
    }

    public float GetPercentForLevel(int level)
    {
        return level == 4 ? 100 : 30 * level;
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status(5, 3, 1f, 0, false )]
public class KillHealStatus : Status
{
	public KillHealStatus()
    {
		Title = "Vampire";
		IconPath = "textures/icons/kill_heal.png";
	}

	public override void Init(Player player)
	{
		base.Init(player);
	}

	public override void Refresh()
    {
		Description = GetDescription(Level);

        Player.Modify(this, PlayerStat.HealthRegen, -GetHpDrainAmountForLevel(Level), ModifierType.Add);
    }

	public override string GetDescription(int newLevel)
	{
		return string.Format("Heal for {0} whenever you kill an enemy within 2 meters but lose {1} HP/s", GetPrintAmountForLevel(Level), GetHpDrainPrintAmountForLevel(Level));
	}

	public override string GetUpgradeDescription(int newLevel)
	{
		return newLevel > 1 ? string.Format( "Heal for {0}→{1} whenever you kill an enemy within 2 meters but lose {2}→{3} HP/s", GetPrintAmountForLevel(newLevel - 1), GetPrintAmountForLevel(newLevel), GetHpDrainPrintAmountForLevel(newLevel - 1), GetHpDrainPrintAmountForLevel(newLevel)) : GetDescription(newLevel);
	}

    public override void OnKill(Enemy enemy)
    {
		var distSqr = (enemy.Position2D - Player.Position2D).LengthSquared;
		if ( distSqr > 2f * 2f )
			return;

		var amount = GetAmountForLevel( Level );

		if ( Player.Health < Player.Stats[PlayerStat.MaxHp] )
			Player.TimeSinceChangeHP = 0f;

		Player.RegenHealth( amount );

		//DamageNumbersLegacy.Create( amount, Player.Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Player.Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.7f );
	}

	public float GetAmountForLevel(int level)
	{
		return 0.1f + level * 0.5f + (level == 5 ? 0.05f : 0f);
	}

    public string GetPrintAmountForLevel(int level)
    {
        return string.Format("{0:0.00}", GetAmountForLevel(level));
    }

    public float GetHpDrainAmountForLevel(int level)
    {
        return level * 0.4f;
    }

    public string GetHpDrainPrintAmountForLevel(int level)
    {
        return string.Format("{0:0.00}", GetHpDrainAmountForLevel(level));
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

[Status(3, 0, 1f, maxDifficulty: 2, isCurse: false )]
public class MoreRerollsStatus : Status
{
	public MoreRerollsStatus()
    {
		Title = "More Rerolls";
		IconPath = "textures/icons/more_rerolls.png";
	}

	public override void Init(Player player)
	{
		base.Init(player);
	}

	public override void Refresh()
    {
		Description = GetDescription(Level);

		Player.Modify(this, PlayerStat.NumRerollsPerLevel, GetAddForLevel(Level), ModifierType.Add);
	}

	public override string GetDescription(int newLevel)
	{
		return string.Format("Gain {0} addition reroll each level", GetAddForLevel(Level));
	}

	public override string GetUpgradeDescription(int newLevel)
    {
		return newLevel > 1 ? string.Format("Gain {0}→{1} addition rerolls each level", GetAddForLevel(newLevel - 1), GetAddForLevel(newLevel)) : GetDescription(newLevel);
	}

	public float GetAddForLevel(int level)
    {
		return 1f * level;
    }
}
using Sandbox;

[Status( 7, 0, 1f, 0, false )]
public class MovespeedStatus : Status
{
	public MovespeedStatus()
	{
		Title = "Fast Shoes";
		IconPath = "textures/icons/shoe.png";
	}

	public override void Init( Player player )
	{
		base.Init( player );
	}

	public override void Refresh()
	{
		Description = GetDescription( Level );

		Player.Modify( this, PlayerStat.MoveSpeed, GetMultForLevel( Level ), ModifierType.Mult );
	}

	public override string GetDescription( int newLevel )
	{
		return string.Format( "Increase movespeed by {0}%", GetPercentForLevel( Level ) );
	}

	public override string GetUpgradeDescription( int newLevel )
	{
		return newLevel > 1 ? string.Format( "Increase movespeed by {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
	}

	public float GetMultForLevel( int level )
	{
		return 1f + 0.2f * level + (level == 7 ? 0.2f : 0f);
	}

	public float GetPercentForLevel( int level )
	{
		return 20 * level + (level == 7 ? 20 : 0);
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class Status
{
	public bool ShouldUpdate { get; protected set; }
	public Player Player { get; protected set; }
	public int Level { get; set; }
	public int MaxLevel { get; set; }
	public TimeSince ElapsedTime { get; protected set; }
	public string Title { get; protected set; }
	public string Description { get; protected set; }
	public string IconPath { get; protected set; }

	public string DataString { get; protected set; }

	public Status()
	{
		Level = 1;
	}

	public virtual void Init( Player player )
	{
		Player = player;
		ElapsedTime = 0f;
		ShouldUpdate = false;
	}

	// when gaining or leveling up
	public virtual void Refresh()
	{

	}

	public virtual void Update( float dt )
	{
		//if (ElapsedTime > 10f)
		//    Player.RemoveStatus(this);
	}

	public virtual void Remove()
	{

	}

	public virtual string GetDescription( int newLevel )
	{
		return "...";
	}

	public virtual string GetUpgradeDescription( int newLevel )
	{
		return "...";
	}

	public virtual void Colliding( Thing other, float percent, float dt ) { }
	public virtual void OnDashStarted() { }
	public virtual void OnDashFinished() { }
	public virtual void OnDashRecharged() { }
	public virtual void OnReload() { }
	//public virtual void OnBurn( Enemy enemy ) { }
	//public virtual void OnFreeze( Enemy enemy ) { }
	//public virtual void OnFear( Enemy enemy ) { }
	public virtual void OnKill( Enemy enemy ) { }
	public virtual void OnHurt( float amount ) { }
	public virtual void OnGainExperience( int xp ) { }
	public virtual void OnLevelUp() { }
	public virtual void OnReroll() { }
	public virtual void OnGainShield() { }
	public virtual void OnLoseShield() { }
	public virtual void OnAddStatus( int typeIdentity ) { }
}
using SpriteTools;
using static Manager;

public enum ModifierType { Set, Add, Mult }
public class ModifierData
{
	public float value;
	public ModifierType type;
	public float priority;

	public ModifierData( float _value, ModifierType _type, float _priority = 0f )
	{
		value = _value;
		type = _type;
		priority = _priority;
	}
}

public enum PlayerStat
{
	AttackTime, AttackSpeed, ReloadTime, ReloadSpeed, MaxAmmoCount, BulletDamage, BulletForce, Recoil, MoveSpeed, NumProjectiles, BulletSpread, BulletInaccuracy, BulletSpeed, BulletLifetime,
	BulletNumPiercing, CritChance, CritMultiplier, LowHealthDamageMultiplier, NumUpgradeChoices, HealthRegen, HealthRegenStill, DamageReductionPercent, PushStrength, CoinAttractRange, CoinAttractStrength, Luck, MaxHp,
	NumDashes, DashInvulnTime, DashCooldown, DashProgress, DashStrength, ThornsPercent, ShootFireIgniteChance, FireDamage, FireLifetime, FireSpreadChance, ShootFreezeChance, FreezeLifetime,
	FreezeTimeScale, FreezeOnMeleeChance, FreezeFireDamageMultiplier, LastAmmoDamageMultiplier, FearLifetime, FearDamageMultiplier, FearOnMeleeChance, BulletDamageGrow, BulletDamageShrink,
	BulletDistanceDamage, NumRerollsPerLevel, FullHealthDamageMultiplier, DamagePerEarlierShot, DamageForSpeed, OverallDamageMultiplier, ExplosionSizeMultiplier, GrenadeVelocity, ExplosionDamageMultiplier,
	BulletDamageMultiplier, ExplosionDamageReductionPercent, NonExplosionDamageIncreasePercent, GrenadeStickyPercent, GrenadeFearChance, FearDrainPercent, FearPainPercent, CrateChanceAdditional,
	AttackSpeedStill, FearDropGrenadeChance, FrozenShardsNum, NoDashInvuln, BulletFlatDamageAddition, GrenadesCanCrit, BulletHealTeammateAmount, HomingBulletChance, PauseWhileChoosing,
	BulletNumBouncing, DashCharm, CharmedEnemyDmgTakenMultiplier, CharmedEnemyDmgDealtMultiplier, BossArrivalTime, RadiusMultiplier, SpecialistStatusAmount, ZoomAmount, MaxFireStacks, XpRepel, HealthPackAmount,
	MaxBulletSpread, ShootRandomDirChance, DashRandomDirChance, MoveSelfDmg, MoveSelfDmgReqDist, MoveSelfDmgAmount, SelfDmgDistanceMoved, IncreasedDmgTaken, ReverseControls, SelfCritChance,
}

public enum DamageType { Melee, Ranged, Explosion, Fire, PlayerBullet, Self, Generic, LavaPuddle, FearPain, }
public enum PlayerDamageType { Enemy, Self, Grenade }

public struct CamShakeData
{
	public float strength;
	public float startTime;
	public float time;
	public EasingType easingType;
	public bool useRealTime;

	public CamShakeData( float _strength, float _startTime, float _time, EasingType _easingType, bool _useRealTime )
	{
		strength = _strength;
		startTime = _startTime;
		time = _time;
		easingType = _easingType;
		useRealTime = _useRealTime;
	}
}

public class Player : Thing
{
	[Property] public GameObject Body { get; set; }
	[Property] public GameObject ArrowAimerPrefab { get; set; }
	[Property] public GameObject BulletPrefab { get; set; }
	[Property] public Sprite EasySprite { get; set; }
	[Property] public Sprite Difficulty1Sprite { get; set; }
	[Property] public Sprite Difficulty2Sprite { get; set; }
	[Property] public Sprite Difficulty3Sprite { get; set; }
	[Property] public Sprite Difficulty4Sprite { get; set; }
	[Property] public Sprite Difficulty5Sprite { get; set; }
	[Property] public Sprite Difficulty6Sprite { get; set; }
	[Property] public Sprite Difficulty7Sprite { get; set; }
	[Property] public Sprite Difficulty8Sprite { get; set; }
	[Property] public Sprite Difficulty9Sprite { get; set; }
	[Property] public Sprite Difficulty10Sprite { get; set; }
	[Property] public Sprite Difficulty11Sprite { get; set; }
	[Property] public Sprite Difficulty12Sprite { get; set; }
	[Property] public Sprite Difficulty13Sprite { get; set; }
	[Property] public Sprite Difficulty14Sprite { get; set; }
	[Property] public Sprite Difficulty15Sprite { get; set; }


	[Sync] public float Health { get; set; }
	private float _regenHpAccumulated;
	private float _drainHpAccumulated;

	[Sync] public Vector2 InputVector { get; set; }

	public GameObject ArrowAimer { get; private set; }
	public SpriteRendererLayer ArrowSprite { get; private set; }
	public Vector2 AimDir { get; private set; }

	[Sync] public bool IsDead { get; private set; }
	public float Timer { get; protected set; }
	[Sync] public bool IsReloading { get; protected set; }
	[Sync] public float ReloadProgress { get; protected set; }

	public const float BASE_MOVE_SPEED = 15f;
	private int _shotNum;

	[Sync] public int Level { get; protected set; }
	public int ExperienceTotal { get; protected set; }
	public int ExperienceCurrent { get; protected set; }
	public int ExperienceRequired { get; protected set; }
	public bool IsChoosingLevelUpReward { get; protected set; }
	public TimeSince TimeSinceLevelUp { get; set; }
	public RealTimeSince RealTimeSinceChoseUpgrade { get; set; }
	public List<Status> LevelUpChoices { get; private set; }

	[Sync] public float DashTimer { get; private set; }
	[Sync] public bool IsDashing { get; private set; }
	[Sync] public Vector2 DashVelocity { get; private set; }
	[Sync] public float DashInvulnTimer { get; private set; }
	private TimeSince _dashCloudTime;
	public float DashProgress { get; protected set; }
	[Sync] public float DashRechargeProgress { get; protected set; }
	[Sync] public int NumDashesAvailable { get; set; }
	public int AmmoCount { get; protected set; }

	public bool IsMoving => Velocity.LengthSquared > 0.05f && !IsDashing;
	//public bool IsMoving => (Position2D - _lastPos2D).LengthSquared > 0.000001f;
	public bool IsInputtingMove => Input.AnalogMove.LengthSquared > 0.01f;
	public bool IsInvulnerable => IsDashing && Stats[PlayerStat.NoDashInvuln] <= 0f;
	public bool IsTimePausedForChoosing => IsChoosingLevelUpReward && Stats[PlayerStat.PauseWhileChoosing] > 0f;


	private float _flashTimer;
	private bool _isFlashing;
	public TimeSince TimeSinceHurt { get; private set; }
	public TimeSince TimeSinceChangeHP { get; set; }

	private GameObject _shieldVfx;

	[Sync] public int NumRerollAvailable { get; set; }

	[Sync] public NetDictionary<PlayerStat, float> Stats { get; private set; } = new();

	public Dictionary<int, Status> Statuses { get; private set; }

	private Dictionary<Status, Dictionary<PlayerStat, ModifierData>> _modifiers_stat = new Dictionary<Status, Dictionary<PlayerStat, ModifierData>>();
	private Dictionary<PlayerStat, float> _original_properties_stat = new Dictionary<PlayerStat, float>();

	private bool _doneFirstUpdate;
	private TimeSince _timeSinceShoot;
	private TimeSince _timeSinceSpawn;

	private Vector2 _lastPos2D;
	private TimeSince _timeSinceTouchLeftSide;
	private bool _hasUnlockedSprinterAchievement;

	private bool _hasUnlockedExperiencedAchievement;

	public TimeSince TimeSinceInputMove { get; set; }
	private bool _hasUnlockedNoMoveAchievement;

	private List<CamShakeData> _camShakeDatas = new();
	public float CamShakeAmount { get; set; }

	public int ChoiceHash { get; set; }

	public TimeSince TimeSinceHurtLava { get; set; }

	public RealTimeSince RealTimeSinceDeath { get; set; }
	private float _arrowDeathAlphaStart;
	private bool _hasPlayedDeathSfx;

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

		Scale = 1f;

		ShadowOpacity = 0.8f;
		ShadowScale = 1.12f;

		Statuses = new Dictionary<int, Status>();
		LevelUpChoices = new List<Status>();
		InitializeStats();

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		ArrowAimer = ArrowAimerPrefab.Clone( WorldPosition );
		ArrowAimer.SetParent( GameObject );
		ArrowAimer.NetworkMode = NetworkMode.Never;
		ArrowSprite = ArrowAimer.Components.Get<SpriteRendererLayer>();
		ArrowSprite.Tint = Color.White.WithAlpha( 0f );

		_timeSinceShoot = 999f;
		_timeSinceSpawn = 0f;

		if ( Manager.Instance.Difficulty < 0 ) Sprite.Sprite = EasySprite;
		else if ( Manager.Instance.Difficulty == 1 ) Sprite.Sprite = Difficulty1Sprite;
		else if ( Manager.Instance.Difficulty == 2 ) Sprite.Sprite = Difficulty2Sprite;
		else if ( Manager.Instance.Difficulty == 3 ) Sprite.Sprite = Difficulty3Sprite;
		else if ( Manager.Instance.Difficulty == 4 ) Sprite.Sprite = Difficulty4Sprite;
		else if ( Manager.Instance.Difficulty == 5 ) Sprite.Sprite = Difficulty5Sprite;
		else if ( Manager.Instance.Difficulty == 6 ) Sprite.Sprite = Difficulty6Sprite;
		else if ( Manager.Instance.Difficulty == 7 ) Sprite.Sprite = Difficulty7Sprite;
		else if ( Manager.Instance.Difficulty == 8 ) Sprite.Sprite = Difficulty8Sprite;
		else if ( Manager.Instance.Difficulty == 9 ) Sprite.Sprite = Difficulty9Sprite;
		else if ( Manager.Instance.Difficulty == 10 ) Sprite.Sprite = Difficulty10Sprite;
		else if ( Manager.Instance.Difficulty == 11 ) Sprite.Sprite = Difficulty11Sprite;
		else if ( Manager.Instance.Difficulty == 12 ) Sprite.Sprite = Difficulty12Sprite;
		else if ( Manager.Instance.Difficulty == 13 ) Sprite.Sprite = Difficulty13Sprite;
		else if ( Manager.Instance.Difficulty == 14 ) Sprite.Sprite = Difficulty14Sprite;
		else if ( Manager.Instance.Difficulty == 15 ) Sprite.Sprite = Difficulty15Sprite;

		//Sprite.LocalScale = 0.5f * Globals.SPRITE_SCALE;
	}

	public void InitializeStats()
	{
		_original_properties_stat.Clear();

		if ( Network.Active )
		{
			RemoveShieldVfx();
		}
		else
		{
			if ( _shieldVfx != null )
			{
				_shieldVfx.Destroy();
				_shieldVfx = null;
			}
		}

		Level = 0;
		ExperienceRequired = GetExperienceReqForLevel( Level + 1 );
		ExperienceTotal = 0;
		ExperienceCurrent = 0;
		Stats[PlayerStat.AttackTime] = 0.15f;
		AmmoCount = 5;
		Stats[PlayerStat.MaxAmmoCount] = AmmoCount;
		Stats[PlayerStat.ReloadTime] = 1.5f;
		Stats[PlayerStat.ReloadSpeed] = 1f;
		Stats[PlayerStat.AttackSpeed] = 1f;
		Stats[PlayerStat.BulletDamage] = 5f;
		Stats[PlayerStat.BulletForce] = 0.55f;
		Stats[PlayerStat.Recoil] = 0f;
		Stats[PlayerStat.MoveSpeed] = 1f;
		Stats[PlayerStat.NumProjectiles] = 1f;
		Stats[PlayerStat.BulletSpread] = 35f;
		Stats[PlayerStat.BulletInaccuracy] = 5f;
		Stats[PlayerStat.BulletSpeed] = 4.5f;
		Stats[PlayerStat.BulletLifetime] = 0.8f;
		Stats[PlayerStat.Luck] = 1f;
		Stats[PlayerStat.CritChance] = 0.05f;
		Stats[PlayerStat.CritMultiplier] = 1.5f;
		Stats[PlayerStat.LowHealthDamageMultiplier] = 1f;
		Stats[PlayerStat.FullHealthDamageMultiplier] = 1f;
		Stats[PlayerStat.ThornsPercent] = 0f;

		Stats[PlayerStat.NumDashes] = 1f;
		NumDashesAvailable = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
		Stats[PlayerStat.DashCooldown] = 3f;
		Stats[PlayerStat.DashInvulnTime] = 0.25f;
		Stats[PlayerStat.DashStrength] = 3f;
		Stats[PlayerStat.BulletNumPiercing] = 0f;
		Stats[PlayerStat.BulletNumBouncing] = 0f;
		Stats[PlayerStat.DashCharm] = 0f;
		Stats[PlayerStat.CharmedEnemyDmgTakenMultiplier] = 1f;
		Stats[PlayerStat.CharmedEnemyDmgDealtMultiplier] = 1f;
		Stats[PlayerStat.BossArrivalTime] = 15 * 60f;
		//Stats[PlayerStat.BossArrivalTime] = (Manager.Instance.Difficulty >= 5 ? 10 : 15) * 60f;
		Stats[PlayerStat.RadiusMultiplier] = 1f;
		Stats[PlayerStat.SpecialistStatusAmount] = 0f;
		Stats[PlayerStat.ZoomAmount] = 0f;
		Stats[PlayerStat.MaxFireStacks] = 0f;
		Stats[PlayerStat.XpRepel] = 0f;
		Stats[PlayerStat.HealthPackAmount] = 20f;
		Stats[PlayerStat.MaxBulletSpread] = 0f;
		Stats[PlayerStat.ShootRandomDirChance] = 0f;
		Stats[PlayerStat.DashRandomDirChance] = 0f;
		Stats[PlayerStat.MoveSelfDmg] = 0f;
		Stats[PlayerStat.MoveSelfDmgReqDist] = 0f;
		Stats[PlayerStat.MoveSelfDmgAmount] = 0f;
		Stats[PlayerStat.SelfDmgDistanceMoved] = 0f;
		Stats[PlayerStat.IncreasedDmgTaken] = 0f;
		Stats[PlayerStat.ReverseControls] = 0f;
		Stats[PlayerStat.SelfCritChance] = 0f;

		Health = 100f;
		Stats[PlayerStat.MaxHp] = 100f;
		//Health = 1f;
		_regenHpAccumulated = 0f;
		_drainHpAccumulated = 0f;

		IsDead = false;
		Radius = 0.10f;
		GridPos = Manager.Instance.GetGridSquareForPos( Position2D );
		AimDir = new Vector2( 0f, 1f );
		NumRerollAvailable = Manager.Instance.Difficulty < 0 ? 4 : (Manager.Instance.Difficulty >= 3 ? 3 : 2);

		Stats[PlayerStat.FireDamage] = 1f;
		Stats[PlayerStat.FireLifetime] = 3f;
		Stats[PlayerStat.ShootFireIgniteChance] = 0f;
		Stats[PlayerStat.FireSpreadChance] = 0f;
		Stats[PlayerStat.ShootFreezeChance] = 0f;
		Stats[PlayerStat.FreezeLifetime] = 4f;
		Stats[PlayerStat.FreezeTimeScale] = 0.55f;
		Stats[PlayerStat.FreezeOnMeleeChance] = 0f;
		Stats[PlayerStat.FreezeFireDamageMultiplier] = 1f;
		Stats[PlayerStat.FearLifetime] = 4.5f;
		Stats[PlayerStat.FearDamageMultiplier] = 1f;
		Stats[PlayerStat.FearOnMeleeChance] = 0f;

		Stats[PlayerStat.CoinAttractRange] = 1.7f;
		Stats[PlayerStat.CoinAttractStrength] = 3.5f;

		Stats[PlayerStat.NumUpgradeChoices] = 3f;
		Stats[PlayerStat.HealthRegen] = Manager.Instance.Difficulty == -1 ? 0.4f : 0f;
		Stats[PlayerStat.HealthRegenStill] = 0f;
		//Stats[PlayerStat.HealthDrain] = 0f;
		Stats[PlayerStat.DamageReductionPercent] = 0f;
		Stats[PlayerStat.PushStrength] = 30f;
		Stats[PlayerStat.LastAmmoDamageMultiplier] = 1f;
		Stats[PlayerStat.BulletDamageGrow] = 0f;
		Stats[PlayerStat.BulletDamageShrink] = 0f;
		Stats[PlayerStat.BulletDistanceDamage] = 0f;
		Stats[PlayerStat.NumRerollsPerLevel] = 1f;
		Stats[PlayerStat.DamagePerEarlierShot] = 0f;
		Stats[PlayerStat.DamageForSpeed] = 0f;
		Stats[PlayerStat.OverallDamageMultiplier] = 1f;
		Stats[PlayerStat.ExplosionSizeMultiplier] = 1f;
		Stats[PlayerStat.GrenadeVelocity] = 8f;
		Stats[PlayerStat.ExplosionDamageMultiplier] = 1f;
		Stats[PlayerStat.BulletDamageMultiplier] = 1f;
		Stats[PlayerStat.ExplosionDamageReductionPercent] = 0f;
		Stats[PlayerStat.NonExplosionDamageIncreasePercent] = 0f;
		Stats[PlayerStat.GrenadeStickyPercent] = 0f;
		Stats[PlayerStat.GrenadeFearChance] = 0f;
		Stats[PlayerStat.FearDrainPercent] = 0f;
		Stats[PlayerStat.FearPainPercent] = 0f;
		Stats[PlayerStat.CrateChanceAdditional] = 0f;
		Stats[PlayerStat.AttackSpeedStill] = 1f;
		Stats[PlayerStat.FearDropGrenadeChance] = 0f;
		Stats[PlayerStat.FrozenShardsNum] = 0f;
		Stats[PlayerStat.NoDashInvuln] = 0f;
		Stats[PlayerStat.BulletFlatDamageAddition] = 0f;
		Stats[PlayerStat.GrenadesCanCrit] = 0f;
		Stats[PlayerStat.BulletHealTeammateAmount] = 0f;
		Stats[PlayerStat.HomingBulletChance] = 0f;
		Stats[PlayerStat.PauseWhileChoosing] = 0f;

		Statuses.Clear();
		_modifiers_stat.Clear();

		_isFlashing = false;
		Sprite.FlashTint = Color.White.WithAlpha( 0f );
		Sprite.Tint = Color.White;
		IsChoosingLevelUpReward = false;
		IsDashing = false;
		IsReloading = true;
		Timer = Stats[PlayerStat.ReloadTime];
		ReloadProgress = 0f;
		DashProgress = 0f;
		DashRechargeProgress = 1f;
		TempWeight = 0f;
		_shotNum = 0;
		TimeSinceHurt = 999f;
		TimeSinceChangeHP = 999f;
		TimeSinceHurtLava = 999f;
		ShadowOpacity = 0.8f;
		ShadowSpriteDirty = true;

		_timeSinceTouchLeftSide = 999f;
		TimeSinceInputMove = 0f;

		TimeSinceLevelUp = 999f;
		RealTimeSinceChoseUpgrade = 999f;

		_camShakeDatas.Clear();

		ChoiceHash = 0;
	}

	public Vector2 AverageVelocity { get; private set; }

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

		AverageVelocity += Velocity * Time.Delta * 8f;
		//AverageVelocity = Utils.DynamicEaseTo( AverageVelocity, Vector2.Zero, 0.02f, Time.Delta );
		AverageVelocity *= (1f - Time.Delta * 2.7f);

		//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
		//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );

		//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
		//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );

		Vector2 anchor = Position2D + AverageVelocity * 0.1f;
		Vector2 perp = Utils.GetPerpendicularVector( AverageVelocity ).Normal;
		var perpA = anchor - perp * 10f;
		var perpB = anchor + perp * 10f;

		//Gizmo.Draw.Color = Color.Green.WithAlpha( 0.3f );
		//Gizmo.Draw.Line( perpA, perpB );

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.03f );
		//Gizmo.Draw.LineSphere( (Vector3)Position2D, 2, 16 );

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );
		//Gizmo.Draw.Text( $"{Stats[PlayerStat.FearPainPercent]}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );

		if ( !_doneFirstUpdate )
		{
			SpawnShadow( ShadowScale, ShadowOpacity );
			Manager.Instance.Camera2D.SetPos( Position2D );

			_doneFirstUpdate = true;
		}

		InputVector = new Vector2( -Input.AnalogMove.y, Input.AnalogMove.x );

		if ( Stats[PlayerStat.ReverseControls] > 0f )
			InputVector *= -1f;

		//if ( Input.Pressed( "Menu" ) )
		//{
		//	Manager.Instance.Restart();
		//	return;
		//}

		HandleCamShaking();

		if ( IsDead )
		{
			Sprite.FlashTint = RealTimeSinceDeath < 0.1f
				? Color.Red
				: Color.White.WithAlpha( 0f );

			ShadowOpacity = Utils.Map( RealTimeSinceDeath, 0f, Manager.FINAL_PANEL_WAIT_TIME, 0.8f, 0f, EasingType.Linear );
			ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );

			ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( RealTimeSinceDeath, 0f, 0.4f, _arrowDeathAlphaStart, 0f, EasingType.SineOut ) );

			if ( !_hasPlayedDeathSfx && RealTimeSinceDeath > 0.7f )
			{
				Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 0.55f, 0.65f ), volume: 1f, maxDist: 5.5f );
				_hasPlayedDeathSfx = true;
			}
		}

		if ( !Manager.Instance.ShouldUpdatePlayer )
			return;

		float dt = Time.Delta;

		if ( MathF.Abs( Velocity.x ) > 0.01f )
			Sprite.SpriteFlags = Velocity.x > 0f ? SpriteFlags.HorizontalFlip : SpriteFlags.None;

		bool hurting = TimeSinceHurt < 0.25f;
		bool attacking = !IsReloading;
		bool moving = Velocity.LengthSquared > 0.01f && InputVector.LengthSquared > 0.1f;

		string stateStr = "";
		if ( IsDead )
			stateStr = "ghost_";
		else if ( hurting && attacking )
			stateStr = "hurt_attack_";
		else if ( hurting )
			stateStr = "hurt_";
		else if ( attacking )
			stateStr = "attack_";

		Sprite.PlayAnimation( $"{stateStr}{(moving ? "walk" : "idle")}" );
		Sprite.PlaybackSpeed = moving ? Utils.Map( Velocity.Length, 0f, 2f, 1.5f, 2f ) : 0.66f;

		Sprite.LocalRotation = new Angles( 0f, -90f + (Velocity.Length * Utils.FastSin( Time.Now * MathF.PI * 6f ) * 1.6f) * (Sprite.SpriteFlags.HasFlag( SpriteFlags.HorizontalFlip ) ? -1f : 1f), 0f );

		if ( !IsDead )
		{
			HandleFlashing( dt );
		}

		if ( IsProxy )
			return;

		// ACHIEVEMENTS
		if ( Manager.Instance.Difficulty >= 0 )
		{
			if ( _lastPos2D.x < -15.7f && WorldPosition.x >= -15.7f )
			{
				_timeSinceTouchLeftSide = 0f;
			}

			if ( _lastPos2D.x < 15.7f && WorldPosition.x >= 15.7f )
			{
				Log.Info( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}" );

				if ( _timeSinceTouchLeftSide < 5.5f )
				{
					if ( !_hasUnlockedSprinterAchievement )
						Sandbox.Services.Achievements.Unlock( "sprinter" );

					_hasUnlockedSprinterAchievement = true;
				}
			}

			if ( !_hasUnlockedNoMoveAchievement )
			{
				if ( IsInputtingMove )
				{
					TimeSinceInputMove = 0f;
				}
				else if ( TimeSinceInputMove > 60f * 10f )
				{
					Sandbox.Services.Achievements.Unlock( "stand_ground" );
					_hasUnlockedNoMoveAchievement = true;
				}
			}
		}

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"ArrowAimer: {ArrowAimer}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );

		//Gizmo.Draw.ScreenText( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}", new Vector2( 50, 50 ) );

		_lastPos2D = Position2D;

		var velocity = Velocity + (IsDashing ? DashVelocity : Vector2.Zero);
		Position2D += velocity * dt;

		if ( Stats[PlayerStat.MoveSelfDmg] > 0f )
		{
			Stats[PlayerStat.SelfDmgDistanceMoved] += velocity.Length * dt;

			if ( !IsInvulnerable && Stats[PlayerStat.SelfDmgDistanceMoved] > Stats[PlayerStat.MoveSelfDmgReqDist] )
			{
				Stats[PlayerStat.SelfDmgDistanceMoved] -= Stats[PlayerStat.MoveSelfDmgReqDist];
				Damage( Stats[PlayerStat.MoveSelfDmgAmount], PlayerDamageType.Self );
				Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 1.25f, 1.45f ), volume: 0.9f, maxDist: 3f );
			}
		}

		WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );

		//Velocity = Utils.DynamicEaseTo( Velocity, Vector2.Zero, 0.2f, dt );
		Velocity *= Math.Max( 1f - dt * 12.9f, 0f );

		if ( Velocity.LengthSquared < 0.001f )
			Velocity = Vector2.Zero;

		TempWeight *= (1f - dt * 4.7f);

		if ( InputVector.LengthSquared > 0f )
		{
			Velocity += InputVector.Normal * Stats[PlayerStat.MoveSpeed] * BASE_MOVE_SPEED * dt;
			//Log.Info( $"dt: {dt}" );
		}

		HandleBounds();

		Manager.Instance.Camera2D.TargetPos = Position2D;

		if ( Input.UsingController )
		{

		}
		else
		{
			AimDir = (Manager.Instance.MouseWorldPos - (Position2D + new Vector2( 0f, 0.5f ))).Normal;

			if ( Stats[PlayerStat.ReverseControls] > 0f )
				AimDir *= -1f;
		}

		if ( ArrowAimer != null && !Manager.Instance.IsPauseMenuOpen && !IsTimePausedForChoosing )
		{
			ArrowAimer.LocalRotation = new Angles( 0f, MathF.Atan2( AimDir.y, AimDir.x ) * (180f / MathF.PI) - 180f, 0f );
			ArrowAimer.LocalPosition = new Vector2( 0f, 0.4f ) + AimDir * Utils.Map( _timeSinceShoot, 0f, 0.25f, 0.6f, 0.55f, EasingType.QuadOut );
			ArrowAimer.LocalScale = new Vector3( Utils.Map( _timeSinceShoot, 0f, 0.25f, 1.25f, 0.75f, EasingType.QuadOut ), 1f, 1f ) * 0.005f;
			ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceShoot, 0f, 0.3f, 1f, 0.3f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, 1f, 0f, 1f, EasingType.Linear ) );
		}

		for ( int dx = -1; dx <= 1; dx++ )
		{
			for ( int dy = -1; dy <= 1; dy++ )
			{
				Manager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );
			}
		}

		if ( !IsDead )
		{
			HandleDashing( dt );
			HandleStatuses( dt );
			HandleShooting( dt );
			HandleRegen( dt );
		}

		if ( IsChoosingLevelUpReward && !Manager.Instance.IsPauseMenuOpen )
		{
			if ( Input.Pressed( "reload" ) ) UseReroll();
			else if ( Input.Pressed( "Slot1" ) ) UseChoiceHotkey( 1 );
			else if ( Input.Pressed( "Slot2" ) ) UseChoiceHotkey( 2 );
			else if ( Input.Pressed( "Slot3" ) ) UseChoiceHotkey( 3 );
			else if ( Input.Pressed( "Slot4" ) ) UseChoiceHotkey( 4 );
			else if ( Input.Pressed( "Slot5" ) ) UseChoiceHotkey( 5 );
			else if ( Input.Pressed( "Slot6" ) ) UseChoiceHotkey( 6 );
		}

		if ( Input.Pressed( "use" ) )
		{
			//AddExperience( 10 );
		}
	}

	void HandleRegen( float dt )
	{
		if ( Math.Abs( Stats[PlayerStat.HealthRegen] ) > 0f )
			RegenHealth( Stats[PlayerStat.HealthRegen] * dt );

		//if ( Math.Abs( Stats[PlayerStat.HealthDrain] ) > 0f )
		//	RegenHealth( Stats[PlayerStat.HealthDrain] * dt );

		if ( Stats[PlayerStat.HealthRegenStill] > 0f && !IsMoving )
		{
			RegenHealth( Stats[PlayerStat.HealthRegenStill] * dt );

			if ( !IsDashing && Health < Stats[PlayerStat.MaxHp] )
				TimeSinceChangeHP = 0f;
		}
	}

	public void RegenHealth( float amount )
	{
		float maxHp = Stats[PlayerStat.MaxHp];
		float hpMissing = maxHp - Health;

		if ( amount > 0f )
		{
			if ( hpMissing <= 0f )
				return;

			_regenHpAccumulated += amount;

			if ( _regenHpAccumulated < 0.85f && hpMissing > _regenHpAccumulated )
				return;

			float hpRecovered = Math.Min( _regenHpAccumulated, hpMissing );

			Health += hpRecovered;

			//var particle = DamageNumbersLegacy.Create( hpRecovered, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.8f );
			//Vector3 velocity = new Vector3( Game.Random.Float(-0.5f, 0.5f), 0f, 0f );
			//Vector3 gravity = new Vector3( 0f, 1.5f, 0f );
			//particle.SetVector( 1, velocity );
			//particle.SetNamedValue( "Gravity", gravity );

			var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
			float size = 1.1f;
			Manager.Instance.SpawnDamageNumber( pos, amount, Color.Green, size, FloaterType.Heal );

			_regenHpAccumulated = 0f;
		}
		else
		{
			_drainHpAccumulated += amount;
			if ( _drainHpAccumulated < -1f )
			{
				var dmgAmount = MathF.Truncate( _drainHpAccumulated );
				_drainHpAccumulated -= dmgAmount;

				Damage( MathF.Abs( dmgAmount ), PlayerDamageType.Self );

				Manager.Instance.PlaySfxNearby( "lava_puddle_03", Position2D, pitch: Game.Random.Float( 1.7f, 1.75f ), volume: 0.15f, maxDist: 4f );
			}
		}
	}

	void HandleDashing( float dt )
	{
		int numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
		if ( NumDashesAvailable < numDashes )
		{
			DashTimer -= dt;
			DashRechargeProgress = Utils.Map( DashTimer, Stats[PlayerStat.DashCooldown], 0f, 0f, 1f );
			if ( DashTimer <= 0f )
			{
				DashRecharged();
			}
		}

		if ( DashInvulnTimer > 0f )
		{
			DashInvulnTimer -= dt;
			DashProgress = Utils.Map( DashInvulnTimer, Stats[PlayerStat.DashInvulnTime], 0f, 0f, 1f );
			if ( DashInvulnTimer <= 0f )
			{
				IsDashing = false;
				//Sprite.Tint = Color.White;
				Sprite.FlashTint = Color.White.WithAlpha( 0f );

				DashFinished();
			}
			else
			{
				if ( Stats[PlayerStat.DashCharm] > 0f )
				{
					if ( IsInvulnerable )
						Sprite.FlashTint = new Color( Game.Random.Float( 0.8f, 1f ), Game.Random.Float( 0f, 0.1f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
					else if ( !_isFlashing )
						Sprite.FlashTint = new Color( Game.Random.Float( 0.9f, 1f ), Game.Random.Float( 0.1f, 0.3f ), Game.Random.Float( 0.9f, 1f ), 0.8f );
				}
				else
				{
					if ( IsInvulnerable )
						Sprite.FlashTint = new Color( Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
				}

				if ( _dashCloudTime > Game.Random.Float( 0.1f, 0.2f ) )
				{
					SpawnDashCloudClient();
					_dashCloudTime = 0f;
				}
			}
		}

		if ( Input.Pressed( "Jump" ) || Input.Pressed( "attack1" ) )
		{
			//Position2D = Manager.Instance.MouseWorldPos;
			////Manager.Instance.Camera2D.SetPos( Position2D );
			//Transform.ClearInterpolation();
			//return;

			Dash();
		}
	}

	public void Dash()
	{
		if ( NumDashesAvailable <= 0 || IsTimePausedForChoosing )
			return;

		Vector2 dashDir = Velocity.LengthSquared > 0f ? Velocity.Normal : AimDir;

		if ( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.DashRandomDirChance] )
			dashDir = Utils.GetRandomVector();

		DashVelocity = dashDir * Stats[PlayerStat.DashStrength];
		TempWeight = 2f;

		if ( NumDashesAvailable == (int)Stats[PlayerStat.NumDashes] )
			DashTimer = Stats[PlayerStat.DashCooldown];

		NumDashesAvailable--;
		IsDashing = true;
		DashInvulnTimer = Stats[PlayerStat.DashInvulnTime];
		DashProgress = 0f;
		DashRechargeProgress = 0f;

		Manager.Instance.PlaySfxNearby( "player.dash", Position2D + dashDir * 0.5f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 1f, maxDist: 4f );
		SpawnDashCloudClient();
		_dashCloudTime = 0f;

		ForEachStatus( status => status.OnDashStarted() );
	}

	public void DashFinished()
	{
		ForEachStatus( status => status.OnDashFinished() );
	}

	public void DashRecharged()
	{
		NumDashesAvailable++;
		var numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
		if ( NumDashesAvailable > numDashes )
			NumDashesAvailable = numDashes;

		if ( NumDashesAvailable < numDashes )
		{
			DashTimer = Stats[PlayerStat.DashCooldown];
			DashRechargeProgress = 0f;
		}
		else
		{
			DashRechargeProgress = 1f;
		}

		ForEachStatus( status => status.OnDashRecharged() );

		Manager.Instance.PlaySfxNearby( "player.dash.recharge", Position2D, pitch: Utils.Map( NumDashesAvailable, 1, numDashes, 1f, 1.2f ), volume: 0.2f, maxDist: 5f );
	}

	void HandleBounds()
	{
		var x_min = Manager.Instance.BOUNDS_MIN.x + Radius;
		var x_max = Manager.Instance.BOUNDS_MAX.x - Radius;
		var y_min = Manager.Instance.BOUNDS_MIN.y;
		var y_max = Manager.Instance.BOUNDS_MAX.y - Radius;

		if ( Position2D.x < x_min )
			Position2D = new Vector2( x_min, Position2D.y );
		else if ( Position2D.x > x_max )
			Position2D = new Vector2( x_max, Position2D.y );

		if ( Position2D.y < y_min )
			Position2D = new Vector2( Position2D.x, y_min );
		else if ( Position2D.y > y_max )
			Position2D = new Vector2( Position2D.x, y_max );
	}

	public int GetExperienceReqForLevel( int level )
	{
		switch ( Manager.Instance.Difficulty )
		{
			case -1:
				return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 240f, EasingType.SineIn ) );
			case 0:
			default:
				return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 320f, EasingType.SineIn ) );
		}
	}

	public void Flash( float time )
	{
		if ( _isFlashing )
			return;

		//Sprite.Tint = new Color( 1f, 0f, 0f );
		Sprite.FlashTint = new Color( 1f, 0f, 0f, 1f );
		_isFlashing = true;
		_flashTimer = time;
	}

	public void Heal( float amount, float flashTime )
	{
		//Sprite.Tint = new Color( 0f, 1f, 0f );
		Sprite.FlashTint = new Color( 0f, 1f, 0f, 1f );
		_isFlashing = true;
		_flashTimer = flashTime;

		if ( IsProxy )
			return;

		if ( Health < Stats[PlayerStat.MaxHp] )
			TimeSinceChangeHP = 0f;

		Health += amount;
		if ( Health > Stats[PlayerStat.MaxHp] )
			Health = Stats[PlayerStat.MaxHp];
	}

	void HandleFlashing( float dt )
	{
		if ( _isFlashing )
		{
			_flashTimer -= dt;
			if ( _flashTimer < 0f )
			{
				_isFlashing = false;
				//Sprite.Tint = Color.White;
				Sprite.FlashTint = Color.White.WithAlpha( 0f );
			}
		}
	}

	[ConCmd( "give_status" )]
	public static void GiveStatus( string name )
	{
		// Cheat only works in the editor
		if ( !Game.IsEditor )
			return;

		var type = TypeLibrary.GetType( name );
		if ( type == null )
		{
			Log.Info( $"No status with name '{name}' found!" );
			return;
		}

		Manager.Instance?.GetLocalPlayer()?.AddStatus( type );
	}

	public void AddStatus( TypeDescription type )
	{
		Status status = null;
		var typeIdentity = type.Identity;

		ForEachStatus( status => status.OnAddStatus( typeIdentity ) );

		if ( Statuses.ContainsKey( typeIdentity ) )
		{
			status = Statuses[typeIdentity];
			status.Level++;
		}

		if ( status == null )
		{
			status = StatusManager.CreateStatus( type );
			Statuses.Add( typeIdentity, status );
			status.Init( this );
		}

		//Sandbox.Services.Stats.Increment( Client, "status", 1, $"{type.Name.ToLowerInvariant()}", new { Status = type.Name.ToLowerInvariant(), Level = status.Level } );

		status.Refresh();

		Manager.Instance.PlaySfxNearby( "click", Position2D, 0.9f, 0.75f, 5f );

		LevelUpChoices.Clear();
		IsChoosingLevelUpReward = false;
		RealTimeSinceChoseUpgrade = 0f;

		CheckForLevelUp();
	}

	public bool HasStatus( TypeDescription type )
	{
		return Statuses.ContainsKey( type.Identity );
	}

	public Status GetStatus( TypeDescription type )
	{
		if ( Statuses.ContainsKey( type.Identity ) )
			return Statuses[type.Identity];

		return null;
	}

	public int GetStatusLevel( TypeDescription type )
	{
		if ( Statuses.ContainsKey( type.Identity ) )
			return Statuses[type.Identity].Level;

		return 0;
	}

	public void Modify( Status caller, PlayerStat statType, float value, ModifierType type, float priority = 0f, bool update = true )
	{
		if ( !_modifiers_stat.ContainsKey( caller ) )
			_modifiers_stat.Add( caller, new Dictionary<PlayerStat, ModifierData>() );

		_modifiers_stat[caller][statType] = new ModifierData( value, type, priority );

		if ( update )
			UpdateProperty( statType );
	}

	public void AdjustBaseStat( PlayerStat statType, float amount, bool update = true )
	{
		if ( !_original_properties_stat.ContainsKey( statType ) )
			_original_properties_stat.Add( statType, Stats[statType] );

		_original_properties_stat[statType] += amount;

		if ( update )
			UpdateProperty( statType );
	}

	void UpdateProperty( PlayerStat statType )
	{
		if ( !_original_properties_stat.ContainsKey( statType ) )
		{
			_original_properties_stat.Add( statType, Stats[statType] );
		}

		float curr_value = _original_properties_stat[statType];
		float curr_set = curr_value;
		bool should_set = false;
		float curr_priority = 0f;
		float total_add = 0f;
		float total_mult = 1f;

		foreach ( Status caller in _modifiers_stat.Keys )
		{
			var dict = _modifiers_stat[caller];
			if ( dict.ContainsKey( statType ) )
			{
				var mod_data = dict[statType];
				switch ( mod_data.type )
				{
					case ModifierType.Set:
						if ( mod_data.priority >= curr_priority )
						{
							curr_set = mod_data.value;
							curr_priority = mod_data.priority;
							should_set = true;
						}
						break;
					case ModifierType.Add:
						total_add += mod_data.value;
						break;
					case ModifierType.Mult:
						total_mult *= mod_data.value;
						break;
				}
			}
		}

		if ( should_set )
			curr_value = curr_set;

		curr_value += total_add;
		curr_value *= total_mult;

		Stats[statType] = curr_value;

		if ( statType == PlayerStat.MaxHp )
			Stats[statType] = Math.Max( curr_value, 1f );
	}

	public void AddExperience( int xp )
	{
		ExperienceTotal += xp;
		ExperienceCurrent += xp;

		//var particle = DamageNumbersLegacy.Create( xp, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: new Color(0.1f, 0.1f, 1f), sizeMultiplier: 0.8f );
		//Vector3 velocity = new Vector3( 0f, 0f, 0f );
		//Vector3 gravity = new Vector3( 0f, 1f, 0f );
		//particle.SetVector( 1, velocity );
		//particle.SetNamedValue( "Gravity", gravity );

		var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
		float size = Utils.Map( xp, 1f, 4f, 0.95f, 1.1f, EasingType.Linear );
		var color = new Color( 0.4f, 0.4f, 1f );
		Manager.Instance.SpawnDamageNumber( pos, xp, color, size, FloaterType.Xp );

		ForEachStatus( status => status.OnGainExperience( xp ) );
		if ( !IsChoosingLevelUpReward )
			CheckForLevelUp();
	}

	public void LoseExperience( int amount )
	{
		ExperienceCurrent = Math.Max( ExperienceCurrent - amount, 0 );
	}

	public void CheckForLevelUp()
	{
		//Log.Info("CheckForLevelUp: " + ExperienceCurrent + " / " + ExperienceRequired + " IsServer: " + Sandbox.Game.IsServer + " Level: " + Level);
		if ( ExperienceCurrent >= ExperienceRequired && Manager.Instance.ShouldUpdatePlayer )
			LevelUp();
	}

	public void LevelUp()
	{
		ExperienceCurrent -= ExperienceRequired;

		Level++;
		ExperienceRequired = GetExperienceReqForLevel( Level + 1 );

		if ( Manager.Instance.Difficulty < 3 )
			NumRerollAvailable += (int)Stats[PlayerStat.NumRerollsPerLevel];

		Manager.Instance.PlaySfxNearby( "levelup", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.5f, 5f );

		ForEachStatus( status => status.OnLevelUp() );

		GenerateLevelUpChoices();
		IsChoosingLevelUpReward = true;
		TimeSinceLevelUp = 0f;

		if ( Manager.Instance.Difficulty >= 0 && !_hasUnlockedExperiencedAchievement && Level >= 85 )
		{
			Sandbox.Services.Achievements.Unlock( "experienced" );
			_hasUnlockedExperiencedAchievement = true;
		}
	}

	public void UseReroll()
	{
		if ( NumRerollAvailable <= 0 )
		{
			// todo: sfx
			return;
		}

		Manager.Instance.PlaySfxNearby( "reroll", Position2D, Utils.Map( NumRerollAvailable, 0, 20, 0.9f, 1.4f, EasingType.QuadIn ), 0.6f, 5f );

		NumRerollAvailable--;

		GenerateLevelUpChoices();

		ForEachStatus( status => status.OnReroll() );
	}

	public void UseChoiceHotkey( int num )
	{
		var index = num - 1;

		if ( !IsChoosingLevelUpReward || index >= LevelUpChoices.Count )
			return;

		AddStatus( TypeLibrary.GetType( LevelUpChoices[index].GetType() ) );
	}

	public float CheckDamageAmount( float damage, DamageType damageType )
	{
		if ( IsInvulnerable )
		{
			return 0f;
		}

		if ( HasStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) && damageType != DamageType.LavaPuddle )
		{
			var shieldStatus = GetStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) as ShieldStatus;
			if ( shieldStatus != null && shieldStatus.IsShielded )
			{
				shieldStatus.LoseShield();
				return 0f;
			}
		}

		if ( Stats[PlayerStat.DamageReductionPercent] > 0f )
			damage *= (1f - MathX.Clamp( Stats[PlayerStat.DamageReductionPercent], 0f, 1f ));

		if ( Stats[PlayerStat.IncreasedDmgTaken] > 0f )
			damage *= (1f + MathX.Clamp( Stats[PlayerStat.IncreasedDmgTaken], 0f, 1f ));

		if ( damageType == DamageType.Explosion && Stats[PlayerStat.ExplosionDamageReductionPercent] > 0f )
			damage *= (1f - MathX.Clamp( Stats[PlayerStat.ExplosionDamageReductionPercent], 0f, 1f ));

		if ( damageType != DamageType.Explosion && Stats[PlayerStat.NonExplosionDamageIncreasePercent] > 0f )
			damage *= (1f + Stats[PlayerStat.NonExplosionDamageIncreasePercent]);

		if ( Manager.Instance.Difficulty < 0 )
			damage *= 0.571429f; // so zombie's 7 dmg becomes 4 dmg

		return damage;
	}

	public void Damage( float damage, PlayerDamageType playerDamageType = PlayerDamageType.Enemy )
	{
		if ( !Manager.Instance.ShouldUpdatePlayer )
			return;

		TimeSinceHurt = 0f;

		bool isCrit = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.SelfCritChance];

		if ( isCrit )
			damage *= 2f;

		if ( damage > 0f )
		{
			TimeSinceChangeHP = 0f;
			Flash( 0.125f );
		}

		var offset = new Vector2(
			Game.Random.Float( -0.1f, 0.1f ),
			Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) + (Health - damage < 0f ? -0.7f : 0f)
		);

		//DamageNumbers.Add( (int)damage, Position2D + Vector2.Up * Radius * 3f + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.2f, color: Color.Red );
		//DamageNumbersLegacy.Create( damage, Position2D + offset, color: isCrit ? Color.Orange : Color.Red );

		var pos = Position2D + offset;

		float size;
		if ( damage < 5f ) size = Utils.Map( damage, 1f, 5f, 1.1f, 1.25f, EasingType.QuadOut );
		else if ( damage < 20f ) size = Utils.Map( damage, 5f, 20f, 1.25f, 1.6f, EasingType.Linear );
		else size = Utils.Map( damage, 20f, 100f, 1.6f, 1.8f, EasingType.Linear );

		var color = isCrit ? Color.Orange : Color.Red;

		Manager.Instance.SpawnDamageNumber( pos, damage, color, size );

		ForEachStatus( status => status.OnHurt( damage ) );

		Health -= damage;

		//if ( damage > 3f )
		//	ShakeCam( Utils.Map( damage, 3f, 25f, 0f, 0.1f ), Utils.Map( damage, 3f, 20f, 0.1f, 0.25f ), EasingType.QuadOut );

		if ( Health <= 0f )
		{
			Die();
			SpawnBlood( damage, sizeMultiplier: Game.Random.Float( 2.5f, 3f ), playbackSpeed: Game.Random.Float( 20f, 25f ), shouldUseRealTime: true );
		}
		else
		{
			SpawnBlood( damage );
		}
	}

	public void AddVelocity( Vector2 vel )
	{
		if ( !Manager.Instance.ShouldUpdatePlayer )
			return;

		Velocity += vel;
	}

	public void SpawnBlood( float damage, float sizeMultiplier = 1f, float playbackSpeed = 0f, bool shouldUseRealTime = false )
	{
		var blood = Manager.Instance.SpawnBloodSplatter( Position2D );

		if ( blood != null )
		{
			blood.LocalScale *= Utils.Map( damage, 1f, 20f, 0.5f, 1.2f, EasingType.QuadIn ) * Game.Random.Float( 0.8f, 1.2f ) * sizeMultiplier;
			blood.Lifetime *= 0.3f;
			blood.ShouldUseRealTime = shouldUseRealTime;

			if ( playbackSpeed > 0f )
				blood.Sprite.PlaybackSpeed = playbackSpeed;
		}
	}

	public void Die()
	{
		if ( IsDead )
			return;

		IsDead = true;
		_hasPlayedDeathSfx = false;
		Sprite.Tint = new Color( 1f, 1f, 1f, 1f );
		Sprite.FlashTint = new Color( 1f, 1f, 1f, 0f );
		//ShadowOpacity = 0.2f;
		_isFlashing = false;
		IsReloading = false;

		RealTimeSinceDeath = 0f;
		_arrowDeathAlphaStart = ArrowSprite.Tint.a;

		var pitch = Game.Random.Float( 1.25f, 1.3f ) * (Manager.Instance.Difficulty < 0 ? 2f : 1f);
		Manager.Instance.PlaySfxNearby( "die", Position2D, pitch, volume: 1.5f, maxDist: 12f );

		Sprite.LocalScale *= 2f;

		Sprite.PlayAnimation( $"death" );
		//Sprite.PlayAnimation( "ghost_idle" );

		ShakeCam( Game.Random.Float( 0.025f, 0.065f ), Game.Random.Float( 0.3f, 0.5f ), EasingType.SineOut, useRealTime: true );

		if ( IsProxy )
			return;

		Manager.Instance.PlayerDied( this );
	}

	public void Revive()
	{
		if ( !IsDead )
			return;

		IsDead = false;
		IsChoosingLevelUpReward = false;
		IsDashing = false;
		IsReloading = true;
		Sprite.Tint = Color.White;
		ShadowOpacity = 0.8f;

		if ( IsProxy )
			return;

		Timer = Stats[PlayerStat.ReloadTime];
		ReloadProgress = 0f;
		DashProgress = 0f;
		ExperienceCurrent = 0;

		Health = Stats[PlayerStat.MaxHp] * 0.33f;
	}

	public void ForEachStatus( Action<Status> action )
	{
		if ( IsProxy )
			return;

		foreach ( var (_, status) in Statuses )
		{
			action( status );
		}
	}

	void HandleStatuses( float dt )
	{
		foreach ( KeyValuePair<int, Status> pair in Statuses )
		{
			Status status = pair.Value;
			if ( status.ShouldUpdate )
				status.Update( dt );
		}
	}

	void HandleShooting( float dt )
	{
		if ( IsReloading )
		{
			ReloadProgress = Utils.Map( Timer, Stats[PlayerStat.ReloadTime], 0f, 0f, 1f );
			Timer -= dt * Stats[PlayerStat.ReloadSpeed];
			if ( Timer <= 0f )
			{
				Reload();
			}
		}
		else
		{
			Timer -= dt * Stats[PlayerStat.AttackSpeed] * (IsMoving ? 1f : Stats[PlayerStat.AttackSpeedStill]);
			if ( Timer <= 0f )
			{
				Shoot( isLastAmmo: AmmoCount == 1 );
				AmmoCount--;

				if ( AmmoCount <= 0 )
				{
					IsReloading = true;

					Timer += Stats[PlayerStat.ReloadTime];
				}
				else
				{
					Timer += Stats[PlayerStat.AttackTime];
				}
			}
		}
	}

	public void Shoot( bool isLastAmmo = false )
	{
		int num_bullets_int = (int)Stats[PlayerStat.NumProjectiles];

		var aimDir = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootRandomDirChance]
			? Utils.GetRandomVector()
			: AimDir;

		var pos = Position2D + AimDir * 0.3f;

		if ( Stats[PlayerStat.MaxBulletSpread] > 0f )
		{
			float increment = 360f / num_bullets_int;

			for ( int i = 0; i < num_bullets_int; i++ )
			{
				var dir = Utils.RotateVector( aimDir, i * increment );
				SpawnBullet( pos, dir, isLastAmmo );
			}
		}
		else
		{
			float start_angle = MathF.Sin( -_shotNum * 2f ) * Stats[PlayerStat.BulletInaccuracy];

			var spread = Stats[PlayerStat.BulletSpread] * num_bullets_int;

			float currAngleOffset = num_bullets_int == 1 ? 0f : -spread * 0.5f;
			float increment = num_bullets_int == 1 ? 0f : spread / (float)(num_bullets_int - 1);

			for ( int i = 0; i < num_bullets_int; i++ )
			{
				var dir = Utils.RotateVector( aimDir, start_angle + currAngleOffset + increment * i );
				SpawnBullet( pos, dir, isLastAmmo );
			}
		}

		Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: Utils.Map( _shotNum, 0f, (float)Stats[PlayerStat.MaxAmmoCount], 1f, 1.25f ), volume: 1f, maxDist: 4f );

		Velocity -= aimDir * Stats[PlayerStat.Recoil];

		_shotNum++;
		_timeSinceShoot = 0f;
	}

	void SpawnBullet( Vector2 pos, Vector2 dir, bool isLastAmmo = false, float damageMult = 1f )
	{
		var damage = (Stats[PlayerStat.BulletDamage] * Stats[PlayerStat.BulletDamageMultiplier] + Stats[PlayerStat.BulletFlatDamageAddition]) * GetDamageMultiplier() * damageMult;
		if ( isLastAmmo )
			damage *= Stats[PlayerStat.LastAmmoDamageMultiplier];

		if ( Stats[PlayerStat.DamagePerEarlierShot] > 0f )
			damage += _shotNum * Stats[PlayerStat.DamagePerEarlierShot];

		if ( Stats[PlayerStat.DamageForSpeed] > 0f )
		{
			damage += Stats[PlayerStat.DamageForSpeed] * Velocity.Length;

			if ( IsDashing )
				damage += Stats[PlayerStat.DamageForSpeed] * DashVelocity.Length;
		}

		var bulletObj = BulletPrefab.Clone( (Vector3)pos );
		var bullet = bulletObj.Components.Get<Bullet>();

		bullet.Velocity = dir * Stats[PlayerStat.BulletSpeed];
		bullet.Shooter = this;
		bullet.TempWeight = 3f;

		bullet.Stats[BulletStat.Damage] = damage;
		bullet.Stats[BulletStat.Force] = Stats[PlayerStat.BulletForce];
		bullet.Stats[BulletStat.Lifetime] = Stats[PlayerStat.BulletLifetime];
		bullet.Stats[BulletStat.NumPiercing] = (int)MathF.Round( Stats[PlayerStat.BulletNumPiercing] );
		bullet.Stats[BulletStat.NumBouncing] = (int)MathF.Round( Stats[PlayerStat.BulletNumBouncing] );
		bullet.Stats[BulletStat.WillIgnite] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFireIgniteChance] ? 1f : 0f;
		bullet.Stats[BulletStat.WillFreeze] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFreezeChance] ? 1f : 0f;
		bullet.Stats[BulletStat.GrowDamageAmount] = Stats[PlayerStat.BulletDamageGrow];
		bullet.Stats[BulletStat.ShrinkDamageAmount] = Stats[PlayerStat.BulletDamageShrink];
		bullet.Stats[BulletStat.DistanceDamageAmount] = Stats[PlayerStat.BulletDistanceDamage];
		bullet.Stats[BulletStat.HealTeammateAmount] = Stats[PlayerStat.BulletHealTeammateAmount];
		bullet.IsHoming = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.HomingBulletChance];

		if ( Stats[PlayerStat.GrenadesCanCrit] <= 0f )
		{
			bullet.Stats[BulletStat.CriticalChance] = Stats[PlayerStat.CritChance];
			bullet.Stats[BulletStat.CriticalMultiplier] = Stats[PlayerStat.CritMultiplier];
		}

		bullet.Init();

		//bullet.GameObject.NetworkSpawn( Network.Owner );
	}

	void Reload()
	{
		AmmoCount = (int)Stats[PlayerStat.MaxAmmoCount];
		IsReloading = false;
		_shotNum = 0;
		ReloadProgress = 0f;

		ForEachStatus( status => status.OnReload() );
	}

	public float GetDamageMultiplier()
	{
		float damageMultiplier = Stats[PlayerStat.OverallDamageMultiplier];

		if ( Stats[PlayerStat.LowHealthDamageMultiplier] > 1f )
			damageMultiplier *= Utils.Map( Health, Stats[PlayerStat.MaxHp], 0f, 1f, Stats[PlayerStat.LowHealthDamageMultiplier] );

		if ( Stats[PlayerStat.FullHealthDamageMultiplier] > 1f && !(Health < Stats[PlayerStat.MaxHp]) )
			damageMultiplier *= Stats[PlayerStat.FullHealthDamageMultiplier];

		return damageMultiplier;
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( IsDead )
			return;

		ForEachStatus( status => status.Colliding( other, percent, dt ) );

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			if ( !Position2D.Equals( other.Position2D ) )
			{
				var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
				Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * spawnFactor * dt;
			}
		}
		else if ( other is Player player )
		{
			if ( !player.IsDead && !Position2D.Equals( other.Position2D ) )
			{
				Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * dt;
			}
		}
	}

	public void SpawnDashCloudClient()
	{
		Manager.Instance.SpawnCloud( Position2D + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.05f );
	}

	public void GenerateLevelUpChoices()
	{
		LevelUpChoices.Clear();

		//if( Level == 1)
		//{
		//	LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
		//	LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( MysteryBoxStatus ) ) ) );
		//	LevelUpChoices.Add( CreateStatus( GetRandomStartingPerk() ) );

		//	LevelUpChoices.Shuffle();

		//	return;
		//}

		bool offerCurses = false;
		if ( Manager.Instance.Difficulty >= 6 )
			offerCurses = IsLevelCursed( Level );

		int numChoices = Math.Clamp( (int)MathF.Round( Stats[PlayerStat.NumUpgradeChoices] ), 1, 6 );
		List<TypeDescription> statusTypes = StatusManager.GetRandomStatuses( this, numChoices, offerCurses );

		for ( int i = 0; i < statusTypes.Count; i++ )
			LevelUpChoices.Add( CreateStatus( statusTypes[i] ) );

		if ( Level == 1 )
		{
			bool alreadyOffered = false;

			foreach ( var status in LevelUpChoices )
			{
				if ( status is PauseWhileChoosingStatus )
				{
					alreadyOffered = true;
					break;
				}
			}

			if ( !alreadyOffered )
			{
				LevelUpChoices.RemoveAt( 1 );
				LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
				LevelUpChoices.Shuffle();
			}
		}

		ChoiceHash++;
	}

	public bool IsLevelCursed( int level )
	{
		if ( Manager.Instance.Difficulty < 6 || level <= 1 )
			return false;

		switch ( Manager.Instance.Difficulty )
		{
			case 6: return level % 10 == 0;
			case 7: return level % 9 == 0;
			case 8: return level % 8 == 0;
			case 9: return level % 7 == 0;
			case 10: return level % 6 == 0;
			case 11: return level % 5 == 0;
			case 12: return level % 4 == 0;
			case 13: return level % 3 == 0;
			case 14: return level % 2 == 0;
			case 15: return level % 3 != 1;
		}

		return false;
	}

	TypeDescription GetRandomStartingPerk()
	{
		List<(TypeDescription Type, float Weight)> perks = new List<(TypeDescription, float)>
		{
			(TypeLibrary.GetType( typeof( DamageStatus ) ), 5f),
			(TypeLibrary.GetType( typeof( MovespeedStatus ) ), 3f),
			(TypeLibrary.GetType( typeof( AttackSpeedStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( NumProjectileStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( PiercingStatus ) ), 3f),
			(TypeLibrary.GetType( typeof( GrenadeShootReloadStatus ) ), 1f),
			(TypeLibrary.GetType( typeof( NumDashesStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( FireIgniteStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( FreezeShootStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( FullHealthDamageStatus ) ), 2f),
			(TypeLibrary.GetType( typeof( MoreRerollsStatus ) ), 4f),
			(TypeLibrary.GetType( typeof( MoreChoicesStatus ) ), 3f),
			(TypeLibrary.GetType( typeof( ReloadSpeedStatus ) ), 1f),
			(TypeLibrary.GetType( typeof( XpDamageStatus ) ), 1f),
			(TypeLibrary.GetType( typeof( HomingBulletStatus ) ), 4f),
			(TypeLibrary.GetType( typeof( ThornsStatus ) ), 1f),
			(TypeLibrary.GetType( typeof( BouncingBulletStatus ) ), 3f),
		};

		TypeDescription chosenPerk = null;

		float totalWeight = perks.Sum( x => x.Weight );
		var rand = Game.Random.Float( 0f, totalWeight );

		for ( int i = perks.Count - 1; i >= 0; i-- )
		{
			var (type, weight) = perks[i];
			rand -= weight;

			if ( rand < 0f )
			{
				chosenPerk = type;
				break;
			}
		}

		return chosenPerk;
	}

	Status CreateStatus( TypeDescription type )
	{
		var status = StatusManager.CreateStatus( type );
		var currLevel = GetStatusLevel( type );
		status.Level = currLevel + 1;
		return status;
	}

	public void Restart()
	{
		Sprite.PlayAnimation( "idle" );
		Sprite.PlaybackSpeed = 0.66f;

		Sprite.Tint = new Color( 1f, 1f, 1f, 1f );

		Sprite.LocalScale = new Vector3( Globals.SPRITE_SCALE );

		if ( IsProxy )
			return;

		Position2D = new Vector3( Game.Random.Float( -3f, 3f ), Game.Random.Float( -3f, 3f ) );
		Manager.Instance.Camera2D.SetPos( Position2D );

		InitializeStats();

		Manager.Instance.PlaySfxNearby( "restart", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.66f, 4f );
	}

	public void SpawnBulletRing( Vector2 pos, int numBullets, Vector2 aimDir, float damageMultMin = 1f, float damageMultMax = 1f )
	{
		float increment = 360f / numBullets;

		for ( int i = 0; i < numBullets; i++ )
		{
			var dir = Utils.RotateVector( aimDir, i * increment );
			float damageMult = Game.Random.Float( damageMultMin, damageMultMax );
			SpawnBullet( pos, dir, false, damageMult );
		}

		Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: 1f, volume: 1f, maxDist: 3f );
	}

	public Grenade SpawnGrenade( Vector2 pos, Vector2 vel )
	{
		var grenadeObj = Manager.Instance.GrenadePrefab.Clone();

		var grenade = grenadeObj.Components.Get<Grenade>();
		grenade.Velocity = vel;
		grenade.ExplosionSizeMultiplier = Stats[PlayerStat.ExplosionSizeMultiplier];
		grenade.Player = this;
		grenade.StickyPercent = Stats[PlayerStat.GrenadeStickyPercent];
		grenade.FearChance = Stats[PlayerStat.GrenadeFearChance];

		if ( Stats[PlayerStat.GrenadesCanCrit] > 0f )
		{
			grenade.CriticalChance = Stats[PlayerStat.CritChance];
			grenade.CriticalMultiplier = Stats[PlayerStat.CritMultiplier];
		}

		//grenadeObj.NetworkSpawn( Network.Owner );
		grenadeObj.WorldPosition = new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) );

		Manager.Instance.AddThing( grenade );

		return grenade;
	}

	public void CreateShieldVfx()
	{
		_shieldVfx = Manager.Instance.ShieldVfxPrefab.Clone( WorldPosition );
		_shieldVfx.Parent = GameObject;
		_shieldVfx.LocalPosition = new Vector3( 0f, 0f, 0.1f );
		_shieldVfx.LocalScale = new Vector3( 1f ) * 1.8f * Globals.SPRITE_SCALE;
		_shieldVfx.LocalRotation = new Angles( 0f, -90f, 0f );
	}

	public void RemoveShieldVfx()
	{
		if ( _shieldVfx != null )
		{
			_shieldVfx.Destroy();
			_shieldVfx = null;
		}
	}

	public void PlaySfx( string name, Vector2 pos, float pitch, float volume )
	{
		var sfx = Sound.Play( name, new Vector3( pos.x, pos.y, Globals.SFX_DEPTH ) );

		if ( sfx != null )
		{
			sfx.Volume = volume;
			sfx.Pitch = pitch;
		}
	}

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

		Manager.Instance.AddPlayer( this );
	}

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

		Manager.Instance.RemovePlayer( this );
	}

	void HandleCamShaking()
	{
		CamShakeAmount = 0f;

		for ( int i = _camShakeDatas.Count - 1; i >= 0; i-- )
		{
			var data = _camShakeDatas[i];
			var time = data.useRealTime ? RealTime.Now : Time.Now;

			if ( time > data.startTime + data.time )
			{
				_camShakeDatas.RemoveAt( i );
			}
			else
			{
				float amount = Utils.Map( time, data.startTime, data.startTime + data.time, data.strength, 0f, data.easingType );
				CamShakeAmount = MathF.Max( amount, CamShakeAmount );
			}
		}
	}

	public void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = false )
	{
		var timeNow = useRealTime ? RealTime.Now : Time.Now;
		_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );
	}
}
using Sandbox;
using Sandbox.ModelEditor.Nodes;
using System.Net.NetworkInformation;

public class Charger : Enemy
{
	private TimeSince _damageTime;
	private const float DAMAGE_TIME = 1f;

	protected float _chargeDelayTimer;
	private const float CHARGE_DELAY_MIN = 2f;
	private const float CHARGE_DELAY_MAX = 6f;

	public bool IsPreparingToCharge { get; private set; }
	public bool IsCharging { get; private set; }
	private float _prepareTimer;
	private const float PREPARE_TIME = 1f;
	protected float _chargeTimer;
	protected float CHARGE_TIME_MIN = 1.8f;
	protected float CHARGE_TIME_MAX = 2.5f;
	private float _chargeTime;
	private float _nextRedirectTime;
	private TimeSince _timeSinceRedirect;

	protected Vector2 _chargeDir;
	protected Vector2 _chargeVel;
	private TimeSince _chargeCloudTimer;

	public override float HeightVariance => 0.03f;
	public override float WidthVariance => 0.015f;

	public float ChargeRange { get; set; }

	protected float REDIRECT_DELAY_MIN = 0.3f;
	protected float REDIRECT_DELAY_MAX = 2.7f;

	protected override void OnAwake()
	{
		//OffsetY = -0.57f;
		ShadowScale = 1.25f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 1.25f;

		base.OnAwake();

		//Sprite.Texture = Texture.Load("textures/sprites/charger.vtex");
		//Sprite.Size = new Vector2( 1f, 1f ) * Scale;

		PushStrength = 25f;

		Radius = 0.275f;

		Health = 75f;

		if ( Manager.Instance.Difficulty < 0 )
			Health = 55f;

		MaxHealth = Health;
		DamageToPlayer = 10f;

		CoinValueMin = 2;
		CoinValueMax = 5;
		CoinChance = 0.7f;

		Sprite.PlayAnimation( AnimSpawnPath );

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		_damageTime = DAMAGE_TIME;
		_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX );

		ChargeRange = 4.2f;
	}

	protected override void UpdatePosition( float dt )
	{
		//Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
		//Gizmo.Draw.Text( $"IsCharging: {IsCharging}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );

		base.UpdatePosition( dt );

		var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);

		if ( IsPreparingToCharge )
		{
			_prepareTimer -= dt;
			if ( _prepareTimer < 0f )
			{
				Charge();
				return;
			}
		}
		else if ( IsCharging )
		{
			_chargeTimer -= dt;
			if ( _chargeTimer < 0f )
			{
				IsCharging = false;
				Sprite.PlayAnimation( AnimIdlePath );
				CanTurn = true;
				DontChangeAnimSpeed = false;
			}
			else
			{
				HandleCharging( dt );
			}

			WorldPosition += (Vector3)(_chargeVel + Velocity) * dt;

			if ( Manager.Instance.Difficulty >= 1 && _timeSinceRedirect > _nextRedirectTime )
			{
				_chargeDir = (targetPos - Position2D).Normal;

				if(Math.Abs( targetPos.x - Position2D.x ) > 0.15f )
					FlipX = targetPos.x > Position2D.x;

				_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );
				_timeSinceRedirect = 0f;
			}

			if ( _chargeCloudTimer > 0.25f )
			{
				SpawnCloudClient( Position2D + new Vector2( 0f, 0.25f ), -_chargeDir * Game.Random.Float( 0.2f, 0.8f ) );
				_chargeCloudTimer = Game.Random.Float( 0f, 0.075f );
			}
		}
		else
		{
			Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

			float speed = 0.75f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);

			if ( Manager.Instance.Difficulty < 0 )
				speed *= 0.85f;

			WorldPosition += (Vector3)Velocity * speed * dt;
		}

		var player_dist_sqr = (targetPos - Position2D).LengthSquared;
		if ( !IsPreparingToCharge && !IsCharging && !IsAttacking && player_dist_sqr < ChargeRange * ChargeRange && Target.IsValid() )
		{
			_chargeDelayTimer -= dt;
			if ( _chargeDelayTimer < 0f )
			{
				PrepareToCharge();
			}
		}
	}

	protected virtual void HandleCharging(float dt)
	{
		float chargeSpeed = Manager.Instance.Difficulty >= 1 ? 12f : 4f; ;
		_chargeVel += _chargeDir * chargeSpeed * Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 0f, 1f, EasingType.Linear ) * dt;
		TempWeight += Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 1f, 6f, EasingType.Linear ) * dt;

		float BUFFER = 0.01f;
		var x_min = Manager.Instance.BOUNDS_MIN.x + Radius + BUFFER;
		var x_max = Manager.Instance.BOUNDS_MAX.x - Radius - BUFFER;
		var y_min = Manager.Instance.BOUNDS_MIN.y + BUFFER;
		var y_max = Manager.Instance.BOUNDS_MAX.y - Radius - BUFFER;

		if ( Position2D.x < x_min && _chargeDir.x < 0f )
		{
			_chargeDir = _chargeDir.WithX( Math.Abs( _chargeDir.x ) );
			_chargeVel = _chargeVel.WithX( Math.Abs( _chargeVel.x ) * 0.1f );
			FlipX = true;
			Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;
		}
		else if ( Position2D.x > x_max && _chargeDir.x > 0f )
		{
			_chargeDir = _chargeDir.WithX( -Math.Abs( _chargeDir.x ) );
			_chargeVel = _chargeVel.WithX( -Math.Abs( _chargeVel.x ) * 0.1f );
			FlipX = false;
			Sprite.SpriteFlags = SpriteFlags.None;
		}

		if ( Position2D.y < y_min && _chargeDir.y < 0f )
		{
			_chargeDir = _chargeDir.WithY( Math.Abs( _chargeDir.y ) );
			_chargeVel = _chargeDir.WithY( Math.Abs( _chargeVel.y ) * 0.1f );
		}
		else if ( Position2D.y > y_max && _chargeDir.y > 0f )
		{
			_chargeDir = _chargeDir.WithY( -Math.Abs( _chargeDir.y ) );
			_chargeVel = _chargeDir.WithY( -Math.Abs( _chargeVel.y ) * 0.1f );
		}
	}

	protected override void HandleDeceleration( float dt )
	{
		if ( IsCharging )
		{
			Velocity *= (1f - dt * 1.75f);

			float decel = Manager.Instance.Difficulty >= 1 ? 3f : 0.5f;
			_chargeVel *= (1f - dt * decel);
		}
		else
		{
			base.HandleDeceleration( dt );
		}
	}

	protected override void UpdateSprite( Thing target )
	{
		if ( !IsCharging )
			base.UpdateSprite( target );
	}

	protected override void HandleAttacking( Thing target, float dt )
	{
		if ( !IsPreparingToCharge && !IsCharging )
			base.HandleAttacking( target, dt );
	}

	public void PrepareToCharge()
	{
		_prepareTimer = PREPARE_TIME;
		IsPreparingToCharge = true;
		Manager.Instance.PlaySfxNearby( "enemy.roar.prepare", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 5f );
		Sprite.PlayAnimation( "charge_start" );
		CanTurn = false;
		CanAttack = false;
		CanAttackAnim = false;
		DontChangeAnimSpeed = true;

		if ( Manager.Instance.Difficulty >= 1 )
		{
			_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );
			_timeSinceRedirect = 0f;
		}
	}

	public void Charge()
	{
		var target_pos = Target.IsValid() && !IsFeared
			? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 1.75f )
			: Position2D + Utils.GetRandomVector() * 0.5f;

		_chargeDir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -10f, 10f ) );

		IsPreparingToCharge = false;
		IsCharging = true;
		_chargeTime = Game.Random.Float( CHARGE_TIME_MIN, CHARGE_TIME_MAX ) * (Manager.Instance.Difficulty >= 1 ? Game.Random.Float(1.25f, Utils.Map(Manager.Instance.Difficulty, 1, 10, 1.75f, 3f, EasingType.SineIn)) : 1f);

		_chargeTimer = _chargeTime;
		CanAttack = true;
		CanAttackAnim = true;

		_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.4f : 1f);
		_chargeVel = Vector2.Zero;
		Sprite.PlayAnimation( "charge_loop" );
		AnimSpeed = 3f;
		FlipX = _chargeDir.x > 0f;
		Sprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
		//Sprite.SpriteFlags = target_pos.x > Position2D.x ? SpriteFlags.HorizontalFlip : SpriteFlags.None;

		Manager.Instance.PlaySfxNearby( "enemy.roar", Position2D, pitch: Game.Random.Float( 0.925f, 1.075f ), volume: 1f, maxDist: 8f );
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
			Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;

			if ( (IsAttacking || IsCharging) && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
			{
				var dmg = DamageToPlayer;
				if ( IsCharmed )
					dmg *= CharmDamageDealtMultiplier;

				enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
				enemy.Target = this;
				_damageTime = 0f;
				Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
			}
		}
		// todo: move collision check to player instead to prevent laggy hits?
		else if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;

				if ( (IsAttacking || IsCharging) && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
				{
					float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );

					if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
					{
						Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );

						player.Damage( dmg );

						if ( dmg > 0f )
							OnDamagePlayer( player, dmg );
					}

					_damageTime = 0f;
				}
			}
		}
	}

	public override void Celebrate()
	{
		base.Celebrate();

		CelebrateAsync();
	}

	async void CelebrateAsync()
	{
		await Task.Delay( Game.Random.Int( 0, 500 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 1f, 2.5f );

		Sprite.PlayAnimation( "cheer_start" );

		await Task.Delay( Game.Random.Int( 200, 400 ) );

		Sprite.PlayAnimation( "cheer" );
	}
}
using Sandbox;

public class RunnerEliteSpecial : RunnerElite
{
	public override float FullOpacity => 0f;

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

		Radius = 0.5075f;
		Scale = 2.275f;
		Sprite.LocalScale = new Vector3( Scale * Game.Random.Float( 1f - HeightVariance, 1f + HeightVariance ), Scale * Game.Random.Float( 1f - WidthVariance, 1f + WidthVariance ), 1f ) * Globals.SPRITE_SCALE;

		Health = 300f;
		MaxHealth = Health;

		ShadowScale = 2.275f;
		ShadowFullOpacity = 0f;
		ShadowSpriteDirty = true;

		PushStrength = 55f;
		AggroRange = 2.25f;
		
		DamageToPlayer = 5f;

		CoinChance = 1f;
		CoinValueMin = 4;
		CoinValueMax = 8;

		Sprite.Tint = Color.White.WithAlpha( FullOpacity );

		MoveSpeed = 0.35f;

		JUMP_DELAY_MIN = 4f;
		JUMP_DELAY_MAX = 10f;
	}

	protected override void SpawnLandingClouds()
	{
		for ( int i = 0; i < Game.Random.Int( 2, 3 ); i++ )
		{
			var dir = Utils.GetRandomVector();
			SpawnCloudClient( Position2D + dir * Game.Random.Float(0.4f, 0.6f), dir * Game.Random.Float( 0.3f, 1.5f ) );
		}
	}

	public override void DealDamage()
	{
		Flash( 0.1f, Color.Red.WithAlpha( 0.25f ) );
	}

	public override void Celebrate()
	{
		if ( IsSpawning )
			FinishSpawning();

		if ( _isJumping )
			return;

		CelebrateAsync();
	}

	async void CelebrateAsync()
	{
		await Task.Delay( Game.Random.Int( 0, 500 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 2f, 4f );

		Sprite.Tint = Color.Red.WithAlpha( 0.25f );

		Sprite.PlayAnimation( "cheer" );
	}
}
using Sandbox;
using static Sandbox.VertexLayout;

public class Spiker : Enemy
{
	private TimeSince _damageTime;
	private const float DAMAGE_TIME = 0.75f;

	private float _shootDelayTimer;
	private const float SHOOT_DELAY_MIN = 2f;
	private const float SHOOT_DELAY_MAX = 4f;

	public bool IsShooting { get; private set; }
	private float _shotTimer;
	private const float SHOOT_TIME = 4f;
	private bool _hasShot;
	private TimeSince _prepareStartTime;
	private bool _hasReversed;

	public override float HeightVariance => 0.02f;
	public override float WidthVariance => 0.01f;

	private bool _moveClockwise;
	public static int SpikerNum { get; set; }
	private float _perpendicularMaxDist;

	private float _digDelayTimer;
	private const float DIG_DELAY_MIN = 4f;
	private const float DIG_DELAY_MAX = 13f;
	public bool IsDigging { get; private set; }
	private TimeSince _timeSinceStartDigging;
	private const float DIG_TIME = 1.2f;

	public float ShootRange { get; set; }

	public float MoveSpeed { get; set; }

	protected override void OnAwake()
	{
		//OffsetY = -0.58f;
		ShadowScale = 1.2f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 1.4f;

		base.OnAwake();

		//AnimSpeed = 4f;
		//Sprite.Texture = Texture.Load("textures/sprites/spiker.vtex");

		PushStrength = 8f;
		Deceleration = 2.57f;
		DecelerationAttacking = 2.35f;
		AggroRange = 0.75f;

		Radius = 0.27f;

		Health = 80f;

		if ( Manager.Instance.Difficulty < 0 )
			Health = 70f;

		MaxHealth = Health;
		DamageToPlayer = 14f;

		CoinValueMin = 2;
		CoinValueMax = 4;
		CoinChance = 0.75f;

		Sprite.PlayAnimation( AnimSpawnPath );

		if ( IsProxy )
			return;
		
		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		_damageTime = DAMAGE_TIME;
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
		_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );

		_moveClockwise = SpikerNum % 2 == 0;
		SpikerNum++;
		_perpendicularMaxDist = Game.Random.Float( 3.5f, 6.5f );

		ShootRange = 4.5f;

		MoveSpeed = 0.9f;
	}

	protected override void UpdatePosition( float dt )
	{
		base.UpdatePosition( dt );

		//if(!IsDigging)
		//{
		//	Gizmo.Draw.Color = Color.White;
		//	Gizmo.Draw.Text( $"{_digDelayTimer}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
		//}

		var targetPos = GetTargetPos();

		if ( IsShooting )
		{
			Velocity *= (1f - dt * (IsAttacking ? DecelerationAttacking : Deceleration));
			if ( !_hasShot && _prepareStartTime > 1f )
			{
				CreateSpike();
				_hasShot = true;
			}

			if ( !_hasReversed && _prepareStartTime > 3f )
			{
				_hasReversed = true;
				Sprite.PlayAnimation( "shoot_reverse" );
			}

			Velocity *= (1f - dt * 4f);

			_shotTimer -= dt;
			if ( _shotTimer < 0f )
			{
				FinishShooting();
				return;
			}
		}
		else if ( IsDigging )
		{
			Velocity *= (1f - dt * 6f);

			if ( _timeSinceStartDigging > DIG_TIME )
			{
				Vector2 pos;
				if ( Target.IsValid() )
				{
					pos = targetPos + Target.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float( 1.5f, 5.5f);
					if ( (Position2D - pos).LengthSquared < MathF.Pow( 0.5f, 2f ) )
						pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 5f );
				}
				else
				{
					pos = Position2D + Game.Random.Float( 4f, 5f );
				}

				FinishDigging( Manager.Instance.ClampToBounds( pos ) );
			}
			else
			{
				float progress = Utils.Map( _timeSinceStartDigging, 0f, DIG_TIME, 0f, 1f );
				//ZPos = Utils.Map( progress, 0f, 1f, 0f, DIG_DEPTH );
				//PlaybackRate = Utils.Map( progress, 0f, 1f, 0f, 1f ) * _personalSpeedScale;

				float shadowOpacity = Utils.Map( progress, 0f, 1f, ShadowOpacity, 0f, EasingType.QuadIn );
				ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );

				VfxOpacity = Utils.Map( progress, 0f, 1f, 1f, 0f, EasingType.QuadIn );

				//Gizmo.Draw.Color = Color.White;
				//Gizmo.Draw.Text( $"{progress}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );

				IgnoreCollision = progress > 0.3f;

				if ( _spawnCloudTime > (0.3f / TimeScale) )
				{
					var cloud = Manager.Instance.SpawnCloud( Position2D + new Vector2( 0f, 0.05f ) );
					cloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );
					_spawnCloudTime = Game.Random.Float( 0f, 0.15f );
				}
			}

			return;
		}
		else
		{
			Vector2 toTarget = (targetPos - Position2D).Normal;

			if(Manager.Instance.Difficulty >= 1)
			{
				if ( (targetPos - Position2D).LengthSquared < MathF.Pow( _perpendicularMaxDist, 2f ) )
					toTarget = Vector2.Lerp( toTarget, new Vector2( toTarget.y, -toTarget.x ) * (_moveClockwise ? -1f : 1f), Utils.Map( (targetPos - Position2D).LengthSquared, MathF.Pow( _perpendicularMaxDist, 2f ), MathF.Pow( 1.5f, 2f ), 0f, 1f ) );
			}

			Velocity += toTarget * 1.0f * dt * (IsFeared ? -1f : 1f);
		}

		float speed = MoveSpeed * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);

		if ( Manager.Instance.Difficulty < 0 )
			speed *= 0.85f;

		WorldPosition += (Vector3)Velocity * speed * dt;

		if ( !IsShooting && !IsDigging && (!IsAttacking || IsCharmed) && !Manager.Instance.IsGameOver )
		{
			var target_dist_sqr = ((Target.IsValid() ? Target.Position2D : targetPos) - Position2D).LengthSquared;
			var rangeSqr = ShootRange * ShootRange;

			if ( Manager.Instance.Difficulty < 0 )
				rangeSqr *= 0.7f;

			if ( target_dist_sqr < rangeSqr )
			{
				_shootDelayTimer -= dt;
				if ( _shootDelayTimer < 0f )
					StartShooting();
			}

			if ( target_dist_sqr < MathF.Pow( 12f, 2f ) && Manager.Instance.Difficulty > 0 )
			{
				_digDelayTimer -= dt;
				if ( _digDelayTimer < 0f )
					StartDigging();
			}
		}
	}

	protected virtual Vector2 GetTargetPos()
	{
		return Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
	}

	protected override void UpdateSprite( Thing target )
	{
		if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) || IsDigging ) 
			return;

		base.UpdateSprite( target );
	}

	public void StartShooting()
	{
		_shotTimer = SHOOT_TIME;
		IsShooting = true;
		CanAttack = false;
		CanTurn = false;
		CanAttackAnim = false;
		_hasShot = false;
		_hasReversed = false;
		_prepareStartTime = 0f;
		Velocity *= 0.25f;
		DontChangeAnimSpeed = true;
		AnimSpeed = 1f;
		Sprite.PlayAnimation( "shoot" );

		ShouldUpdateAfterGameOver = true;
	}

	public virtual void CreateSpike()
	{
		var target_pos = Target.IsValid()
			? Target.Position2D + Target.Velocity * Game.Random.Float( 0.2f, 2f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.8f
			: Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 6f );

		var BUFFER = 0.1f;

		var spike = Manager.Instance.SpawnEnemySpike( new Vector2( Math.Clamp( target_pos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), Math.Clamp( target_pos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) ) );

		if(IsCharmed)
		{
			spike.BecomeCharmed();
		}

		Manager.Instance.PlaySfxNearby( "spike.prepare", target_pos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1.5f, maxDist: 5f );
	}

	public void FinishShooting()
	{
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 2.5f : 1f);
		IsShooting = false;
		CanAttack = true;
		CanAttackAnim = true;
		CanTurn = true;
		DontChangeAnimSpeed = false;
		Sprite.PlayAnimation( AnimIdlePath );

		ShouldUpdateAfterGameOver = false;
		if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
			Celebrate();
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
			Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;

			if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
			{
				var dmg = DamageToPlayer;
				if ( IsCharmed )
					dmg *= CharmDamageDealtMultiplier;

				enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
				enemy.Target = this;
				_damageTime = 0f;
				Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
			}
		}
		// todo: move collision check to player instead to prevent laggy hits?
		else if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;

				if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
				{
					float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );

					if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
					{
						Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );

						player.Damage( dmg );

						if( dmg > 0f )
							OnDamagePlayer( player, dmg );
					}

					_damageTime = 0f;
				}
			}
		}
	}

	public override void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )
	{
		base.Damage( damage, player, addVel, addTempWeight, isCrit, damageType );

		if ( Game.Random.Float( 0f, 1f ) < Utils.Map( damage, 1f, 20f, 0.1f, 0.7f ) )
			_digDelayTimer *= Game.Random.Float( 0.6f, 0.95f );
	}

	public void StartDigging()
	{
		IsDigging = true;
		_timeSinceStartDigging = 0f;
		CanAttack = false;
		CanAttackAnim = false;
		CanTurn = false;
		Velocity *= 0.5f;
		Sprite.PlayAnimation( "dig" );
		Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.5f, 0.55f ), volume: 0.6f, maxDist: 7.5f );
		//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
		//SS2Game.Current.DustCloudClient( Position2D );

		ShouldUpdateAfterGameOver = true;
	}

	void FinishDigging( Vector2 pos )
	{
		Position2D = pos;
		//WorldPosition = ((Vector3)Position2D).WithZ( ZPos );

		Transform.ClearInterpolation();

		IsDigging = false;
		CanAttack = true;
		CanAttackAnim = true;
		CanTurn = true;
		IgnoreCollision = false;
		//SetAnim( "Attack1" );

		_moveClockwise = !_moveClockwise;

		_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );

		//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
		//SS2Game.Current.DustCloudClient( Position2D );

		IsSpawning = true;
		//_hasDug = true;
		TimeSinceSpawn = 0f;
		//SpawnProgress = 0f;

		//ShadowRadiusModifier = 1.5f;
		//ShadowOpacityModifier = 0f;

		Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.85f, 0.9f ), volume: 0.6f, maxDist: 7.5f );

		ShadowSprite.Tint = Color.Black.WithAlpha( 0f );
		VfxOpacity = 0f;
	}

	public override void Celebrate()
	{
		base.Celebrate();

		if ( IsShooting || IsDigging )
			return;

		CelebrateAsync();
	}

	async void CelebrateAsync()
	{
		await Task.Delay( Game.Random.Int( 0, 500 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 1.5f, 3.5f );

		Sprite.PlayAnimation( "cheer_start" );

		await Task.Delay( Game.Random.Int( 200, 400 ) );

		Sprite.PlayAnimation( "cheer" );
	}

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

		ShouldUpdateAfterGameOver = false;
		if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
			Celebrate();
	}
}
@using Sandbox;
@using Sandbox.UI;
@namespace SS1
@inherits Panel
@attribute [StyleSheet("BossNametag.razor.scss")]

<root>
	@{
		var currHp = Manager.Instance.Difficulty >= 5
			? ( (Boss.IsValid() ? Math.Max(Boss.Health, 0f) : 0f) + (OtherBoss.IsValid() ? Math.Max(OtherBoss.Health, 0f) : 0f) )
			: Boss.Health;

		var maxHp = Manager.Instance.Difficulty >= 5
			? 14000f
			: Boss.MaxHealth;

		var hpPercent = currHp / maxHp;

		var bgColor = Lerp3(new Color(0f, 0.75f, 0f), new Color(0.75f, 0.75f, 0f), new Color(1f, 0f, 0f), 1f - hpPercent);
	}

	<div class="hpbar">
		<div class="hpbardelta" style="width:@(hpPercent * 100f)%;"></div>
		<div class="hpbaroverlay" style="width:@(hpPercent * 100f)%; background-color:@(bgColor.Rgba);"></div>

		<div class="name_label">@(Manager.Instance.Difficulty >= 5 ? "BOSSES" : "BOSS")</div>
			<div class="hp_label">
			<div class="label">@($"{(int)Math.Ceiling(currHp)}")</div>
			<div class="label">/</div>
			<div class="label">@($"{(int)maxHp}")</div>
		</div>
	</div>
</root>

@code
{
	public Boss Boss { get; set; }
	public Boss OtherBoss { get; set; }

	protected override int BuildHash()
	{
		var currHp = Manager.Instance.Difficulty >= 5
			? ( (Boss.IsValid() ? Math.Max(Boss.Health, 0f) : 0f) + (OtherBoss.IsValid() ? Math.Max(OtherBoss.Health, 0f) : 0f) )
			: Boss.Health;

		return HashCode.Combine(
			currHp
		);
	}

	Color Lerp3(Color a, Color b, Color c, float t)
	{
		if(t < 0.5f) // 0.0 to 0.5 goes to a -> b
			return Color.Lerp(a, b, t / 0.5f);
		else // 0.5 to 1.0 goes to b -> c
			return Color.Lerp(b, c, (t - 0.5f) / 0.5f);
	}
}
/// <summary>
/// Handles firing (hitscan bullets or spawning projectiles) for a weapon.
/// </summary>
[Title( "Shoot Component" ), Icon( "gps_fixed" )]
public sealed class ShootComponent : WeaponComponent
{
	[Property] public string FireButton { get; set; } = "Attack1";
	[Property] public GameObject MuzzlePoint { get; set; }
	[Property] public WeaponData Data { get; set; }

	// Driven by WeaponData at runtime
	public float BaseDamage { get; set; }
	public float BulletRange { get; set; } = 5000f;
	public int BulletCount { get; set; } = 1;
	public float BulletForce { get; set; } = 1f;
	public float BulletSize { get; set; } = 2f;
	public float BulletSpread { get; set; } = 0f;
	public float FireDelay { get; set; } = 0.1f;
	public bool DisableBulletImpacts { get; set; }
	public SoundEvent FireSound { get; set; }
	public bool FireSoundLoop { get; set; }
	public bool FireSoundOnlyOnStart { get; set; }
	public string ActivateSound { get; set; }
	public string DryFireSound { get; set; }
	public string ProjectilePrefab { get; set; }
	public string ShootEffectPrefab { get; set; }
	public string ImpactEffectPrefab { get; set; }
	public string MuzzleFlashPrefab { get; set; }
	public Color TracerColor { get; set; } = Color.Yellow;
	public float TracerSpeed { get; set; } = 8000f;
	public float TracerLength { get; set; } = 300f;

	private TimeUntil _timeUntilCanFire;
	private SoundHandle _activeSound;
	private SoundHandle _fireSoundHandle;
	private bool _isFiring;

	protected override void OnStart()
	{
		if ( Data == null ) return;
		BaseDamage           = Data.BaseDamage;
		BulletRange          = Data.BulletRange;
		BulletCount          = Data.BulletCount;
		BulletForce          = Data.BulletForce;
		BulletSize           = Data.BulletSize;
		BulletSpread         = Data.BulletSpread;
		FireDelay            = Data.FireDelay;
		DisableBulletImpacts = Data.DisableBulletImpacts;
		FireSoundLoop        = Data.FireSoundLoop;
		FireSoundOnlyOnStart = Data.FireSoundOnlyOnStart;
		TracerColor          = Data.TracerColor;
		TracerSpeed          = Data.TracerSpeed;
		TracerLength         = Data.TracerLength;

		if ( !string.IsNullOrEmpty( Data.ProjectilePrefab ) )   ProjectilePrefab   = Data.ProjectilePrefab;
		if ( Data.FireSound.IsValid() )                          FireSound          = Data.FireSound;
		if ( !string.IsNullOrEmpty( Data.ActivateSound ) )      ActivateSound      = Data.ActivateSound;
		if ( !string.IsNullOrEmpty( Data.DryFireSound ) )       DryFireSound       = Data.DryFireSound;
		if ( !string.IsNullOrEmpty( Data.ShootEffectPrefab ) )  ShootEffectPrefab  = Data.ShootEffectPrefab;
		if ( !string.IsNullOrEmpty( Data.ImpactEffectPrefab ) ) ImpactEffectPrefab = Data.ImpactEffectPrefab;
		if ( !string.IsNullOrEmpty( Data.MuzzleFlashPrefab ) )  MuzzleFlashPrefab  = Data.MuzzleFlashPrefab;
	}

	public override void Simulate()
	{
		base.Simulate();

		if ( Player == null ) return;

		if ( WishesToFire() )
		{
			if ( CanFire() )
			{
				if ( !_isFiring )
				{
					_isFiring = true;
					if ( FireSound.IsValid() )
					{
						if ( FireSoundLoop )
							_fireSoundHandle = GameObject.PlaySound( FireSound );
						else if ( FireSoundOnlyOnStart )
							GameObject.PlaySound( FireSound );
					}
				}

				Fire();
			}
			else
			{
				// Dry fire if no ammo
				var ammo = GetComponent<AmmoComponent>();
				if ( ammo != null && !ammo.HasEnoughAmmo() && Input.Pressed( FireButton ) )
				{
					if ( !string.IsNullOrEmpty( DryFireSound ) )
						Sound.Play( DryFireSound, Player.WorldPosition );
				}
			}
		}
		else
		{
			StopFiring();
		}
	}

	private void StopFiring()
	{
		if ( !_isFiring ) return;
		_isFiring = false;
		_activeSound?.Stop();
		_activeSound = null;
		if ( _fireSoundHandle.IsValid() )
		{
			_fireSoundHandle.Stop();
			_fireSoundHandle = default;
		}
	}

	protected override void OnDeactivate() => StopFiring();

	private void Fire()
	{
		_timeUntilCanFire = FireDelay;
		TimeSinceActivated = 0;

		RunGameEvent( $"{Name}.fire" );

		// Local feedback: fire sound and activate effects
		if ( FireSound.IsValid() && !FireSoundOnlyOnStart && !FireSoundLoop )
			GameObject.PlaySound( FireSound );

		// Muzzle flash
		if ( !string.IsNullOrEmpty( MuzzleFlashPrefab ) )
		{
			var muzzlePos = MuzzlePoint?.WorldPosition ?? Player.WorldPosition;
			var muzzleRot = MuzzlePoint?.WorldRotation ?? Player.WorldRotation;
			var flashFile = ResourceLibrary.Get<PrefabFile>( MuzzleFlashPrefab );
			if ( flashFile != null )
			{
				var flash = SceneUtility.GetPrefabScene( flashFile )?.Clone();
				if ( flash != null )
				{
					flash.WorldPosition = muzzlePos;
					flash.WorldRotation = muzzleRot;
					flash.NetworkSpawn();
				}
			}
		}

		// Shoot effect for projectile weapons (no hit endpoint available)
		if ( !string.IsNullOrEmpty( ShootEffectPrefab ) && Data?.Mode == WeaponData.FiringMode.Projectile )
		{
			var muzzlePos = MuzzlePoint?.WorldPosition ?? Player.WorldPosition;
			var muzzleRot = MuzzlePoint?.WorldRotation ?? Player.WorldRotation;
			BroadcastShootEffect( muzzlePos, muzzleRot, muzzlePos + muzzleRot.Forward * BulletRange );
		}

		if ( _activeSound == null && !string.IsNullOrEmpty( ActivateSound ) )
			_activeSound = Sound.Play( ActivateSound, Player.WorldPosition );

		// Route shot processing through host
		if ( Data?.Mode == WeaponData.FiringMode.Projectile )
		{
			if ( Networking.IsHost )
				SpawnProjectile();
			else
				ServerSpawnProjectile();
		}
		else
		{
			ShootBullet();
		}
	}

	private bool WishesToFire() => (Player?.IsBot == true ? Player.BotFirePrimary : Input.Down( FireButton )) && Player?.ActiveWeapon == Weapon;

	private bool CanFire()
	{
		if ( _timeUntilCanFire > 0 ) return false;
		if ( Weapon != null && Weapon.GameObject.Tags.Has( "reloading" ) ) return false;

		var ammo = GetComponent<AmmoComponent>();
		if ( ammo != null && !ammo.HasEnoughAmmo() ) return false;

		return true;
	}

	private void ShootBullet()
	{
		if ( Player == null ) return;

		Game.SetRandomSeed( (int)Time.Now );
		var aimRay = Player.AimRay;

		for ( int i = 0; i < BulletCount; i++ )
		{
			var forward = aimRay.Forward;
			if ( BulletSpread > 0 )
				forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * BulletSpread * 0.25f;
			forward = forward.Normal;

			var start = aimRay.Position;

			if ( Networking.IsHost )
				ProcessShot( start, forward );
			else
				ServerShootBullet( start, forward );
		}
	}

	/// <summary>Sent from owner client to host for authoritative shot processing.</summary>
	[Rpc.Host]
	private void ServerShootBullet( Vector3 start, Vector3 forward )
	{
		ProcessShot( start, forward );
	}

	private void ProcessShot( Vector3 start, Vector3 forward )
	{
		var end = start + forward.Normal * BulletRange;

		var tr = Scene.Trace.Ray( start, end )
			.WithAnyTags( "solid", "player" )
			.IgnoreGameObject( Player?.GameObject )
			.Size( BulletSize )
			.Run();

		if ( !DisableBulletImpacts && tr.Hit )
		{
			BroadcastImpactEffect( tr.EndPosition, tr.Normal );
		}

		// Shoot effect for hitscan — use ray start and actual hit endpoint
		if ( !string.IsNullOrEmpty( ShootEffectPrefab ) )
		{
			BroadcastShootEffect( start, Rotation.Identity, tr.EndPosition );
		}

		if ( tr.Hit )
		{
			var hitPawn = tr.GameObject?.Components.Get<PlayerPawn>( FindMode.EnabledInSelfAndDescendants );
			if ( hitPawn != null )
				hitPawn.TakeDamage( BaseDamage, Player );
		}

		if ( string.IsNullOrEmpty( ShootEffectPrefab ) )
			BroadcastTracerEffect( start, tr.EndPosition );
	}

	private void SpawnProjectile()
	{
		if ( string.IsNullOrEmpty( ProjectilePrefab ) ) return;
		var prefabFile = ResourceLibrary.Get<PrefabFile>( ProjectilePrefab );
		if ( prefabFile == null )
		{
			Log.Warning( $"[ShootComponent] ProjectilePrefab not found: {ProjectilePrefab}" );
			return;
		}
		var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
		if ( go != null )
		{
			var proj = go.Components.Get<Projectile>( FindMode.EnabledInSelfAndDescendants );
			if ( proj != null && Data != null )
				proj.Data = Data;
			proj?.Launch( Player, null );
			go.NetworkSpawn();
		}
	}

	/// <summary>Sent from owner client to host to spawn and simulate the projectile authoritatively.</summary>
	[Rpc.Host]
	private void ServerSpawnProjectile()
	{
		SpawnProjectile();
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void BroadcastShootEffect( Vector3 position, Rotation rotation, Vector3 endPosition )
	{
		if ( string.IsNullOrEmpty( ShootEffectPrefab ) ) return;
		var prefabFile = ResourceLibrary.Get<PrefabFile>( ShootEffectPrefab );
		if ( prefabFile == null ) return;

		var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
		if ( go == null ) return;
		go.WorldPosition = position;
		go.WorldRotation = rotation;

		// Find BeamEffect in prefab (configured in editor), fallback to creating one
		var beam = go.GetComponent<BeamEffect>( true );

		//Log.Info( $"Shoot effect beam: {go}, start: {position}, end: {endPosition}" );

		beam.TargetGameObject.WorldPosition = endPosition;
		beam.TargetGameObject.Transform.ClearInterpolation();
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void BroadcastImpactEffect( Vector3 position, Vector3 normal )
	{
		if ( string.IsNullOrEmpty( ImpactEffectPrefab ) ) return;
		var prefabFile = ResourceLibrary.Get<PrefabFile>( ImpactEffectPrefab );
		if ( prefabFile == null ) return;
		var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
		if ( go == null ) return;
		go.WorldPosition = position;
		go.WorldRotation = Rotation.LookAt( normal );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private void BroadcastTracerEffect( Vector3 start, Vector3 end )
	{
		var go = new GameObject( true, "BulletTracer" );
		go.WorldPosition = start;
		var tracer = go.Components.Create<Tracer>();
		tracer.EndPoint = end;
		tracer.DistancePerSecond = TracerSpeed;
		tracer.Length = TracerLength;
		tracer.LineColor = new Gradient(
			new Gradient.ColorFrame( 0, TracerColor ),
			new Gradient.ColorFrame( 1, TracerColor.WithAlpha( 0 ) )
		);
	}

	public override void OnGameEvent( string eventName )
	{
		if ( eventName == "sprint.stop" ) _timeUntilCanFire = 0.2f;
		if ( eventName == "aimcomponent.start" ) _timeUntilCanFire = 0.15f;
		if ( eventName == "shootcomponent.fire" ) TimeSinceActivated = 0;
	}
}
@using Sandbox.UI
@using Sandbox.UI.Components
@inherits Panel
<style>
    Info {
        position: absolute;
        width: 100%;
        bottom: 0;
        height: 100%;
        align-items: center;
        justify-content: center;
        transition: all 0.5s sin-ease-in-out;
        flex-direction: row;
    }

    .arc-root {
        position: absolute;
        width: 600px;
        height: 600px;
        margin-bottom: 16px;
        margin-right: 12px;
    }

    .seg {
        position: absolute;
        opacity: 0.22;
        width: 10px;
        height: 18px;
    }

    .seg.health {
        background-color: rgba(95, 230, 120, 0.95);
    }

    .seg.health.active {
        opacity: 1;
        box-shadow: 0 0 8px rgba(95, 230, 120, 0.45);
    }

    .seg.armour {
        background-color: rgba(65, 195, 255, 0.95);
    }

    .seg.armour.active {
        opacity: 1;
        box-shadow: 0 0 8px rgba(65, 195, 255, 0.4);
    }

    .arc-label {
        position: absolute;
        font-family: "Wallpoet";
        font-size: 24px;
        color: rgba(210, 245, 255, 0.9);
        text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
    }

    .arc-label.hp {
        left: 144px;
        bottom: 122px;
    }

    .arc-label.sh {
        right: 144px;
        bottom: 122px;
    }

    .sWarning {
        text-align: center;
        position: absolute;
        font-family: "Wallpoet";
        font-size: 44px;
        top: 22%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: rgba(39, 181, 238, 1);
        z-index: 30;
    }
</style>

<root>
    @if ( _showWarning )
    {
        <label class="sWarning">- -[WARNING SHIELD DOWN]- -</label>
    }
    <div class="arc-root">
        @foreach ( var s in GetHealthArc() )
        {
            <div class="seg health @(s.Active ? "active" : "")"
                 style="left:@($"{s.X:F1}px"); top:@($"{s.Y:F1}px"); transform:rotate(@($"{s.Angle + 90f:F1}deg"));"></div>
        }
        @foreach ( var s in GetShieldArc() )
        {
            <div class="seg armour @(s.Active ? "active" : "")"
                 style="left:@($"{s.X:F1}px"); top:@($"{s.Y:F1}px"); transform:rotate(@($"{s.Angle + 90f:F1}deg"));"></div>
        }
        <div class="arc-label hp">HP @MathF.Round(_health)</div>
        <div class="arc-label sh">SH @MathF.Round(_shield)</div>
    </div>
</root>

@code
{
    private PlayerPawn Pawn => LocalPlayer.Pawn;
    private float _health => Pawn?.Health ?? 0f;
    private float _healthMax => Pawn?.MaxHealth ?? 100f;
    private float _shield => Pawn?.Shield ?? 0f;
    private float _shieldMax => Pawn?.MaxShield ?? 0f;

    private bool _showWarning =>
        PilotGame.Gamemode != FPGameMode.Instagib &&
        (Pawn?.IsAlive ?? false) &&
        _health > 0f &&
        _shieldMax > 0f &&
        _shield <= 0f;

    private float HealthProgress => _healthMax > 0f ? Math.Clamp( _health / _healthMax, 0f, 1f ) : 0f;
    private float ShieldProgress => _shieldMax > 0f ? Math.Clamp( _shield / _shieldMax, 0f, 1f ) : 0f;

    private readonly struct ArcSeg
    {
        public ArcSeg( float x, float y, float angle, bool active )
        {
            X = x;
            Y = y;
            Angle = angle;
            Active = active;
        }

        public float X { get; }
        public float Y { get; }
        public float Angle { get; }
        public bool Active { get; }
    }

    private IEnumerable<ArcSeg> GetHealthArc() => BuildArc( HealthProgress, 256f, 135, 225f, 28);
    private IEnumerable<ArcSeg> GetShieldArc() => BuildArc( ShieldProgress, 256f, 45, -45f, 28 );

    private static IEnumerable<ArcSeg> BuildArc( float progress, float radius, float start, float end, int count )
    {
        const float cx = 300f;
        const float cy = 300f;
        for ( int i = 0; i < count; i++ )
        {
            var t = count <= 1 ? 1f : i / (float)(count - 1);
            var angle = start + (end - start) * t;
            var rad = angle.DegreeToRadian();
            var x = cx + MathF.Cos( rad ) * radius;
            var y = cy + MathF.Sin( rad ) * radius;
            yield return new ArcSeg( x, y, angle, (i + 1) / (float)count <= progress );
        }
    }

    protected override int BuildHash()
        => HashCode.Combine( _health, _shield, _healthMax, _shieldMax, Pawn?.IsAlive );
}
@using Sandbox.UI
@inherits Panel

<style>
    ScreenOverlay {
        position: absolute;
        width: 100%;
        height: 100%;
        align-items: center;
        justify-content: center;
        transition: all 0.5s sin-ease-in-out;
        flex-direction: row;
        z-index: 10;

        &.hidden {
            opacity: 0;
            pointer-events: none;
        }
        .inner {
            width: 95%;
            height: 95%;
            border: 5px solid rgba(255, 255, 255, 1);
            background-image: url("ui/overlay/grid.png");
            background-size: 12.5% 25%;
            opacity: 0.02;
            image-rendering: nearest-neighbor;
        }

        .icon {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translateX(-50%) translateY(-50%);
            transition: opacity 0.1s ease;
            opacity: 0.025;

            &.crosshair {
                aspect-ratio: 1;
                width: 5%;
                opacity: 0.075;
            }
        }
    }
</style>

<root>
    <img class="icon" src="ui/crosshair/crosshair.png" />
    <img class="icon crosshair" src="ui/crosshair/crosshair2.png" />
    <label class="inner"></label>
</root>

@code
{
    public override void Tick()
    {
        SetClass( "hidden", !(LocalPlayer.Pawn?.IsAlive ?? false) );
    }
}
@using System.Threading.Tasks;
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Network;
@namespace Battlebugs
@inherits Panel
@attribute [StyleSheet]

<root>
	<label class="header">Lobbies</label>
	<div class="lobbies">
		@if (!refreshing && list.Count > 0)
		{
			@foreach (var lobby in list)
			{
				<div class="lobby" onclick=@(() => JoinLobby(lobby))>
					<img src="ui/gamepad.png" />
					<div class="info">
						<label class="name">@lobby.Name</label>
						@* <label class="desc">Looking for opponent...</label> *@
					</div>
					<div class="players">
						<i>person</i>
						<label>@lobby.Members/2</label>
					</div>
				</div>
			}
		}
		else
		{
			<div class="no-lobbies">
				No lobbies found...
			</div>
		}
		@* Uncomment the block below to preview a lobby entry *@
		@* <div class="lobby">
		<img src="ui/gamepad.png" />
		<div class="info">
		<label class="name">Carson vs Bakscratch</label>
		<label class="desc">Waiting for players...</label>
		</div>
		<div class="players">
		<i>person</i>
		<label>1/2</label>
		</div>
		</div> *@
	</div>
</root>

@code
{
	List<LobbyInformation> list = new();
	bool refreshing = true;

	protected override void OnAfterTreeRender(bool firstTime)
	{
		base.OnAfterTreeRender(firstTime);

		if (firstTime)
		{
			_ = RefreshLobbyList();
		}
	}

	async Task RefreshLobbyList()
	{
		while (true)
		{
			await Refresh();
			await Task.DelayRealtimeSeconds(5f);
		}
	}

	async Task Refresh()
	{
		refreshing = true;
		StateHasChanged();

		list = await Networking.QueryLobbies();

		refreshing = false;
		StateHasChanged();
	}

	void JoinLobby(LobbyInformation lobby)
	{
		Networking.Connect(lobby.LobbyId);
	}

	protected override int BuildHash() => System.HashCode.Combine("");
}
using Sandbox.UI;

namespace GuessIt;

public partial class GameCanvas
{
	// Drawing Variables
	bool IsDrawing = false;
	List<Vector2> DrawingPoints = new List<Vector2>();

	// UI Variables
	Image Canvas { get; set; }

	public void SetTexture( Texture texture )
	{
		Canvas.Texture = texture;
	}

	public void MarkDirty()
	{
		Canvas?.MarkRenderDirty();
	}

	protected override void OnAfterTreeRender( bool firstRender )
	{
		base.OnAfterTreeRender( firstRender );

		if ( Canvas is not null && GameMenu.Instance?.Canvas is not null )
		{
			Canvas.Texture = GameMenu.Instance.Canvas;
		}
	}

	protected override void OnMouseDown( MousePanelEvent e )
	{
		base.OnMouseDown( e );

		Log.Info( $"[GameCanvas] OnMouseDown LocalPos={e.LocalPosition} CanvasSize={Canvas?.Box?.Rect.Size} PlayerIsDrawing={Player.Local?.IsDrawing}" );

		DrawingPoints.Clear();
		IsDrawing = true;
		AddPoint( e.LocalPosition );
	}

	protected override void OnMouseMove( MousePanelEvent e )
	{
		base.OnMouseMove( e );

		if ( IsDrawing )
		{
			bool returnVal = AddPoint( e.LocalPosition );
			if ( returnVal == false )
			{
				Log.Info( $"[GameCanvas] AddPoint failed during move, stopping draw" );
				StopDrawing();
			}
		}
	}

	protected override void OnMouseUp( MousePanelEvent e )
	{
		base.OnMouseUp( e );

		StopDrawing();
	}

	void StopDrawing()
	{
		if ( !IsDrawing ) return;

		IsDrawing = false;
		if ( DrawingPoints.Count > 0 )
		{
			GameMenu.Instance.BroadcastDraw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
		}
		else
		{
			var canvasSize = Canvas?.Box?.Rect.Size ?? Vector2.One;
			var pos = MousePosition / canvasSize * new Vector2( 320, 240 );
			GameMenu.Instance.BroadcastDraw( pos, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
		}
		DrawingPoints.Clear();
	}

	bool AddPoint( Vector2 vec2 )
	{
		if ( !(Player.Local?.IsDrawing ?? false) ) return false;
		if ( !IsDrawing ) return false;

		var canvasSize = Canvas?.Box?.Rect.Size ?? Vector2.Zero;
		if ( canvasSize.x <= 0 || canvasSize.y <= 0 )
		{
			Log.Warning( $"[GameCanvas] Canvas size is zero or invalid: {canvasSize}" );
			return false;
		}

		Vector2 pos = (vec2 / canvasSize) * new Vector2( 320, 240 );
		if ( pos.x < 0 || pos.x > 320 || pos.y < 0 || pos.y > 240 ) return false;
		DrawingPoints.Add( pos );

		if ( DrawingPoints.Count > 1 )
		{
			GameMenu.Instance.Draw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
		}
		else
		{
			GameMenu.Instance.Draw( DrawingPoints[0], GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
		}

		if ( DrawingPoints.Count >= 5 )
		{
			var last = DrawingPoints.LastOrDefault();
			GameMenu.Instance.BroadcastDraw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
			DrawingPoints.Clear();
			DrawingPoints.Add( last );
		}

		return true;
	}
}
@using Sandbox;
@using Sandbox.UI;
@attribute [StyleSheet]

@namespace GuessIt

<root class="message">
    @if (!string.IsNullOrEmpty(Name))
    {
        <label class="name">@Name</label>
    }
    <label class="content">@Content</label>
</root>

@code
{
    public string Name { get; set; }
    public string Content { get; set; }

    public void SetMessage(string name, string content)
    {
        Name = name;
        Content = content;
    }
}
using System;
using Sandbox;

namespace Facepunch.BombRoyale;

/// <summary>
/// Spawns explosion cloud particles along a line between two points,
/// with yellow spark/ember sprites flying off.
/// </summary>
[Title( "Bomb Explosion Effect" )]
[Category( "Bomb Royale" )]
public class BombExplosionEffect : Component
{
	private static readonly Gradient FireGradient = new Gradient(
		new Gradient.ColorFrame( 0.000f, Color.White ),
		new Gradient.ColorFrame( 0.121f, new Color( 1.0f, 0.816f, 0.0f ) ),
		new Gradient.ColorFrame( 0.230f, new Color( 1.0f, 0.616f, 0.145f ) ),
		new Gradient.ColorFrame( 0.437f, new Color( 1.0f, 0.231f, 0.0f ) ),
		new Gradient.ColorFrame( 1.000f, new Color( 0.165f, 0.035f, 0.0f ) )
	);

	public static void Create( Scene scene, Vector3 startPosition, Vector3 endPosition )
	{
		var go = new GameObject( false, "BombExplosion" )
		{
			WorldPosition = startPosition
		};

		var effect = go.AddComponent<BombExplosionEffect>();
		effect.StartPosition = startPosition;
		effect.EndPosition = endPosition;

		go.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 3f;
		go.Enabled = true;
	}

	private Vector3 StartPosition { get; set; }
	private Vector3 EndPosition { get; set; }

	private const int CloudCount = 20;
	private const int SparkCount = 50;
	private const float EffectLifetime = 2f;
	private const float BaseScale = 0.5f;

	private struct CloudParticle
	{
		public SceneObject SceneObject;
		public float BornTime;
		public float Lifetime;
		public Angles SpinRate;
		public Angles CurrentAngles;
		public float InitialScale;
	}

	private struct SparkParticle
	{
		public SceneObject SceneObject;
		public float BornTime;
		public float Lifetime;
		public Vector3 Velocity;
	}

	private CloudParticle[] _clouds;
	private SparkParticle[] _sparks;

	protected override void OnEnabled()
	{
		SpawnClouds();
		SpawnSparks();
	}

	private void SpawnClouds()
	{
		_clouds = new CloudParticle[CloudCount];
		var model = Model.Load( "models/particles/explosion/explosioncloud.vmdl" );

		for ( var i = 0; i < CloudCount; i++ )
		{
			var t = (float)i / (CloudCount - 1);
			var position = Vector3.Lerp( StartPosition, EndPosition, t );

			var so = new SceneObject( Scene.SceneWorld, model )
			{
				Transform = new Transform( position, Rotation.Random, 1f ),
				RenderingEnabled = true
			};

			_clouds[i] = new CloudParticle
			{
				SceneObject = so,
				BornTime = Time.Now,
				Lifetime = EffectLifetime,
				SpinRate = new Angles(
					Game.Random.Float( -30f, 30f ),
					Game.Random.Float( -30f, 30f ),
					Game.Random.Float( -30f, 30f )
				),
				CurrentAngles = new Angles(
					Game.Random.Float( -360f, 360f ),
					Game.Random.Float( -360f, 360f ),
					Game.Random.Float( -360f, 360f )
				),
				InitialScale = Game.Random.Float( 0.5f, 1.0f ) * BaseScale
			};
		}
	}

	private void SpawnSparks()
	{
		_sparks = new SparkParticle[SparkCount];
		var sparkModel = Model.Load( "models/dev/sphere.vmdl" );

		for ( var i = 0; i < SparkCount; i++ )
		{
			var t = Game.Random.Float( 0f, 1f );
			var position = Vector3.Lerp( StartPosition, EndPosition, t );
			position += Vector3.Random.Normal * 12f;

			var so = new SceneObject( Scene.SceneWorld, sparkModel )
			{
				Transform = new Transform( position, Rotation.Identity, Game.Random.Float( 0.01f, 0.03f ) ),
				RenderingEnabled = true,
				ColorTint = new Color( 1f, 0.85f, 0.2f )
			};

			_sparks[i] = new SparkParticle
			{
				SceneObject = so,
				BornTime = Time.Now,
				Lifetime = Game.Random.Float( 1f, 2f ),
				Velocity = new Vector3(
					Game.Random.Float( -50f, 50f ),
					Game.Random.Float( -50f, 50f ),
					Game.Random.Float( 20f, 80f )
				)
			};
		}
	}

	protected override void OnPreRender()
	{
		UpdateClouds();
		UpdateSparks();
	}

	private void UpdateClouds()
	{
		if ( _clouds == null ) return;

		for ( var i = 0; i < _clouds.Length; i++ )
		{
			ref var p = ref _clouds[i];
			if ( !p.SceneObject.IsValid() ) continue;

			var age = Time.Now - p.BornTime;
			var normalizedAge = age / p.Lifetime;

			if ( normalizedAge >= 1f )
			{
				p.SceneObject.Delete();
				continue;
			}

			var sizeCurve = EvaluateSizeCurve( age );
			var scale = p.InitialScale * sizeCurve;

			p.CurrentAngles += p.SpinRate * Time.Delta;

			var color = FireGradient.Evaluate( normalizedAge );

			const float fadeStart = 0.25f;
			var alpha = 1f;

			if ( normalizedAge > fadeStart )
				alpha = 1f - ((normalizedAge - fadeStart) / (1f - fadeStart));

			p.SceneObject.Transform = new Transform( p.SceneObject.Transform.Position, p.CurrentAngles.ToRotation(), scale );
			p.SceneObject.ColorTint = color.WithAlpha( alpha );
		}
	}

	private void UpdateSparks()
	{
		if ( _sparks == null ) return;

		var dt = Time.Delta;
		const float gravity = -150f;

		for ( var i = 0; i < _sparks.Length; i++ )
		{
			ref var s = ref _sparks[i];
			if ( !s.SceneObject.IsValid() ) continue;

			var age = Time.Now - s.BornTime;
			var normalizedAge = age / s.Lifetime;

			if ( normalizedAge >= 1f )
			{
				s.SceneObject.Delete();
				continue;
			}

			s.Velocity += Vector3.Up * gravity * dt;
			var pos = s.SceneObject.Transform.Position + s.Velocity * dt;

			var alpha = 1f - normalizedAge;
			var color = Color.Lerp( new Color( 1f, 1f, 0.8f ), new Color( 1f, 0.5f, 0f ), normalizedAge );

			s.SceneObject.Transform = new Transform( pos, Rotation.Identity, s.SceneObject.Transform.Scale );
			s.SceneObject.ColorTint = color.WithAlpha( alpha );
		}
	}

	private static float EvaluateSizeCurve( float age )
	{
		if ( age <= 0f ) return 0f;
		if ( age >= EffectLifetime ) return 0f;

		if ( age < 0.05f )
			return (age / 0.05f) * 0.7f;

		var t = (age - 0.05f) / (EffectLifetime - 0.05f);
		return MathX.Lerp( 0.7f, 0f, t * t );
	}

	protected override void OnDisabled() => Cleanup();
	protected override void OnDestroy() => Cleanup();

	private void Cleanup()
	{
		if ( _clouds != null )
		{
			for ( var i = 0; i < _clouds.Length; i++ )
			{
				if ( _clouds[i].SceneObject.IsValid() )
					_clouds[i].SceneObject.Delete();
			}
			_clouds = null;
		}

		if ( _sparks != null )
		{
			for ( var i = 0; i < _sparks.Length; i++ )
			{
				if ( _sparks[i].SceneObject.IsValid() )
					_sparks[i].SceneObject.Delete();
			}
			_sparks = null;
		}
	}
}
using Sandbox;
using Editor;

namespace Facepunch.BombRoyale;

public enum DiseaseType
{
	None,
	MoveSlow,
	MoveFast,
	RandomBomb,
	Teleport,
	LowRange
}

public static class DiseaseTypeExtensions
{
	public static string GetName( this DiseaseType type )
	{
		return type switch
		{
			DiseaseType.MoveSlow => "⏪ Move Slow",
			DiseaseType.MoveFast => "⏩ Move Fast",
			DiseaseType.RandomBomb => "💩 Drop Random Bombs",
			DiseaseType.Teleport => "🔀 Swap Places",
			DiseaseType.LowRange => "💣 Minimum Bomb Range",
			_ => "Nothing",
		};
	}
}
using System.Linq;
using Sandbox;
using Editor;

namespace Facepunch.BombRoyale;

[Group( "Bomb Royale" )]
[Title( "Post Processing" )]
public sealed class PostProcessing : Component
{
	private Pixelate Pixelate { get; set; }
	private ColorAdjustments ColorAdjustments { get; set; }
	private Sharpen Sharpen { get; set; }
	private ChromaticAberration ChromaticAberration { get; set; }
	
	protected override void OnStart()
	{
		Pixelate = Components.Get<Pixelate>();
		ColorAdjustments = Components.Get<ColorAdjustments>();
		Sharpen = Components.Get<Sharpen>();
		ChromaticAberration = Components.Get<ChromaticAberration>();
		
		base.OnStart();
	}

	protected override void OnUpdate()
	{
		var sum = ScreenShake.List.OfType<ScreenShake.Random>().Sum( s => (1f - s.Progress) );

		Pixelate.Scale = 0.02f * sum;
		ColorAdjustments.Saturation = 1.1f;

		if ( StateSystem.Active is SummaryState )
		{
			ColorAdjustments.Saturation = 0.25f;
		}

		ColorAdjustments.Contrast = 1f;

		var me = Player.Me;

		if ( me.IsValid() && me.LastTakeDamageTime.Absolute > 0f && me.LastTakeDamageTime < 1f )
		{
			var delta = 1f - ((1f / 1f) * me.LastTakeDamageTime);
			Pixelate.Scale += (0.05f * delta);
			ColorAdjustments.Saturation -= (0.5f * delta);
			ColorAdjustments.Contrast += (0.1f * delta);
		}

		ChromaticAberration.Scale = 0.03f + (0.05f * sum);
		Sharpen.Scale = 0.1f;
		
		base.OnUpdate();
	}
}
using Sandbox;

namespace Facepunch.BombRoyale;

/// <summary>
/// Ring sprite particles rising upward on player respawn.
/// Configured via prefabs/effects/respawn.prefab
/// </summary>
public static class RespawnEffect
{
	public static void Create( Scene scene, Vector3 position, Color color )
	{
		var go = GameObject.Clone( "prefabs/effects/respawn.prefab", new CloneConfig
		{
			StartEnabled = true,
			Transform = new Transform( position ),
			Name = "RespawnEffect"
		} );

		if ( !go.IsValid() ) return;

		var effect = go.Components.GetInDescendantsOrSelf<ParticleEffect>();
		if ( effect.IsValid() )
			effect.Tint = color;
	}
}
using Sandbox;
using System;

namespace Facepunch.BombRoyale;

[PickupChance( 0.2f )]
public class SpeedBoost : Pickup
{
	public override string PickupSound => "pickup.good";
	public override string Icon => "textures/speedboost.png";
	public override Color Color => Color.Orange;

	protected override bool OnPickup( Player player )
	{
		if ( player.SpeedBoosts == 4 )
			return false;
		
		Chat.AddPlayerEvent( "pickup", Network.Owner.DisplayName, player.GetTeamColor(), $"has gained some speed!" );
		
		var previousSpeed = player.SpeedBoosts;
		player.SpeedBoosts = Math.Min( player.SpeedBoosts + 1, 4 );

		if ( previousSpeed < 4 && player.SpeedBoosts == 4 )
			player.UnlockAchievement( "go_fast" );
		
		return true;
	}
}
using Sandbox;
using System.Linq;

namespace Facepunch.BombRoyale;

public class GameState : BaseState
{
	public override string Name => "FIGHT";
	public override int TimeLeft => RoundEndTime.Relative.CeilToInt();

	[Sync] private TimeUntil RoundEndTime { get; set; }

	private SoundHandle Music { get; set; }

	protected override void OnEnter()
	{
		if ( Networking.IsHost )
		{
			foreach ( var player in BombRoyale.Players )
			{
				player.Respawn();
			}

			RoundEndTime = 180f;
		}

		Sound.Play( "round.start" );
		Music = Sound.Play( "battle.music" );
	}

	protected override void OnUpdate()
	{
		if ( Networking.IsHost )
		{
			var alivePlayers = BombRoyale.Players
				.Count( p => p.LifeState == LifeState.Alive );

			if ( RoundEndTime || ( Connection.All.Count > 1 && alivePlayers <= 1 ) )
			{
				StateSystem.Set<SummaryState>();
			}
		}
		
		base.OnUpdate();
	}

	protected override void OnLeave()
	{
		Music?.Stop();
		Music = null;
	}
}
@using System
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@implements Component.INetworkListener
@namespace Facepunch.BombRoyale

<root class=@(IsOpen ? "open" : "closed")>
	<div @ref="EntryCanvas" class="entries"></div>

	<div class="input">
		<textentry @onblur=@OnEntryBlur @onsubmit=@OnEntrySubmit @ref="Entry"></textentry>
	</div>
</root>

@code
{
	private static Chat Instance { get; set; }
	private TextEntry Entry { get; set; }
	private Panel EntryCanvas { get; set; }
	private bool IsOpen { get; set; }

	[Rpc.Broadcast]
	public static void AddPlayerEvent( string eventName, string name, Color color, string message )
	{
		if ( !Instance.IsValid() ) return;
		Instance.AddNamedMessage( name, color, message, eventName );
	}
	
	public void AddNamedMessage( string name, Color color, string message, string className = null )
	{
		var entry = new ChatboxEntry()
		{
			Name = name,
			Color = color,
			Message = message
		};

		if ( !string.IsNullOrEmpty( className ) )
		{
			entry.AddClass( className );
		}

		if ( string.IsNullOrEmpty( name ) )
		{
			entry.AddClass( "no-name" );
		}

		EntryCanvas.AddChild( entry );
	}

	public void AddMessage( string message, string className = null )
	{
		var entry = new ChatboxEntry()
		{
			Message = message
		};

		if ( !string.IsNullOrEmpty( className ) )
		{
			entry.AddClass( className );
		}

		entry.AddClass( "no-name" );

		EntryCanvas.AddChild( entry );
	}

	protected override void OnAwake()
	{
		Instance = this;
		base.OnAwake();
	}

	protected override void OnUpdate()
	{
		if ( !Entry.IsValid() ) return;

		EntryCanvas.PreferScrollToBottom = true;
		Panel.AcceptsFocus = false;

		if ( !IsOpen && Input.Pressed( "chat" ) )
		{
			Entry.Focus();
			IsOpen = true;
		}
	}
	
	private void OnEntrySubmit()
	{
		if ( !string.IsNullOrWhiteSpace( Entry.Text ) )
		{
			SendText( Sandbox.Utility.Steam.PersonaName, Entry.Text.Trim() );
		}
	}
	
	private void OnEntryBlur()
	{
		Entry.Text = string.Empty;
		IsOpen = false;
	}

	[Rpc.Broadcast]
	private static void SendText( string author, string message )
	{
		Instance?.AddNamedMessage( author, Color.Cyan, message );
	}

	void INetworkListener.OnConnected( Connection channel )
	{
		if ( IsProxy ) return;

		SendText( "🛎️", $"{channel.DisplayName} has joined the game" );
	}

	void INetworkListener.OnDisconnected( Connection channel )
	{
		if ( IsProxy ) return;

		SendText(  "💨", $"{channel.DisplayName} has left the game" );
	}
	
	protected override int BuildHash()
	{
		return HashCode.Combine( IsOpen );
	}
}
using Sandbox;

public class DoorComponent : BaseInteractor
{
	[Property] public bool isOpen {get; set; } = false;

	Rotation startRotation;
	Rotation targetRotation;

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

		startRotation = WorldRotation;

		targetRotation = startRotation * Rotation.From( new Angles( 0, 90, 0 ) );
	}

	protected override void OnUpdate()
	{
		if( isOpen )
		{
			WorldRotation = Rotation.Slerp( WorldRotation, targetRotation, Time.Delta * 5.0f );
		}
		else
		{
			WorldRotation = Rotation.Slerp( WorldRotation, startRotation, Time.Delta * 5.0f );
		}
	}
	public override void OnUsed()
	{
		isOpen = !isOpen;
	}
}
public sealed class IkReachOut : Component
{
	[Property] public GameObject TargetGameObject { get; set; }
	[Property] public float Radius { get; set; }
	[Property] public Angles HandRotation { get; set; }
	[Property] public TagSet IgnoreCollision { get; set; }

	TimeUntil timeUntilRetry;

	protected override void OnUpdate()
	{
		if ( timeUntilRetry > 0 )
			return;

		var dir = (TargetGameObject.WorldPosition - WorldPosition);
		if ( !TargetGameObject.Enabled )
		{
			dir = (WorldRotation.Forward + Vector3.Random) * Radius;
		}

		var tr = Scene.Trace
			.Sphere( 2, WorldPosition, WorldPosition + dir.Normal * Radius )
			.IgnoreGameObjectHierarchy( GameObject.Root )
			.WithoutTags( IgnoreCollision )
			.Run();

		if ( tr.Hit )
		{
			TargetGameObject.WorldPosition = tr.EndPosition;
			TargetGameObject.WorldRotation = Rotation.LookAt( tr.Normal ) * Rotation.From( HandRotation );
			TargetGameObject.Enabled = true;
		}
		else
		{
			if ( TargetGameObject.Enabled )
				timeUntilRetry = 0.5f;

			TargetGameObject.Enabled = false;
		}
	}
}
using Sandbox.UI.Construct;
using System;
using System.Linq;

namespace Sandbox.UI.Tests.Elements;

[StyleSheet]
public class TextEntryTest : Panel
{
	public TextEntryTest()
	{
		Style.FlexWrap = Wrap.Wrap;
		Style.JustifyContent = Justify.Center;
		Style.AlignItems = Align.Center;
		Style.AlignContent = Align.Center;

		AddTest( "", "Aligned Left" );
		AddTest( " text-align: right;", "Aligned Right" );
		AddTest( " text-align: center;", "Aligned Center" );


		{
			var te = AddTest( "", "With Prefix" );
			te.Prefix = "https://";
		}

		{
			var te = AddTest( "", "With Suffix" );
			te.Suffix = "@gmail.com";
		}

		{
			var te = AddTest( "", "Maxlength 4" );
			te.MaxLength = 4;
		}

		{
			var te = AddTest( "", "Minlength 4" );
			te.MinLength = 4;
		}

		{
			var te = AddTest( "", "Only Vowels" );
			te.CharacterRegex = "[aeiouyw]";
		}

		{
			var te = AddTest( "", "Email Address" );
			te.StringRegex = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
		}

		{
			var te = AddTest( "", "" );
			te.Placeholder = "History";

			te.AddEventListener( "onsubmit", () =>
			{
				if ( string.IsNullOrEmpty( te.Text ) )
					return;

				te.AddToHistory( te.Text );
				te.Text = "";
				te.Focus();
			} );
		}

		{
			var te = AddTest( "", "" );
			te.Placeholder = "History With Cookie";
			te.HistoryCookie = "ui-test-textentry-history";

			te.AddEventListener( "onsubmit", () =>
			{
				if ( string.IsNullOrEmpty( te.Text ) )
					return;

				te.AddToHistory( te.Text );
				te.Text = "";
				te.Focus();
			} );
		}

		var input = AddTest( "", "" );
		input.Placeholder = "Auto Complete";
		input.AutoComplete = DoAutoComplete;

		{
			var te = AddTest( "", "Multiline" );
			te.Multiline = true;
			te.Style.Height = Length.Pixels( 128 );
		}

		{
			var te = AddTest( "", "" );
			te.Placeholder = "Emoji Replacement ( :rainbow_flag: )";
			te.AllowEmojiReplace = true;
		}
	}

	string[] DoAutoComplete( string partial )
	{
		return new[]
		{
			"dave",
			"sharon",
			"andrew",
			"phillip",
			"peter",
			"lewis",
			"anthony",
			"jamie"
		}
		.Where( x => x.StartsWith( partial ) )
		.ToArray();
	}

	private TextEntry AddTest( string style, string text )
	{
		var p = AddChild<TextEntry>( "" );
		p.Placeholder = text;
		p.Style.Set( style );

		return p;
	}
}
public sealed class ParticlePhysics : ParticleController
{
	protected override void OnParticleCreated( Particle p )
	{
		var body = new PhysicsBody( Scene.PhysicsWorld );
		body.Position = p.Position;

		//	body.AddBoxShape( BBox.FromPositionAndSize( 0, 10 ), Rotation.Identity );

		var shape = body.AddSphereShape( new Sphere( 0, 10 ) );

		shape.Tags.Add( "particle" );
		shape.EnableTouch = false;
		shape.EnableTouchPersists = false;

		body.EnableCollisionSounds = false;

		body.GravityEnabled = true;
		body.MotionEnabled = true;
		body.Velocity = p.Velocity * 2;
		body.Mass = 100;
		body.RebuildMass();
		body.Sleeping = false;

		p.Set( "Physics", body );
	}

	protected override void OnParticleStep( Particle particle, float delta )
	{
		var body = particle.Get<PhysicsBody>( "Physics" );
		if ( body is null ) return;

		particle.Position = body.Position;
		particle.Angles = body.Rotation;
	}

	protected override void OnParticleDestroyed( Particle p )
	{
		var body = p.Get<PhysicsBody>( "Physics" );
		if ( body is not null )
		{
			body.Remove();
		}

		p.Set<PhysicsBody>( "Physics", null );
	}



}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox

<root>
	
	<div class="test1">Red Square Before</div>

	<div class="test2">Red Square After</div>

	<div class="test3">Red Square Before And After</div>

	<div class="test4">Red Square On Hover</div>

	<div class="test5">Red Square On Blur</div>

	<div class="test6">Content</div>

	<div class="test7">Icon Content</div>

	<div class="test8" tooltip="Should correctly have Child highlighted in yellow - with a red and green square before and after."><div>With</div><div>Last</div><div>Child</div></div>

</root>

@code
{

	[Property, TextArea] public string MyStringValue { get; set; } = "Hello World!";

	/// <summary>
	/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
	/// </summary>
	protected override int BuildHash() => System.HashCode.Combine( MyStringValue );
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel

<root onclick="@( () => OnSelected( Scene ) )">

    <div class="title">@Scene.GetMetadata("Title", Scene.ResourceName)</div>
    <div class="description">@Scene.GetMetadata("Description")</div>

</root>

@code
{

    [Parameter] public new SceneFile Scene { get; set; }
    [Parameter] public Action<SceneFile> OnSelected { get; set; }

    protected override int BuildHash() => System.HashCode.Combine(Scene);
}
 
namespace Opium;

public partial class Actor
{
	[Property, Group( "Movement" )] public OpiumCharacterController CharacterController { get; set; }
	[Property, Group( "Movement" )] public float BaseMovementSpeed { get; set; } = 100f;
	[Property, Group( "Data" ), ReadOnly] public Vector3 WishVelocity { get; set; }

	public Vector3 WishMove;

	public virtual Angles GetEyeAngles()
	{
		return new Angles( 0, Transform.Rotation.Yaw(), 0 );
	}

	/// <summary>
	/// Get the current friction for this actor
	/// </summary>
	/// <returns></returns>
	protected virtual float GetFriction()
	{
		if ( !CharacterController.IsOnGround ) return 0.1f;
		if ( CurrentFrictionOverride is not null ) return CurrentFrictionOverride.Value;

		return 4.0f;
	}

	protected virtual void BuildWishInput()
	{
	//	WishMove = 0;
	}


	[ConVar( "op_dev_speed" )]
	private static float DevSpeed { get; set; } = 0f;

	protected virtual float GetWishSpeed()
	{
		if ( CurrentSpeedOverride is not null ) return CurrentSpeedOverride.Value;

		if ( this is Opium.PlayerController player && DevSpeed > 0f )
		{
			return DevSpeed;
		}


		// Default speed
		return BaseMovementSpeed;
	}

	protected virtual void BuildWishVelocity()
	{
		WishVelocity = 0;
		if ( WishMove.x < 0f ) WishMove.x *= 0.7f;

		var rot = GetEyeAngles().WithPitch(0).WithRoll(0).ToRotation();

		var wishDirection = WishMove * rot;
		wishDirection = wishDirection.WithZ( 0 );

		WishVelocity = wishDirection * GetWishSpeed();

		if ( !CharacterController.IsOnGround ) WishVelocity *= 0.1f;
	}

	protected float baseAcceleration = 10f;

	protected Vector3 HalfGravity => Scene.PhysicsWorld.Gravity * Time.Delta * 0.5f;

	protected virtual void UpdateMovement()
	{
		BuildWishInput();
		DoMechanicsUpdate();
		BuildWishVelocity();
		Accelerate();
	}

	protected virtual void Move()
	{
		CharacterController.Move();
	}

	private void ApplyGravity()
	{
		if ( !CharacterController.IsOnGround )
		{
			CharacterController.Velocity += HalfGravity;
		}
		else
		{
			CharacterController.Velocity = CharacterController.Velocity.WithZ( 0 );
		}
	}

	protected virtual void Accelerate()
	{
		CharacterController.ApplyFriction( GetFriction() );

		if ( CurrentAccelerationOverride is not null )
		{
			CharacterController.Acceleration = CurrentAccelerationOverride.Value;
		}
		else
		{
			CharacterController.Acceleration = baseAcceleration;
		}

		if ( CharacterController.IsOnGround )
		{
			CharacterController.Accelerate( WishVelocity );
			CharacterController.Velocity = CharacterController.Velocity.WithZ( 0 );
		}
		else
		{
			CharacterController.Velocity += HalfGravity;
			CharacterController.Accelerate( WishVelocity );
		}

		Move();
		ApplyGravity();
	}
}
global using System;
global using Sandbox;
global using System.Linq;
global using System.Threading.Tasks;
global using System.Collections.Generic;
using Sandbox.Utility;

namespace Opium;

public partial class DeathCameraEffect : WeaponEffect
{
	public TimeUntil TimeUntilHitFloor { get; set; } = 0.5f;
	public TimeUntil TimeUntilRotated { get; set; } = 1.5f;

	[Property] public Curve HitFloorCurve { get; set; }
	[Property] public Curve RotationCurve { get; set; }

	[Property] public Vector3 PositionOffset { get; set; }
	[Property] public Angles Angles { get; set; }

	protected override void OnEnabled()
	{
		TimeUntilHitFloor = 0.5f;
		TimeUntilRotated = 1.5f;
	}

	public override bool TickEffect()
	{
		if ( Player is not null )
		{
			Player.CameraPositionOffset = PositionOffset * HitFloorCurve.Evaluate( TimeUntilHitFloor.Fraction.Clamp( 0, 1 ) );
			Player.CameraRotationOffset *= Rotation.From( Angles * RotationCurve.Evaluate( TimeUntilRotated.Fraction.Clamp( 0, 1 ) ) );
		}

		return false;
	}
}
using Sandbox.Services;

namespace Opium;

public partial class LeaderboardSystem
{
	private static DateTimeOffset startTime = DateTimeOffset.UtcNow;

//	[ConCmd( "op_dev_leaderboard_test" )]
	public static void RecordSpeedrun()
	{
		Log.Info( $"Recording speedrun time of {(DateTimeOffset.UtcNow - startTime).TotalSeconds}" );
		Sandbox.Services.Stats.SetValue( "speedrun", Math.Ceiling( (DateTimeOffset.UtcNow - startTime).TotalSeconds ) );
	}

	public static async Task<IEnumerable<Leaderboards.Entry>> GetEntries()
	{
		var board = Sandbox.Services.Leaderboards.Get( "speedrun" );
		board.Group = "global";
		board.MaxEntries = 20;
		await board.Refresh();
		return board.Entries;
	}

	[ActionGraphNode( "opium.leaderbaords.markstart" ), Category( "Opium/Leaderboard" ), Title( "Mark Start Time" )]
	public static void MarkStartTime()
	{
		startTime = DateTimeOffset.UtcNow;
	}
}
using Opium;
using Sandbox;

public sealed class ActionGraphInteract : BaseInteract
{
	[Property] public Action OnUsed { get; set; }

	[Property] bool Toggled { get; set; } = false;

	public override void OnUse( GameObject player )
	{
		if ( Toggled )
		{
			Toggled = false;

			OnUsed?.Invoke();

		}
		else
		{
			Toggled = true;

			OnUsed?.Invoke();
		}
	}
}
using Sandbox;
using System.Net.Sockets;

public sealed class ObstacleMover : BaseInteract
{
	[Property, Group( "Movement" )] public GameObject MoveTo { get; set; }
	[Property, Group( "Movement" )] public Curve Curve { get; set; } = new Curve( new Curve.Frame( 1.0f, 1.0f ), new Curve.Frame( 1.0f, 1.0f ) );
	[Property, Group( "Movement" ), Range(1,20)] public float MovementDuration { get; set; } = 2.0f;

	bool isMoving = false;
	float elapsedTime = 0f;

	Vector3 startPos;
	Rotation startRot;

	Vector3 endPos;
	Rotation endRot;

	bool Moved { get; set; } = false;

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

		Curve.Evaluate( 0 );

		startPos = Transform.Position;
		startRot = Transform.Rotation;

		if ( MoveTo == null ) return;

		endPos = MoveTo.Transform.Position;
		endRot = MoveTo.Transform.Rotation;
	}

	protected override void OnUpdate()
	{
		if ( isMoving )
		{
			elapsedTime += Time.Delta;

			float progress = elapsedTime / MovementDuration;
			progress = Math.Clamp( progress, 0, 1 );

			float curvedProgress = Curve.Evaluate( progress );

			Transform.Position = Vector3.Lerp( startPos, endPos, curvedProgress );
			Transform.Rotation = Rotation.Lerp( startRot, endRot, curvedProgress );

			if ( progress >= 1f )
			{
				isMoving = false;
				elapsedTime = 0;
				Moved = true;
			}
		}
	}

	public override void OnUse( GameObject player )
	{
		if ( Moved ) return;

		isMoving = true;
		elapsedTime = 0;
	}
}
using System.Numerics;

namespace Opium.AI;

// Should probably all be in HostileNPC
partial class Agent
{
	/// <summary>
	/// Does this agent have line of sight to a specified GameObject?
	/// </summary>
	private bool HasLineOfSight( GameObject gameObject )
	{
		// TODO(alex): line of sight cone here

		if ( gameObject == null )
			return false;

		var trace = Scene.Trace
			.Ray( CameraObject.WorldPosition, gameObject.WorldPosition )
			.IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "pickup" )
			.Run();

		if ( !trace.Hit )
			return false;

		return trace.GameObject == gameObject || gameObject.IsDescendant( trace.GameObject );
	}

	/// <summary>
	/// Find ALL players in the scene
	/// </summary>
	public IEnumerable<Opium.PlayerController> GetAllPlayers()
	{
		return Scene.GetAllObjects( true )
			.Where( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) != null )
			.Select( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) );
	}

	private IEnumerable<Actor> CachedLOS { get; set; } = new List<Actor>();
	private TimeSince TimeSinceLOSAcquired = 1f;
	private float LOSFrequency => 1f;

	[ConVar( "op_dev_ai_los" )]
	public static bool LOSDebug { get; set; } = false;

	private IEnumerable<Actor> FindActorsInLineOfSight( float range = -1 )
	{
		var forward = CameraObject.WorldRotation.Forward;
		var losSphere = new Sphere( CameraObject.WorldPosition + forward * ( range * 2f ), range * 2f );

		if ( LOSDebug )
		{
			Gizmo.Transform = global::Transform.Zero;
			Gizmo.Draw.Color = Color.White.WithAlpha( 0.25f );
			Gizmo.Draw.LineSphere( losSphere, 32 );
			Gizmo.Draw.Line( CameraObject.WorldPosition, CameraObject.WorldPosition + forward * (range / 2f) );
		}

		if ( TimeSinceLOSAcquired < LOSFrequency )
		{
			return CachedLOS;
		}

		TimeSinceLOSAcquired = 0;

		if ( range < 0 )
		{
			range = DefaultDetectionRange;
		}

		CachedLOS = Scene.FindInPhysics( losSphere )
			.Select( x => x.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) )
			.Where( x => HasLineOfSight( x?.GameObject ) )
			.Where( x => x.IsAlive );

		return CachedLOS;
	}

	public IEnumerable<Actor> FindEnemiesInLineOfSight( float range = -1 )
	{
		if ( range < 0 ) range = DefaultDetectionRange;

		return FindActorsInLineOfSight( range ).Where( Hates );
	}

	public Actor FindClosestEnemyInLineOfSight( float range = -1 )
	{
		if ( range < 0 ) range = DefaultDetectionRange;

		return FindActorsInLineOfSight( range ).FirstOrDefault( Hates );
	}

	/// <summary>
	/// Can this agent see a player?
	/// </summary>
	public bool CanSeeAnyPlayer()
	{
		var target = FindActorsInLineOfSight();
		return (target is not null);
	}

	/// <summary>
	/// Make the agent look at someone.
	/// TODO: Make this use proper Actor movement 
	/// </summary>
	public void LookAt( Vector3 position, float smoothing = 5f )
	{
		var lookRot = Rotation.LookAt( position - CameraObject.WorldPosition );
		var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );

		WorldRotation = Rotation.Slerp( WorldRotation, lookRotAngles.ToRotation(), smoothing * Time.Delta );
	}

	/// <summary>
	/// Make the agent look at someone.
	/// TODO: Make this use proper Actor movement 
	/// </summary>
	public void LookAt( Actor target )
	{
		var lookRot = Rotation.LookAt( target.WorldPosition - CameraObject.WorldPosition );
		var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );
		WorldRotation = lookRotAngles.ToRotation();	
	}

	/// <summary>
	/// Gets a path (using the navmesh) between two vectors.
	/// </summary>
	public List<Vector3> GetPath( Vector3 pointA, Vector3 pointB )
	{
		var navPath = Scene.NavMesh.GetSimplePath( pointA, pointB );
		return navPath;
	}

	/// <inheritdoc cref="GetPath(Vector3, Vector3)"/>
	public List<Vector3> GetPath( Vector3 target )
	{
		return GetPath( WorldPosition, target );
	}

	TimeUntil nextStimuli = 0f;

	[Property] public float LineOfSightRange { get; set; } = -1;

	public void FindStimuli()
	{
		var enemy = FindClosestEnemyInLineOfSight( LineOfSightRange );

		// Can we actually see anyone?
		if ( enemy == null )
			return;

		LastStimulus = new EnemySpottedStimulus( enemy );

		if ( enemy.ActiveWeapon is MeleeWeapon melee && nextStimuli )
		{
			var swinging = melee.MainAttack.State == AttackState.Windup;
			var rand = Game.Random.Next( 1, 10 );

			if ( swinging && rand >= 3 && nextStimuli )
			{
				nextStimuli = 1;
				Scene.BroadcastStimulus( new AnticipateHitStimulus( enemy.WorldPosition ) );
			}
		}
	}

	public DoorComponent FindNearestDoor( float range = -1f )
	{
		if ( range <= 0 )
			range = 128f;

		var bbox = new BBox( WorldPosition - range, WorldPosition + range );

		var door = Scene.FindInPhysics( bbox )
			.Where( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) != null )
			.Select( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) )
			.FirstOrDefault();

		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.LineBBox( bbox );

		return door;
	}
}
using Sandbox;
using System.Text.Json.Serialization;

namespace Opium.AI;

public abstract class AgentAnimator : Component
{
	/// <summary>
	/// The agent in question
	/// </summary>
	[Property] public Agent Agent { get; set; }

	/// <summary>
	/// Are we in a full body sequence? (Turn off upper body / lower body mask )
	/// </summary>
	[Property, ReadOnly, JsonIgnore] public bool IsFullBody { get; set; }
	[Property, ReadOnly, JsonIgnore] public UpperBodyState UpperBodyState { get; set; }
	[Property, ReadOnly, JsonIgnore] public LowerBodyState LowerBodyState { get; set; }
	[Property, ReadOnly, JsonIgnore] public FullBodyState FullBodyState { get; set; }

	Vector2 lerpedMove = 0;

	protected override void OnUpdate()
	{
		var stateMachine = Agent?.StateMachine;

		Agent?.Model?.Set( "upper_state", (int)UpperBodyState );
		Agent?.Model?.Set( "lower_state", (int)LowerBodyState );

		Agent?.Model?.Set( "mask_fullbody", IsFullBody );
		// Only active when mask_fullbody = 1
		Agent?.Model?.Set( "full_body_state", (int)FullBodyState );

		lerpedMove = lerpedMove.LerpTo( Agent.WishMove, Time.Delta * 4f );
		Agent?.Model?.Set( "move_x", lerpedMove.y );
		Agent?.Model?.Set( "move_y", lerpedMove.x );
	}
}

public enum UpperBodyState : int
{
	Default = 0, // Idle
	Charge = 1, // Holding a weapon at an enemy
	Attack = 2, // Attacking right now,
	Block = 3, // Blocking an attack
}

public enum LowerBodyState : int
{
	Default = 0, // Traversal
	KickingDoor = 1,
}

public enum FullBodyState : int
{
	Idle = 0,
	AttackStun = 1,
	BlockStun = 2
}
using System.Diagnostics;

namespace Opium.AI;

public abstract partial class StateMachine : Component
{
	[Property] public Agent Agent { get; set; }

	private State currentState;
	[Property] public State CurrentState
	{
		get => currentState;
		set
		{
			if ( currentState == value ) return;

			var previousState = currentState;
			currentState = value;

			OnStateChanged( previousState, currentState );
		}
	}
	public IEnumerable<State> States => Components.GetAll<State>( FindMode.EnabledInSelfAndDescendants );

	public TimeSince TimeInState;

	public abstract void Tick();

	/// <summary>
	/// Set state where type is T
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public void SetState<T>() where T : State
	{
		var state = Components.Get<T>( FindMode.EnabledInSelfAndDescendants );
		if ( state is not null )
		{
			CurrentState = state;
		}
	}

	public void OnStateChanged( State before, State after )
	{
		Log.Info( $"FSM state changed from {before} to {after}" );

		// Cancel any walk tasks that are in a state?
		Agent.CancelWalk();

		before?.OnStateExit( after );
		after?.OnStateEnter( before );


		if ( before != after )
		{
			TimeInState = 0;
		}
	}

	public virtual void OnEvent( string eventName, params object[] obj )
	{
		foreach ( var state in States )
		{
			state.OnEvent( eventName, obj );
		}
	}

	internal void InternalTick()
	{
		Tick();
	}

	public void UpdateStateMachine()
	{
		DrawDebug();
		InternalTick();

		foreach ( var state in States.OrderByDescending( x => x.Priority ) )
		{
			state.Agent = Agent;

			var sw = Stopwatch.StartNew();
			bool shouldEnterState = state.ShouldEnterState( this );

			sw.Stop();

			if ( shouldEnterState )
			{
				CurrentState = state;
				break;
			}

		}

		if ( CurrentState is not null )
		{
			if ( CurrentState.CanTick() )
				CurrentState.InternalTick();
		}
	}

	[ConVar( "op_dev_ai_debug" )]
	public static bool DebugEnabled { get; set; } = false;

	private void DrawDebug()
	{
		if ( !DebugEnabled )
			return;

		var distanceToCamera = Scene.Camera.Transform.Position.Distance( Transform.Position );

		if ( distanceToCamera > 30000f )
			return;

		var eyePos = Vector3.Up * 64f;
		var lineHeight = 16f;
		var currentLine = 0;

		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );

		void DebugText( object obj )
		{
			var transform = GameObject.Transform.World;
			var position = transform.Position + eyePos;

			var screenPos = Scene.Camera.PointToScreenNormal( position );
			var offset = Vector2.Up * lineHeight * currentLine;

			var pos = screenPos * Screen.Size;
			Gizmo.Draw.ScreenText( $"{obj}", pos - offset, "Consolas" );

			currentLine++;
		}

		DebugText( $"Velocity: {Agent.WishVelocity}" );
		DebugText( $"Mechanics: {string.Join( ", ", Agent.Mechanics.Where( x => x.IsActive ) )}" );
		DebugText( $"State: {CurrentState}" );
		DebugText( $"Name: {GameObject.Parent.Name}" );
		DebugText( $"LastStimulus: {Agent.LastStimulus}" );
	}
}