561 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 System.Numerics;
using Sandbox;

public sealed class ExplosionKiller : Component
{
	/// <summary>
	/// When a player leaves this area, kill them
	/// </summary>
	[Property]
	public float KillRange { get; set; } = 1000f;

	[Property]
	public GameObject Explosion;

	[Property]
	public SoundEvent ExplosionSound;

	TimeSince lastCheck;

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

		lastCheck = 0;
	}

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

		if (lastCheck > 1)
		{
			var players = Scene.GetAll<PlayerController>().ToArray();
			Game.Random.Shuffle( players );
			foreach (var player in players)
			{
				if (player.WorldPosition.Distance(Vector3.Zero) > KillRange && player.GetComponent<Health>().IsAlive)
				{
					var dtag = new TagSet();
					dtag.Add( "explosion" );
					player.GetComponent<IDamageable>().OnDamage( new DamageInfo()
					{
						Damage = 1000,
						Tags = dtag,
					} );

					Explosion.Clone(player.WorldPosition);

					Sound.Play( ExplosionSound, player.WorldPosition );

					lastCheck = 0.2f;
					return;
				}
			}

			lastCheck = 0;
		}
	}

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

		Gizmo.Draw.Color = Color.Red;
		Gizmo.Draw.LineCircle( Vector3.Zero, Vector3.Up, Vector3.Forward, KillRange, 0, 360, 64 );
	}
}
using System;
using static System.Net.Mime.MediaTypeNames;

namespace CryptidHunt;

public partial class DiscoverableArea : Component, Component.ITriggerListener
{
	[Property]
	public string AreaName { get; set; } = "World";
	public bool Activated { get; set; } = false;

	public DiscoverableArea() { }

	public void OnTriggerEnter( Collider other )
	{
		if ( !Active || Activated ) return;
		if ( !other.GameObject.Parent.Components.TryGet<Player>( out var player, FindMode.EnabledInSelf ) ) return;

		Activated = true;

		GameUI.OpenZoneHint( AreaName );
	}
}
namespace CryptidHunt;

public enum PolewikState
{
	Idle,
	Patrolling,
	Stalking,
	Following,
	Attacking,
	Fleeing,
	Pain,
	Yell,
	AttackPersistent,
	Jumpscare
}

public partial class Polewik : Component
{
	[Property]
	public SkinnedModelRenderer ModelRenderer { get; set; }

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

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

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

	public TimeSince LastDamage { get; set; } = 10f;

	public bool Alive { get; set; } = true;

	public float JumpscareDistance => 120f;
	public float DetectDistance => 1400f;
	public float StalkingDistance => 700f;
	public float AttackDistance => 500f;
	public float GiveUpDistance => 3600f;
	public float GiveUpAfter => 25f;
	public float AttackAfterStalking => 15f;
	public float AttackAfterStalling => 90f;
	public float WaitUntilNextAttack => 30f;

	[Property]
	public SoundEvent HeartbeatSound { get; set; }
	public SoundHandle Heartbeart { get; set; }

	public Vector3 StuckPosition;
	public TimeSince LastStuck;
	private bool _firstYell = true;

	public Vector3? FirstInterceptPoint()
	{
		var posA = Player.Instance.WorldPosition;
		var velA = Player.Instance.Controller.Velocity.WithZ( 0f );
		var posB = WorldPosition;
		var speedB = CurrentSpeed;

		var toTarget = posA - posB;
		var a = Vector3.Dot( velA, velA ) - speedB * speedB;
		var b = 2f * Vector3.Dot( velA, toTarget );
		var c = Vector3.Dot( toTarget, toTarget );

		// Handle near-zero 'a' (velA magnitude ≈ speedB) as linear:
		if ( MathF.Abs( a ) < 1e-6f )
		{
			// bt + c = 0  →  t = -c/b
			if ( MathF.Abs( b ) < 1e-6f ) return null;
			var tLin = -c / b;
			if ( tLin <= 0f ) return null;
			return posA + velA * tLin;
		}

		var disc = b * b - 4f * a * c;
		if ( disc < 0f ) return null;

		var sqrtDisc = MathF.Sqrt( disc );
		var t1 = (-b + sqrtDisc) / (2f * a);
		var t2 = (-b - sqrtDisc) / (2f * a);

		float t;
		if ( t1 > 0f && t2 > 0f )
			t = MathF.Min( t1, t2 );
		else if ( t1 > 0f )
			t = t1;
		else if ( t2 > 0f )
			t = t2;
		else
			return null;

		return posA + velA * t;
	}

	PolewikState currentState { get; set; } = PolewikState.Patrolling;

	public PolewikState CurrentState
	{
		get => currentState;
		set
		{
			currentState = value;

			if ( value == PolewikState.Patrolling )
			{
				NavigateTo( NearestNode.WorldPosition );
				CurrentPathId = PatrolPath.IndexOf( NearestNode );
				Heartbeart?.Stop();
			}

			if ( value == PolewikState.Pain )
			{
				Heartbeart?.Stop();
				Agent.Stop();
				Agent.Velocity = 0f;
				_lastAttack = 0f;

				Sound.Play( "pain", WorldPosition );

				Task.RunInThreadAsync( async () =>
				{
					await Task.MainThread();
					ModelRenderer.Set( "growl", true );
					await Task.DelaySeconds( 0.1f );
					ModelRenderer.Set( "growl", false );
					await Task.DelaySeconds( 0.9f );
					CurrentState = PolewikState.Fleeing;
				} );

			}

			if ( value == PolewikState.Yell )
			{
				Heartbeart?.Stop();
				GameTask.RunInThreadAsync( async () =>
				{
					await Task.MainThread();
					await Task.DelaySeconds( 0.5f );

					Player.Instance.AddCameraShake( 4f, 5f );

					await Task.DelaySeconds( 1f );

					ModelRenderer.Set( "howl", true );
					var howl = Sound.Play( "howl_far", WorldPosition );
					howl.Volume *= _firstYell ? 1f : MathX.Remap( Player.Instance.WorldPosition.Distance( WorldPosition ), 500f, 3000f, 0.8f, 0.2f );
					_firstYell = false;
					await Task.DelaySeconds( 4.5f );
					CurrentState = PolewikState.AttackPersistent;
				} );
			}

			if ( value == PolewikState.Fleeing )
			{
				Heartbeart?.Stop();
				_lastAttack = 0f;

				TargetPosition = FurthestNode.WorldPosition;
				NavigateTo( FurthestNode.WorldPosition );
				CurrentPathId = PatrolPath.IndexOf( FurthestNode );

				GameTask.RunInThreadAsync( async () =>
				{
					await Task.MainThread();
					await GameTask.DelaySeconds( Game.Random.Float( 3f, 6f ) );
					CurrentState = PolewikState.Patrolling;
				} );
			}

			if ( value == PolewikState.Stalking )
			{
				_startedStalking = 0f;
				Heartbeart ??= Sound.Play( HeartbeatSound );
			}

			if ( value == PolewikState.Following || value == PolewikState.AttackPersistent )
			{
				Heartbeart ??= Sound.Play( HeartbeatSound );
				_startedFollowing = 0f;
			}

			if ( value == PolewikState.Attacking )
			{
				_lastAttack = 0f;
				Heartbeart ??= Sound.Play( HeartbeatSound );
				ModelRenderer.Set( "leap", true );

				Sound.Play( "jump", WorldPosition );

				GameTask.RunInThreadAsync( async () =>
				{
					await Task.MainThread();
					await GameTask.DelaySeconds( 0.05f );
					ModelRenderer.Set( "leap", false );
					await GameTask.DelaySeconds( 0.9f );

					if ( CurrentState == PolewikState.Attacking )
						CurrentState = PolewikState.Following;
				} );
			}

			if ( value == PolewikState.Jumpscare )
			{
				Heartbeart?.Stop();
				_lastAttack = 0f;

				Agent.Stop();
				Agent.Velocity = 0f;
				WorldRotation = Rotation.LookAt( Vector3.Direction( WorldPosition, Player.Instance.WorldPosition ) );

				Player.Instance.LockInputs = true;

				ModelRenderer.Set( "attack", true );
				var mouth = ModelRenderer.GetAttachmentObject( "mouth" );
				var spit = SpitPrefab.Clone( mouth.WorldPosition, mouth.WorldRotation );
				spit.SetParent( mouth );

				Sound.Play( "jumpscare", WorldPosition );

				Player.Instance.AddCameraShake( 1.6f, 15f );

				GameTask.RunInThreadAsync( async () =>
				{
					await Task.MainThread();
					await GameTask.DelaySeconds( 1.2f );

					Player.Instance.ChangeHolding(null, true);
					Player.Instance.HP -= 1;

					await GameTask.DelaySeconds( 1f );

					if ( CurrentState == PolewikState.Jumpscare )
						CurrentState = PolewikState.Fleeing;

					Player.Instance.LockInputs = false;
					// TODO: Make player camera follow the camera attachment
				} );
			}
		}
	}

	[Property]
	public List<GameObject> PatrolPath { get; set; } = new List<GameObject>();
	public int CurrentPathId { get; set; } = 0;
	public Vector3 TargetPosition { get; set; }

	[Property]
	public NavMeshAgent Agent { get; set; }

	public Dictionary<PolewikState, float> Speeds = new()
	{
		{ PolewikState.Idle, 0f },
		{ PolewikState.Patrolling, 350f },
		{ PolewikState.Stalking, 200f },
		{ PolewikState.Following, 420f },
		{ PolewikState.Attacking, 1800f },
		{ PolewikState.Fleeing, 700f },
		{ PolewikState.Pain, 0f },
		{ PolewikState.Yell, 0f },
		{ PolewikState.AttackPersistent, 410f },
		{ PolewikState.Jumpscare, 0f }

	};
	public float CurrentSpeed => Speeds[CurrentState];

	private float _hp { get; set; } = 100f;

	public float HP
	{
		get => _hp;
		set
		{
			if ( !Alive ) return;

			var damage = _hp - value;
			_hp = value;

			LastDamage = 0f;

			if ( HP <= 0 )
			{
				RagdollModel();
				Sound.Play( "pain", WorldPosition );
				Alive = false;
			}
			else
			{
				if ( damage >= 10f && CurrentState != PolewikState.Pain && CurrentState != PolewikState.Fleeing )
					CurrentState = PolewikState.Pain;

				if ( damage > 0f )
					BloodParticle.Clone( WorldPosition + Vector3.Up * 30f, WorldRotation );
			}
		}
	}

	private BBox _towerZone = BBox.FromPoints( new List<Vector3>() { new Vector3( 2868f, 5616f, 612f ), new Vector3( 2436f, 5932f, 132f ) } ); // too lazy...

	TimeSince _startedStalking;
	TimeSince _startedFollowing;
	TimeSince _lastAttack;
	public GameObject ClosestNodeTo( Vector3 pos ) => PatrolPath.OrderBy( x => x.WorldPosition.Distance( pos ) ).FirstOrDefault();
	public GameObject NearestNode => ClosestNodeTo( WorldPosition );
	public GameObject FurthestNode => PatrolPath.OrderBy( x => x.WorldPosition.Distance( WorldPosition ) ).LastOrDefault();
	public bool WithinAttackRange => !_towerZone.Contains( Player.Instance.WorldPosition ) && Player.Instance.WorldPosition.Distance( WorldPosition ) <= AttackDistance && Math.Abs( Player.Instance.WorldPosition.z - WorldPosition.z ) <= 200f;
	public bool OutsideDistance => _towerZone.Contains( Player.Instance.WorldPosition ) || Player.Instance.WorldPosition.Distance( WorldPosition ) >= GiveUpDistance || Math.Abs( Player.Instance.WorldPosition.z - WorldPosition.z ) > 200f;
	public bool OutsidePersistentDistance => Player.Instance.WorldPosition.Distance( WorldPosition ) >= GiveUpDistance * 4f || Math.Abs( Player.Instance.WorldPosition.z - WorldPosition.z ) > 200f;

	protected override void OnStart()
	{
		if ( ModelRenderer.IsValid() )
			ModelRenderer.OnFootstepEvent += OnFootstepEvent;

		_startedStalking = 0f;
		_startedFollowing = 0f;
		_lastAttack = 0f;
	}

