3653 results

using Sandbox;
using System.Drawing;

public class FloaterLock : Component
{
	public int NumTurns { get; set; }

	private TimeSince _timeSinceSpawn;

	public float Lifetime { get; set; }
	public bool ShowLocked { get; set ; }

	private float _startScale;
	private float _endScale;
	private Vector2 _velocity;
	private float _deceleration;

	public float Scale { get; set; }
	public float Opacity { get; set; }

	public void Init( int numTurns )
	{
		NumTurns = numTurns;

		Lifetime = 1.25f;
		_velocity = new Vector2( 0f, 12f );
		_deceleration = 4f;
		Scale = _startScale = 1f;
		_endScale = 1f;
	}

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

		_timeSinceSpawn = 0f;

		Manager.Instance.PlaySfx( "lock_start", new Vector3( WorldPosition.x, WorldPosition.y, Manager.Instance.Camera.WorldPosition.z - 100f ), volume: 0.9f, pitch: Game.Random.Float( 1.05f, 1.13f ) );
	}

	protected override void OnUpdate()
	{
		Opacity = Utils.Map( _timeSinceSpawn, 0f, 0.3f, 0f, 1f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, Lifetime - 0.1f, 1f, 0.1f, EasingType.ExpoIn ) * Utils.Map( _timeSinceSpawn, Lifetime - 0.3f, Lifetime - 0.05f, 1f, 0f, EasingType.Linear );
		Scale = Utils.Map( _timeSinceSpawn, 0f, Lifetime, _startScale, _endScale, EasingType.SineOut );

		WorldPosition += new Vector3( _velocity.x, _velocity.y, 0f ) * Time.Delta;
		_velocity *= (1f - _deceleration * Time.Delta);

		if( !ShowLocked && _timeSinceSpawn > 0.5f )
		{
			ShowLocked = true;
			_velocity = new Vector2( 0f, -20f );
			_deceleration = 7f;

			Manager.Instance.PlaySfx( "lock", new Vector3( WorldPosition.x, WorldPosition.y, Manager.Instance.Camera.WorldPosition.z - 100f ), volume: 2f, pitch: Game.Random.Float( 0.97f, 1.03f ) );
		}

		if ( _timeSinceSpawn > Lifetime )
		{
			Manager.Instance.NumFloaters--;
			GameObject.Destroy();
		}
	}
}
using Microsoft.VisualBasic;
using Sandbox;
using Sandbox.UI;
using System.Runtime;
using System.Threading.Tasks;

public enum CardType { None, Apple, Bomb, Rocket, Telephone, Trumpet, Pawn, Balloon, Spider, Umbrella, UmbrellaClosed, Juggler, Coin, Vampire, Maracas, Microwave, Raccoon, Rock, Bear, Candle, 
	CrystalBall, Snake, Rose, RoseWilted, Bell, Ant, Twins, Compass, Antenna, Wizard, AncientScroll, JumpingSpider, Bento, Ninja, Flashlight, Bodybuilder, Helicopter, FlyingMoney,
	Syringe, Cellphone, Crab, Magnet, Stethoscope, UFO, Teacher, Tractor, FortuneCookie, Dolphin, Detective, Genie, Potion, Mirror, FaxMachine, Dice, Coffee, Tree, Chipmunk,
	Dancer, Wine, King, Police, Ogre, Clown, MoneyBag, Bank, Steak, MagicWand, Carrot, Caterpillar, Butterfly, Tornado, Map, Cockroach, Broccoli, Donut, Cheese, CheeseMoldy, GuideDog, Taco, Burrito,
	Skunk, CarePackage, Cash, BirthdayCake, Gem, Pickaxe, Mountain, DiamondRing, Rabbit, RedEnvelope,
}

// StockMarket, StockMarketDown, Key, AlarmClock, ATM

public class Card : Component, IEventHandler
{
	public GameObject Model { get; set; }
	public ModelRenderer ModelRenderer { get; set; }
	public GameObject IconObj { get; set; }
	public GameObject IconBackObj { get; set; }
	public bool IsFlagShown { get; set; }

	public float CardIconScale { get; set; } = 1f;

	public bool IsHovered { get; set; }
	public bool IsRevealed { get; set; }
	public bool IsSpawning { get; set; }

	public int NumTimesRevealed { get; set; }
	public int NumTimesChosen { get; set; }

	public CardType CardType { get; private set; }
	public CardType OriginalCardType { get; private set; }

	public virtual bool IsFoodOrBeverage => false;
	public virtual bool IsAlive => false;
	public virtual bool CantBeMoved => false;

	public IntVector2 GridPos { get; set; }

	public bool IsShaking { get; private set; }
	private TimeSince _timeSinceShakeStart; // todo: replace with timer? this can become negative somehow
	private float _shakeTime;
	private EasingType _shakeEasingType;
	public Vector2 ShakeOffset { get; set; }
	private float _shakeStrength;

	public bool IsMovementControlled { get; set; }
	private Vector3 _moveStartPos;
	private Vector3 _moveTargetPos;
	private float _moveTime;
	private TimeSince _timeSinceMoveStart;
	private EasingType _moveEasingType;
	private bool _moveRemoveControlAfter;

	public int HP { get; set; }

	public bool IsLocked { get; set; }
	public int LockTurnsRemaining { get; set; }
	public bool WasJustLocked { get; set; }

	public int CardTypeID { get; set; }

	public float IconOpacity { get; set; }
	public bool IsExploding;
	private TimeSince _timeSinceStartExploding;
	private float _explodeTime;
	private EasingType _explodeEasingType;
	private Vector3 _explodeStartPos;
	private EasingType _explodeShakeEasingType;

	public bool IsScaling;
	private TimeSince _timeSinceScale;
	private float _scaleTime;
	private float _scaleAmount;
	private EasingType _scaleEasingType;