	protected override void OnFixedUpdate()
	{
		if ( HP <= 0f ) return;

		ComputeAnimation();
		Agent.MaxSpeed = CurrentSpeed;

		/*
		DebugOverlay.Sphere( Position, JumpscareDistance, Color.Red, 0f, false );
		DebugOverlay.Sphere( Position, DetectDistance, Color.Green );
		DebugOverlay.Sphere( Position, StalkingDistance, Color.Yellow );
		DebugOverlay.Sphere( Position, AttackDistance, Color.Orange );
		DebugOverlay.Sphere( Position, GiveUpDistance, Color.Blue );*/

		if ( CurrentState != PolewikState.Idle && CurrentState != PolewikState.Pain && CurrentState != PolewikState.Jumpscare )
		{
			ComputePath();

			if ( StuckPosition.Distance( WorldPosition ) > 100f )
			{
				StuckPosition = WorldPosition;
				LastStuck = 0f;
			}

			if ( LastStuck >= 5f )
			{
				LastStuck = 0f;
				WorldPosition = TargetPosition;
			}
		}

		if ( CurrentState == PolewikState.Patrolling )
		{
			NavigateTo( TargetPosition );

			if ( _lastAttack >= WaitUntilNextAttack && Player.Instance.WorldPosition.Distance( WorldPosition ) <= DetectDistance )
			{
				_lastAttack = 0f;
				CurrentState = PolewikState.Stalking;
			}

			if ( _lastAttack >= AttackAfterStalling )
			{
				_lastAttack = 0f;
				CurrentState = PolewikState.Yell;
			}

			Agent.UpdateRotation = true;
		}

		if ( CurrentState == PolewikState.Stalking )
		{
			NavigateTo( ClosestNodeTo( Player.Instance.WorldPosition ).WorldPosition );
			Agent.UpdateRotation = false;
			WorldRotation = Rotation.LookAt( Vector3.Direction( WorldPosition, Player.Instance.WorldPosition ) );

			if ( Player.Instance.WorldPosition.Distance( WorldPosition ) <= StalkingDistance || _startedStalking >= AttackAfterStalking )
			{
				Sound.Play( "scream_scare", WorldPosition );
				CurrentState = PolewikState.Following;
			}

			if ( OutsideDistance )
				CurrentState = PolewikState.Fleeing;

			var lookingTrace = Scene.Trace.Sphere( 200f, Player.Instance.Camera.WorldPosition, Player.Instance.Camera.WorldPosition + Player.Instance.Camera.WorldRotation.Forward * 2000f )
				.WithTag( "Polewik" )
				.IgnoreStatic()
				.IgnoreGameObjectHierarchy( Player.Instance.GameObject )
				.Run();

			if ( lookingTrace.Hit && lookingTrace.GameObject == GameObject )
				CurrentState = PolewikState.Following;
		}

		if ( CurrentState == PolewikState.Following )
		{
			var intercept = FirstInterceptPoint();
			if ( intercept != null )
				NavigateTo( intercept.Value );

			if ( _startedFollowing >= GiveUpAfter )
				CurrentState = PolewikState.Patrolling;

			if ( WithinAttackRange )
				CurrentState = PolewikState.Attacking;

			if ( OutsideDistance )
				CurrentState = PolewikState.Fleeing;
		}

		if ( CurrentState == PolewikState.AttackPersistent )
		{
			var intercept = FirstInterceptPoint();
			if ( intercept != null )
				NavigateTo( intercept.Value );

			if ( _startedFollowing >= GiveUpAfter * 2f )
				CurrentState = PolewikState.Patrolling;

			if ( WithinAttackRange )
				CurrentState = PolewikState.Attacking;

			if ( OutsidePersistentDistance )
				CurrentState = PolewikState.Fleeing;
		}

		if ( CurrentState == PolewikState.Attacking )
		{
			var intercept = FirstInterceptPoint();
			if ( intercept != null )
				NavigateTo( intercept.Value );

			if ( Player.Instance.WorldPosition.Distance( WorldPosition ) <= 100f )
				CurrentState = PolewikState.Jumpscare;

			if ( OutsideDistance )
				CurrentState = PolewikState.Fleeing;
		}

		if ( CurrentState == PolewikState.Fleeing && PatrolPath != null )
		{
			NavigateTo( TargetPosition );
		}
	}

	protected override void OnUpdate()
	{
		if ( CurrentState == PolewikState.Jumpscare )
		{
			Player.Instance.CameraPosition = WorldTransform.PointToWorld( new Vector3( 90f, 0f, 70f ) );
			Player.Instance.CameraRotation = Rotation.LookAt( Vector3.Direction( Player.Instance.Camera.WorldPosition, Camera.WorldPosition ), Vector3.Up );
		}
	}

	public void OnFootstepEvent( SceneModel.FootstepEvent footstepEvent )
	{
		var footTrace = Scene.Trace.Ray( WorldPosition, WorldPosition + Vector3.Down * 10f )
			.Radius( 2f )
			.IgnoreDynamic()
			.IgnoreGameObjectHierarchy( GameObject )
			.Run();

		if ( !footTrace.Hit ) return;

		var tag = footTrace.Tags
			.Where( x => x != "solid" && x != "world" )
			.FirstOrDefault();

		var sound = tag switch
		{
			"metal" => "footstep-metal",
			"grass" => "footstep-grass",
			"dirt" => "footstep-dirt",
			_ => "footstep-concrete"
		};

		Sound.Play( sound, footTrace.EndPosition ).Volume *= Agent.Velocity.WithZ( 0f ).Length / 7f;
	}

	public virtual void ComputeAnimation()
	{
		ModelRenderer.Set( "speed", Agent.Velocity.Length / 3 );

		if ( CurrentState == PolewikState.Following ||
			CurrentState == PolewikState.Stalking ||
			CurrentState == PolewikState.Attacking ||
			CurrentState == PolewikState.AttackPersistent ||
			CurrentState == PolewikState.Jumpscare )
		{

			var local = WorldTransform.PointToLocal( Player.Instance.WorldPosition );
			ModelRenderer.Set( "lookat", local.WithX( Math.Max( local.x, 0 ) ) + Vector3.Forward * 300f );
		}
	}

	public virtual bool NavigateTo( Vector3 pos )
	{
		Agent.MoveTo( pos );
		return true;
	}

	public virtual void ComputePath()
	{
		if ( PatrolPath == null ) return;

		TargetPosition = PatrolPath[CurrentPathId].WorldPosition;

		if ( WorldPosition.Distance( TargetPosition ) < MathF.Max( Agent.Velocity.Length, 100f ) * 20f * Time.Delta )
			CurrentPathId = (CurrentPathId + 1) % PatrolPath.Count;
	}

	public async void RagdollModel()
	{
		if ( !ModelRenderer.IsValid() ) return;

		var ragdoll = ModelRenderer.GameObject.AddComponent<ModelPhysics>();
		ragdoll.Renderer = ModelRenderer;
		ragdoll.Model = ModelRenderer.Model;

		Agent.Velocity = Vector3.Zero;
		Agent.Enabled = false;

		await Task.DelayRealtimeSeconds( 5f );
		GameUI.BlackScreen();
		await Task.DelayRealtimeSeconds( 2.5f );

		GameManager.Instance.EndGame();
	}
}
@using Sandbox;
@using Sandbox.UI;

@namespace CryptidHunt

<root>
    <div class="sliderFull" style="width: @((Fraction*100))%"></div>
</root>

@code
{
    [Property]
    public ComputerScreen Screen { get; set; }
    public float Fraction { get; set; } = 0.5f;
    public bool IsDragging { get; set; } = false;

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

        if (IsDragging)
            Fraction = MathX.Clamp( MousePosition.x / (Box.Right - Box.Left), 0f, 1f );

        DarknessSlider.Opacity = Fraction;
    }

    protected override void OnMouseDown(MousePanelEvent e)
    {
        base.OnMouseDown(e);
        IsDragging = true;
    }

    protected override void OnMouseUp(MousePanelEvent e)
    {
        base.OnMouseUp(e);
        IsDragging = false;
    }



    /// <summary>
    /// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
    /// </summary>
    protected override int BuildHash() => System.HashCode.Combine( Time.Now );
}
using System.IO;
using System.Text;
using Braxnet;
using Clover.Items;
using Clover.Persistence;
using Clover.Player;
using Clover.Ui;
using Clover.Utilities;

namespace Clover.Carriable;

[Category( "Clover/Carriable" )]
public class Paintbrush : BaseCarriable
{
	[Property] public SoundEvent PaintSound { get; set; }
	[Property] public SoundEvent TextureChangeSound { get; set; }

	public string CurrentTextureName { get; set; }
	public string CurrentTexturePath => $"decals/{CurrentTextureName}.decal";

	public override void OnUseDown()
	{
		var itemColliders = Player.PlayerInteract.InteractCollider.Touching;
		foreach ( var itemCollider in itemColliders )
		{
			if ( itemCollider.GameObject.Components.TryGet<PictureFrame>( out var pictureFrame ) )
			{
				if ( string.IsNullOrWhiteSpace( CurrentTexturePath ) )
				{
					Player.Notify( Notifications.NotificationType.Error, "No texture selected" );
					return;
				}

				pictureFrame.TexturePath = CurrentTexturePath;

				SoundEx.Play( PaintSound, Player.WorldPosition );
				ParticleManager.PoofAt( pictureFrame.WorldPosition );

				return;
			}

			if ( itemCollider.GameObject.Components.TryGet<Pumpkin>( out var pumpkin ) )
			{
				if ( string.IsNullOrWhiteSpace( CurrentTexturePath ) )
				{
					Player.Notify( Notifications.NotificationType.Error, "No texture selected" );
					return;
				}

				pumpkin.TexturePath = CurrentTexturePath;

				SoundEx.Play( PaintSound, Player.WorldPosition );
				ParticleManager.PoofAt( pumpkin.WorldPosition );

				return;
			}

			if ( itemCollider.GameObject.Components.TryGet<SnowmanPiece>( out var snowmanPiece ) )
			{
				if ( string.IsNullOrWhiteSpace( CurrentTexturePath ) )
				{
					Player.Notify( Notifications.NotificationType.Error, "No texture selected" );
					return;
				}

				snowmanPiece.TexturePath = CurrentTexturePath;

				SoundEx.Play( PaintSound, Player.WorldPosition );
				ParticleManager.PoofAt( snowmanPiece.WorldPosition );

				return;
			}
		}

		var pos = Player.GetAimingGridPosition();

		if ( pos == Vector2Int.Zero )
		{
			Log.Error( "Invalid position" );
			return;
		}

		var item = Player.World.GetItem<FloorDecal>( pos, 16f );

		if ( item != null )
		{
			if ( string.IsNullOrWhiteSpace( CurrentTexturePath ) || CurrentTexturePath == item.TexturePath )
			{
				Log.Info( $"Removing decal {item.TexturePath}" );
				item.WorldItem.RemoveFromWorld();
			}
			else
			{
				Log.Info( $"Updating decal {item.TexturePath} -> {CurrentTexturePath}" );
				item.TexturePath = CurrentTexturePath;
				item.UpdateDecal();
			}

			SoundEx.Play( PaintSound, Player.WorldPosition );
			ParticleManager.PoofAt( item.WorldPosition );
		}
		else
		{
			if ( string.IsNullOrWhiteSpace( CurrentTexturePath ) )
			{
				Player.Notify( Notifications.NotificationType.Error, "No texture selected" );
				return;
			}

			var playerRotation = World.GetItemRotationFromDirection(
				World.Get4Direction( Player.PlayerController.Yaw ) );

			Log.Info( $"Spawning decal at {pos} with texture {CurrentTexturePath}" );

			/*var newItem = PersistentItem.Create<Persistence.FloorDecal>( Data.ItemData.GetById( "floor_decal" ) );
			if ( newItem == null ) throw new System.Exception( "Failed to create floor decal" );

			newItem.TexturePath = CurrentTexturePath;

			var node = World.SpawnPersistentNode( newItem, pos, playerRotation, World.ItemPlacement.FloorDecal, false );*/

			var newPItem = PersistentItem.Create( Data.ItemData.Get( "floor_decal" ) );

			newPItem.SetSaveData( "TexturePath", CurrentTexturePath );

			WorldItem worldItem;

			try
			{
				worldItem = Player.World.SpawnPlacedItem( newPItem, pos, playerRotation );
			}
			catch ( System.Exception e )
			{
				Player.Notify( Notifications.NotificationType.Error, $"Failed to spawn decal: {e.Message}" );
				return;
			}

			SoundEx.Play( PaintSound, Player.WorldPosition );

			ParticleManager.PoofAt( worldItem.WorldPosition );

			// fade in the decal
			/* if ( node is Items.FloorDecal decal2 )
			{
				decal2.Decal.Modulate = new Godot.Color( 1, 1, 1, 0f );
				var tween = GetTree().CreateTween();
				tween.TweenProperty( decal2.Decal, "modulate:a", 1f, 0.1f );
			} */
		}
	}

	public override string GetUseName()
	{
		return "Paint";
	}

	public override IEnumerable<MainUi.InputData> GetInputs()
	{
		yield return new MainUi.InputData( "WheelUp", "Next texture" );
		yield return new MainUi.InputData( "WheelDown", "Previous texture" );
	}

	protected override void OnStart()
	{
		CurrentTextureName = Decals.GetAllDecals().FirstOrDefault();
	}

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

		if ( Input.MouseWheel.y != 0 )
		{
			var decals = Decals.GetAllDecals();

			if ( !decals.Any() )
			{
				Player.Notify( Notifications.NotificationType.Error, "No decals found" );
				return;
			}

			var index = decals.IndexOf( CurrentTextureName );

			index += Input.MouseWheel.y > 0 ? 1 : -1;

			if ( index < 0 ) index = decals.Count - 1;
			if ( index >= decals.Count ) index = 0;

			CurrentTextureName = decals[index];

			if ( string.IsNullOrEmpty( CurrentTextureName ) )
			{
				Log.Warning( "No texture selected" );
				return;
			}

			Sound.Play( TextureChangeSound );

			Log.Info( $"Selected texture: {CurrentTextureName}" );

			// Player.Notify( Notifications.NotificationType.Info, $"Selected texture: {CurrentTexture}" );
		}
	}
}
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Clover.Player;
using Clover.Ui;

namespace Clover;

public class GameManager : Component, Component.INetworkListener, ISceneStartup
{
	public static GameManager Instance;

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

	protected override void OnAwake()
	{
		base.OnAwake();
		Instance = this;
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();
		Instance = null;
	}

	protected override void OnStart()
	{
		_ = Bootstrap();
	}

	private async Task Bootstrap()
	{
		if ( IsProxy ) return;

		Log.Info( "GameManager is booting up" );

		await WorldManager.Instance.LoadWorld( WorldManager.Instance.DefaultWorldData );

		/*if ( !_spawnQueue.Contains( Connection.Local ) )
		{
			OnConnected( Connection.Local );
		}*/

		Log.Info( "GameManager has booted up" );
	}

	public static JsonSerializerOptions JsonOptions = new()
	{
		WriteIndented = true,
		IncludeFields = true,
		Converters = { new JsonStringEnumConverter() },
		DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
	};

	private TimeSince _lastSave;

	public void SaveTimer()
	{
		if ( _lastSave < 60f )
		{
			return;
		}

		_lastSave = 0f;

		if ( !AutoSave ) return;

		if ( Networking.IsHost )
		{
			foreach ( var world in WorldManager.Instance.Worlds )
			{
				world.Value.Save();
			}
		}

		PlayerCharacter.Local?.Save();
	}

	[ConVar( "clover_autosave" )] public static bool AutoSave { get; set; } = true;

	protected override void OnFixedUpdate()
	{
		base.OnFixedUpdate();
		SaveTimer();
		SpawnPlayers();
	}

	private void SpawnPlayers()
	{
		if ( IsProxy ) return;

		foreach ( var channel in _spawnQueue.ToList() )
		{
			SpawnPlayer( channel );
		}
	}

	private readonly List<Connection> _spawnQueue = new();

	public void OnConnected( Connection channel )
	{
		Log.Info( $"Player '{channel.DisplayName}' has joined the game" );
		// _spawnQueue.Add( channel );
	}

	public void OnDisconnected( Connection channel )
	{
		Log.Info( $"Player '{channel.DisplayName}' has left the game" );
	}

	public void OnBecameHost( Connection channel )
	{
		Log.Info( $"Player '{channel.DisplayName}' has become the host" );
	}

	public static void LoadRealm()
	{
		Game.ActiveScene.LoadFromFile( "scenes/clover.scene" );
	}

	public void SpawnPlayer( Connection channel )
	{
		if ( Scene.GetAllComponents<PlayerCharacter>().Any( x => x.Network.Owner == channel ) )
		{
			Log.Warning( $"Player '{channel.DisplayName}' already spawned" );
			_spawnQueue.Remove( channel );
			return;
		}

		if ( !PlayerPrefab.IsValid() )
			return;

		// Spawn this object and make the client the owner
		var player = PlayerPrefab.Clone( new Transform(), name: $"Player - {channel.DisplayName}" );
		player.NetworkSpawn( channel );

		// Notify any listeners that a player has spawned
		Scene.RunEvent<IPlayerSpawned>( x => x.OnPlayerSpawned( player.GetComponent<PlayerCharacter>() ) );

		var island = WorldManager.Island;

		if ( island.IsValid() )
		{
			var spawnPoint = island.GetEntrance( "spawn" );
			if ( spawnPoint.IsValid() )
			{
				player.GetComponent<PlayerCharacter>().SetLayer( island.Layer );
				player.GetComponent<PlayerCharacter>().TeleportTo( spawnPoint.EntranceId );
			}
			else
			{
				Log.Error( "No spawn point found in the world" );
			}
		}
		else
		{
			Log.Error( "No active world found" );
		}

		_spawnQueue.Remove( channel );
	}

	[Rpc.Owner]
	public void RequestSpawn( string playerId )
	{
		var caller = Rpc.Caller;
		Log.Info( $"Player '{caller.DisplayName}' has requested to spawn player '{playerId}'" );
		_spawnQueue.Add( caller );
	}

	public void OnHostPreInitialize( SceneFile scene )
	{
		Log.Info( "BOOT" );
		PlayerCharacter.SpawnPlayerId = null;
	}

	public void OnHostInitialize()
	{
		Log.Info( "BOOT" );
		PlayerCharacter.SpawnPlayerId = null;
	}

	public void OnClientInitialize()
	{
		Log.Info( "BOOT" );
		PlayerCharacter.SpawnPlayerId = null;
	}
}
using Clover.Player;

namespace Clover.Interactable;

public interface IInteract
{
	bool CanInteract( PlayerCharacter player ) { return true; }

	/// <summary>
	///  Called when the player presses down the interact button. Only called once.
	/// </summary>
	/// <param name="player"></param>
	void StartInteract( PlayerCharacter player );

	/// <summary>
	///  Called when the player releases the interact button. Only called once.
	/// </summary>
	/// <param name="player"></param>
	void FinishInteract( PlayerCharacter player ) { }

	string GetInteractName();

	void StartInteractHost( PlayerCharacter player ) { }
	void FinishInteractHost( PlayerCharacter player ) { }
}
using Clover.Persistence;
using Clover.Ui;
using Clover.Utilities;
using Sandbox.Diagnostics;
using Sandbox.Utility;

namespace Clover.Items;

public class DecalItem : Component, IPersistent, IPaintEvent
{
	private Decals.DecalData _decalData;
	protected Texture DecalTexture;

	private string _texturePath;


	[Sync]
	public string TexturePath
	{
		get => _texturePath;
		set
		{
			_texturePath = value;
			UpdateDecal();
		}
	}

	[Sync] public string DecalHash { get; set; }

	public void UpdateDecal()
	{
		if ( Scene.IsEditor ) return;
		if ( string.IsNullOrEmpty( TexturePath ) ) return;

		if ( IsProxy )
		{
			FileSystem.Data.CreateDirectory( "decalcache" );
			if ( FileSystem.Data.FileExists( $"decalcache/{DecalHash}.decal" ) )
			{
				_decalData = Decals.ReadDecal( $"decalcache/{DecalHash}.decal" );
				DecalTexture = Decals.GetDecalTexture( _decalData.ToRpc() );
				var material1 = Material.Create( $"{DecalHash}.vmat", "shaders/floor_decal.shader" );
				material1.Set( "Color", DecalTexture );
				// ModelRenderer.MaterialOverride = material1;
				OnMaterialUpdate( material1 );
				Log.Info( $"Updated cached decal '{_decalData.Name}' with texture: {TexturePath}" );
				return;
			}

			Log.Info( $"Decal '{TexturePath}' not found in cache, requesting..." );

			RequestDecal();
			return;
		}

		// Update decal
		var material = Material.Create( $"{TexturePath}.vmat", "shaders/floor_decal.shader" );

		try
		{
			_decalData = Decals.ReadDecal( TexturePath );
		}
		catch ( System.Exception e )
		{
			Log.Error( e.Message );
			return;
		}

		DecalTexture = _decalData.Texture;

		material.Set( "Color", _decalData.Texture );

		// ModelRenderer.MaterialOverride = material;
		OnMaterialUpdate( material );

		DecalHash = _decalData.GetHash();

		Log.Info( $"Updated decal '{_decalData.Name}' with texture: {TexturePath}" );
	}

	[Rpc.Owner]
	private void RequestDecal()
	{
		Assert.True( Networking.IsHost );

		var caller = Rpc.Caller;

		if ( string.IsNullOrEmpty( _texturePath ) )
		{
			Log.Warning( "Texture path is null or empty" );
			return;
		}

		if ( string.IsNullOrEmpty( _decalData.Name ) )
		{
			Log.Warning( "Decal name is null or empty" );
			return;
		}

		var rpcDecal = _decalData.ToRpc();

		Log.Info( $"Sending decal '{rpcDecal.Name}' by '{rpcDecal.Author}' to {caller}" );

		using ( Rpc.FilterInclude( caller ) )
		{
			RecieveDecal( _texturePath, rpcDecal );
		}
	}


	[Rpc.Broadcast]
	public void RecieveDecal( string filename, Decals.DecalDataRpc decal )
	{
		Log.Info( $"Recieved decal '{filename}':" );
		Log.Info( $"Size: {decal.Width}x{decal.Height}" );
		Log.Info( $"Name: {decal.Name}" );
		Log.Info( $"Author: {decal.Author}" );
		Log.Info( $"Palette: {decal.Palette}" );
		Log.Info( $"Image: {decal.Image?.Length} bytes" );

		if ( string.IsNullOrEmpty( decal.Name ) )
		{
			Log.Error( "Decal name is null or empty" );
			return;
		}

		_decalData = decal.ToDecalData();
		// _decalData = Decals.ToDecalData( decal );

		var hash = _decalData.GetHash();

		DecalTexture = Decals.GetDecalTexture( decal );

		var material = Material.Create( $"{hash}.vmat", "shaders/floor_decal.shader" );
		material.Set( "Color", DecalTexture );
		// ModelRenderer.MaterialOverride = material;
		OnMaterialUpdate( material );

		FileSystem.Data.CreateDirectory( "decalcache" );
		var file = FileSystem.Data.OpenWrite( $"decalcache/{hash}.decal" );
		Decals.WriteDecal( file, decal.ToDecalData() );
		file.Close();
	}

	public virtual void OnMaterialUpdate( Material material )
	{
	}

	public virtual void OnSave( PersistentItem item )
	{
		item.SetSaveData( "TexturePath", TexturePath );
	}

	public virtual void OnLoad( PersistentItem item )
	{
		TexturePath = item.GetSaveData<string>( "TexturePath" );
		// UpdateDecal();
	}

	void IPaintEvent.OnFileSaved( string path )
	{
		if ( TexturePath == path )
		{
			Log.Info( "Updating decal" );
			UpdateDecal();

			RecieveDecal( path, _decalData.ToRpc() );
		}
	}

	[ConCmd( "clover_delete_old_decals" )]
	public static void DeleteOldDecals()
	{
		var decals = Game.ActiveScene.GetAllComponents<DecalItem>();
		foreach ( var decal in decals )
		{
			if ( decal.DecalHash == null )
			{
				Log.Info( $"Deleting old decal: {decal.TexturePath}" );
				// decal.Delete();
			}
		}
	}
}
using Clover.Persistence;

namespace Clover.Items;

public class PictureFrame : DecalItem, IPersistent
{
	[RequireComponent] public WorldItem WorldItem { get; private set; }
	[Property] public ModelRenderer ModelRenderer { get; set; }


	public override void OnMaterialUpdate( Material material )
	{
		base.OnMaterialUpdate( material );

		ModelRenderer.SetMaterialOverride( material, "image" );
	}
}
using Clover.Carriable;
using Clover.Components;
using Clover.Data;
using Clover.Interactable;
using Clover.Inventory;
using Clover.Items;
using Clover.Npc;
using Clover.Ui;

namespace Clover.Player;

[Title( "Player Interact" )]
[Icon( "inventory" )]
[Category( "Clover/Player" )]
public class PlayerInteract : Component
{
	[RequireComponent] public PlayerCharacter Player { get; set; }

	private IInteract _currentInteractable;

	[Property] public BoxCollider InteractCollider { get; set; }

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

	[Property] public SoundEvent UseFailSound { get; set; }
	[Property] public SoundEvent PickUpFailSound { get; set; }

	public GameObject InteractionTarget { get; set; }

	protected override void OnAwake()
	{
		if ( IsProxy ) return;
		if ( Cursor != null )
		{
			Cursor.Parent = null;
		}
	}

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