	public bool AreRevealMarksShown { get; set; }

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

	}

	public virtual void Init(CardType cardType)
	{
		CardType = OriginalCardType = cardType;

		Model = GameObject.Children.Find( x => x.Name == "model" );
		ModelRenderer = Model.Components.Get<ModelRenderer>();

		IconOpacity = 1f;

		IconObj = GameObject.Children.Find( x => x.Name == "icon" );
		var cardIcon = IconObj.Components.Get<CardIcon>( includeDisabled: true );
		cardIcon.Card = this;

		IconBackObj = GameObject.Children.Find( x => x.Name == "icon_back" );
		var cardIconBack = IconBackObj.Components.Get<CardIconBack>( includeDisabled: true );
		cardIconBack.Card = this;

		SetMaterial( cardType );
	}

	public void SetMaterial(CardType cardType)
	{
		var matFilename = GetMaterialFilename( cardType );
		if ( !string.IsNullOrEmpty( matFilename ) )
			ModelRenderer.MaterialOverride = Material.Load( matFilename );
	}

	public void SetCardType(CardType newType)
	{
		CardType = newType;
		SetMaterial( newType );
	}

	protected override void OnUpdate()
	{
		//if ( Input.Down( "Duck" ) )
		//{
		//	var emoji = Card.GetIconEmoji( CardType );
		//	if ( string.IsNullOrEmpty( emoji ) )
		//	{
		//		Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition + new Vector3( -0.5f, -0.5f, -1f ) ), size: 24f );

		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition ), size: 24f );
		//	}
		//	else
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( emoji, new global::Transform( WorldPosition ), size: 60f );
		//	}

		//	if ( IsMovementControlled )
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( "➡️", new global::Transform( WorldPosition + new Vector3( -15.5f, -19f, -1f ) ), size: 30f );
		//	}

		//	if ( IsLocked )
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( "🔒", new global::Transform( WorldPosition + new Vector3( 15.5f, -19f, -1f ) ), size: 30f );

		//		Gizmo.Draw.Color = Color.Red.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{LockTurnsRemaining}", new global::Transform( WorldPosition + new Vector3( 15.5f, -21f, 1f ) ), size: 20f );
		//	}

		//	Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
		//	Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) + new Vector3( -0.5f, -0.5f, -1f ) ), size: 16 );

		//	Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//	Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) ), size: 16 );
		//}

		HandleMoving();
		HandleShaking();

		if(IsExploding)
		{
			HandleExploding();
			return;
		}

		if ( IsScaling )
		{
			HandleScaling();
		}
		
		//if(!IsScaling && !IsExploding)
		//{
		//	Transform.LocalScale = new Vector3( Utils.DynamicEaseTo( Transform.LocalScale.x, IsHovered ? 1.05f : 1f, 0.3f, Time.Delta ) );
		//}

		if( IsSpawning )
		{
			// do nothing
		}
		else if( IsMovementControlled )
		{
			LocalPosition += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f ) * Time.Delta * 10f;
		}
		else
		{
			var targetPos = Manager.GetCardPos( GridPos );

			if ( IsShaking )
				targetPos += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f );

			var height = 10f + ((IsRevealed || IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_OR_HOVERED : 0f) + ((IsRevealed && IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_AND_HOVERED : 0f);
			var zPos = MathX.Lerp( LocalPosition.z, height, Time.Delta * 20f );

			LocalPosition = Vector3.Lerp( LocalPosition, targetPos, Time.Delta * 10f ).WithZ( zPos );
		}

		var targetRot = Rotation.FromPitch( IsRevealed ? 0f : 180f );
		WorldRotation = Rotation.Lerp( WorldRotation, targetRot, Time.Delta * 15f );
	}

	public void MoveToPos( Vector3 pos, float time, EasingType easingType = EasingType.Linear, bool removeControlAfter = false)
	{
		//Log.Info( $"Card - MoveToPos: {pos}" );

		IsMovementControlled = true;
		_moveStartPos = LocalPosition;
		_moveTargetPos = pos;
		_moveTime = time;
		_timeSinceMoveStart = 0f;
		_moveEasingType = easingType;
		_moveRemoveControlAfter = removeControlAfter;
	}

	void HandleMoving()
	{
		if ( !IsMovementControlled )
			return;

		if(_timeSinceMoveStart > _moveTime)
		{
			LocalPosition = _moveTargetPos;

			if( _moveRemoveControlAfter )
				IsMovementControlled = false;
		}
		else 
		{
			LocalPosition = Vector3.Lerp( _moveStartPos, _moveTargetPos, Utils.Map( _timeSinceMoveStart, 0f, _moveTime, 0f, 1f, _moveEasingType ) );
		}
	}

	protected void HandleShaking()
	{
		if ( IsShaking )
		{
			if ( _timeSinceShakeStart > _shakeTime )
				IsShaking = false;
			else
				ShakeOffset = Utils.GetRandomVector() * _shakeStrength * Utils.Map( _timeSinceShakeStart, 0f, _shakeTime, 1f, 0f, _shakeEasingType );
		}
	}

	void HandleExploding()
	{
		LocalScale = new Vector3( Utils.Map(_timeSinceStartExploding, 0f, _explodeTime, 1f, Globals.CARD_EXPLODE_SCALE, _explodeEasingType) );
		IconOpacity = Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 1f, 0f, EasingType.SineIn );

		LocalPosition = _explodeStartPos + Vector3.Random * Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 0f, 2f, _explodeShakeEasingType) * Utils.Map( _timeSinceStartExploding, _explodeTime * 0.9f, _explodeTime, 1f, 0f );
	}

	void HandleScaling()
	{
		if(_timeSinceScale > 0.25f)
		{
			IsScaling = false;
			LocalScale = new Vector3( 1f );
		}
		else
		{
			LocalScale = Utils.MapReturn( _timeSinceScale, 0f, _scaleTime, 1f, _scaleAmount, _scaleEasingType);
		}
	}

	public static string GetName( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.CrystalBall: return "Crystal Ball";
			case CardType.AncientScroll: return "Ancient Scroll";
			case CardType.JumpingSpider: return "Jumping Spider";
			case CardType.Rock: return "Boulder";
			case CardType.Syringe: return "Morphine";
			case CardType.FlyingMoney: return "Flying Money";
			case CardType.FortuneCookie: return "Fortune Cookie";
			//case CardType.AlarmClock: return "Alarm Clock";
			case CardType.FaxMachine: return "Fax Machine";
			case CardType.MoneyBag: return "Money Bag";
			case CardType.MagicWand: return "Magic Wand";
			//case CardType.StockMarket: return "Stock Market";
			case CardType.GuideDog: return "Guide Dog";
			case CardType.CarePackage: return "Care Package";
			case CardType.BirthdayCake: return "Birthday Cake";
			case CardType.DiamondRing: return "Diamond Ring";
			case CardType.RedEnvelope: return "Red Envelope";
		}

		return cardType.ToString();
	}

	public static string GetFilename( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "apple";
			case CardType.Bomb: return "bomb";
			case CardType.Rocket: return "rocket";
			case CardType.Telephone: return "telephone";
			case CardType.Trumpet: return "trumpet";
			case CardType.Pawn: return "pawn";
			case CardType.Balloon: return "balloon";
			case CardType.Spider: return "spider";
			case CardType.Umbrella: return "umbrella";
			case CardType.UmbrellaClosed: return "umbrella_closed";
			case CardType.Juggler: return "juggler";
			case CardType.Coin: return "coin";
			case CardType.Vampire: return "vampire";
			case CardType.Maracas: return "maracas";
			case CardType.Microwave: return "microwave";
			case CardType.Raccoon: return "raccoon";
			case CardType.Rock: return "rock";
			case CardType.Bear: return "bear";
			case CardType.Candle: return "candle";
			case CardType.CrystalBall: return "crystal_ball";
			case CardType.Snake: return "snake";
			case CardType.Rose: return "rose";
			case CardType.RoseWilted: return "rose_wilted";
			case CardType.Bell: return "bell";
			case CardType.Ant: return "ant";
			case CardType.Twins: return "twins";
			case CardType.Compass: return "compass";
			case CardType.Antenna: return "antenna";
			case CardType.Wizard: return "wizard";
			case CardType.AncientScroll: return "scroll";
			case CardType.JumpingSpider: return "spider_jumping";
			case CardType.Bento: return "bento";
			//case CardType.StockMarket: return "investment";
			//case CardType.StockMarketDown: return "investment_down";
			case CardType.Ninja: return "ninja";
			case CardType.Flashlight: return "flashlight";
			case CardType.Bodybuilder: return "bodybuilder";
			case CardType.Helicopter: return "helicopter";
			case CardType.FlyingMoney: return "flying_money";
			case CardType.Syringe: return "syringe";
			case CardType.Cellphone: return "cellphone";
			case CardType.Crab: return "crab";
			case CardType.Magnet: return "magnet";
			case CardType.Stethoscope: return "stethoscope";
			case CardType.UFO: return "ufo";
			case CardType.Teacher: return "teacher";
			case CardType.Tractor: return "tractor";
			case CardType.FortuneCookie: return "fortune_cookie";
			case CardType.Dolphin: return "dolphin";
			case CardType.Detective: return "detective";
			case CardType.Genie: return "genie";
			case CardType.Potion: return "potion";
			case CardType.Mirror: return "mirror";
			//case CardType.AlarmClock: return "alarm_clock";
			case CardType.FaxMachine: return "fax_machine";
			case CardType.Dice: return "dice";
			case CardType.Coffee: return "coffee";
			case CardType.Tree: return "tree";
			case CardType.Chipmunk: return "chipmunk";
			case CardType.Dancer: return "dancer";
			case CardType.Wine: return "wine";
			case CardType.King: return "king";
			case CardType.Police: return "police";
			case CardType.Ogre: return "ogre";
			case CardType.Clown: return "clown";
			case CardType.MoneyBag: return "money_bag";
			case CardType.Bank: return "bank";
			case CardType.Steak: return "steak";
			case CardType.MagicWand: return "magic_wand";
			case CardType.Carrot: return "carrot";
			case CardType.Caterpillar: return "caterpillar";
			case CardType.Butterfly: return "butterfly";
			case CardType.Tornado: return "tornado";
			case CardType.Map: return "map";
			case CardType.Cockroach: return "cockroach";
			//case CardType.Key: return "key";
			case CardType.Broccoli: return "broccoli";
			//case CardType.ATM: return "atm";
			case CardType.Donut: return "donut";
			case CardType.Cheese: return "cheese";
			case CardType.CheeseMoldy: return "cheese_moldy";
			case CardType.GuideDog: return "guide_dog";
			case CardType.Taco: return "taco";
			case CardType.Burrito: return "burrito";
			case CardType.Skunk: return "skunk";
			case CardType.CarePackage: return "care_package";
			case CardType.Cash: return "cash";
			case CardType.BirthdayCake: return "birthday_cake";
			case CardType.Gem: return "gem";
			case CardType.Pickaxe: return "pickaxe";
			case CardType.Mountain: return "mountain";
			case CardType.DiamondRing: return "diamond_ring";
			case CardType.Rabbit: return "rabbit";
			case CardType.RedEnvelope: return "red_envelope";
		}

		return "";
	}

	public static string GetIconFilename( CardType cardType )
	{
		return $"textures/{GetFilename( cardType )}.png";
	}

	public static string GetMaterialFilename( CardType cardType )
	{
		return $"materials/cards/{GetFilename( cardType )}.vmat";
	}

	public static string GetIconEmoji( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "🍎";
			case CardType.Bomb: return "💣";
			case CardType.Rocket: return "🚀";
			case CardType.Telephone: return "☎️";
			case CardType.Trumpet: return "🎺";
			case CardType.Pawn: return "♟️";
			case CardType.Balloon: return "🎈";
			case CardType.Spider: return "🕷️";
			case CardType.Umbrella: return "☂️";
			case CardType.UmbrellaClosed: return "🌂";
			case CardType.Juggler: return "🤹";
			case CardType.Coin: return "🟡";
			case CardType.Vampire: return "🧛";
			case CardType.Raccoon: return "🦝";
			case CardType.Rock: return "🗿";
			case CardType.Bear: return "🐻";
			case CardType.Candle: return "🕯️";
			case CardType.CrystalBall: return "🔮";
			case CardType.Snake: return "🐍";
			case CardType.Rose: return "🌹";
			case CardType.RoseWilted: return "🥀";
			case CardType.Bell: return "🔔";
			case CardType.Ant: return "🐜";
			case CardType.Twins: return "👯";
			case CardType.Compass: return "🧭";
			case CardType.Antenna: return "📡";
			case CardType.Wizard: return "🧙";
			case CardType.AncientScroll: return "📜";
			case CardType.JumpingSpider: return "🕷️";
			case CardType.Bento: return "🍱";
			//case CardType.StockMarket: return "📈";
			//case CardType.StockMarketDown: return "📉";
			case CardType.Ninja: return "🐱‍👤";
			case CardType.Flashlight: return "🔦";
			case CardType.Bodybuilder: return "🏋️";
			case CardType.Helicopter: return "🚁";
			case CardType.FlyingMoney: return "💸";
			case CardType.Syringe: return "💉";
			case CardType.Cellphone: return "📱";
			case CardType.Crab: return "🦀";
			case CardType.Magnet: return "🧲";
			case CardType.Stethoscope: return "🩺";
			case CardType.UFO: return "🛸";
			case CardType.Teacher: return "👩‍🏫";
			case CardType.Tractor: return "🚜";
			case CardType.FortuneCookie: return "🥠";
			case CardType.Dolphin: return "🐬";
			case CardType.Detective: return "🕵️";
			case CardType.Genie: return "🧞";
			case CardType.Potion: return "🧪";
			case CardType.Mirror: return "🔘";
			//case CardType.AlarmClock: return "⏰";
			case CardType.FaxMachine: return "📠";
			case CardType.Dice: return "🎲";
			case CardType.Coffee: return "☕";
			case CardType.Tree: return "🌳";
			case CardType.Chipmunk: return "🐿️";
			case CardType.Dancer: return "💃";
			case CardType.Wine: return "🍷";
			case CardType.King: return "🤴";
			case CardType.Police: return "👮";
			case CardType.Ogre: return "👹";
			case CardType.Clown: return "🤡";
			case CardType.MoneyBag: return "💰";
			case CardType.Bank: return "🏦";
			case CardType.Steak: return "🥩";
			case CardType.MagicWand: return "✨";
			case CardType.Carrot: return "🥕";
			case CardType.Caterpillar: return "🐛";
			case CardType.Butterfly: return "🦋";
			case CardType.Tornado: return "🌪️";
			case CardType.Map: return "🗺️";
			case CardType.Cockroach: return "𓆣";
			//case CardType.Key: return "🔑";
			case CardType.Broccoli: return "🥦";
			//case CardType.ATM: return "🏧";
			case CardType.Donut: return "🍩";
			case CardType.Cheese: return "🧀";
			case CardType.CheeseMoldy: return "🤮";
			case CardType.GuideDog: return "🦮";
			case CardType.Taco: return "🌮";
			case CardType.Burrito: return "🌯";
			case CardType.Skunk: return "🦨";
			case CardType.CarePackage: return "📦";
			case CardType.Cash: return "💵";
			case CardType.BirthdayCake: return "🎂";
			case CardType.Gem: return "💎";
			case CardType.Pickaxe: return "⛏️";
			case CardType.Mountain: return "⛰️";
			case CardType.DiamondRing: return "💍";
			case CardType.Rabbit: return "🐇";
			case CardType.RedEnvelope: return "🧧";
		}

		return "";
	}

	public static string GetCardDescription( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "✅Match: +1 HP";
			case CardType.Bomb: return "✅Match: reveal nearby cards then shuffle them";
			case CardType.Rocket: return "❌Mismatch: swap position";
			case CardType.Telephone: return "✅Match: shake nearby pairs";
			case CardType.Trumpet: return "Match other cards: 30% chance to reveal";
			case CardType.Pawn: return "➡️Turn start: 50% to try to move upward";
			case CardType.Balloon: return "Floats upward";
			case CardType.Spider: return "Starts in a corner";
			case CardType.Umbrella: return "👁️Revealed: toggle open/closed";
			case CardType.Juggler: return "❌Mismatch: rearrange nearby cards";
			case CardType.Coin: return "✅Match: +$2";
			case CardType.Vampire: return "While hovered: shake if you lose HP";
			case CardType.Maracas: return "Makes a distinctive sound when shook";
			case CardType.Microwave: return "➡️Turn start: shake a nearby food/drink";
			case CardType.Raccoon: return "➡️Turn start: shake an adjacent card";
			case CardType.Rock: return "Falls downward, pushing other cards";
			case CardType.Bear: return "Food/drink revealed: 20% chance to shake";
			case CardType.Candle: return "✅Match: each reveal a nearby card";
			case CardType.CrystalBall: return "❌Mismatch: shake cards, including other card's match";
			case CardType.Snake: return "Shake when an adjacent card is matched";
			case CardType.Rose: return "When moved: shake and wilt";
			case CardType.Bell: return "Rings when moved or shook";
			case CardType.Ant: return "➡️Turn start: move to nearby empty space";
			case CardType.Twins: return "👁️Revealed: nudge toward match";
			case CardType.Compass: return "❌Mismatch: nudge toward other card's match";
			case CardType.Antenna: return "👁️Revealed: sound indicates distance to match";
			case CardType.Wizard: return "⏩Every 3 turns: shuffle some cards\n💔Damage Wizard: respawn some cards and shuffle them";
			case CardType.AncientScroll: return "✅Match: reveal all cards (after next turn, shuffle all cards)";
			case CardType.JumpingSpider: return "Starts in a corner\n❌Mismatch: swaps with an unknown card";
			case CardType.Bento: return "✅Match: +1 HP for each consecutive match";
			//case CardType.StockMarket: return "✅Match: gain half your money if the market is up, lose half if the market is down";
			case CardType.Ninja: return "➡️Turn start: 25% chance to swap position";
			case CardType.Flashlight: return "👆Chosen: reveal a nearby card";
			case CardType.Bodybuilder: return "➡️Turn start: move adjacent card upwards, pushing other cards";
			case CardType.Helicopter: return "❌Mismatch: relocate along with an adjacent card";
			case CardType.FlyingMoney: return "✅Match: +$3\n❌Mismatch: shuffle with some nearby cards";
			case CardType.Syringe: return "✅Match: heal all HP, but time is halved until end of level";
			case CardType.Cellphone: return "❌Mismatch: vibrate nearest cellphone";
			case CardType.Crab: return "Moves sideways to empty spaces";
			case CardType.Magnet: return "➡️Turn start: pull card toward it";
			case CardType.Stethoscope: return "✅Match/❌Mismatch: shake nearby cards with a heartbeat";
			case CardType.UFO: return "✅Match: abduct and shuffle nearby cards";
			case CardType.Teacher: return "✅Match: arrange row alphabetically";
			case CardType.Tractor: return "✅Match/❌Mismatch: shift row one space";
			case CardType.FortuneCookie: return "✅Match: +1 HP, and reveal 5 cards that you haven't seen before";
			case CardType.Dolphin: return "Revealed on 4 consecutive mismatches";
			case CardType.Detective: return "➡️Turn start: 50% chance to reveal a nearby card";
			case CardType.Genie: return "✅Match: next card chosen shakes its match";
			case CardType.Potion: return "✅Match: your HP is set to half";
			case CardType.Mirror: return "✅Match: flip the level horizontally";
			//case CardType.AlarmClock: return "70% chance to shake when timer goes below 1s";
			case CardType.FaxMachine: return "👆Chosen: shake 3 cards, including another fax machine";
			case CardType.Dice: return "❌Mismatch: roll a 6 to get +$2 (rolling 1 loses $1)\n✅Match: Roll a 12 to get +$5 (rolling 2 loses $2)";
			case CardType.Coffee: return "✅Match: +2 HP, and your next turn lasts 3 seconds";
			case CardType.Tree: return "✅Match: reveal an apple";
			case CardType.Chipmunk: return "➡️Turn start: move toward a tree";
			case CardType.Dancer: return "❌Mismatch: dance with multiple partners";
			case CardType.Wine: return "✅Match: +2 HP\nShake if an adjacent card shakes";
			case CardType.King: return "💔Damage King: he decides your next choice";
			case CardType.Police: return "❌Mismatch: Reveal and lock a nearby card for 2 turns\n💔Damage Police: Lock some nearby cards for 1 turn, then shuffle";
			case CardType.Ogre: return "💔Damage Ogre: shuffle with nearby cards";
			case CardType.Clown: return "💔Damage Clown: swap multiple times";
			case CardType.MoneyBag: return "✅Match: +$5";
			case CardType.Bank: return "✅Match: transform all coins into money bags";
			case CardType.Steak: return "✅Match: -$1 and +4 HP";
			case CardType.MagicWand: return "❌Mismatch: send other card adjacent to its match";
			case CardType.Carrot: return "✅Match: +3 HP, and timer is doubled next turn";
			case CardType.Caterpillar: return "➡️Turn start: 20% chance to shake and transform into a butterfly";
			case CardType.Tornado: return "➡️Turn start: swap 2 adjacent cards";
			case CardType.Map: return "✅Match: reveal all cards between";
			case CardType.Cockroach: return "Can't be matched until no other cards remain";
			//case CardType.Key: return "❌Mismatch: unlock other card\n✅Match: unlock all cards";
			case CardType.Broccoli: return "✅Match: +3 HP";
			//case CardType.ATM: return "✅Match: +$1 for every 2 matches you've made this level (max: $7)";
			case CardType.Donut: return "✅Match: +1 HP for each adjacent empty space";
			case CardType.Cheese: return "✅Match: +3 HP if not moldy\nGoes moldy after 7 turns";
			case CardType.GuideDog: return "When you mismatch other cards, 50% chance to point toward one of their matches";
			case CardType.Taco: return "✅Match: +1 Max HP";
			case CardType.Burrito: return "✅Match: +2 Max HP";
			case CardType.Skunk: return "Can't be matched while adjacent to any non-skunks";
			case CardType.CarePackage: return "✅Match: +$1 for every 2 HP missing";
			case CardType.Cash: return "✅Match: +$3 if both cash are nearby eachother, otherwise +$1";
			case CardType.BirthdayCake: return "✅Match: +1 HP and +$2";
			case CardType.Gem: return "✅Match: +$4";
			case CardType.Pickaxe: return "✅Match: transform all boulders into gems";
			case CardType.Mountain: return "Can't be moved";
			case CardType.DiamondRing: return "✅Match: +$3 if no other cards remain, otherwise +$1";
			case CardType.Rabbit: return "➡️Turn start: 50% chance to move towards its match";
			case CardType.RedEnvelope: return "✅Match: if you have less than $5, set your money to $5";
		}

		return "";
	}

	public static string GetCardExplanation( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ant: return "\"nearby\" means 1 space away\n(including diagonal)";
			case CardType.Snake: return "\"adjacent\" means 1 space away\n(NOT including diagonal)";
		}

		return "";
	}

	public static bool HasHP( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ogre: return true;
			case CardType.Clown: return true;
			case CardType.Police: return true;
			case CardType.Wizard: return true;
			case CardType.King: return true;
		}

		return false;
	}

	public static int GetMaxHP( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ogre: return 2;
			case CardType.Clown: return 2;
			case CardType.Police: return 2;
			case CardType.Wizard: return 3;
			case CardType.King: return 3;
		}

		return 0;
	}

	public void SetRevealedFromAsync(bool revealed)
	{
		IsRevealed = revealed;

		if( Card.HasHP(CardType) || IsLocked )
			IconObj.Enabled = revealed;

		if ( revealed )
		{
			NumTimesRevealed++;

			if(Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown)
				ShowRevealMarks();
		}
	}

	public void SetRevealedInstant()
	{
		IsRevealed = true;

		if ( Card.HasHP( CardType ) || IsLocked )
			IconObj.Enabled = true;

		WorldRotation = Rotation.FromPitch( 0f );

		NumTimesRevealed++;

		if ( Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown )
			ShowRevealMarks();
	}

	public void SetHiddenInstant()
	{
		IsRevealed = false;

		if ( Card.HasHP( CardType ) )
			IconObj.Enabled = false;

		WorldRotation = Rotation.FromPitch( 180f );
	}

	public void Shake( float strength, float time = 0.6f, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
	{
		IsShaking = true;
		_timeSinceShakeStart = 0f;
		_shakeTime = time;
		_shakeEasingType = easingType;
		_shakeStrength = strength;

		if ( playSfx || ( OverrideShakeSfx && ( IsRevealed || !OnlyOverrideShakeSfxWhenRevealed ) )  )
			PlayShakeSfx();
	}

	public virtual void PlayShakeSfx()
	{
		//Manager.Instance.PlayCardSfx( "shake_1", this, volume: 1.1f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
		Manager.Instance.PlayCardSfx( "shake_3", this, volume: 1.35f, pitch: Game.Random.Float( 0.94f, 1.06f ) );
	}

	public virtual bool OverrideShakeSfx => false;
	public virtual bool OnlyOverrideShakeSfxWhenRevealed => false;

	public virtual void PlayHurtSfx(Card card0, Card card1)
	{
		Manager.Instance.PlayCardSfxBetween( "shake_1", card0, card1, volume: 1.3f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
	}

	// return false if needed to change start pos
	public virtual bool ValidateStartingGridPos()
	{
		return true;
	}

	public virtual bool ShouldHandleEvent( EventType eventType )
	{
		return false;
	}

	public virtual async Task HandleEventAsync(EventType eventType)
	{
		await Task.Frame();
	}

	public virtual string GetEventText( EventType eventType )
	{
		return GetCardDescription( CardType );
	}

	public async Task Hurt()
	{
		if(!Card.HasHP(CardType))
		{
			Log.Error( $"Trying to hurt card {CardType} with no HP!" );
			return;
		}

		HP--;
		await Manager.Instance.ShakeCard(this, playSfx: false);
	}

	public void Lock(int numTurns)
	{
		IsLocked = true;
		LockTurnsRemaining = numTurns;
		WasJustLocked = true;
	}

	public void Unlock()
	{
		IsLocked = false;
	}

	public virtual bool CanBeMatched()
	{
		return true;
	}

	public void StartExploding(float explodeTime)
	{
		IsExploding = true;
		_timeSinceStartExploding = 0f;
		_explodeTime = explodeTime;
		_explodeStartPos = LocalPosition;

		int rand = Game.Random.Int( 0, 5 );
		switch(rand)
		{
			case 0: _explodeEasingType = EasingType.SineIn; break;
			case 1: _explodeEasingType = EasingType.SineIn; break;
			case 2: _explodeEasingType = EasingType.QuadIn; break;
			case 3: _explodeEasingType = EasingType.QuadIn; break;
			case 4: _explodeEasingType = EasingType.QuartIn; break;
			case 5: _explodeEasingType = EasingType.Linear; break;
		}

		rand = Game.Random.Int( 0, 7 );
		switch ( rand )
		{
			case 0: _explodeShakeEasingType = EasingType.SineIn; break;
			case 1: _explodeShakeEasingType = EasingType.QuadIn; break;
			case 2: _explodeShakeEasingType = EasingType.QuartIn; break;
			case 3: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 4: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 5: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 6: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 7: _explodeShakeEasingType = EasingType.Linear; break;
		}
	}

	public void Explode( int breakNum )
	{
		var scale = LocalScale.x * Model.LocalScale.x;
		Manager.Instance.SpawnCardBreak( WorldPosition, LocalRotation, scale, ModelRenderer.MaterialOverride, breakNum );
		Manager.Instance.PlayCardSfx( "explosion", this, volume: Game.Random.Float( 0.4f, 0.55f ), pitch: Game.Random.Float( 0.4f, 2f ) );

		Manager.Instance.CardBreakParticlesPrefab.Clone( WorldPosition.WithZ( 25f ) );
		Manager.Instance.CardBreakShockwavePrefab.Clone( WorldPosition.WithZ( 55f ) );

		GameObject.Destroy();
	}

	public void StartScaling(float time, float amount, EasingType easingType)
	{
		IsScaling = true;
		_timeSinceScale = 0f;
		_scaleTime = time;
		_scaleAmount = amount;
		_scaleEasingType = easingType;
	}

	public void ShowFlag()
	{
		IsFlagShown = true;
		IconBackObj.Enabled = true;
	}

	public void HideFlag()
	{
		IsFlagShown = false;

		if ( !AreRevealMarksShown )
			IconBackObj.Enabled = false;
	}

	public void ShowRevealMarks()
	{
		AreRevealMarksShown = true;
		IconBackObj.Enabled = true;
	}
}
using Sandbox;
using System.Net;
using System.Threading.Tasks;

public class CardBank : Card
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		switch ( eventType )
		{
			case EventType.Match:
				return Manager.Instance.ChosenCards[1] == this;
		}

		return false;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 150 );

		var coins = Manager.Instance.Cards.Where( x => x.CardType == CardType.Coin ).ToList();
		if(coins.Count > 0)
		{
			Manager.Instance.PlayCardSfxBetween( "bank", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1f, pitch: Game.Random.Float( 0.9f, 1.1f ) );

			await Task.DelayRealtime( 50 );

			foreach ( var coin in coins )
				coin.SetCardType( CardType.MoneyBag );

			await Task.DelayRealtime( 300 );
		}

		await Task.DelayRealtime( 800 );

		Manager.Instance.PopEventMessage();
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardCockroach : Card
{
	public override bool IsAlive => true;

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Mismatch && Manager.Instance.ChosenCards[0].CardType == CardType.Cockroach && Manager.Instance.ChosenCards[1].CardType == CardType.Cockroach && !IsLocked;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 850 );

		Manager.Instance.PopEventMessage();
	}

	public override bool CanBeMatched()
	{
		foreach(var card in Manager.Instance.Cards)
		{
			if ( card.CardType != CardType.Cockroach )
				return false;
		}

		return true;
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardDancer : Card
{
	public override bool IsAlive => true;

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Mismatch && Manager.Instance.ChosenCards.Contains( this ) && !Manager.Instance.IsMismatchALockedMatch;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		Manager.Instance.PushEventMessage( this, eventType );

		List<Card> neighbourCards = new();
		List<IntVector2> neighboursEmpty = new();
		Card lastPartnerCard = null;

		await Task.DelayRealtime( 300 );

		Manager.Instance.PlayCardSfx( "dancer", this, volume: 1.1f, pitch: Game.Random.Float( 0.98f, 1.02f ) );

		for (int i = 0; i < 4; i++)
		{
			neighbourCards.Clear();
			neighboursEmpty.Clear();

			EvaluateNeighbour( GridPos + new IntVector2( -1, 0 ), neighbourCards, neighboursEmpty );
			EvaluateNeighbour( GridPos + new IntVector2( 1, 0 ), neighbourCards, neighboursEmpty );
			EvaluateNeighbour( GridPos + new IntVector2( 0, -1 ), neighbourCards, neighboursEmpty );
			EvaluateNeighbour( GridPos + new IntVector2( 0, 1 ), neighbourCards, neighboursEmpty );

			IntVector2 targetGridPos;
			Card targetCard = null;

			var validNeighbourCards = neighbourCards.Where( x => x != lastPartnerCard ).ToList();

			if ( validNeighbourCards.Count > 0 )
			{
				validNeighbourCards.Shuffle();
				targetCard = validNeighbourCards.First();
				targetGridPos = targetCard.GridPos;
			}
			else
			{
				neighboursEmpty.Shuffle();
				targetGridPos = neighboursEmpty.First();
			}

			if ( targetCard != null )
			{
				await Manager.Instance.ShakeCard( this );
				await Manager.Instance.ShakeCard( targetCard );

				lastPartnerCard = targetCard;

				await Task.DelayRealtime( 650 );

				if(i < 3)
				{
					await Manager.Instance.SwapCardPositions( this, targetCard );
					await Task.DelayRealtime( 250 );
				}
			}
			else
			{
				lastPartnerCard = null;

				if ( i < 3 )
				{
					Manager.Instance.RemoveCardGridPos( this );
					await Manager.Instance.SetCardGridPos( this, targetGridPos );

					await Task.DelayRealtime( 400 );
				}
			}
		}

		await Task.DelayRealtime( 100 );

		Manager.Instance.PopEventMessage();

		await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
	}

	void EvaluateNeighbour(IntVector2 gridPos, List<Card> neighbourCards, List<IntVector2> empty)
	{
		if ( !Manager.Instance.IsGridPosInBounds( gridPos ) )
			return;

		var card = Manager.Instance.GetCardAtGridPos( gridPos );
		if ( card != null )
			neighbourCards.Add( card );
		else
			empty.Add( gridPos );
	}
}
using Sandbox;
using System.Threading.Tasks;
using System.Xml.Linq;