		if ( Cursor.IsValid() )
		{
			Cursor.Destroy();
		}
	}

	public bool CanInteract()
	{
		if ( Player.ItemPlacer.IsPlacing || Player.ItemPlacer.IsMoving ) return false;
		if ( Player.InCutscene ) return false;
		if ( Player.VehicleRider.Vehicle.IsValid() ) return false;
		if ( InteractionTarget.IsValid() )
		{
			if ( InteractionTarget.GetComponent<BaseNpc>().IsValid() ) return false;
		}

		if ( Player.Equips.TryGetEquippedItem<BaseCarriable>( Equips.EquipSlot.Tool, out var tool ) &&
		     tool.ShouldDisableMovement() ) return false;

		return true;
	}

	public bool CanPickUp()
	{
		if ( Player.Equips.TryGetEquippedItem<BaseCarriable>( Equips.EquipSlot.Tool, out var tool ) &&
		     tool.ShouldDisableMovement() ) return false;

		return true;
	}

	public bool HasInteractable()
	{
		return FindInteractable() != null;
	}

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

		if ( !CanInteract() )
		{
			return;
		}

		var interactable = FindInteractable();
		var moveable = FindMoveable();
		var pickupableNode = GetPickupableNode();

		if ( Input.Pressed( "use" ) )
		{
			if ( interactable != null )
			{
				_currentInteractable = interactable;
				_currentInteractable.StartInteract( Player );

				if ( !Networking.IsHost )
				{
					using ( Rpc.FilterInclude( Connection.Host ) )
					{
						_currentInteractable.StartInteractHost( Player );
					}
				}

				Input.Clear( "use" );
			}
			else
			{
				Log.Warning( "No interactable found" );
				// Notifications.Instance.AddNotification( Notifications.NotificationType.Warning, "No interactable found" );
				Sound.Play( UseFailSound, WorldPosition );
			}

			return;
		}
		else if ( Input.Released( "use" ) )
		{
			if ( _currentInteractable != null )
			{
				_currentInteractable.FinishInteract( Player );

				if ( !Networking.IsHost )
				{
					using ( Rpc.FilterInclude( Connection.Host ) )
					{
						_currentInteractable.FinishInteractHost( Player );
					}
				}

				_currentInteractable = null;
				Input.Clear( "use" );
			}

			return;
		}

		if ( Input.Pressed( "pickup" ) && CanPickUp() )
		{
			if ( pickupableNode != null )
			{
				if ( pickupableNode.CanPickup( Player ) )
				{
					pickupableNode.OnPickup( Player );
					return;
				}
			}

			Log.Warning( "No pickupable node found" );

			Sound.Play( PickUpFailSound, WorldPosition );
			return;
		}

		/*if ( Input.Pressed( "move" ) )
		{
			if ( !Player.World.Data.DisableItemPlacement )
			{
				if ( moveable.IsValid() )
				{
					Log.Info( "Moving..." );

					Mouse.Visible = true;

					Player.ItemPlacer.StartMovingPlacedItem( moveable.GetComponent<WorldItem>() );

					Input.Clear( "move" );
				}
				else
				{
					Mouse.Visible = false;
					Log.Warning( "No interactable found" );
					// Notifications.Instance.AddNotification( Notifications.NotificationType.Warning, "No interactable found" );
					Sound.Play( UseFailSound, WorldPosition );
				}
			}

			return;
		}*/


		GameObject target = null;
		if ( interactable is Component interactableComponent )
		{
			target = interactableComponent.GameObject;
		}
		else if ( moveable.IsValid() )
		{
			target = moveable;
		}
		else if ( pickupableNode is Component pickupableNodeComponent && pickupableNode.CanPickup( Player ) )
		{
			target = pickupableNodeComponent.GameObject;
		}

		if ( target != null )
		{
			if ( target.Components.TryGet<WorldItem>( out var worldItem ) )
			{
				worldItem.ItemHighlight.Enabled = true;
			}
		}


		if ( Cursor.IsValid() && Cursor.Enabled )
		{
			var gridPosition = Player.GetAimingGridPosition();
			var worldPosition = WorldManager.Instance.ActiveWorld.ItemGridToWorld( gridPosition );
			Cursor.WorldPosition = worldPosition;
		}
	}

	public IPickupable GetPickupableNode()
	{
		var touchingItems = InteractCollider.Touching;

		foreach ( var collider in touchingItems )
		{
			if ( collider.GameObject.Components.TryGet<IPickupable>( out var pickupable ) )
			{
				return pickupable;
			}
		}

		return null;
	}

	/*public WorldItem GetWorldItemFromInteract()
	{
		foreach ( var collider in InteractCollider.Touching )
		{
			var checkGameObject = collider.GameObject;

			while ( checkGameObject != null )
			{
				if ( checkGameObject.Components.TryGet<IInteract>( out var interactable ) )
				{
					if ( checkGameObject.Components.TryGet<WorldItem>( out var worldItem ) )
					{
						return worldItem;
					}
				}

				checkGameObject = checkGameObject.Parent;
			}
		}

		return null;
	}*/

	public IInteract FindInteractable()
	{
		foreach ( var collider in InteractCollider.Touching )
		{
			/*var checkGameObject = collider.GameObject;

			while ( checkGameObject != null )
			{

				if ( checkGameObject.Components.TryGet<IInteract>( out var interactable ) )
				{


					return interactable;
				}

				checkGameObject = checkGameObject.Parent;
			}*/

			/*if ( collider.GameObject.Components.TryGet<IInteract>( out var interactable,
				    FindMode.EverythingInSelfAndAncestors ) )
			{
				return interactable;
			}*/

			var components = collider.GameObject.Components.GetAll<IInteract>( FindMode.EverythingInSelfAndAncestors );
			foreach ( var component in components )
			{
				if ( component.CanInteract( Player ) )
				{
					return component;
				}
			}
		}

		// Log.Info( "# Reached root, no interactable found." );

		return null;
	}

	public GameObject FindMoveable()
	{
		foreach ( var collider in InteractCollider.Touching )
		{
			var worldItem = collider.GameObject.Components.Get<WorldItem>( FindMode.EverythingInSelfAndAncestors );
			if ( worldItem != null && worldItem.CanPickup( Player ) )
			{
				return collider.GameObject;
			}
		}

		return null;
	}
}
using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Clover.Data;
using Sandbox.Diagnostics;

namespace Clover;

[Category( "Clover/World" )]
public class WorldManager : Component, Component.INetworkSpawn
{
	public static WorldManager Instance { get; private set; }

	public static World Island => Instance.GetWorld( "island" );

	// [Property] public List<World> Worlds { get; set; } = new();
	[Property, Sync, Change, ReadOnly] public NetDictionary<int, World> Worlds { get; set; } = new();

	[Property, JsonIgnore, ReadOnly] public int ActiveWorldIndex { get; set; }
	[Property, JsonIgnore, ReadOnly] public World ActiveWorld => !Scene.IsEditor ? GetWorld( ActiveWorldIndex ) : null;

	[Property] public WorldData DefaultWorldData { get; set; }

	public delegate void WorldUnloadEventHandler( World world );

	public delegate void WorldLoadedEventHandler( World world );

	public delegate void ActiveWorldChangedEventHandler( World world );


	[Property] public WorldLoadedEventHandler WorldLoaded { get; set; }
	[Property] public WorldUnloadEventHandler WorldUnload { get; set; }
	[Property] public ActiveWorldChangedEventHandler ActiveWorldChanged { get; set; }


	public string CurrentWorldDataPath { get; set; }

	public bool IsLoading;

	public const float WorldOffset = 1024;

	// public Array LoadingProgress { get; set; } = new Array();

	protected override void OnAwake()
	{
		base.OnAwake();
		Instance = this;
	}

	public void OnNetworkSpawn( Connection owner )
	{
		Instance = this;
	}

	public void OnWorldsChanged()
	{
		Log.Info( "Worlds changed." );
		RebuildVisibility();
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();
		Instance = null;
	}

	public World GetWorld( string id )
	{
		var val = Worlds.Values.FirstOrDefault( w => w.Data.ResourceName == id );
		if ( !val.IsValid() )
		{
			Log.Warning( $"World not found: {id}, searching scene..." );
			val = Scene.GetAllComponents<World>().FirstOrDefault( w => w.Data.ResourceName == id );
		}

		return val;
	}

	public World GetWorld( int index )
	{
		if ( index < 0 )
		{
			return null;
		}


		var val = Worlds.Values.FirstOrDefault( w => w.Layer == index );
		if ( !val.IsValid() )
		{
			Log.Warning( $"World not found at index: {index}, searching scene..." );
			val = Scene.GetAllComponents<World>().FirstOrDefault( w => w.Layer == index );
		}

		return val;
	}

	public void SetActiveWorld( int index )
	{
		Log.Info( $"Setting active world to index: {index}" );
		ActiveWorldIndex = index;

		if ( !ActiveWorld.IsValid() )
		{
			Log.Warning( $"Active world is not valid: {index}" );
		}

		RebuildVisibility();
		ActiveWorldChanged?.Invoke( ActiveWorld );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldChanged( ActiveWorld ) );
	}

	private void RebuildVisibility()
	{
		if ( Worlds.Count == 0 )
		{
			Log.Warning( "No worlds to rebuild visibility for." );
			return;
		}

		Log.Info( $"Rebuilding world visibility for {Worlds.Count} worlds..." );

		// rebuild world visibility
		for ( var i = 0; i < Worlds.Count; i++ )
		{
			var isVisible = i == ActiveWorldIndex;
			var world = Worlds.TryGetValue( i, out var w ) ? w : null;
			if ( world == null )
			{
				Log.Warning( $"World not found at index: {i}" );
				continue;
			}

			world.Tags.Remove( "worldlayer_invisible" );
			world.Tags.Remove( "worldlayer_visible" );

			if ( isVisible )
			{
				world.Tags.Add( "worldlayer_visible" );
			}
			else
			{
				world.Tags.Add( "worldlayer_invisible" );
			}
		}

		// rebuild object visibility
		foreach ( var layerObject in Scene.GetAllComponents<WorldLayerObject>() )
		{
			layerObject.RebuildVisibility( layerObject.Layer );
		}
	}

	public void SetActiveWorld( World world )
	{
		ActiveWorldIndex = world.Layer;
		RebuildVisibility();
	}

	protected override void OnStart()
	{
		Instance = this;
	}

	public bool HasWorld( string id )
	{
		return Worlds.Values.Any( w => w.Data.ResourceName == id );
	}

	public bool HasWorld( WorldData data )
	{
		return Worlds.Values.Any( w => w.Data == data );
	}

	public async Task<World> LoadWorld( WorldData data )
	{
		Log.Info( $"Loading world: {data.ResourceName}" );

		// use the first available index
		var index = 0;
		while ( Worlds.ContainsKey( index ) )
		{
			index++;
		}

		if ( !data.Prefab.IsValid() )
		{
			Log.Error( $"Invalid prefab for world: {data.ResourceName}" );
			return null;
		}

		var gameObject = data.Prefab.Clone();

		// gameObject.BreakFromPrefab();

		var world = gameObject.GetComponent<World>();
		world.Data = data; // already set
		world.Layer = index;

		gameObject.WorldPosition = new Vector3( new Vector3( 0, 0, index * WorldOffset ) );
		gameObject.Transform.ClearInterpolation();
		gameObject.SetParent( GameObject );

		gameObject.Tags.Add( "dworld" );
		gameObject.Tags.Add( $"dworldlayer_{index}" );

		gameObject.NetworkMode = NetworkMode.Object;
		gameObject.NetworkSpawn();

		Worlds[index] = world;

		world.Setup();

		await world.Load();

		Log.Info( $"Loaded world: {data.ResourceName}, now has {Worlds.Count} worlds." );

		RebuildVisibility();

		// ActiveWorldChanged?.Invoke( world );

		OnWorldLoadedRpc( data.ResourceName );

		// return dummy task to shut up the compiler

		await Task.Frame();

		return world;
	}

	public async Task<World> GetWorldOrLoad( WorldData data )
	{
		Assert.NotNull( data, "World data is null." );
		var world = GetWorld( data.ResourceName );
		return world.IsValid() ? world : await LoadWorld( data );
	}

	[Rpc.Owner]
	public void RequestLoadWorld( string id )
	{
		var worldData = ResourceLibrary.GetAll<WorldData>().FirstOrDefault( w => w.ResourceName == id );
		if ( worldData != null )
		{
			_ = LoadWorld( worldData );
		}
		else
		{
			Log.Warning( $"Could not find world with id: {id}" );
		}
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	public void OnWorldLoadedRpc( string id )
	{
		Log.Info( $"World loaded: {id}" );
		var world = GetWorld( id );

		if ( !world.IsValid() )
		{
			Log.Error( $"World not found: {id}" );
			return;
		}

		WorldLoaded?.Invoke( world );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldLoaded( world ) );
		world.OnWorldLoaded();

		foreach ( var world2 in Worlds )
		{
			Log.Info( $"World #{world2.Key}: {world2.Value.Data.ResourceName}" );
		}
	}


	public void UnloadWorld( string id )
	{
		var world = GetWorld( id );
		if ( world.IsValid() )
		{
			UnloadWorld( world );
		}
	}

	public void UnloadWorld( World world )
	{
		Log.Info( $"Unloading world: {world.Data.ResourceName}" );
		world.OnWorldUnloaded();
		world.DestroyGameObject();
		Worlds.Remove( world.Layer );
		RebuildVisibility();
		WorldUnload?.Invoke( world );
		Scene.RunEvent<IWorldEvent>( x => x.OnWorldUnloaded( world ) );
	}

	public void UnloadWorld( int index )
	{
		var world = GetWorld( index );
		if ( world.IsValid() )
		{
			UnloadWorld( world );
		}
	}


	[ConCmd( "clover_world_load" )]
	public static void LoadWorldCmd( string id )
	{
		var worldManager = NodeManager.WorldManager;
		var worldData = ResourceLibrary.GetAll<WorldData>().FirstOrDefault( w => w.ResourceName == id );
		if ( worldData != null )
		{
			_ = worldManager.LoadWorld( worldData );
		}
		else
		{
			Log.Warning( $"Could not find world with id: {id}" );
		}
	}

	[ConCmd( "clover_world_set_active" )]
	public static void SetActiveWorldCmd( int index )
	{
		NodeManager.WorldManager.SetActiveWorld( index );
	}

	[ConCmd( "clover_world_move_to_entrance" )]
	public static void MoveToEntranceCmd( int worldIndex, string entranceId )
	{
		var world = Instance.GetWorld( worldIndex );
		if ( !world.IsValid() ) throw new Exception( $"Invalid world index: {worldIndex}" );

		var entrance = world.GetEntrance( entranceId );
		if ( entrance == null ) throw new Exception( $"Invalid entrance id: {entranceId}" );

		Instance.SetActiveWorld( worldIndex );

		var player = NodeManager.Player;

		player.WorldLayerObject.SetLayer( worldIndex, true );

		player.WorldPosition = entrance.WorldPosition;
		player.GetComponent<CameraController>().SnapCamera();
	}

	[ConCmd( "clover_world_save_all" )]
	public static void SaveAllCmd()
	{
		foreach ( var world in Instance.Worlds.Values )
		{
			world.Save();
		}
	}

	/*public WorldNodeLink GetWorldNodeLink( GameObject gameObject )
	{
		foreach ( var world in Worlds.Values )
		{
			var link = world.GetNodeLink( gameObject );
			if ( link != null )
			{
				return link;
			}
		}

		return null;
	}*/
}

public interface IWorldEvent
{
	void OnWorldLoaded( World world ) { }
	void OnWorldUnloaded( World world ) { }
	void OnWorldChanged( World world ) { }
}
@using System
@using Clover.Carriable
@using Clover.Components
@using Clover.Player
@using Sandbox.UI;
@inherits Panel
@namespace Clover.Ui
@attribute [StyleSheet]

<root>
	@if ( PlayAlongMaster.IsValid() && !string.IsNullOrEmpty( PlayAlongMaster.PlaybackTitle ) )
	{
		<div class="playalong">
			<h1>@( $"Play along with {PlayAlongMaster.Player.Network.Owner.DisplayName}" )</h1>
			<section>
				<h2>Song</h2>
				<main>
					@PlayAlongMaster.PlaybackTitle
				</main>
			</section>
			<section>
				<h2>Tracks</h2>
				<main class="tracks">
					@for ( int i = 0; i < PlayAlongMaster.PlaybackTracksInstruments.Count; i++ )
					{
						var i1 = i;
						<button class="track-button @( PlayAlongMaster.IsPlaybackTrackEnabled( i ) ? "enabled" : "disabled" ) @( PlayAlongMaster.PlaybackTracksInstruments[i1] == Instrument ? "me" : "other" ) button-sounds" @onclick=@( () => PlayAlongMaster.RequestTrackPlayback( Instrument, i1 ) )>
							<div class="index">@i1</div>
							<div class="player">
								@if ( PlayAlongMaster.PlaybackTracksInstruments[i].IsValid() )
								{
									<img src="@( $"avatar://{PlayAlongMaster.PlaybackTracksInstruments[i].Network.Owner.SteamId}" )"/>
								}
							</div>
						</button>
					}
				</main>
			</section>
		</div>
	}
	<div class="playback">
		<h1>
			@( $"Loaded: {Instrument.PlaybackTitle}" )
		</h1>
		@if ( !Instrument.IsPlayingBack )
		{
			<div class="midis">
				@foreach ( var midi in GetMidiFiles() )
				{
					<button class="midi button-sounds @( Instrument.PlaybackTitle == midi ? "active" : "" )" @onclick=@( () => Instrument.LoadFile( midi ) )>
						@midi
					</button>
				}
			</div>
		}
		<div class="actions">
			<button class="clover-button button-sounds @( string.IsNullOrEmpty( Instrument.PlaybackTitle ) ? "disabled" : "" )" @onclick=@( () => Instrument.StartPlayback() )>Play</button>
			<button class="clover-button button-sounds" @onclick=@( () => Instrument.StopPlayback() )>Stop</button>
			@*<TextEntry Numeric=@( true ) Value:[email protected]/>*@
			<section class="transpose">
				<h2 class="title">Transpose</h2>
				<main>
					<button class="clover-button small button-sounds" @onclick=@( () => Instrument.TransposePlayback -= 12 )>-1</button>
					<div class="value">@( Instrument.TransposePlayback / 12 )</div>
					<button class="clover-button small button-sounds" @onclick=@( () => Instrument.TransposePlayback += 12 )>+1</button>
				</main>
			</section>
			<section>
				<h2 class="title">Tracks</h2>
				<main class="tracks">
					@for ( int i = 0; i < Instrument.PlaybackTracksInstruments.Count; i++ )
					{
						var i1 = i;
						<button class="track-button @( Instrument.IsPlaybackTrackEnabled( i ) ? "enabled" : "disabled" ) @( Instrument.PlaybackTracksInstruments[i1] == Instrument ? "me" : "other" ) button-sounds" @onclick=@( () => Instrument.ToggleTrackPlayback( i1 ) )>
							<div class="index">@i1</div>
							<div class="player">
								@if ( Instrument.PlaybackTracksInstruments[i].IsValid() )
								{
									<img src="@( $"avatar://{Instrument.PlaybackTracksInstruments[i].Network.Owner.SteamId}" )"/>
								}
							</div>
						</button>
					}
				</main>
			</section>
			<section class="progress">
				<h2 class="title">Progress</h2>
				<main>
					@*<div class="progress-bar">
						<div class="progress-bar-fill" style="width: @( Instrument.PlaybackProgress * 100 )px"></div>
					</div>*@
					@( $"{(Instrument.PlaybackProgress * 100):0.00}%" )
				</main>
			</section>
		</div>
	</div>
	<div class="notes">
		@foreach ( var note in Enum.GetValues( typeof(BaseInstrument.Note) ) )
		{
			<div class="note @( BaseInstrument.IsBlackNote( (BaseInstrument.Note)note ) ? "black" : "white" )">
				<div class="name">@BaseInstrument.NoteNames[(BaseInstrument.Note)note]</div>
				<div class="action">
					<Image [email protected]( $"PlayNote{(BaseInstrument.Note)note}", InputGlyphSize.Small, GlyphStyle.Knockout )/>
				</div>
			</div>
		}
	</div>
</root>

@code {

	private BaseInstrument Instrument => PlayerCharacter.Local.Equips.GetEquippedItem<BaseInstrument>( Equips.EquipSlot.Tool );

	private BaseInstrument PlayAlongMaster => Scene.GetAllComponents<BaseInstrument>().FirstOrDefault( x => x != Instrument && x.Player.IsValid() && x.WorldPosition.Distance( Instrument.WorldPosition ) < 300 );

	private List<string> GetMidiFiles()
	{
		return FileSystem.Data.FindFile( "midi", "*.mid" ).ToList();
	}

	protected override int BuildHash()
	{
		return HashCode.Combine( Instrument?.PlaybackProgress, PlayAlongMaster?.PlaybackTracksInstruments.Select( x => x.Network.Owner.SteamId ).ToArray() );
	}

}
@using System
@using Clover.Data
@using Sandbox;
@using Sandbox.Network
@using Sandbox.UI;
@inherits Panel
@namespace Clover.Ui

<root>
	@if ( Lobbies.Any() )
	{
		@foreach ( var lobby in Lobbies )
		{
			<div class="lobby">
				<h2>@lobby.Name</h2>
				<button @onclick=@( () => Networking.Connect( lobby.LobbyId ) )>Join</button>
			</div>
		}
	}
	@if ( _isRefreshing )
	{
		<div class="loading">Refreshing...</div>
	}
	<button @onclick=@( () => Refresh() )>Refresh</button>
	<div style="flex-direction: column;" onclick=@( () => Clipboard.SetText( Game.SteamId.ToString() ) )>
		
		<span>@($"Steam ID: {Game.SteamId}")</span>
		<span>@($"Host: {Connection.Host.SteamId}")</span>
		<span>@($"Address: {Connection.Host.Address}")</span>
		<span>@($"Party: {Connection.Host.PartyId}")</span>
	</div>
</root>

@code {
	
	private List<LobbyInformation> Lobbies = new();
	
	private bool _isRefreshing = false;

	private async void Refresh()
	{
		_isRefreshing = true;
		Lobbies = await Networking.QueryLobbies();
		_isRefreshing = false;
	}

	protected override int BuildHash()
	{
		return HashCode.Combine( _isRefreshing, Lobbies );
	}

}
@using System
@using Clover.Carriable
@using Clover.Components
@using Clover.Player
@using Clover.WorldBuilder
@using Sandbox;
@using Sandbox.UI;
@using Clover.Ui;
@inherits PanelComponent
@namespace Clover

<root>
	@if ( PlayerCharacter.Local.IsValid() )
	{
		<div class="is-saving" @ref=" IsSavingPanel">
			<i class="icon">save</i>
		</div>

		<div class="status @( ShouldShowUi ? "active" : "" )">
			<div class="weather">
				<Image [email protected]()/>
			</div>
			<div class="time">@TimeManager.Time.ToString( "HH:mm" )</div>
		</div>

		<div class="cutscene-bars @( PlayerCharacter.Local.InCutscene ? "active" : "" )">
			<div class="bar top"></div>
			<div class="bar bottom"></div>
		</div>

		@if ( ShowInputs )
		{
			<div class="inputs @( ShouldShowUi ? "active" : "" )">
				@{
					var group = "";
					var actionPairsPrinted = new List<string>();
				}
				@foreach ( var input in GetCurrentInputs().OrderBy( x => x.Group ).ThenBy( x => x.ActionPair ) )
				{
					if ( input.Group != group )
					{
						group = input.Group;
						<div class="input-group">@group</div>
					}

					if ( !string.IsNullOrEmpty( input.ActionPair ) && actionPairsPrinted.Contains( input.ActionPair ) )
					{
						continue;
					}

					if ( !string.IsNullOrEmpty( input.ActionPair ) && !actionPairsPrinted.Contains( input.ActionPair ) )
					{
						actionPairsPrinted.Add( input.ActionPair );

						var allPairs = GetCurrentInputs().Where( x => x.ActionPair == input.ActionPair ).ToList();

						<div class="input-entry">
							<div class="input-glyphs">
								@foreach ( var pair in allPairs )
								{
									<Image [email protected]( pair.Action, InputGlyphSize.Small, GlyphStyle.Dark )/>
								}
							</div>
							<div class="input-name">@input.Name</div>
						</div>
					}
					else
					{
						<div class="input-entry">
							<div class="input-glyphs">
								<Image [email protected]( input.Action, InputGlyphSize.Small, GlyphStyle.Dark )/>
							</div>
							<div class="input-name">@input.Name</div>
						</div>
					}
				}
			</div>
		}

		@if ( PlayerCharacter.Local.Components.TryGet<HideAndSeek>( out var hideAndSeek ) && HideAndSeek.Leader.IsValid() && HideAndSeek.Leader.IsRoundActive )
		{
			if ( hideAndSeek.IsBlind )
			{
				<div class="hide-and-seek-blind">
					<h1>Hiding...</h1>
					<div class="sub">
						<div class="blind-icon">
							<i class="icon">visibility_off</i>
						</div>
						<div class="blind-text">@Math.Round( HideAndSeek.Leader.BlindSecondsLeft )</div>
					</div>
				</div>
			}
			else
			{
				<div class="hide-and-seek">
					@if ( hideAndSeek.IsSeeker )
					{
						<h1>Seeking...</h1>
					}
					else
					{
						<h1>Hiding...</h1>
					}
					@if ( hideAndSeek.BlindSecondsLeft > 0 )
					{
						<div class="time">
							<i class="icon">visibility_off</i>
							<span>@Math.Round( hideAndSeek.BlindSecondsLeft )</span>
						</div>
					}
					else
					{
						<div class="time">
							<i class="icon">timer</i>
							<span>@Math.Round( hideAndSeek.RoundSecondsLeft )</span>
						</div>
					}
				</div>
			}
		}

		@if ( PlayerCharacter.Local.Equips.TryGetEquippedItem<BaseInstrument>( Equips.EquipSlot.Tool, out var instrument ) && instrument.IsPlaying )
		{
			<InstrumentUi/>
		}
	}
	else if ( string.IsNullOrEmpty( PlayerCharacter.SpawnPlayerId ) )
	{
		<PlayerSelect/>
	}
</root>
@using System
@using Clover.Data
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Clover.Ui

<root>
	<ScenePanel @ref=" _scenePanel"/>
</root>
using Sandbox;

public sealed class CardBreaker : Component
{
	protected override void OnStart()
	{

	}