public class CardDolphin : Card
{
	public override bool IsAlive => true;

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Mismatch && !Manager.Instance.ChosenCards.Contains(this) && Manager.Instance.Stats[StatType.ConsecutiveMismatches] == 3;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 250 );

		Manager.Instance.PlayCardSfx( "card_flip", this, volume: 0.8f, pitch: Game.Random.Float( 1.15f, 1.2f ) );
		await Task.DelayRealtime( 100 );
		Manager.Instance.PlayCardSfx( "dolphin", this, volume: 0.8f, pitch: Game.Random.Float( 1f, 1.15f ) );
		await Manager.Instance.RevealCard( this );

		await Task.DelayRealtime( Game.Random.Int(400, 700) );

		Manager.Instance.HideCard( this );
		Manager.Instance.PlayCardSfx( "card_flip", this, volume: 0.7f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

		Manager.Instance.PopEventMessage();
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardGenie : Card
{
	public override bool IsAlive => true;

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Match && Manager.Instance.ChosenCards[0] == this;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 250 );

		Manager.Instance.PlayCardSfxBetween( "genie", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 0.6f, pitch: Game.Random.Float( 1.25f, 1.26f ) );

		Manager.Instance.AddStatus( "StatusGenie" );

		await Task.DelayRealtime( 1750 );

		Manager.Instance.PopEventMessage();
	}
}