	public void Init(Material material, Rotation rot, float scale)
	{
		LocalRotation = rot;
		LocalScale = new Vector3( scale );

		for ( int i = GameObject.Children.Count - 1; i >= 0; i-- )
		{
			var child = GameObject.Children[i];
			child.SetParent( null );

			var modelRenderer = child.Components.Get<ModelRenderer>();
			modelRenderer.Tint = Color.White;
			modelRenderer.MaterialOverride = material;

			var rigidBody = child.Components.Get<Rigidbody>();
			rigidBody.ApplyImpulse( (child.WorldPosition - WorldPosition).Normal * Game.Random.Float( 10f, 100f ) * rigidBody.PhysicsBody.Mass );
			rigidBody.ApplyImpulse( new Vector3( 0f, 0f, 1f ) * Game.Random.Float( 400f, 800f ) * rigidBody.PhysicsBody.Mass );
			rigidBody.AngularVelocity = Vector3.Random * Game.Random.Float( -10f, 10f );
		}

		GameObject.Destroy();
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using System.Text.Json;

public enum EasingType
{
	None = -1,
	Linear = 0,
	SineIn, SineOut, SineInOut,
	QuadIn, QuadOut, QuadInOut,
	CubicIn, CubicOut, CubicInOut,
	QuartIn, QuartOut, QuartInOut,
	QuintIn, QuintOut, QuintInOut,
	ExpoIn, ExpoOut, ExpoInOut,
	ExtremeIn, ExtremeOut, ExtremeInOut,
	ElasticIn, ElasticOut, ElasticInOut,
	ElasticSoftIn, ElasticSoftOut, ElasticSoftInOut,
	BackIn, BackOut, BackInOut,
	BounceIn, BounceOut, BounceInOut
};

public struct IntVector2 : IEquatable<IntVector2>
{
	public static implicit operator Vector2( IntVector2 vec )
	{
		return new Vector2( vec.x, vec.y );
	}

	public static explicit operator IntVector2( Vector2 vec )
	{
		return new IntVector2( (int)vec.x, (int)vec.y );
	}

	/// <summary>
	/// A vector with zero for all components.
	/// </summary>
	public static readonly IntVector2 Zero = new IntVector2( 0, 0 );

	/// <summary>
	/// A normalized vector along the positive X axis.
	/// </summary>
	public static readonly IntVector2 UnitX = new IntVector2( 1, 0 );

	/// <summary>
	/// A normalized vector along the positive Y axis.
	/// </summary>
	public static readonly IntVector2 UnitY = new IntVector2( 0, 1 );

	#region Operators
	/// <summary>
	/// The identity operator.
	/// </summary>
	public static IntVector2 operator +( IntVector2 vec )
	{
		return vec;
	}

	/// <summary>
	/// Component-wise addition of a vector to another.
	/// </summary>
	public static IntVector2 operator +( IntVector2 a, IntVector2 b )
	{
		return new IntVector2( a.x + b.x, a.y + b.y );
	}

	/// <summary>
	/// Finds the negation of a vector.
	/// </summary>
	public static IntVector2 operator -( IntVector2 vec )
	{
		return new IntVector2( -vec.x, -vec.y );
	}

	/// <summary>
	/// Component-wise subtraction of a vector from another.
	/// </summary>
	public static IntVector2 operator -( IntVector2 a, IntVector2 b )
	{
		return new IntVector2( a.x - b.x, a.y - b.y );
	}

	/// <summary>
	/// Component-wise multiplication of a vector by another.
	/// </summary>
	public static IntVector2 operator *( IntVector2 a, IntVector2 b )
	{
		return new IntVector2( a.x * b.x, a.y * b.y );
	}

	/// <summary>
	/// Multiplies a vector by a scalar.
	/// </summary>
	public static IntVector2 operator *( IntVector2 vec, int val )
	{
		return new IntVector2( vec.x * val, vec.y * val );
	}

	/// <summary>
	/// Multiplies a vector by a scalar.
	/// </summary>
	public static Vector2 operator *( IntVector2 vec, float val )
	{
		return new Vector2( vec.x * val, vec.y * val );
	}

	/// <summary>
	/// Multiplies a vector by a scalar.
	/// </summary>
	public static IntVector2 operator *( int val, IntVector2 vec )
	{
		return new IntVector2( vec.x * val, vec.y * val );
	}

	/// <summary>
	/// Multiplies a vector by a scalar.
	/// </summary>
	public static Vector2 operator *( float val, IntVector2 vec )
	{
		return new Vector2( vec.x * val, vec.y * val );
	}

	/// <summary>
	/// Component-wise division of a vector by another.
	/// </summary>
	public static IntVector2 operator /( IntVector2 a, IntVector2 b )
	{
		return new IntVector2( a.x / b.x, a.y / b.y );
	}

	/// <summary>
	/// Component-wise division of a vector by another.
	/// </summary>
	public static Vector2 operator /( Vector2 a, IntVector2 b )
	{
		return new Vector2( a.x / b.x, a.y / b.y );
	}

	/// <summary>
	/// Component-wise division of a vector by another.
	/// </summary>
	public static Vector2 operator /( IntVector2 a, Vector2 b )
	{
		return new Vector2( a.x / b.x, a.y / b.y );
	}

	/// <summary>
	/// Division of this vector by a scalar.
	/// </summary>
	public static IntVector2 operator /( IntVector2 vec, int val )
	{
		return new IntVector2( vec.x / val, vec.y / val );
	}

	public static Vector2 operator /( IntVector2 vec, float val )
	{
		return new Vector2( vec.x / val, vec.y / val );
	}

	public static IntVector2 operator /( int val, IntVector2 vec )
	{
		return new IntVector2( val / vec.x, val / vec.y );
	}

	public static Vector2 operator /( float val, IntVector2 vec )
	{
		return new Vector2( val / vec.x, val / vec.y );
	}
	#endregion

	/// <summary>
	/// Horizontal component.
	/// </summary>
	public int x;

	/// <summary>
	/// Vertical component.
	/// </summary>
	public int y;

	/// <summary>
	/// Floating point magnitude of the vector.
	/// </summary>
	public float Length { get { return (float)MathF.Sqrt( x * x + y * y ); } }

	/// <summary>
	/// Magnitude of the vector in Taxicab geometry.
	/// </summary>
	public int ManhattanLength { get { return Math.Abs( x ) + Math.Abs( y ); } }

	/// <summary>
	/// Sum of each component squared.
	/// </summary>
	public int LengthSquared { get { return x * x + y * y; } }

	/// <summary>
	/// Gets a normalized vector in the same direction as this one.
	/// </summary>
	public Vector2 Normalized { get { return LengthSquared >= float.Epsilon ? this / Length : Zero; } }

	/// <summary>
	/// Gets a vector equal to this one rotated counter-clockwise by 90 degrees.
	/// </summary>
	public IntVector2 Left { get { return new IntVector2( -y, x ); } }

	/// <summary>
	/// Gets a vector equal to this one rotated clockwise 90 degrees.
	/// </summary>
	public IntVector2 Right { get { return new IntVector2( y, -x ); } }

	/// <summary>
	/// Gets a vector equal to this one rotated 180 degrees.
	/// </summary>
	public IntVector2 Back { get { return -this; } }

	/// <summary>
	/// Constructs a vector from the given X and Y components.
	/// </summary>
	public IntVector2( int x, int y )
	{
		this.x = x; this.y = y;
	}

	/// <summary>
	/// Finds the scalar product of this vector and another.
	/// </summary>
	public int Dot( IntVector2 vec )
	{
		return x * vec.x + y * vec.y;
	}

	public override bool Equals( object obj )
	{
		return obj is IntVector2 && Equals( (IntVector2)obj );
	}

	/// <summary>
	/// Tests for equality with another vector.
	/// </summary>
	public bool Equals( IntVector2 vec )
	{
		return x == vec.x && y == vec.y;
	}

	public override int GetHashCode()
	{
		return x ^ y;
	}

	/// <summary>
	/// Gets a string representing this vector in (X, Y) format.
	/// </summary>
	public override string ToString()
	{
		return string.Format( "({0}, {1})", x, y );
	}
}

public struct AStarEdge<T>
{
	/// <summary>
	/// The node this connection leads to.
	/// </summary>
	public readonly T Dest;

	/// <summary>
	/// The cost of using this connection.
	/// </summary>
	public readonly float Cost;

	internal AStarEdge( T dest, float cost )
	{
		Dest = dest;
		Cost = cost;
	}
}

public static class Utils
{
	// projects vector A onto B
	public static Vector3 ProjectVector( Vector3 a, Vector3 b )
	{
		float dotProduct = Vector3.Dot( a, b );
		float lengthSquared = Vector3.Dot( b, b );
		return (dotProduct / lengthSquared) * b;
	}

	// returns vector B or C, whichever is in the closest direction compared to A
	public static Vector3 FindCloserVector( Vector3 a, Vector3 b, Vector3 c )
	{
		// Normalize B and C
		b = b.Normal;
		c = c.Normal;

		// Compute dot products
		float dotProductB = Vector3.Dot( a, b );
		float dotProductC = Vector3.Dot( a, c );

		// Determine which dot product is larger
		return dotProductB > dotProductC ? b : c;
	}

	public static GameObject FindChild( this Component component, string name )
	{
		return component.GameObject.Children.Where( x => x.Name == name ).FirstOrDefault();
	}

	public static Color GetRandomColor( float total = 1.5f )
	{
		float r = Game.Random.Float( 0f, 1f );
		total = Math.Max( total - r, 0f );
		float g = MathF.Min( Game.Random.Float( 0f, 1f ), total );
		total = Math.Max( total - g, 0f );
		float b = MathF.Min( Game.Random.Float( 0f, 1f ), total );

		return new Color( r, g, b );
	}

	public static T ReadJsonCustom<T>( this BaseFileSystem fs, string filename, T defaultValue = default( T ) )
	{
		string text = fs.ReadAllText( filename );
		if ( string.IsNullOrWhiteSpace( text ) )
		{
			return defaultValue;
		}

		JsonSerializerOptions options = new JsonSerializerOptions
		{
			ReadCommentHandling = JsonCommentHandling.Skip,
			PropertyNameCaseInsensitive = true,
			AllowTrailingCommas = true,
			Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
		};
		return JsonSerializer.Deserialize<T>( text, options );
	}

	public const float Deg2Rad = MathF.PI / 180f;
	public const float Rad2Deg = 360f / (MathF.PI * 2f);

	public static Vector2 RotateVector( Vector2 v, float degrees )
	{
		var rads = Deg2Rad * degrees;
		var ca = MathF.Cos( rads );
		var sa = MathF.Sin( rads );

		return new Vector2( ca * v.x - sa * v.y, sa * v.x + ca * v.y );
	}

	public static Vector2 RotatePointAround( Vector2 p, Vector2 anchor, float degrees )
	{
		var offset = p - anchor;
		var newVec = RotateVector( offset, degrees );
		return anchor + newVec;
	}

	public static Vector2 DegreesToVector( float degrees )
	{
		var rads = Deg2Rad * degrees;
		return new Vector2( MathF.Cos( rads ), MathF.Sin( rads ) );
	}

	public static float VectorToDegrees( Vector2 vec )
	{
		float radians = (float)Math.Atan2( vec.y, vec.x );
		float degrees = radians * (180f / (float)Math.PI);
		return degrees;
	}

	public static Vector2 GetPerpendicularVector( Vector2 vec )
	{
		return new Vector2( -vec.y, vec.x );
	}

	public static int GetDistance( int x, int y )
	{
		return (int)Math.Round( Math.Sqrt( x * x + y * y ) );
	}

	public static Vector2 GetRandomVector()
	{
		return DegreesToVector( Game.Random.Float( 0f, 360f ) );
	}

	public static float FastSin( float input )
	{
		// wrap input angle to -PI..PI
		while ( input < -3.14159265f )
			input += 6.28318531f;
		while ( input > 3.14159265f )
			input -= 6.28318531f;

		return 1.27323954f * input + 0.405284735f * (input < 0f ? 1f : -1f) * input * input;
	}

	public static float DynamicEaseTo( float current, float goal, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goal;

		return current + (goal - current) * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
	}

	public static Vector2 DynamicEaseTo( Vector2 current, Vector2 goal, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goal;
		var diff = goal - current;

		return current + diff * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
	}

	public static Vector3 DynamicEaseTo( Vector3 current, Vector3 goal, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goal;
		var diff = goal - current;

		return current + diff * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
	}

	public static Rotation DynamicEaseTo( Rotation currentRot, Rotation goalRot, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goalRot;

		Vector3 current = currentRot.Angles().AsVector3();
		Vector3 goal = goalRot.Angles().AsVector3();

		var diff = goal - current;

		var output = current + diff * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
		return Rotation.From( new Angles( output ) );
	}

	public static float EaseUnclamped( float value, EasingType easingType )
	{
		switch ( easingType )
		{
			case EasingType.SineIn: return SineIn( value );
			case EasingType.SineOut: return SineOut( value );
			case EasingType.SineInOut: return SineInOut( value );

			case EasingType.QuadIn: return QuadIn( value );
			case EasingType.QuadOut: return QuadOut( value );
			case EasingType.QuadInOut: return QuadInOut( value );

			case EasingType.CubicIn: return CubicIn( value );
			case EasingType.CubicOut: return CubicOut( value );
			case EasingType.CubicInOut: return CubicInOut( value );

			case EasingType.QuartIn: return QuartIn( value );
			case EasingType.QuartOut: return QuartOut( value );
			case EasingType.QuartInOut: return QuartInOut( value );

			case EasingType.QuintIn: return QuintIn( value );
			case EasingType.QuintOut: return QuintOut( value );
			case EasingType.QuintInOut: return QuintInOut( value );

			case EasingType.ExpoIn: return ExpoIn( value );
			case EasingType.ExpoOut: return ExpoOut( value );
			case EasingType.ExpoInOut: return ExpoInOut( value );

			case EasingType.ExtremeIn: return ExtremeIn( value );
			case EasingType.ExtremeOut: return ExtremeOut( value );
			case EasingType.ExtremeInOut: return ExtremeInOut( value );

			case EasingType.ElasticIn: return ElasticIn( value );
			case EasingType.ElasticOut: return ElasticOut( value );
			case EasingType.ElasticInOut: return ElasticInOut( value );

			case EasingType.ElasticSoftIn: return ElasticSoftIn( value );
			case EasingType.ElasticSoftOut: return ElasticSoftOut( value );
			case EasingType.ElasticSoftInOut: return ElasticSoftInOut( value );

			case EasingType.BackIn: return BackIn( value );
			case EasingType.BackOut: return BackOut( value );
			case EasingType.BackInOut: return BackInOut( value );

			case EasingType.BounceIn: return BounceIn( value );
			case EasingType.BounceOut: return BounceOut( value );
			case EasingType.BounceInOut: return BounceInOut( value );
			default: return value;
		}
	}

	public static float Map( float value, float inputMin, float inputMax, float outputMin, float outputMax, EasingType easingType = EasingType.Linear, bool clamp = true )
	{
		if ( inputMin.Equals( inputMax ) || outputMin.Equals( outputMax ) )
			return outputMin;

		//            if(inputMin.Equals(inputMax) || outputMin.Equals(outputMax))
		//                return outputMax;

		if ( clamp )
		{
			// clamp input
			if ( inputMax > inputMin )
			{
				if ( value < inputMin ) value = inputMin;
				else if ( value > inputMax ) value = inputMax;
			}
			else if ( inputMax < inputMin )
			{
				if ( value > inputMin ) value = inputMin;
				else if ( value < inputMax ) value = inputMax;
			}
		}

		var ratio = EaseUnclamped( (value - inputMin) / (inputMax - inputMin), easingType );

		var outVal = outputMin + ratio * (outputMax - outputMin);

		//            // clamp output
		//            if(outputMax < outputMin) {
		//                if(outVal < outputMax) outVal = outputMax;
		//                else if(outVal > outputMin) outVal = outputMin;
		//            } else {
		//                if(outVal > outputMax) outVal = outputMax;
		//                else if(outVal < outputMin) outVal = outputMin;
		//            }

		return outVal;
	}

	public static float MapReturn( float value, float inputMin, float inputMax, float outputMin, float outputMax, EasingType easingType = EasingType.Linear )
	{
		var halfway = inputMin + (inputMax - inputMin) * 0.5f;
		if ( value < halfway ) return Map( value, inputMin, halfway, outputMin, outputMax, easingType );
		else return Map( value, halfway, inputMax, outputMax, outputMin, GetOppositeEasingType( easingType ) );
	}

	public static float EasePercent( float percent, EasingType easingType )
	{
		return Map( percent, 0f, 1f, 0f, 1f, easingType );
	}

	public static EasingType GetOppositeEasingType( EasingType easingType )
	{
		var opposite = EasingType.Linear;
		switch ( easingType )
		{
			case EasingType.SineIn: opposite = EasingType.SineOut; break;
			case EasingType.SineOut: opposite = EasingType.SineIn; break;
			case EasingType.SineInOut: opposite = EasingType.SineInOut; break;

			case EasingType.QuadIn: opposite = EasingType.QuadOut; break;
			case EasingType.QuadOut: opposite = EasingType.QuadIn; break;
			case EasingType.QuadInOut: opposite = EasingType.QuadInOut; break;

			case EasingType.CubicIn: opposite = EasingType.CubicOut; break;
			case EasingType.CubicOut: opposite = EasingType.CubicIn; break;
			case EasingType.CubicInOut: opposite = EasingType.CubicInOut; break;

			case EasingType.QuartIn: opposite = EasingType.QuartOut; break;
			case EasingType.QuartOut: opposite = EasingType.QuartIn; break;
			case EasingType.QuartInOut: opposite = EasingType.QuartInOut; break;

			case EasingType.QuintIn: opposite = EasingType.QuintOut; break;
			case EasingType.QuintOut: opposite = EasingType.QuintIn; break;
			case EasingType.QuintInOut: opposite = EasingType.QuintInOut; break;

			case EasingType.ExpoIn: opposite = EasingType.ExpoOut; break;
			case EasingType.ExpoOut: opposite = EasingType.ExpoIn; break;
			case EasingType.ExpoInOut: opposite = EasingType.ExpoInOut; break;

			case EasingType.ExtremeIn: opposite = EasingType.ExtremeOut; break;
			case EasingType.ExtremeOut: opposite = EasingType.ExtremeIn; break;
			case EasingType.ExtremeInOut: opposite = EasingType.ExtremeInOut; break;

			case EasingType.ElasticIn: opposite = EasingType.ElasticOut; break;
			case EasingType.ElasticOut: opposite = EasingType.ElasticIn; break;
			case EasingType.ElasticInOut: opposite = EasingType.ElasticInOut; break;

			case EasingType.ElasticSoftIn: opposite = EasingType.ElasticSoftOut; break;
			case EasingType.ElasticSoftOut: opposite = EasingType.ElasticSoftIn; break;
			case EasingType.ElasticSoftInOut: opposite = EasingType.ElasticSoftInOut; break;

			case EasingType.BackIn: opposite = EasingType.BackOut; break;
			case EasingType.BackOut: opposite = EasingType.BackIn; break;
			case EasingType.BackInOut: opposite = EasingType.BackInOut; break;

			case EasingType.BounceIn: opposite = EasingType.BounceOut; break;
			case EasingType.BounceOut: opposite = EasingType.BounceIn; break;
			case EasingType.BounceInOut: opposite = EasingType.BounceInOut; break;
		}

		return opposite;
	}

	public static float SineIn( float t ) { return 1f - MathF.Cos( t * MathF.PI * 0.5f ); }
	public static float SineOut( float t ) { return MathF.Sin( t * (MathF.PI * 0.5f) ); }
	public static float SineInOut( float t ) { return -0.5f * (MathF.Cos( MathF.PI * t ) - 1f); }

	public static float QuadIn( float t ) { return t * t; }
	public static float QuadOut( float t ) { return t * (2f - t); }
	public static float QuadInOut( float t ) { return t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t; }

	public static float CubicIn( float t ) { return t * t * t; }
	public static float CubicOut( float t ) { var t1 = t - 1f; return t1 * t1 * t1 + 1f; }
	public static float CubicInOut( float t ) { return t < 0.5f ? 4f * t * t * t : (t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f; }

	public static float QuartIn( float t ) { return t * t * t * t; }
	public static float QuartOut( float t ) { var t1 = t - 1f; return 1f - t1 * t1 * t1 * t1; }
	public static float QuartInOut( float t ) { return t < 0.5f ? 4f * t * t * t : (t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f; }

	public static float QuintIn( float t ) { return t * t * t * t * t; }
	public static float QuintOut( float t ) { var t1 = t - 1f; return 1f + t1 * t1 * t1 * t1 * t1; }
	public static float QuintInOut( float t ) { var t1 = t - 1f; return t < 0.5f ? 16f * t * t * t * t * t : 1f + 16f * t1 * t1 * t1 * t1 * t1; }

	public static float ExpoIn( float t ) { return MathF.Pow( 2f, 10f * (t - 1f) ); }
	public static float ExpoOut( float t ) { return 1f - MathF.Pow( 2f, -10f * t ); }
	public static float ExpoInOut( float t ) { return t < 0.5f ? ExpoIn( t * 2f ) * 0.5f : 1f - ExpoIn( 2f - t * 2f ) * 0.5f; }

	public static float ExtremeIn( float t ) { return MathF.Pow( 10f, 10f * (t - 1f) ); }
	public static float ExtremeOut( float t ) { return 1f - MathF.Pow( 10f, -10f * t ); }
	public static float ExtremeInOut( float t ) { return t < 0.5f ? ExtremeIn( t * 2f ) * 0.5f : 1f - ExtremeIn( 2f - t * 2f ) * 0.5f; }

	public static float ElasticIn( float t ) { return 1f - ElasticOut( 1f - t ); }
	public static float ElasticOut( float t ) { var p = 0.3f; return MathF.Pow( 2f, -10f * t ) * MathF.Sin( (t - p / 4f) * (2f * (float)Math.PI) / p ) + 1f; }
	public static float ElasticInOut( float t ) { return t < 0.5f ? ElasticIn( t * 2f ) * 0.5f : 1f - ElasticIn( 2f - t * 2f ) * 0.5f; }

	public static float ElasticSoftIn( float t ) { return 1f - ElasticSoftOut( 1f - t ); }
	public static float ElasticSoftOut( float t ) { var p = 0.5f; return MathF.Pow( 2f, -10f * t ) * MathF.Sin( (t - p / 4f) * (2f * (float)Math.PI) / p ) + 1f; }
	public static float ElasticSoftInOut( float t ) { return t < 0.5f ? ElasticSoftIn( t * 2f ) * 0.5f : 1f - ElasticSoftIn( 2f - t * 2f ) * 0.5f; }

	public static float BackIn( float t ) { var p = 1f; return t * t * ((p + 1f) * t - p); }
	public static float BackOut( float t ) { var p = 1f; var scaledTime = t / 1f - 1f; return scaledTime * scaledTime * ((p + 1f) * scaledTime + p) + 1f; }
	public static float BackInOut( float t )
	{
		var p = 1f;
		var scaledTime = t * 2f;
		var scaledTime2 = scaledTime - 2f;
		var s = p * 1.525f;

		if ( scaledTime < 1f ) return 0.5f * scaledTime * scaledTime * ((s + 1f) * scaledTime - s);
		else return 0.5f * (scaledTime2 * scaledTime2 * ((s + 1f) * scaledTime2 + s) + 2f);
	}

	public static float BounceIn( float t ) { return 1f - BounceOut( 1f - t ); }
	public static float BounceOut( float t )
	{
		var scaledTime = t / 1f;

		if ( scaledTime < 1 / 2.75f )
		{
			return 7.5625f * scaledTime * scaledTime;
		}
		else if ( scaledTime < 2 / 2.75 )
		{
			var scaledTime2 = scaledTime - 1.5f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.75f;
		}
		else if ( scaledTime < 2.5 / 2.75 )
		{
			var scaledTime2 = scaledTime - 2.25f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.9375f;
		}
		else
		{
			var scaledTime2 = scaledTime - 2.625f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.984375f;
		}
	}
	public static float BounceInOut( float t )
	{
		if ( t < 0.5 ) return BounceIn( t * 2f ) * 0.5f;
		else return BounceOut( t * 2f - 1f ) * 0.5f + 0.5f;
	}

	public static void Shuffle<T>( this IList<T> list )
	{
		System.Random rng = new System.Random();
		int n = list.Count;
		while ( n > 1 )
		{
			n--;
			int k = rng.Next( n + 1 );
			T value = list[k];
			list[k] = list[n];
			list[n] = value;
		}
	}

	public static string FirstCharToUpper( this string input ) =>
		input switch
		{
			null => throw new ArgumentNullException( nameof( input ) ),
			"" => throw new ArgumentException( $"{nameof( input )} cannot be empty", nameof( input ) ),
			_ => string.Concat( input[0].ToString().ToUpper(), input.AsSpan( 1 ) )
		};

	public static string GetRandomIcon( string i0, string i1 ) { int rand = Game.Random.Int( 0, 1 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; } }
	public static string GetRandomIcon( string i0, string i1, string i2 ) { int rand = Game.Random.Int( 0, 2 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; } }
	public static string GetRandomIcon( string i0, string i1, string i2, string i3 ) { int rand = Game.Random.Int( 0, 3 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; case 3: return i3; } }
	public static string GetRandomIcon( string i0, string i1, string i2, string i3, string i4 ) { int rand = Game.Random.Int( 0, 4 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; case 3: return i3; case 4: return i4; } }
	public static string GetRandomIcon( string i0, string i1, string i2, string i3, string i4, string i5 ) { int rand = Game.Random.Int( 0, 5 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; case 3: return i3; case 4: return i4; case 5: return i5; } }
	public static string GetRandomIcon( string i0, string i1, string i2, string i3, string i4, string i5, string i6 ) { int rand = Game.Random.Int( 0, 6 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; case 3: return i3; case 4: return i4; case 5: return i5; case 6: return i6; } }
	public static string GetRandomIcon( string i0, string i1, string i2, string i3, string i4, string i5, string i6, string i7 ) { int rand = Game.Random.Int( 0, 7 ); switch ( rand ) { case 0: default: return i0; case 1: return i1; case 2: return i2; case 3: return i3; case 4: return i4; case 5: return i5; case 6: return i6; case 7: return i7; } }


	private class NodeInfo<T>
	{
		private const int MaxPoolSize = 8192;

		private static List<NodeInfo<T>> _sPool;

		internal static NodeInfo<T> Create( T node, NodeInfo<T> prev = null, float costAdd = 0f )
		{
			NodeInfo<T> nodeInfo;

			if ( _sPool == null || _sPool.Count == 0 )
			{
				nodeInfo = new NodeInfo<T>();
			}
			else
			{
				nodeInfo = _sPool[_sPool.Count - 1];
				_sPool.RemoveAt( _sPool.Count - 1 );
			}

			nodeInfo.Node = node;
			nodeInfo.Prev = prev;

			if ( prev == null )
			{
				nodeInfo.Depth = 0;
				nodeInfo.Cost = 0f;
			}
			else
			{
				nodeInfo.Depth = prev.Depth + 1;
				nodeInfo.Cost = prev.Cost + costAdd;
			}

			return nodeInfo;
		}

		internal static void Pool( NodeInfo<T> nodeInfo )
		{
			if ( _sPool == null ) _sPool = new List<NodeInfo<T>>( MaxPoolSize );
			if ( _sPool.Count >= MaxPoolSize ) return;

			_sPool.Add( nodeInfo );
		}

		private float _heuristic;

		public T Node { get; private set; }
		public NodeInfo<T> Prev { get; private set; }
		public int Depth { get; private set; }
		public float Cost { get; private set; }
		public float Total { get; private set; }

		public float Heuristic
		{
			get { return _heuristic; }
			set
			{
				_heuristic = value;
				Total = Cost + value;
			}
		}
	}

	/// <summary>
	/// Convenience method to produce a graph connection for use when calling AStar().
	/// </summary>
	/// <typeparam name="T">Graph node type.</typeparam>
	/// <param name="dest">Destination node of the connection.</param>
	/// <param name="cost">Cost of taking the connection.</param>
	public static AStarEdge<T> Edge<T>( T dest, float cost )
	{
		return new AStarEdge<T>( dest, cost );
	}

	private static class AStarWrapper<T>
		where T : IEquatable<T>
	{
		public static NodeInfo<T> FirstMatchOrDefault( List<NodeInfo<T>> list, T toCompare )
		{
			var count = list.Count;

			for ( var i = count - 1; i >= 0; --i )
			{
				var item = list[i];
				if ( item.Node.Equals( toCompare ) )
					return item;
			}

			return null;
		}

		private static List<NodeInfo<T>> _sOpen;
		private static List<NodeInfo<T>> _sClosed;

		public static bool AStar( T origin, T target, List<T> destList,
			Func<T, IEnumerable<AStarEdge<T>>> adjFunc, Func<T, T, float> heuristicFunc )
		{
			var open = _sOpen ?? (_sOpen = new List<NodeInfo<T>>());
			var clsd = _sClosed ?? (_sClosed = new List<NodeInfo<T>>());

			open.Clear();
			clsd.Clear();

			var first = NodeInfo<T>.Create( origin );
			first.Heuristic = heuristicFunc( origin, target );

			open.Add( first );

			try
			{
				while ( open.Count > 0 )
				{
					NodeInfo<T> cur = null;
					foreach ( var node in open )
					{
						if ( cur == null || node.Total < cur.Total ) cur = node;
					}

					if ( cur.Node.Equals( target ) )
					{
						for ( var i = cur.Depth; i >= 0; --i )
						{
							destList.Add( cur.Node );
							cur = cur.Prev;
						}
						destList.Reverse();
						return true;
					}

					open.Remove( cur );
					clsd.Add( cur );

					foreach ( var adj in adjFunc( cur.Node ) )
					{
						var node = NodeInfo<T>.Create( adj.Dest, cur, adj.Cost );
						var existing = FirstMatchOrDefault( clsd, adj.Dest );

						if ( existing != null )
						{
							if ( existing.Cost <= node.Cost ) continue;

							clsd.Remove( existing );
							node.Heuristic = existing.Heuristic;

							NodeInfo<T>.Pool( existing );
						}

						existing = FirstMatchOrDefault( open, adj.Dest );

						if ( existing != null )
						{
							if ( existing.Cost <= node.Cost ) continue;

							open.Remove( existing );
							node.Heuristic = existing.Heuristic;

							NodeInfo<T>.Pool( existing );
						}
						else
						{
							node.Heuristic = heuristicFunc( node.Node, target );
						}

						open.Add( node );
					}
				}
				return false;

			}
			finally
			{
				foreach ( var nodeInfo in open )
				{
					NodeInfo<T>.Pool( nodeInfo );
				}

				foreach ( var nodeInfo in clsd )
				{
					NodeInfo<T>.Pool( nodeInfo );
				}
			}
		}
	}

	/// <summary>
	/// An implementation of the AStar path finding algorithm.
	/// </summary>
	/// <typeparam name="T">Graph node type.</typeparam>
	/// <param name="origin">Node to start path finding from.</param>
	/// <param name="target">The goal node to reach.</param>
	/// <param name="adjFunc">Function returning the neighbouring connections for a node.</param>
	/// <param name="heuristicFunc">Function returning the estimated cost of travelling between two nodes.</param>
	/// <returns>A sequence of nodes representing a path if one is found, otherwise an empty array.</returns>
	public static bool AStar<T>( T origin, T target, List<T> destPath,
		Func<T, IEnumerable<AStarEdge<T>>> adjFunc, Func<T, T, float> heuristicFunc )
		where T : IEquatable<T>
	{
		return AStarWrapper<T>.AStar( origin, target, destPath, adjFunc, heuristicFunc );
	}
}
using Sandbox;
using System.Threading.Tasks;

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

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.TurnStart && Manager.Instance.GetNumNearbyEmptyGridPositions( GridPos, adjacentOnly: false ) > 0;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		var emptyGridPos = Manager.Instance.GetRandomNearbyGridPos( GridPos, empty: true, adjacentOnly: false );

		await Task.DelayRealtime( 200 );

		Manager.Instance.PlayCardSfx( "card_move", this, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );

		await Task.DelayRealtime( 100 );

		Manager.Instance.RemoveCardGridPos( this );
		await Manager.Instance.SetCardGridPos( this, emptyGridPos );

		await Task.DelayRealtime( 300 );

		await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardCompass : Card
{
	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 )
	{
		var otherCard = Manager.Instance.ChosenCards[0] == this ? Manager.Instance.ChosenCards[1] : Manager.Instance.ChosenCards[0];

		var matches = Manager.Instance.GetCardsOfType( otherCard.CardType, except: otherCard );
		if ( matches.Count == 0 )
			return;

		var match = matches[Game.Random.Int( 0, matches.Count - 1 )];

		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 300 );

		MoveToPos( LocalPosition + (match.LocalPosition - LocalPosition).Normal * 5f, 0.25f, EasingType.QuadInOut, removeControlAfter: true );

		Manager.Instance.PlayCardSfx( "compass", this, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 0.95f ) );

		await Task.DelayRealtime( 500 );

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

public class CardFlashlight : Card
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Choose && Manager.Instance.ChosenCard == this;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		var nearbyCards = Manager.Instance.GetNearbyCards( GridPos ).Where(x => !x.IsRevealed).ToList();
		if ( nearbyCards.Count == 0 )
			return;

		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 150 );

		Manager.Instance.PlayCardSfx( "flashlight", this, volume: 1.2f, pitch: Game.Random.Float( 1f, 1.05f ) );

		await Task.DelayRealtime( 150 );

		Card cardToReveal = nearbyCards[Game.Random.Int( 0, nearbyCards.Count - 1 )]; ;
		await Manager.Instance.RevealCard( cardToReveal );

		await Task.DelayRealtime( 500 );

		Manager.Instance.PlayCardSfx( "flashlight", this, volume: 1.2f, pitch: Game.Random.Float( 0.75f, 0.8f ) );

		await Task.DelayRealtime( 250 );

		Manager.Instance.HideCard( cardToReveal );

		await Task.DelayRealtime( 200 );

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

public class CardRabbit : Card
{
	public override bool IsAlive => true;
	
	public override bool ValidateStartingGridPos()
	{
		var rabbits = Manager.Instance.Cards.Where( x => x.CardType == CardType.Rabbit ).ToList();

		var rabbit0 = rabbits[0];
		var rabbit1 = rabbits[1];

		int farthestDist = GetFarthestDistance( rabbit0, rabbit1 );

		if ( rabbits.Count < 2 || farthestDist > 3 )
			return true;

		int NUM_TRIES = 100;
		for ( int i = 0; i < NUM_TRIES; i++ )
		{
			var newGridPos = Manager.Instance.GetRandomGridPos( except: rabbit0.GridPos );
			var otherCard = Manager.Instance.GetCardAtGridPos( newGridPos );

			if ( GetFarthestDistance( otherCard, rabbit1 ) > 3 && otherCard.CardType != CardType.Map )
			{
				Manager.Instance.SwapCardPositionsNonAsync( rabbit0, otherCard );
				break;
			}
		}

		if ( !Manager.IsNearby( rabbit0.GridPos, rabbit1.GridPos ) )
			return true;

		NUM_TRIES = 100;
		for ( int i = 0; i < NUM_TRIES; i++ )
		{
			var newGridPos = Manager.Instance.GetRandomGridPos( except: rabbit0.GridPos );
			var otherCard = Manager.Instance.GetCardAtGridPos( newGridPos );

			if ( !Manager.IsNearby( newGridPos, rabbit1.GridPos ) && otherCard.CardType != CardType.Map )
			{
				Manager.Instance.SwapCardPositionsNonAsync( rabbit0, otherCard );
				break;
			}
		}

		return false;
	}

	int GetFarthestDistance(Card card0, Card card1)
	{
		return Math.Max(Math.Abs( card0.GridPos.x - card1.GridPos.x ), Math.Abs( card0.GridPos.y - card1.GridPos.y ));
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.TurnStart && Game.Random.Float(0f, 1f) < 0.5f;
	}

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

		if ( Manager.IsAdjacent( GridPos, match.GridPos ) )
			return;

		var path = Manager.Instance.GetPathTo( GridPos, match.GridPos );

		if ( path.Count <= 1 )
			return;

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

		await Task.DelayRealtime( 150 );

		IntVector2 targetGridPos = path.FirstOrDefault();

		var targetCard = Manager.Instance.GetCardAtGridPos( targetGridPos );

		if ( targetCard != null )
			Manager.Instance.PlayCardSfxBetween( "card_move", this, targetCard, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );
		else
			Manager.Instance.PlayCardSfx( "card_move", this, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );

		this.MoveToPos( this.LocalPosition.WithZ( Game.Random.Float( 70f, 90f ) ), 0.3f, EasingType.SineOut );
		targetCard?.MoveToPos( targetCard.LocalPosition.WithZ( Game.Random.Float( 70f, 90f ) ), 0.25f, EasingType.SineOut );

		await Task.DelayRealtime( 250 );

		this.MoveToPos( Manager.GetCardPos( targetGridPos ).WithZ( this.LocalPosition.z ), 0.45f, EasingType.SineInOut );
		targetCard?.MoveToPos( Manager.GetCardPos( this.GridPos ).WithZ( targetCard.LocalPosition.z ), 0.45f, EasingType.SineInOut );

		await Task.DelayRealtime( 450 );

		this.MoveToPos( this.LocalPosition.WithZ( Globals.CARD_DEFAULT_HEIGHT + Globals.CARD_ADD_HEIGHT_REVEALED_OR_HOVERED ), 0.2f, EasingType.SineOut );
		targetCard?.MoveToPos( targetCard.LocalPosition.WithZ( Globals.CARD_DEFAULT_HEIGHT ), 0.2f, EasingType.SineOut );

		await Task.DelayRealtime( 200 );

		if ( targetCard != null )
		{
			await Manager.Instance.SwapCardPositions( this, targetCard );
		}
		else
		{
			Manager.Instance.RemoveCardGridPos( this );
			await Manager.Instance.SetCardGridPos( this, targetGridPos );
		}

		this.IsMovementControlled = false;
		if ( targetCard != null )
			targetCard.IsMovementControlled = false;

		await Task.DelayRealtime( 300 );

		//Manager.Instance.PopEventMessage();

		await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
	}
}
using Sandbox;
using System.Threading.Tasks;

public class CardSteak : Card
{
	public override bool IsFoodOrBeverage => true;

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

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

		await Task.DelayRealtime( 50 );

		Manager.Instance.PlayCardSfxBetween( "meat_pay", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.2f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		await Task.DelayRealtime( 50 );

		int loseMoneyAmount = 1;

		Manager.Instance.SpawnLoseMoneyFloater( loseMoneyAmount, WorldPosition );

		await Manager.Instance.LoseMoney( loseMoneyAmount );

		await Task.DelayRealtime( 900 );

		Manager.Instance.PlayCardSfxBetween( "meat_eat", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.3f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		int healAmount = 4 + (int)Manager.Instance.Stats[StatType.FoodAdditionalHeal];

		Manager.Instance.SpawnHealHPFloater( healAmount, WorldPosition );

		await Manager.Instance.GainHP( healAmount );

		await Task.DelayRealtime( 800 );

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

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

		int healAmount = 3;

		Manager.Instance.HP = Math.Min( Manager.Instance.HP + healAmount, Manager.Instance.MaxHP );

		Manager.Instance.TimeSinceHPChanged = 0f;

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

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

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

		// todo: better sfx
		Manager.Instance.PlaySfx( "bandage", tr.EndPosition.WithZ( camera.WorldPosition.z - 100f ), volume: 0.7f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

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

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

		Manager.Instance.Stats[StatType.ExistingItemExtraChance] += 1f;
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

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

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

		await Task.DelayRealtime( 200 );

		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( "medic_helmet", tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF ), volume: 2.1f, pitch: Game.Random.Float( 1.2f, 1.3f ) );

		await Task.DelayRealtime( 300 );

		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.Threading.Tasks;

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

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

		List<Card> validCards = new();
		foreach ( var card in Manager.Instance.Cards )
			validCards.Add( card );

		await Task.DelayRealtime( 150 );

		Manager.Instance.PlaySfxCenter( "satellite", volume: 0.95f, pitch: Game.Random.Float( 0.9f, 1f ) );

		await Task.DelayRealtime( 350 );

		validCards.Shuffle();

		for ( int i = 0; i < Math.Min( Level * 3, validCards.Count ); i++ )
		{
			var card = validCards[i];

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

			await Task.DelayRealtime( 360 );
		}

		await Task.DelayRealtime( 400 );

		for ( int i = 0; i < Math.Min( Level * 3, validCards.Count ); i++ )
		{
			var card = validCards[i];

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

			await Task.DelayRealtime( 300 );
		}

		await Task.DelayRealtime( 200 );

		Manager.Instance.PopEventMessage();

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

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

		Manager.Instance.Stats[StatType.ShopExtraItems] += 1f;
	}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;

public class RelicTriangleRuler : Relic
{
	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.Match && Manager.Instance.Stats[StatType.NumMatches] % (5 - Level) == 0;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		List<Card> validCards = new();

		CheckCornerCard( new IntVector2( 0, 0 ), validCards );
		CheckCornerCard( new IntVector2( 0, Manager.Instance.GridHeight - 1 ), validCards );
		CheckCornerCard( new IntVector2( Manager.Instance.GridWidth - 1, 0 ), validCards );
		CheckCornerCard( new IntVector2( Manager.Instance.GridWidth - 1, Manager.Instance.GridHeight - 1 ), validCards );

		if ( validCards.Count == 0 )
			return;

		validCards.Shuffle();

		var cardToReveal = validCards.First();

		await Task.DelayRealtime( 300 );

		Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 100 );

		Manager.Instance.PlaySfxCenter( "triangle_ruler", volume: 0.3f, pitch: Game.Random.Float( 1.5f, 1.55f ) );

		await Task.DelayRealtime( 200 );

		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( 850 );

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

		await Task.DelayRealtime( 350 );

		Manager.Instance.PopEventMessage();
	}

	void CheckCornerCard( IntVector2 gridPos, List<Card> cards )
	{
		var card = Manager.Instance.GetCardAtGridPos( gridPos );

		if ( card != null && !Manager.Instance.ChosenCards.Contains( card ) && !card.IsRevealed )
			cards.Add( card );
	}
}