public class StatusGenie : StatusEffect
{
	public float TimerReductionAmount { get; set; }

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Choose;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		var validCards = Manager.Instance.Cards.Where(x => x.CardType == Manager.Instance.ChosenCard.CardType && x != Manager.Instance.ChosenCard).ToList();
		if ( validCards.Count == 0 )
			return;

		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 250 );

		Manager.Instance.PlayCardSfx( "genie_choose", Manager.Instance.ChosenCard, volume: 0.9f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		await Task.DelayRealtime( 750 );

		validCards.Shuffle();

		await Manager.Instance.ShakeCard( validCards.FirstOrDefault() );
		
		await Task.DelayRealtime( 750 );

		Manager.Instance.PopEventMessage();

		Manager.Instance.RemoveStatus( this );
	}

	public override string GetEventIcon( EventType eventType )
	{
		return "textures/genie.png";
	}

	public override string GetEventText( EventType eventType )
	{
		return "Shake matching card";
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardKey : Card
{
	//public override bool ShouldHandleEvent( EventType eventType )
	//{
	//	return eventType == EventType.Mismatch && Manager.Instance.ChosenCards.Contains(this) || eventType == EventType.Match && Manager.Instance.ChosenCards[1] == this;
	//}

	//public override async Task HandleEventAsync( EventType eventType )
	//{
	//	if ( eventType == EventType.Mismatch )
	//	{
	//		var key = Manager.Instance.ChosenCards[0].CardType == CardType.Key ? Manager.Instance.ChosenCards[0] : Manager.Instance.ChosenCards[1];
	//		var otherCard = Manager.Instance.ChosenCards[0].CardType == CardType.Key ? Manager.Instance.ChosenCards[1] : Manager.Instance.ChosenCards[0];

	//		if ( key.IsLocked || !otherCard.IsLocked )
	//			return;

	//		Manager.Instance.PushEventMessage( this, eventType );

	//		await Task.DelayRealtime( 50 );

	//		Manager.Instance.PlayCardSfxBetween( "key", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.5f, pitch: Game.Random.Float( 0.9f, 1.1f ) );

	//		await Task.DelayRealtime( 50 );

	//		await Manager.Instance.UnlockCard( otherCard );

	//		await Task.DelayRealtime( 900 );

	//		Manager.Instance.PopEventMessage();
	//	}
 //       else if (eventType == EventType.Match)
	//	{
	//		Manager.Instance.PushEventMessage( this, eventType );

	//		await Task.DelayRealtime( 50 );

	//		var lockedCards = Manager.Instance.Cards.Where( x => x.IsLocked ).ToList();

	//		if ( lockedCards.Count == 0 )
	//		{
	//			await Task.DelayRealtime( 800 );

	//			Manager.Instance.PopEventMessage();

	//			return;
	//		}

	//		Manager.Instance.PlayCardSfxBetween( "key", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.5f, pitch: Game.Random.Float( 0.9f, 1.1f ) );

	//		await Task.DelayRealtime( 50 );

	//		foreach ( var card in lockedCards )
	//		{
	//			await Manager.Instance.UnlockCard( card );
	//		}

	//		await Task.DelayRealtime( 1000 );

	//		Manager.Instance.PopEventMessage();
	//	}
	//}

	//public override string GetEventText( EventType eventType )
	//{
	//	if ( eventType == EventType.Mismatch )
	//	{
	//		return "❌Mismatch: unlock other card";
	//	}
	//	else
	//	{
	//		return "✅Match: unlock all cards";
	//	}
	//}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicGuideDog : Relic
{
	public override void Init()
	{
		base.Init();

		MaxLevel = 1;
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicMammothMeat : Relic
{
	public override void LevelUp()
	{
		base.LevelUp();

		var mouse = Mouse.Position;
		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( mouse );

		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		Manager.Instance.PlaySfxCenter( "mammoth_meat", volume: 0.75f, pitch: Game.Random.Float( 0.85f, 0.9f ) );

		int maxHpAmount = 8;
		Manager.Instance.SpawnMaxHPFloater( maxHpAmount, tr.EndPosition );// + new Vector3(0f, -30f, 0f) );
		Manager.Instance.MaxHP += maxHpAmount;

		//int hpAmount = 5;
		//Manager.Instance.HP = Math.Min( Manager.Instance.HP + hpAmount, Manager.Instance.MaxHP );
		Manager.Instance.TimeSinceHPChanged = 0f;

		//Manager.Instance.SpawnHealHPFloater( hpAmount, tr.EndPosition );
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicMantlepieceClock : Relic
{
	public override void LevelUp()
	{
		base.LevelUp();

		Manager.Instance.Stats[StatType.NumExtraBountyTurns] += (Level == 1 ? 2f : 1f);
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicMedicalLicense : Relic
{
	public override void Init()
	{
		base.Init();

		MaxLevel = 1;
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.OverhealHP;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		await Task.DelayRealtime( 1000 );

		Manager.Instance.PushEventMessage( this, eventType );

		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );

		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		Manager.Instance.PlaySfx( "medical_license", tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF ), volume: 1.2f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		await Task.DelayRealtime( 350 );

		int moneyAmount = (int)Manager.Instance.Stats[StatType.LatestOverhealAmount] + (int)Manager.Instance.Stats[StatType.EarnExtraMoney];

		Manager.Instance.SpawnGainMoneyFloater( moneyAmount, tr.EndPosition );

		await Manager.Instance.GainMoney( moneyAmount );

		await Task.DelayRealtime( 650 );

		Manager.Instance.PopEventMessage();
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicMouseTrap : Relic
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.ClaimBounty;
	}

	public override async Task HandleEventAsync(EventType eventType)
	{
		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 100 );

		Manager.Instance.PlaySfxCenter( "mouse_trap", volume: 0.8f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		await Task.DelayRealtime( 400 );

		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		int maxHP = Level;
		Manager.Instance.SpawnMaxHPFloater( maxHP, tr.EndPosition );
		Manager.Instance.MaxHP += maxHP;

		Manager.Instance.TimeSinceHPChanged = 0f;

		await Task.DelayRealtime( 500 );
		
		Manager.Instance.PopEventMessage();
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicSacrificialBlade : Relic
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.HurtCards;
	}

	public override async Task HandleEventAsync(EventType eventType)
	{
		Manager.Instance.PushEventMessage( this, eventType );

		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		Manager.Instance.PlaySfx( "sacrificial_blade", tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF ), volume: 0.75f, pitch: Game.Random.Float( 1.6f, 1.65f ) );

		await Task.DelayRealtime( 450 );

		int healAmount = Level;

		Manager.Instance.SpawnHealHPFloater( healAmount, tr.EndPosition );

		await Task.DelayRealtime( 350 );

		await Manager.Instance.GainHP( healAmount );

		await Task.DelayRealtime( 250 );

		Manager.Instance.PopEventMessage();
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

public class RelicSign : Relic
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Mismatch && Game.Random.Float(0f, 1f) < Level * 0.25f;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		List<Card> validCards = new();
		foreach ( var card in Manager.Instance.Cards )
		{
			if ( card.NumTimesRevealed > 0 && !Manager.Instance.ChosenCards.Contains(card) && !card.IsRevealed )
				validCards.Add( card );
		}

		if ( validCards.Count == 0 )
			return;

		Manager.Instance.PushEventMessage( this, eventType );

		// todo: sfx

		await Task.DelayRealtime( 250 );

		validCards.Shuffle();

		var cardToReveal = validCards.First();

		Manager.Instance.PlayCardSfx( "card_flip", cardToReveal, volume: 0.8f, pitch: Game.Random.Float( 1.15f, 1.2f ) );
		await Manager.Instance.RevealCard( cardToReveal );

		await Task.DelayRealtime( 750 );

		Manager.Instance.HideCard( cardToReveal );
		Manager.Instance.PlayCardSfx( "card_flip", cardToReveal, volume: 0.7f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

		await Task.DelayRealtime( 300 );

		Manager.Instance.PopEventMessage();
	}
}

using Sandbox.Diagnostics;

public sealed class PlayerInventory : Component, IPlayerEvent, ILocalPlayerEvent
{
	[RequireComponent] public Player Player { get; set; }


	public List<BaseWeapon> Weapons => Scene.Components.GetAll<BaseWeapon>( FindMode.EverythingInSelfAndDescendants ).Where( x => x.Network.OwnerId == Network.OwnerId ).OrderBy( x => x.InventorySlot ).ThenBy( x => x.InventoryOrder ).ToList();

	public BaseWeapon ActiveWeapon { get; private set; }

	public void GiveDefaultWeapons()
	{
		Pickup( "weapons/hands.prefab" );
		Pickup( "weapons/camera.prefab" );
	}

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

		var weapon = prefab.Components.Get<BaseWeapon>( true );
		Assert.NotNull( weapon );

		IPlayerEvent.PostToGameObject( Player.GameObject, e => e.OnWeaponAdded( weapon ) );
		ILocalPlayerEvent.Post( e => e.OnWeaponAdded( weapon ) );
	}

	protected override void OnUpdate()
	{
		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.OnPlayerUpdate( Player );
		}
	}

	public void SwitchWeapon( BaseWeapon weapon )
	{
		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.GameObject.Enabled = false;
		}

		ActiveWeapon = weapon;

		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.GameObject.Enabled = true;
		}
	}

	void IPlayerEvent.OnSpawned()
	{
		GiveDefaultWeapons();
	}

	void ILocalPlayerEvent.OnCameraMove( ref Angles angles )
	{
		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.OnCameraMove( Player, ref angles );
		}
	}

	void ILocalPlayerEvent.OnCameraPostSetup( Sandbox.CameraComponent camera )
	{
		if ( ActiveWeapon.IsValid() )
		{
			ActiveWeapon.OnCameraSetup( Player, camera );
		}
	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>
	
    <div class="table">

        <div class="header row">
            <div class="name">Name</div>
            <div class="stat">⏲️</div>
        </div>

        @foreach ( var entry in Connection.All )
        {
            string specialClass = "";
            if (entry == Connection.Local) specialClass = "me";

            <div class="row @specialClass">
                <div class="name">

                    @if ( entry.IsHost )
                    {
                        <div>👑</div>
                    }

                    @entry.DisplayName


                </div>
                <div class="stat">@GetTime( entry )</div>
            </div>
        }

    </div>

</root>

@code
{

    string GetTime( Connection c )
    {
        TimeSpan time = DateTime.UtcNow - c.ConnectionTime;

        if (time.TotalMinutes < 60)
            return time.ToString("mm\\m\\ s\\s");

        return time.ToString("hh\\h\\ \\m\\m");
    }

    protected override void OnUpdate()
    {
        SetClass( "hidden", !Input.Down( "score" )  );
    }

    /// <summary>
    /// update every second
    /// </summary>
    protected override int BuildHash() => System.HashCode.Combine( RealTime.Now.CeilToInt() );
}
using Sandbox;

public sealed class SoundEmitter : Component
{
	[Property] public string SoundString { get; set; }

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

		Sound.Play( SoundString, Transform.Position );
	}
	protected override void OnUpdate()
	{

	}
}
using Sandbox;
using Sandbox.Citizen;

public sealed class MapOverrider : Component
{
	[Property] GameObject FrogPrefab { get; set; }
	protected override void OnEnabled()
	{
		base.OnEnabled();
		/*
		{
			var obj = Scene.GetAllComponents<SkinnedModelRenderer>();

			foreach ( var item in obj )
			{
				if ( item.Model.ResourceName == "frog_test_subject_01a" )
					item.Set( "sit", 1 );
					item.Set( "sit_pose", Random.Shared.Float( 3 ) );
					item.Set( "scale_height", Random.Shared.Float( 0.75f, 1.5f ) );
					var ran = Random.Shared.Int( 0, 1 );
					item.MaterialGroup = ran == 1 ? "default" : "Orange";
			}
		}
		*/
		{
			var npc = Scene.GetAllObjects( true ).Where( x => x.Name == "js_npc_text" ).ToList();

			foreach ( var item in npc )
			{
				item.DestroyImmediate();
				Log.Info( "Destroyed" );
			}
		}
	}

	protected override void OnUpdate()
	{

	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@implements Component.INetworkListener

<root>

	<div class="output">
		@foreach (var entry in Entries)
		{
            <div class="chat_entry">
				<div class="author">@entry.author</div>
				<div class="message">@entry.message</div>
			</div>
		}
	</div>

	<div class="input">
		<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
	</div>
	
</root>

@code
{
    TextEntry InputBox;

    public record Entry( ulong steamid, string author, string message, RealTimeSince timeSinceAdded );
    List<Entry> Entries = new();

    protected override void OnUpdate()
    {
        if (InputBox is null)
            return;

        Panel.AcceptsFocus = false;

        if ( Input.Pressed( "chat" ) )
        {
            InputBox.Focus();
        }


        if ( Entries.RemoveAll( x => x.timeSinceAdded > 20.0f ) > 0 )
        {
            StateHasChanged();
        }

        SetClass( "open", InputBox.HasFocus );
    }

    void ChatFinished()
    {
        var text = InputBox.Text;
        InputBox.Text = "";

        if (string.IsNullOrWhiteSpace(text))
            return;

        AddText( text );
    }

    [Broadcast]
    public void AddText( string message )
    {
        message = message.Truncate( 300 );

        if (string.IsNullOrWhiteSpace(message))
            return;

        var author = Rpc.Caller.DisplayName;
        var steamid = Rpc.Caller.SteamId;

		Log.Info($"{author}: {message}");

        Entries.Add(new Entry(steamid, author, message, 0.0f));
		StateHasChanged();
	}

    [Broadcast] // todo: only from host/owner
    public void AddSystemText(string message)
    {
        message = message.Truncate(300);

        if (string.IsNullOrWhiteSpace(message))
            return;

        Entries.Add(new Entry(0, "ℹ️", message, 0.0f));
        StateHasChanged();
    }

	void Component.INetworkListener.OnConnected( Connection channel )
	{
		if ( IsProxy ) return;

		AddSystemText( $"{channel.DisplayName} has joined the game" );
	}

	void Component.INetworkListener.OnDisconnected( Connection channel )
	{
		if ( IsProxy ) return;

		AddSystemText( $"{channel.DisplayName} has left the game" );
	}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@if (Network.OwnerConnection is Connection owner && owner == Connection.Local)
{
    <root class=@(IsOpen ? "open" : "closed")>
        <div class="bar">

            <label class="heightbar"></label>
            <label class="flag"></label>

            <div class="heightbarpos" style="height:@(CalculateProgress(Height) * 100)%;">
                <label class="heighttext" text="< @(Height)" />
            </div>

            <div class="maxheightbarpos" style="height:@(CalculateProgress(MaxHeight) * 100)%;">
                <label class="maxheighttext" text="@(MaxHeight)>" />
            </div>

            @foreach (var player in Scene.GetAllComponents<JumperPlayerStuff>())
            {
                if (owner == player.Network.OwnerConnection) continue;
                <div class="maxheightotherbarpos" style="border-top: 4px solid @player.rndColor; height:@(CalculateProgress(player.Height) * 100)%;">
                    <img class="maxheightothertext" style=" box-shadow: 3px 3px @player.rndColor; background-image: url( avatar:@player.Network.OwnerConnection.SteamId )">
                </div>
            }
        </div>
    </root>
}

@code
{
    float TotalHeight { get; set; }
    float Height { get; set; }
    float MaxHeight { get; set; }
    bool IsOpen { get; set; }

    // Find first player stats class that isn't owned by a proxy (is ours)
    JumperPlayerStuff PlayerStats => GameObject.Components.Get<JumperPlayerStuff>(FindMode.InParent);
    JumperDistanceRuler Ruler { get; set; }

    public Color32 rndColor = Color.Random;
    public string hexColor;

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

        if (IsProxy) return;

        Height = PlayerStats.Height;
        MaxHeight = PlayerStats.MaxHeight;

        TotalHeight = Scene.GetAllComponents<JumperDistanceRuler>().FirstOrDefault().Distance;
    }

    private float CalculateProgress(float height)
    {
        return height.LerpInverse(0, Scene.GetAllComponents<JumperDistanceRuler>().FirstOrDefault().Distance);
    }
    protected override int BuildHash()
    {
        IsOpen ^= Input.Pressed("slot1");

        return HashCode.Combine(Time.Delta);
    }
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root class=@(Visible ? "visible" : "")>
    <label class="message">@OutputText</label>
    <label class="name">@NPCName</label>
    <label class="background"></label>
</root>

@code {
    public string Message { get; set; } = "";

    private bool Visible => TimeSinceDisplayed < 4;
    private RealTimeSince TimeSinceDisplayed = 999;

    public string OutputText { get; set; }
    public float Delay { get; set; } = .1f;
    public string NPCName { get; set; } = "Ben";
    public string Voice { get; set; } = "beep1";

    protected override void OnEnabled()
    {
        base.OnEnabled();
        TimeSinceDisplayed = 999;
    }

    public static bool IsTalking()
    {
        return true;
    }

    private async Task RevealTextAsync(string message)
    {
        Random rand = new Random();
        foreach (char c in message)
        {
            IsTalking();
            OutputText += c;
            TimeSinceDisplayed = 0f;
            await Task.DelaySeconds((float)GetRandomNumber(0.05f, 0.2f));
            var snd = Sound.Play(Voice);
            snd.Pitch = (float)GetRandomNumber(0.9f, 1.1f);
            snd.Volume = 0.25f;
        }
    }

    static Random random = new Random();
    public double GetRandomNumber(double minimum, double maximum)
    {
        return random.NextDouble() * (maximum - minimum) + minimum;
    }

    public void DisplayMessage(string message)
    {
        Message = message;
        OutputText = null;
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();
        if (OutputText == Message)
        {
            return;
        }

        if (OutputText == null)
        {
            RevealTextAsync(Message);
        }
        Message = OutputText;
    }

    protected override int BuildHash()
    {
        return HashCode.Combine(OutputText, Visible ? 1 : 0);
    }

}
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 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.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("");
}