s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (222 total matches found)
facepunch.ss1 / ui/panels/infopanel.razor.scss
Game game
InfoPanel {
    position: absolute;
    width: 280px;
    left: 20px;
    bottom: 20px;
    top: 20px;
    align-self: flex-end;
    pointer-events: none;
    justify-content: flex-start;
    flex-direction: column-reverse;
    gap: 10px;
    //background-color: red;

    .info_dash_container {
        width: 100%;
        //flex-direction: column-reverse;
        flex-direction: row;
        align-items: center;
        gap: 8px;
    }

    .dash {
        position: relative;
        flex-shrink: 1;
        width: 100%;
        height: 28px;
        background-color: #0005;
    }

    .info_bar {
        position: relative;
        width: 100%;
        overflow: hidden;
        height: 28px;
        background-color: #00000099;
        //border-left: 2px solid #fff;
        //border-right: 2px solid #fff;
        flex-shrink: 1;
        //border: 2px solid black;

        .info_bar_overlay {
            position: absolute;
            left: 0px;
            width: 100%;
            height: 100%;
        }
    }

    .data_container {
        width: 100%;
        height: 100%;
        flex-direction: row-reverse;
    }

    .info_bar_label {
        position: absolute;
        color: #fff;
        font-size: 26px;
        align-items: center;
        font-weight: bolder;
        font-family: "Mini Pixel-7";
        //width: 100%;
        left: 0px;
        justify-content: flex-start;
        padding-left: 5px;
        padding-top: 1px;
        text-stroke: 5px black;
        opacity: 0.7;
    }

    .data {
        position: relative;
        color: #ffffffaa;
        font-size: 26px;
        align-items: center;
        font-family: "Mini Pixel-7";
        //left: 0px;
        padding-right: 5px;
        padding-top: 1px;
        text-stroke: 5px black;
        opacity: 0.9;
        //letter-spacing: 1px;
    }
    //.data_center {
    //    position: absolute;
    //    color: #ffffff88;
    //    font-size: 26px;
    //    align-items: center;
    //    justify-content: center;
    //    font-family: "Mini Pixel-7";
    //left: 0px;
    //    width: 100%;
    //    padding-left: 5px;
    //    padding-top: 1px;
    //    text-stroke: 3px #00000099;
    //    opacity: 0.9;
    //}
}
facepunch.ss1 / Camera2D.cs
Game game
using Sandbox;

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

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

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

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

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

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

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

	public void SetPos( Vector2 pos )
	{
		CameraPos = pos;
		TargetPos = pos;
		WorldPosition = ((Vector3)pos).WithZ( WorldPosition.z );
	}
}
facepunch.ss1 / FloatingDamageNumber.cs
Game game
using Sandbox;

public class FloatingDamageNumber : Component
{
	private RealTimeSince _timeSince;

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

		_timeSince = 0f;
	}

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

		if ( _timeSince > 1f )
			GameObject.Destroy();
	}
}
facepunch.ss1 / LavaPuddle.cs
Game game
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using static Manager;

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

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

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

	private float _expandTime;

	public float DamageToPlayer { get; set; }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	public string GetPrintAmountForLevel( int level )
	{
		return string.Format( "{0:0.0}", GetAddForLevel( level ) );
	}
}
facepunch.ss1 / status/CritChanceStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 5 + 8 * level + (level == 7 ? 4 : 0);
	}
}
facepunch.ss1 / status/CurseAimDir.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 25 * level;
	}
}
facepunch.ss1 / status/CurseAttackSpeedStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 20 * level;
	}
}
facepunch.ss1 / status/DashStrengthStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 18 * level + (level == 4 ? 3 : 0);
	}
}
facepunch.ss1 / status/FearDropGrenadeStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return level * 7;
	}
}
facepunch.ss1 / status/FearPainStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 2 + level * 7 + (level == 5 ? 1 : 0);
	}
}
facepunch.ss1 / status/GrenadeFearStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

    public float GetPercentForLevel(int level)
    {
        return level == 4 ? 100 : 30 * level;
    }
}
facepunch.ss1 / status/KillHealStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

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

		var amount = GetAmountForLevel( Level );

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

		Player.RegenHealth( amount );

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

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

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

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

    public string GetHpDrainPrintAmountForLevel(int level)
    {
        return string.Format("{0:0.00}", GetHpDrainAmountForLevel(level));
    }
}
facepunch.ss1 / status/MoreRerollsStatus.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

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

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

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

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

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

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

	public float GetAddForLevel(int level)
    {
		return 1f * level;
    }
}
facepunch.ss1 / status/MovespeedStatus.cs
Game game
using Sandbox;

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

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

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

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

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

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

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

	public float GetPercentForLevel( int level )
	{
		return 20 * level + (level == 7 ? 20 : 0);
	}
}
facepunch.ss1 / status/Status.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

	public string DataString { get; protected set; }

	public Status()
	{
		Level = 1;
	}

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

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

	}

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

	public virtual void Remove()
	{

	}

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

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

	public virtual void Colliding( Thing other, float percent, float dt ) { }
	public virtual void OnDashStarted() { }
	public virtual void OnDashFinished() { }
	public virtual void OnDashRecharged() { }
	public virtual void OnReload() { }
	//public virtual void OnBurn( Enemy enemy ) { }
	//public virtual void OnFreeze( Enemy enemy ) { }
	//public virtual void OnFear( Enemy enemy ) { }
	public virtual void OnKill( Enemy enemy ) { }
	public virtual void OnHurt( float amount ) { }
	public virtual void OnGainExperience( int xp ) { }
	public virtual void OnLevelUp() { }
	public virtual void OnReroll() { }
	public virtual void OnGainShield() { }
	public virtual void OnLoseShield() { }
	public virtual void OnAddStatus( int typeIdentity ) { }
}
facepunch.ss1 / things/enemies/Charger.cs
Game game
using Sandbox;
using Sandbox.ModelEditor.Nodes;
using System.Net.NetworkInformation;

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

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

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

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

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

	public float ChargeRange { get; set; }

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

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

		Scale = 1.25f;

		base.OnAwake();

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

		PushStrength = 25f;

		Radius = 0.275f;

		Health = 75f;

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

		MaxHealth = Health;
		DamageToPlayer = 10f;

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

		Sprite.PlayAnimation( AnimSpawnPath );

		if ( IsProxy )
			return;

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

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

		ChargeRange = 4.2f;
	}

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

		base.UpdatePosition( dt );

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

						player.Damage( dmg );

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

					_damageTime = 0f;
				}
			}
		}
	}

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

		CelebrateAsync();
	}

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

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

		Sprite.PlayAnimation( "cheer_start" );

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

		Sprite.PlayAnimation( "cheer" );
	}
}
facepunch.ss1 / things/enemies/RunnerEliteSpecial.cs
Game game
using Sandbox;

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

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

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

		Health = 300f;
		MaxHealth = Health;

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

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

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

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

		MoveSpeed = 0.35f;

		JUMP_DELAY_MIN = 4f;
		JUMP_DELAY_MAX = 10f;
	}

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

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

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

		if ( _isJumping )
			return;

		CelebrateAsync();
	}

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

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

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

		Sprite.PlayAnimation( "cheer" );
	}
}
facepunch.ss1 / things/enemies/Spiker.cs
Game game
using Sandbox;
using static Sandbox.VertexLayout;

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

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

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

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

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

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

	public float ShootRange { get; set; }

	public float MoveSpeed { get; set; }

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

		Scale = 1.4f;

		base.OnAwake();

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

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

		Radius = 0.27f;

		Health = 80f;

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

		MaxHealth = Health;
		DamageToPlayer = 14f;

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

		Sprite.PlayAnimation( AnimSpawnPath );

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

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

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

		ShootRange = 4.5f;

		MoveSpeed = 0.9f;
	}

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

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

		var targetPos = GetTargetPos();

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

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

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

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

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

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

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

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

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

				IgnoreCollision = progress > 0.3f;

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

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

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

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

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

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

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

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

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

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

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

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

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

		base.UpdateSprite( target );
	}

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

		ShouldUpdateAfterGameOver = true;
	}

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

		var BUFFER = 0.1f;

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

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

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

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

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

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

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

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

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

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

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

						player.Damage( dmg );

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

					_damageTime = 0f;
				}
			}
		}
	}

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

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

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

		ShouldUpdateAfterGameOver = true;
	}

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

		Transform.ClearInterpolation();

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

		_moveClockwise = !_moveClockwise;

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

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

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

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

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

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

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

		if ( IsShooting || IsDigging )
			return;

		CelebrateAsync();
	}

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

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

		Sprite.PlayAnimation( "cheer_start" );

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

		Sprite.PlayAnimation( "cheer" );
	}

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

		ShouldUpdateAfterGameOver = false;
		if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
			Celebrate();
	}
}
facepunch.ss1 / things/Player.cs
Game game
using static Manager;

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

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

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

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

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

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

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


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

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

	public GameObject ArrowAimer { get; private set; }
	public Sprite2D ArrowSprite { get; private set; }
	public Vector2 AimDir { get; private set; }
	private Vector2 _controllerAimTarget;
	private const float CONTROLLER_AIM_DEADZONE = 0.25f;

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

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

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

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

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


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

	private GameObject _shieldVfx;

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

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

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

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

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

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

	private bool _hasUnlockedExperiencedAchievement;

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

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

	public int ChoiceHash { get; set; }

	public TimeSince TimeSinceHurtLava { get; set; }

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

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

		Scale = 1f;

		ShadowOpacity = 0.8f;
		ShadowScale = 1.12f;

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

		if ( IsProxy )
			return;

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

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

		_timeSinceShoot = 999f;
		_timeSinceSpawn = 0f;

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

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

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

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

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

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

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

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

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

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

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

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

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

		_timeSinceTouchLeftSide = 999f;
		TimeSinceInputMove = 0f;

		TimeSinceLevelUp = 999f;
		RealTimeSinceChoseUpgrade = 999f;

		_camShakeDatas.Clear();

		ChoiceHash = 0;
	}

	public Vector2 AverageVelocity { get; private set; }

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

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

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

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

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

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

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

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

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

			_doneFirstUpdate = true;
		}

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

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

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

		HandleCamShaking();

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

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

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

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

		if ( !Manager.Instance.ShouldUpdatePlayer )
			return;

		float dt = Time.Delta;

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

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

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

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

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

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

		if ( IsProxy )
			return;

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

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

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

					_hasUnlockedSprinterAchievement = true;
				}
			}

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

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

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

		_lastPos2D = Position2D;

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

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

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

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

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

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

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

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

		HandleBounds();

		Manager.Instance.Camera2D.TargetPos = Position2D;

		if ( Input.UsingController )
		{
			// raw stick rather than AnalogLook, which is scaled by look sensitivity and affected by invert-y (SDL stick y is positive downward)
			var stick = new Vector2( Input.GetAnalog( InputAnalog.RightStickX ), -Input.GetAnalog( InputAnalog.RightStickY ) );
			if ( stick.Length > CONTROLLER_AIM_DEADZONE )
				_controllerAimTarget = stick.Normal;

			if ( _controllerAimTarget.LengthSquared > 0f )
			{
				// reverse here rather than when storing the target, so it still flips when ReverseControls toggles while the stick is released
				var target = Stats[PlayerStat.ReverseControls] > 0f ? -_controllerAimTarget : _controllerAimTarget;

				// rotate by angle, since a linear lerp toward the exact opposite direction never changes direction
				// keeps turning after the stick is released, so a quick flick still reaches the target
				float currentDegrees = Utils.VectorToDegrees( AimDir );
				float deltaDegrees = ((Utils.VectorToDegrees( target ) - currentDegrees + 540f) % 360f) - 180f;
				AimDir = Utils.DegreesToVector( currentDegrees + deltaDegrees * MathF.Min( Time.Delta * 25f, 1f ) );
			}
		}
		else
		{
			AimDir = (Manager.Instance.MouseWorldPos - (Position2D + new Vector2( 0f, 0.5f ))).Normal;

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

			// forget the last stick direction, so switching back to the controller keeps the mouse aim instead of swinging back to it
			_controllerAimTarget = Vector2.Zero;
		}

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

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

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

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

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

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

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

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

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

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

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

			_regenHpAccumulated += amount;

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

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

			Health += hpRecovered;

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

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

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

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

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

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

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

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

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

		bool pressedDash = Input.Pressed( "Dash" ) && !Gamepad.IsAClickingUi;
		if ( (pressedDash ||Input.Pressed( "RB" )) && !Manager.Instance.IsPauseMenuOpen )
		{
			//Position2D = Manager.Instance.MouseWorldPos;
			////Manager.Instance.Camera2D.SetPos( Position2D );
			//Transform.ClearInterpolation();
			//return;

			Dash();
		}
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if ( IsProxy )
			return;

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

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

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

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

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

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

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

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

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

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

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

		status.Refresh();

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

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

		CheckForLevelUp();
	}

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

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

		return null;
	}

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

		return 0;
	}

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

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

		if ( update )
			UpdateProperty( statType );
	}

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

		_original_properties_stat[statType] += amount;

		if ( update )
			UpdateProperty( statType );
	}

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

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

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

		if ( should_set )
			curr_value = curr_set;

		curr_value += total_add;
		curr_value *= total_mult;

		Stats[statType] = curr_value;

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

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

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

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

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

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

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

	public void LevelUp()
	{
		ExperienceCurrent -= ExperienceRequired;

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

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

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

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

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

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

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

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

		NumRerollAvailable--;

		GenerateLevelUpChoices();

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

	/// <summary>
	/// Number of level up choices that have a hotkey (Slot1..Slot6 actions).
	/// </summary>
	public const int NUM_CHOICE_HOTKEYS = 6;

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

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

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

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

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

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

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

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

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

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

		return damage;
	}

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

		TimeSinceHurt = 0f;

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

		if ( isCrit )
			damage *= 2f;

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

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

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

		var pos = Position2D + offset;

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

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

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

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

		Health -= damage;

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

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

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

		Velocity += vel;
	}

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

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

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

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

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

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

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

		Sprite.LocalScale *= 2f;

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

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

		if ( IsProxy )
			return;

		Manager.Instance.PlayerDied( this );
	}

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

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

		if ( IsProxy )
			return;

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

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

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

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

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

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

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

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

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

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

		var pos = Position2D + AimDir * 0.3f;

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

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

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

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

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

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

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

		_shotNum++;
		_timeSinceShoot = 0f;
	}

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

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

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

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

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

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

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

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

		bullet.Init();

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

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

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

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

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

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

		return damageMultiplier;
	}

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

		if ( IsDead )
			return;

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

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

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

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

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

		//	LevelUpChoices.Shuffle();

		//	return;
		//}

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

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

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

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

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

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

		ChoiceHash++;
	}

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

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

		return false;
	}

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

		TypeDescription chosenPerk = null;

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

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

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

		return chosenPerk;
	}

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

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

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

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

		if ( IsProxy )
			return;

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

		InitializeStats();

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

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

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

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

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

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

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

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

		Manager.Instance.AddThing( grenade );

		return grenade;
	}

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

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

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

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

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

		Manager.Instance.AddPlayer( this );
	}

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

		Manager.Instance.RemovePlayer( this );
	}

	void HandleCamShaking()
	{
		CamShakeAmount = 0f;

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

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

	public void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = false )
	{
		var timeNow = useRealTime ? RealTime.Now : Time.Now;
		_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 222,
    "Files": [
        {
            "Ident": "facepunch.ss1",
            "Path": "ui/panels/infopanel.razor.scss",
            "FileName": "infopanel.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "InfoPanel {\r\n    position: absolute;\r\n    width: 280px;\r\n    left: 20px;\r\n    bottom: 20px;\r\n    top: 20px;\r\n    align-self: flex-end;\r\n    pointer-events: none;\r\n    justify-content: flex-start;\r\n    flex-direction: column-reverse;\r\n    gap: 10px;\r\n    //background-color: red;\r\n\r\n    .info_dash_container {\r\n        width: 100%;\r\n        //flex-direction: column-reverse;\r\n        flex-direction: row;\r\n        align-items: center;\r\n        gap: 8px;\r\n    }\r\n\r\n    .dash {\r\n        position: relative;\r\n        flex-shrink: 1;\r\n        width: 100%;\r\n        height: 28px;\r\n        background-color: #0005;\r\n    }\r\n\r\n    .info_bar {\r\n        position: relative;\r\n        width: 100%;\r\n        overflow: hidden;\r\n        height: 28px;\r\n        background-color: #00000099;\r\n        //border-left: 2px solid #fff;\r\n        //border-right: 2px solid #fff;\r\n        flex-shrink: 1;\r\n        //border: 2px solid black;\r\n\r\n        .info_bar_overlay {\r\n            position: absolute;\r\n            left: 0px;\r\n            width: 100%;\r\n            height: 100%;\r\n        }\r\n    }\r\n\r\n    .data_container {\r\n        width: 100%;\r\n        height: 100%;\r\n        flex-direction: row-reverse;\r\n    }\r\n\r\n    .info_bar_label {\r\n        position: absolute;\r\n        color: #fff;\r\n        font-size: 26px;\r\n        align-items: center;\r\n        font-weight: bolder;\r\n        font-family: \"Mini Pixel-7\";\r\n        //width: 100%;\r\n        left: 0px;\r\n        justify-content: flex-start;\r\n        padding-left: 5px;\r\n        padding-top: 1px;\r\n        text-stroke: 5px black;\r\n        opacity: 0.7;\r\n    }\r\n\r\n    .data {\r\n        position: relative;\r\n        color: #ffffffaa;\r\n        font-size: 26px;\r\n        align-items: center;\r\n        font-family: \"Mini Pixel-7\";\r\n        //left: 0px;\r\n        padding-right: 5px;\r\n        padding-top: 1px;\r\n        text-stroke: 5px black;\r\n        opacity: 0.9;\r\n        //letter-spacing: 1px;\r\n    }\r\n    //.data_center {\r\n    //    position: absolute;\r\n    //    color: #ffffff88;\r\n    //    font-size: 26px;\r\n    //    align-items: center;\r\n    //    justify-content: center;\r\n    //    font-family: \"Mini Pixel-7\";\r\n    //left: 0px;\r\n    //    width: 100%;\r\n    //    padding-left: 5px;\r\n    //    padding-top: 1px;\r\n    //    text-stroke: 3px #00000099;\r\n    //    opacity: 0.9;\r\n    //}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "Camera2D.cs",
            "FileName": "Camera2D.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\npublic class Camera2D : Component\r\n{\r\n\t[Property] public CameraComponent Camera { get; set; }\r\n\r\n\tpublic Vector2 TargetPos { get; set; }\r\n\tpublic Vector2 CameraPos { get; set; }\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\t//Vector2 newPos = Vector2.Lerp( (Vector2)WorldPosition, TargetPos, Time.Delta * 3f );\r\n\t\tCameraPos = Utils.DynamicEaseTo( CameraPos, TargetPos, 0.2f, Time.Delta );\r\n\r\n\t\tvar newPos = CameraPos + Utils.GetRandomVector() * Manager.Instance.Player.CamShakeAmount;\r\n\r\n\t\tfloat bounds_zoom = 1f + Manager.Instance.Player.Stats[PlayerStat.ZoomAmount] * 0.44f;\r\n\t\tvar XDIST = 10.75f / bounds_zoom;\r\n\t\tvar Y_MIN = -8.8f / bounds_zoom;\r\n\t\tvar Y_MAX = 8.9f / bounds_zoom;\r\n\t\tnewPos = new Vector2( MathX.Clamp( newPos.x, -XDIST, XDIST ), MathX.Clamp( newPos.y, Y_MIN, Y_MAX ) );\r\n\r\n\t\tWorldPosition = ((Vector3)newPos).WithZ( WorldPosition.z );\r\n\t}\r\n\r\n\tpublic void SetPos( Vector2 pos )\r\n\t{\r\n\t\tCameraPos = pos;\r\n\t\tTargetPos = pos;\r\n\t\tWorldPosition = ((Vector3)pos).WithZ( WorldPosition.z );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "FloatingDamageNumber.cs",
            "FileName": "FloatingDamageNumber.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\npublic class FloatingDamageNumber : Component\r\n{\r\n\tprivate RealTimeSince _timeSince;\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\t_timeSince = 0f;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\tif ( _timeSince > 1f )\r\n\t\t\tGameObject.Destroy();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "LavaPuddle.cs",
            "FileName": "LavaPuddle.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox.UI;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Linq.Expressions;\r\nusing System.Text;\r\nusing System.Threading.Tasks;\r\nusing static Manager;\r\n\r\npublic class LavaPuddle : Component\r\n{\r\n\t[Property] public Sprite2D Sprite { get; set; }\r\n\r\n\tpublic TimeSince TimeSinceSpawn { get; set; }\r\n\tpublic float Lifetime { get; set; }\r\n\t\r\n\tpublic float Radius { get; set; }\r\n\tpublic float FullRadius { get; set; }\r\n\r\n\tprivate float _timeOffset;\r\n\tprivate TimeSince _timeSinceDamagePlayer;\r\n\tprivate const float DAMAGE_INTERVAL = 0.25f;\r\n\r\n\tprivate float _expandTime;\r\n\r\n\tpublic float DamageToPlayer { get; set; }\r\n\r\n\tpublic Color ColorA { get; set; }\r\n\tpublic Color ColorB { get; set; }\r\n\r\n\tpublic Vector2 Position2D\r\n\t{\r\n\t\tget { return (Vector2)WorldPosition; }\r\n\t\tset { WorldPosition = new Vector3( value.x, value.y, WorldPosition.z ); }\r\n\t}\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\tif ( Game.Random.Float( 0f, 1f ) < 0.5f )\r\n\t\t\tSprite.SpriteFlags = SpriteFlags.HorizontalFlip;\r\n\r\n\t\tRadius = 0f;\r\n\t\tLocalScale = new Vector3( Radius * 1f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;\r\n\r\n\t\t//LocalScale = new Vector3( Game.Random.Float( 0.4f, 0.6f ), Game.Random.Float( 0.15f, 0.25f ), 1f ) * Globals.SPRITE_SCALE;\r\n\t\t//LocalScale = new Vector3( Game.Random.Float( 0.33f, 0.48f ), Game.Random.Float( 0.4f, 0.63f ), 1f ) * 5f * Globals.SPRITE_SCALE;\r\n\r\n\t\t_timeOffset = Game.Random.Float( 0f, 99f );\r\n\r\n\t\tTimeSinceSpawn = 0f;\r\n\t\t_expandTime = Game.Random.Float( 0.4f, 0.5f );\r\n\r\n\t\tDamageToPlayer = 2f + Math.Min(Manager.Instance.Difficulty, 5);\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\tfloat FADE_IN_TIME = 0.3f;\r\n\t\tvar color = Color.Lerp( ColorA, ColorB, 0.5f + Utils.FastSin( TimeSinceSpawn * 16f ) * 0.5f );\r\n\t\tvar opacity = 0.3f \r\n\t\t\t* Utils.Map( TimeSinceSpawn, 0f, FADE_IN_TIME, 0f, 1f, EasingType.SineOut ) \r\n\t\t\t* Utils.Map( TimeSinceSpawn, Lifetime - 1f, Lifetime, 1f, 0f, EasingType.SineIn ) \r\n\t\t\t* ( 0.85f + Utils.FastSin( _timeOffset + Time.Now * 16f ) * 0.15f );\r\n\t\tSprite.Tint = color.WithAlpha( opacity );\r\n\r\n\t\t//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );\r\n\t\t//Gizmo.Draw.LineSphere( WorldPosition, Radius, 20 );\r\n\r\n\t\tif (TimeSinceSpawn < _expandTime )\r\n\t\t{\r\n\t\t\tRadius = Utils.Map( TimeSinceSpawn, 0f, _expandTime, FullRadius * 0.05f, FullRadius, EasingType.QuadOut );\r\n\t\t\tLocalScale = new Vector3( Radius * 0.98f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;\r\n\t\t}\r\n\r\n\t\tif ( TimeSinceSpawn > Lifetime )\r\n\t\t{\r\n\t\t\tManager.Instance.RemoveLavaPuddle( this );\r\n\t\t\tGameObject.Destroy();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif( TimeSinceSpawn > FADE_IN_TIME && TimeSinceSpawn < Lifetime - 0.5f ) \r\n\t\t{\r\n\t\t\tvar player = Manager.Instance.Player;\r\n\t\t\tfloat distSqr = (player.Position2D - Position2D).LengthSquared;\r\n\r\n\t\t\tif( distSqr < MathF.Pow(Radius, 1.9f) && _timeSinceDamagePlayer > DAMAGE_INTERVAL && player.TimeSinceHurtLava > DAMAGE_INTERVAL && !player.IsDead )\r\n\t\t\t{\r\n\t\t\t\tfloat currDmg = Utils.Map( TimeSinceSpawn, FADE_IN_TIME, FADE_IN_TIME * 2f, 1f, DamageToPlayer );\r\n\t\t\t\tfloat dmg = player.CheckDamageAmount( currDmg, DamageType.LavaPuddle );\r\n\r\n\t\t\t\tif ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )\r\n\t\t\t\t{\r\n\t\t\t\t\tplayer.Damage( dmg );\r\n\r\n\t\t\t\t\t_timeSinceDamagePlayer = 0f;\r\n\t\t\t\t\tplayer.TimeSinceHurtLava = 0f;\r\n\r\n\t\t\t\t\tManager.Instance.PlaySfxNearby( \"lava_puddle_03\", player.Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1f, maxDist: 4f );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/BulletLifetimeDamageStatus.cs",
            "FileName": "BulletLifetimeDamageStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 5, 0, 1f, 0, false )]\r\npublic class BulletLifetimeDamageStatus : Status\r\n{\r\n\tpublic BulletLifetimeDamageStatus()\r\n\t{\r\n\t\tTitle = \"Growing Bullets\";\r\n\t\tIconPath = \"textures/icons/bullet_lifetime_damage.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.BulletDamageGrow, GetAddForLevel( Level ), ModifierType.Add );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Bullets grow their damage by {0} per second\", GetPrintAmountForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Bullets grow their damage by {0}\u2192{1} per second\", GetPrintAmountForLevel( newLevel - 1 ), GetPrintAmountForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetAddForLevel( int level )\r\n\t{\r\n\t\treturn 0.2f + 1.5f * level + (level == 7 ? 1.5f : 0f);\r\n\t}\r\n\r\n\tpublic string GetPrintAmountForLevel( int level )\r\n\t{\r\n\t\treturn string.Format( \"{0:0.0}\", GetAddForLevel( level ) );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/CritChanceStatus.cs",
            "FileName": "CritChanceStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 7, 0, 1f, 0, false )]\r\npublic class CritChanceStatus : Status\r\n{\r\n\tpublic CritChanceStatus()\r\n\t{\r\n\t\tTitle = \"Sharp Bullets\";\r\n\t\tIconPath = \"textures/icons/crit_chance.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.CritChance, GetAddForLevel( Level ), ModifierType.Add );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Increase critical chance from 5%\u2192{0}%\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Increase critical chance from {0}%\u2192{1}%\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetAddForLevel( int level )\r\n\t{\r\n\t\treturn 0.05f + 0.08f * level + (level == 7 ? 0.04f : 0f);\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 5 + 8 * level + (level == 7 ? 4 : 0);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/CurseAimDir.cs",
            "FileName": "CurseAimDir.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status(4, 0, 0.5f, 0, true)]\r\npublic class CurseAimDir : Status\r\n{\r\n\tpublic CurseAimDir()\r\n    {\r\n\t\tTitle = \"Random Aim\";\r\n\t\tIconPath = \"textures/icons/curse_shoot_random_dir.png\";\r\n\t}\r\n\r\n\tpublic override void Init(Player player)\r\n\t{\r\n\t\tbase.Init(player);\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n    {\r\n\t\tDescription = GetDescription(Level);\r\n\r\n\t\tPlayer.Modify(this, PlayerStat.ShootRandomDirChance, GetChanceForLevel(Level), ModifierType.Add);\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"{0}% chance to shoot in a random direction\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"{0}%\u2192{1}% chance to shoot in a random direction\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetChanceForLevel( int level )\r\n\t{\r\n\t\treturn 0.25f * level;\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 25 * level;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/CurseAttackSpeedStatus.cs",
            "FileName": "CurseAttackSpeedStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 7, 0, 1f, 0, true )]\r\npublic class CurseAttackSpeedStatus : Status\r\n{\r\n\tpublic CurseAttackSpeedStatus()\r\n\t{\r\n\t\tTitle = \"Lazy Shooting\";\r\n\t\tIconPath = \"textures/icons/curse_attack_speed.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.AttackSpeed, GetMultForLevel( Level ), ModifierType.Mult );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Decrease attack speed by {0}%\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Decrease attack speed by {0}%\u2192{1}%\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetMultForLevel( int level )\r\n\t{\r\n\t\treturn 1f - 0.20f * level;\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 20 * level;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/DashStrengthStatus.cs",
            "FileName": "DashStrengthStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 4, 0, 1f, 0, false )]\r\npublic class DashStrengthStatus : Status\r\n{\r\n\tpublic DashStrengthStatus()\r\n\t{\r\n\t\tTitle = \"Leg Day\";\r\n\t\tIconPath = \"textures/icons/dash_strength.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.DashStrength, GetMultForLevel( Level ), ModifierType.Mult );\r\n\t\tPlayer.Modify( this, PlayerStat.DashInvulnTime, GetMultForLevel( Level ), ModifierType.Mult );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"You dash {0}% longer\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"You dash {0}%\u2192{1}% longer\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetMultForLevel( int level )\r\n\t{\r\n\t\treturn 1f + 0.18f * level + (level == 4 ? 0.03f : 0f);\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 18 * level + (level == 4 ? 3 : 0);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/FearDropGrenadeStatus.cs",
            "FileName": "FearDropGrenadeStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 7, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]\r\npublic class FearDropGrenadeStatus : Status\r\n{\r\n\tpublic FearDropGrenadeStatus()\r\n\t{\r\n\t\tTitle = \"Bomb Curse\";\r\n\t\tIconPath = \"textures/icons/fear_drop_grenade.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.FearDropGrenadeChance, GetAddForLevel( Level ), ModifierType.Add ); ;\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Enemies you scare have a {0}% chance to drop a grenade on death\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Enemies you scare have a {0}%\u2192{1}% chance to drop a grenade on death\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetAddForLevel( int level )\r\n\t{\r\n\t\treturn level * 0.07f;\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn level * 7;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/FearPainStatus.cs",
            "FileName": "FearPainStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status( 5, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]\r\npublic class FearPainStatus : Status\r\n{\r\n\tpublic FearPainStatus()\r\n\t{\r\n\t\tTitle = \"Killer Stress\";\r\n\t\tIconPath = \"textures/icons/fear_pain.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.FearPainPercent, GetAddForLevel( Level ), ModifierType.Add );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Enemies you scare lose {0}% of their remaining HP each second\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Enemies you scare lose {0}%\u2192{1}% of their remaining HP each second\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetAddForLevel( int level )\r\n\t{\r\n\t\treturn 0.02f + level * 0.07f + (level == 5 ? 0.01f : 0f);\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 2 + level * 7 + (level == 5 ? 1 : 0);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/GrenadeFearStatus.cs",
            "FileName": "GrenadeFearStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status(4, 0, 1f, 0, false, typeof(GrenadeShootReloadStatus), typeof(FearDropGrenadeStatus))]\r\npublic class GrenadeFearStatus : Status\r\n{\r\n    public GrenadeFearStatus()\r\n    {\r\n        Title = \"Terrorism\";\r\n        IconPath = \"textures/icons/grenade_fear.png\";\r\n    }\r\n\r\n    public override void Init(Player player)\r\n    {\r\n        base.Init(player);\r\n    }\r\n\r\n    public override void Refresh()\r\n    {\r\n        Description = GetDescription(Level);\r\n\r\n        Player.Modify(this, PlayerStat.GrenadeFearChance, GetAddForLevel(Level), ModifierType.Add); ;\r\n    }\r\n\r\n    public override string GetDescription(int newLevel)\r\n    {\r\n        return string.Format(\"Your grenades have a {0}% chance to scare enemies they hurt\", GetPercentForLevel(Level));\r\n    }\r\n\r\n    public override string GetUpgradeDescription(int newLevel)\r\n    {\r\n        return newLevel > 1 ? string.Format(\"Your grenades have a {0}%\u2192{1}% chance to scare enemies they hurt\", GetPercentForLevel(newLevel - 1), GetPercentForLevel(newLevel)) : GetDescription(newLevel);\r\n    }\r\n\r\n    public float GetAddForLevel(int level)\r\n    {\r\n        return level == 4 ? 1f : 0.3f * level;\r\n    }\r\n\r\n    public float GetPercentForLevel(int level)\r\n    {\r\n        return level == 4 ? 100 : 30 * level;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/KillHealStatus.cs",
            "FileName": "KillHealStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status(5, 3, 1f, 0, false )]\r\npublic class KillHealStatus : Status\r\n{\r\n\tpublic KillHealStatus()\r\n    {\r\n\t\tTitle = \"Vampire\";\r\n\t\tIconPath = \"textures/icons/kill_heal.png\";\r\n\t}\r\n\r\n\tpublic override void Init(Player player)\r\n\t{\r\n\t\tbase.Init(player);\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n    {\r\n\t\tDescription = GetDescription(Level);\r\n\r\n        Player.Modify(this, PlayerStat.HealthRegen, -GetHpDrainAmountForLevel(Level), ModifierType.Add);\r\n    }\r\n\r\n\tpublic override string GetDescription(int newLevel)\r\n\t{\r\n\t\treturn string.Format(\"Heal for {0} whenever you kill an enemy within 2 meters but lose {1} HP/s\", GetPrintAmountForLevel(Level), GetHpDrainPrintAmountForLevel(Level));\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription(int newLevel)\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Heal for {0}\u2192{1} whenever you kill an enemy within 2 meters but lose {2}\u2192{3} HP/s\", GetPrintAmountForLevel(newLevel - 1), GetPrintAmountForLevel(newLevel), GetHpDrainPrintAmountForLevel(newLevel - 1), GetHpDrainPrintAmountForLevel(newLevel)) : GetDescription(newLevel);\r\n\t}\r\n\r\n    public override void OnKill(Enemy enemy)\r\n    {\r\n\t\tvar distSqr = (enemy.Position2D - Player.Position2D).LengthSquared;\r\n\t\tif ( distSqr > 2f * 2f )\r\n\t\t\treturn;\r\n\r\n\t\tvar amount = GetAmountForLevel( Level );\r\n\r\n\t\tif ( Player.Health < Player.Stats[PlayerStat.MaxHp] )\r\n\t\t\tPlayer.TimeSinceChangeHP = 0f;\r\n\r\n\t\tPlayer.RegenHealth( amount );\r\n\r\n\t\t//DamageNumbersLegacy.Create( amount, Player.Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Player.Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.7f );\r\n\t}\r\n\r\n\tpublic float GetAmountForLevel(int level)\r\n\t{\r\n\t\treturn 0.1f + level * 0.5f + (level == 5 ? 0.05f : 0f);\r\n\t}\r\n\r\n    public string GetPrintAmountForLevel(int level)\r\n    {\r\n        return string.Format(\"{0:0.00}\", GetAmountForLevel(level));\r\n    }\r\n\r\n    public float GetHpDrainAmountForLevel(int level)\r\n    {\r\n        return level * 0.4f;\r\n    }\r\n\r\n    public string GetHpDrainPrintAmountForLevel(int level)\r\n    {\r\n        return string.Format(\"{0:0.00}\", GetHpDrainAmountForLevel(level));\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/MoreRerollsStatus.cs",
            "FileName": "MoreRerollsStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\n[Status(3, 0, 1f, maxDifficulty: 2, isCurse: false )]\r\npublic class MoreRerollsStatus : Status\r\n{\r\n\tpublic MoreRerollsStatus()\r\n    {\r\n\t\tTitle = \"More Rerolls\";\r\n\t\tIconPath = \"textures/icons/more_rerolls.png\";\r\n\t}\r\n\r\n\tpublic override void Init(Player player)\r\n\t{\r\n\t\tbase.Init(player);\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n    {\r\n\t\tDescription = GetDescription(Level);\r\n\r\n\t\tPlayer.Modify(this, PlayerStat.NumRerollsPerLevel, GetAddForLevel(Level), ModifierType.Add);\r\n\t}\r\n\r\n\tpublic override string GetDescription(int newLevel)\r\n\t{\r\n\t\treturn string.Format(\"Gain {0} addition reroll each level\", GetAddForLevel(Level));\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription(int newLevel)\r\n    {\r\n\t\treturn newLevel > 1 ? string.Format(\"Gain {0}\u2192{1} addition rerolls each level\", GetAddForLevel(newLevel - 1), GetAddForLevel(newLevel)) : GetDescription(newLevel);\r\n\t}\r\n\r\n\tpublic float GetAddForLevel(int level)\r\n    {\r\n\t\treturn 1f * level;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/MovespeedStatus.cs",
            "FileName": "MovespeedStatus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\n[Status( 7, 0, 1f, 0, false )]\r\npublic class MovespeedStatus : Status\r\n{\r\n\tpublic MovespeedStatus()\r\n\t{\r\n\t\tTitle = \"Fast Shoes\";\r\n\t\tIconPath = \"textures/icons/shoe.png\";\r\n\t}\r\n\r\n\tpublic override void Init( Player player )\r\n\t{\r\n\t\tbase.Init( player );\r\n\t}\r\n\r\n\tpublic override void Refresh()\r\n\t{\r\n\t\tDescription = GetDescription( Level );\r\n\r\n\t\tPlayer.Modify( this, PlayerStat.MoveSpeed, GetMultForLevel( Level ), ModifierType.Mult );\r\n\t}\r\n\r\n\tpublic override string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn string.Format( \"Increase movespeed by {0}%\", GetPercentForLevel( Level ) );\r\n\t}\r\n\r\n\tpublic override string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn newLevel > 1 ? string.Format( \"Increase movespeed by {0}%\u2192{1}%\", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );\r\n\t}\r\n\r\n\tpublic float GetMultForLevel( int level )\r\n\t{\r\n\t\treturn 1f + 0.2f * level + (level == 7 ? 0.2f : 0f);\r\n\t}\r\n\r\n\tpublic float GetPercentForLevel( int level )\r\n\t{\r\n\t\treturn 20 * level + (level == 7 ? 20 : 0);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "status/Status.cs",
            "FileName": "Status.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Threading.Tasks;\r\n\r\npublic class Status\r\n{\r\n\tpublic bool ShouldUpdate { get; protected set; }\r\n\tpublic Player Player { get; protected set; }\r\n\tpublic int Level { get; set; }\r\n\tpublic int MaxLevel { get; set; }\r\n\tpublic TimeSince ElapsedTime { get; protected set; }\r\n\tpublic string Title { get; protected set; }\r\n\tpublic string Description { get; protected set; }\r\n\tpublic string IconPath { get; protected set; }\r\n\r\n\tpublic string DataString { get; protected set; }\r\n\r\n\tpublic Status()\r\n\t{\r\n\t\tLevel = 1;\r\n\t}\r\n\r\n\tpublic virtual void Init( Player player )\r\n\t{\r\n\t\tPlayer = player;\r\n\t\tElapsedTime = 0f;\r\n\t\tShouldUpdate = false;\r\n\t}\r\n\r\n\t// when gaining or leveling up\r\n\tpublic virtual void Refresh()\r\n\t{\r\n\r\n\t}\r\n\r\n\tpublic virtual void Update( float dt )\r\n\t{\r\n\t\t//if (ElapsedTime > 10f)\r\n\t\t//    Player.RemoveStatus(this);\r\n\t}\r\n\r\n\tpublic virtual void Remove()\r\n\t{\r\n\r\n\t}\r\n\r\n\tpublic virtual string GetDescription( int newLevel )\r\n\t{\r\n\t\treturn \"...\";\r\n\t}\r\n\r\n\tpublic virtual string GetUpgradeDescription( int newLevel )\r\n\t{\r\n\t\treturn \"...\";\r\n\t}\r\n\r\n\tpublic virtual void Colliding( Thing other, float percent, float dt ) { }\r\n\tpublic virtual void OnDashStarted() { }\r\n\tpublic virtual void OnDashFinished() { }\r\n\tpublic virtual void OnDashRecharged() { }\r\n\tpublic virtual void OnReload() { }\r\n\t//public virtual void OnBurn( Enemy enemy ) { }\r\n\t//public virtual void OnFreeze( Enemy enemy ) { }\r\n\t//public virtual void OnFear( Enemy enemy ) { }\r\n\tpublic virtual void OnKill( Enemy enemy ) { }\r\n\tpublic virtual void OnHurt( float amount ) { }\r\n\tpublic virtual void OnGainExperience( int xp ) { }\r\n\tpublic virtual void OnLevelUp() { }\r\n\tpublic virtual void OnReroll() { }\r\n\tpublic virtual void OnGainShield() { }\r\n\tpublic virtual void OnLoseShield() { }\r\n\tpublic virtual void OnAddStatus( int typeIdentity ) { }\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "things/enemies/Charger.cs",
            "FileName": "Charger.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\nusing Sandbox.ModelEditor.Nodes;\r\nusing System.Net.NetworkInformation;\r\n\r\npublic class Charger : Enemy\r\n{\r\n\tprivate TimeSince _damageTime;\r\n\tprivate const float DAMAGE_TIME = 1f;\r\n\r\n\tprotected float _chargeDelayTimer;\r\n\tprivate const float CHARGE_DELAY_MIN = 2f;\r\n\tprivate const float CHARGE_DELAY_MAX = 6f;\r\n\r\n\tpublic bool IsPreparingToCharge { get; private set; }\r\n\tpublic bool IsCharging { get; private set; }\r\n\tprivate float _prepareTimer;\r\n\tprivate const float PREPARE_TIME = 1f;\r\n\tprotected float _chargeTimer;\r\n\tprotected float CHARGE_TIME_MIN = 1.8f;\r\n\tprotected float CHARGE_TIME_MAX = 2.5f;\r\n\tprivate float _chargeTime;\r\n\tprivate float _nextRedirectTime;\r\n\tprivate TimeSince _timeSinceRedirect;\r\n\r\n\tprotected Vector2 _chargeDir;\r\n\tprotected Vector2 _chargeVel;\r\n\tprivate TimeSince _chargeCloudTimer;\r\n\r\n\tpublic override float HeightVariance => 0.03f;\r\n\tpublic override float WidthVariance => 0.015f;\r\n\r\n\tpublic float ChargeRange { get; set; }\r\n\r\n\tprotected float REDIRECT_DELAY_MIN = 0.3f;\r\n\tprotected float REDIRECT_DELAY_MAX = 2.7f;\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\t//OffsetY = -0.57f;\r\n\t\tShadowScale = 1.25f;\r\n\t\tShadowFullOpacity = 0.8f;\r\n\t\tShadowOpacity = 0f;\r\n\r\n\t\tScale = 1.25f;\r\n\r\n\t\tbase.OnAwake();\r\n\r\n\t\t//Sprite.Texture = Texture.Load(\"textures/sprites/charger.vtex\");\r\n\t\t//Sprite.Size = new Vector2( 1f, 1f ) * Scale;\r\n\r\n\t\tPushStrength = 25f;\r\n\r\n\t\tRadius = 0.275f;\r\n\r\n\t\tHealth = 75f;\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\tHealth = 55f;\r\n\r\n\t\tMaxHealth = Health;\r\n\t\tDamageToPlayer = 10f;\r\n\r\n\t\tCoinValueMin = 2;\r\n\t\tCoinValueMax = 5;\r\n\t\tCoinChance = 0.7f;\r\n\r\n\t\tSprite.PlayAnimation( AnimSpawnPath );\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tCollideWith.Add( typeof( Enemy ) );\r\n\t\tCollideWith.Add( typeof( Player ) );\r\n\r\n\t\t_damageTime = DAMAGE_TIME;\r\n\t\t_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX );\r\n\r\n\t\tChargeRange = 4.2f;\r\n\t}\r\n\r\n\tprotected override void UpdatePosition( float dt )\r\n\t{\r\n\t\t//Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);\r\n\t\t//Gizmo.Draw.Text( $\"IsCharging: {IsCharging}\", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );\r\n\r\n\t\tbase.UpdatePosition( dt );\r\n\r\n\t\tvar targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);\r\n\r\n\t\tif ( IsPreparingToCharge )\r\n\t\t{\r\n\t\t\t_prepareTimer -= dt;\r\n\t\t\tif ( _prepareTimer < 0f )\r\n\t\t\t{\r\n\t\t\t\tCharge();\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( IsCharging )\r\n\t\t{\r\n\t\t\t_chargeTimer -= dt;\r\n\t\t\tif ( _chargeTimer < 0f )\r\n\t\t\t{\r\n\t\t\t\tIsCharging = false;\r\n\t\t\t\tSprite.PlayAnimation( AnimIdlePath );\r\n\t\t\t\tCanTurn = true;\r\n\t\t\t\tDontChangeAnimSpeed = false;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tHandleCharging( dt );\r\n\t\t\t}\r\n\r\n\t\t\tWorldPosition += (Vector3)(_chargeVel + Velocity) * dt;\r\n\r\n\t\t\tif ( Manager.Instance.Difficulty >= 1 && _timeSinceRedirect > _nextRedirectTime )\r\n\t\t\t{\r\n\t\t\t\t_chargeDir = (targetPos - Position2D).Normal;\r\n\r\n\t\t\t\tif(Math.Abs( targetPos.x - Position2D.x ) > 0.15f )\r\n\t\t\t\t\tFlipX = targetPos.x > Position2D.x;\r\n\r\n\t\t\t\t_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );\r\n\t\t\t\t_timeSinceRedirect = 0f;\r\n\t\t\t}\r\n\r\n\t\t\tif ( _chargeCloudTimer > 0.25f )\r\n\t\t\t{\r\n\t\t\t\tSpawnCloudClient( Position2D + new Vector2( 0f, 0.25f ), -_chargeDir * Game.Random.Float( 0.2f, 0.8f ) );\r\n\t\t\t\t_chargeCloudTimer = Game.Random.Float( 0f, 0.075f );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tVelocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);\r\n\r\n\t\t\tfloat speed = 0.75f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);\r\n\r\n\t\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\t\tspeed *= 0.85f;\r\n\r\n\t\t\tWorldPosition += (Vector3)Velocity * speed * dt;\r\n\t\t}\r\n\r\n\t\tvar player_dist_sqr = (targetPos - Position2D).LengthSquared;\r\n\t\tif ( !IsPreparingToCharge && !IsCharging && !IsAttacking && player_dist_sqr < ChargeRange * ChargeRange && Target.IsValid() )\r\n\t\t{\r\n\t\t\t_chargeDelayTimer -= dt;\r\n\t\t\tif ( _chargeDelayTimer < 0f )\r\n\t\t\t{\r\n\t\t\t\tPrepareToCharge();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected virtual void HandleCharging(float dt)\r\n\t{\r\n\t\tfloat chargeSpeed = Manager.Instance.Difficulty >= 1 ? 12f : 4f; ;\r\n\t\t_chargeVel += _chargeDir * chargeSpeed * Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 0f, 1f, EasingType.Linear ) * dt;\r\n\t\tTempWeight += Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 1f, 6f, EasingType.Linear ) * dt;\r\n\r\n\t\tfloat BUFFER = 0.01f;\r\n\t\tvar x_min = Manager.Instance.BOUNDS_MIN.x + Radius + BUFFER;\r\n\t\tvar x_max = Manager.Instance.BOUNDS_MAX.x - Radius - BUFFER;\r\n\t\tvar y_min = Manager.Instance.BOUNDS_MIN.y + BUFFER;\r\n\t\tvar y_max = Manager.Instance.BOUNDS_MAX.y - Radius - BUFFER;\r\n\r\n\t\tif ( Position2D.x < x_min && _chargeDir.x < 0f )\r\n\t\t{\r\n\t\t\t_chargeDir = _chargeDir.WithX( Math.Abs( _chargeDir.x ) );\r\n\t\t\t_chargeVel = _chargeVel.WithX( Math.Abs( _chargeVel.x ) * 0.1f );\r\n\t\t\tFlipX = true;\r\n\t\t\tSprite.SpriteFlags = SpriteFlags.HorizontalFlip;\r\n\t\t}\r\n\t\telse if ( Position2D.x > x_max && _chargeDir.x > 0f )\r\n\t\t{\r\n\t\t\t_chargeDir = _chargeDir.WithX( -Math.Abs( _chargeDir.x ) );\r\n\t\t\t_chargeVel = _chargeVel.WithX( -Math.Abs( _chargeVel.x ) * 0.1f );\r\n\t\t\tFlipX = false;\r\n\t\t\tSprite.SpriteFlags = SpriteFlags.None;\r\n\t\t}\r\n\r\n\t\tif ( Position2D.y < y_min && _chargeDir.y < 0f )\r\n\t\t{\r\n\t\t\t_chargeDir = _chargeDir.WithY( Math.Abs( _chargeDir.y ) );\r\n\t\t\t_chargeVel = _chargeDir.WithY( Math.Abs( _chargeVel.y ) * 0.1f );\r\n\t\t}\r\n\t\telse if ( Position2D.y > y_max && _chargeDir.y > 0f )\r\n\t\t{\r\n\t\t\t_chargeDir = _chargeDir.WithY( -Math.Abs( _chargeDir.y ) );\r\n\t\t\t_chargeVel = _chargeDir.WithY( -Math.Abs( _chargeVel.y ) * 0.1f );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void HandleDeceleration( float dt )\r\n\t{\r\n\t\tif ( IsCharging )\r\n\t\t{\r\n\t\t\tVelocity *= (1f - dt * 1.75f);\r\n\r\n\t\t\tfloat decel = Manager.Instance.Difficulty >= 1 ? 3f : 0.5f;\r\n\t\t\t_chargeVel *= (1f - dt * decel);\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tbase.HandleDeceleration( dt );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void UpdateSprite( Thing target )\r\n\t{\r\n\t\tif ( !IsCharging )\r\n\t\t\tbase.UpdateSprite( target );\r\n\t}\r\n\r\n\tprotected override void HandleAttacking( Thing target, float dt )\r\n\t{\r\n\t\tif ( !IsPreparingToCharge && !IsCharging )\r\n\t\t\tbase.HandleAttacking( target, dt );\r\n\t}\r\n\r\n\tpublic void PrepareToCharge()\r\n\t{\r\n\t\t_prepareTimer = PREPARE_TIME;\r\n\t\tIsPreparingToCharge = true;\r\n\t\tManager.Instance.PlaySfxNearby( \"enemy.roar.prepare\", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 5f );\r\n\t\tSprite.PlayAnimation( \"charge_start\" );\r\n\t\tCanTurn = false;\r\n\t\tCanAttack = false;\r\n\t\tCanAttackAnim = false;\r\n\t\tDontChangeAnimSpeed = true;\r\n\r\n\t\tif ( Manager.Instance.Difficulty >= 1 )\r\n\t\t{\r\n\t\t\t_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );\r\n\t\t\t_timeSinceRedirect = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Charge()\r\n\t{\r\n\t\tvar target_pos = Target.IsValid() && !IsFeared\r\n\t\t\t? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 1.75f )\r\n\t\t\t: Position2D + Utils.GetRandomVector() * 0.5f;\r\n\r\n\t\t_chargeDir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -10f, 10f ) );\r\n\r\n\t\tIsPreparingToCharge = false;\r\n\t\tIsCharging = true;\r\n\t\t_chargeTime = Game.Random.Float( CHARGE_TIME_MIN, CHARGE_TIME_MAX ) * (Manager.Instance.Difficulty >= 1 ? Game.Random.Float(1.25f, Utils.Map(Manager.Instance.Difficulty, 1, 10, 1.75f, 3f, EasingType.SineIn)) : 1f);\r\n\r\n\t\t_chargeTimer = _chargeTime;\r\n\t\tCanAttack = true;\r\n\t\tCanAttackAnim = true;\r\n\r\n\t\t_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.4f : 1f);\r\n\t\t_chargeVel = Vector2.Zero;\r\n\t\tSprite.PlayAnimation( \"charge_loop\" );\r\n\t\tAnimSpeed = 3f;\r\n\t\tFlipX = _chargeDir.x > 0f;\r\n\t\tSprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;\r\n\t\t//Sprite.SpriteFlags = target_pos.x > Position2D.x ? SpriteFlags.HorizontalFlip : SpriteFlags.None;\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"enemy.roar\", Position2D, pitch: Game.Random.Float( 0.925f, 1.075f ), volume: 1f, maxDist: 8f );\r\n\t}\r\n\r\n\tpublic override void Colliding( Thing other, float percent, float dt )\r\n\t{\r\n\t\tbase.Colliding( other, percent, dt );\r\n\r\n\t\tif ( other is Enemy enemy && !enemy.IsDying )\r\n\t\t{\r\n\t\t\tvar spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );\r\n\t\t\tVelocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;\r\n\r\n\t\t\tif ( (IsAttacking || IsCharging) && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )\r\n\t\t\t{\r\n\t\t\t\tvar dmg = DamageToPlayer;\r\n\t\t\t\tif ( IsCharmed )\r\n\t\t\t\t\tdmg *= CharmDamageDealtMultiplier;\r\n\r\n\t\t\t\tenemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );\r\n\t\t\t\tenemy.Target = this;\r\n\t\t\t\t_damageTime = 0f;\r\n\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );\r\n\t\t\t}\r\n\t\t}\r\n\t\t// todo: move collision check to player instead to prevent laggy hits?\r\n\t\telse if ( other is Player player )\r\n\t\t{\r\n\t\t\tif ( !player.IsDead )\r\n\t\t\t{\r\n\t\t\t\tVelocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;\r\n\r\n\t\t\t\tif ( (IsAttacking || IsCharging) && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );\r\n\r\n\t\t\t\t\tif ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );\r\n\r\n\t\t\t\t\t\tplayer.Damage( dmg );\r\n\r\n\t\t\t\t\t\tif ( dmg > 0f )\r\n\t\t\t\t\t\t\tOnDamagePlayer( player, dmg );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t_damageTime = 0f;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void Celebrate()\r\n\t{\r\n\t\tbase.Celebrate();\r\n\r\n\t\tCelebrateAsync();\r\n\t}\r\n\r\n\tasync void CelebrateAsync()\r\n\t{\r\n\t\tawait Task.Delay( Game.Random.Int( 0, 500 ) );\r\n\r\n\t\tSprite.PlaybackSpeed = Game.Random.Float( 1f, 2.5f );\r\n\r\n\t\tSprite.PlayAnimation( \"cheer_start\" );\r\n\r\n\t\tawait Task.Delay( Game.Random.Int( 200, 400 ) );\r\n\r\n\t\tSprite.PlayAnimation( \"cheer\" );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "things/enemies/RunnerEliteSpecial.cs",
            "FileName": "RunnerEliteSpecial.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\npublic class RunnerEliteSpecial : RunnerElite\r\n{\r\n\tpublic override float FullOpacity => 0f;\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\tRadius = 0.5075f;\r\n\t\tScale = 2.275f;\r\n\t\tSprite.LocalScale = new Vector3( Scale * Game.Random.Float( 1f - HeightVariance, 1f + HeightVariance ), Scale * Game.Random.Float( 1f - WidthVariance, 1f + WidthVariance ), 1f ) * Globals.SPRITE_SCALE;\r\n\r\n\t\tHealth = 300f;\r\n\t\tMaxHealth = Health;\r\n\r\n\t\tShadowScale = 2.275f;\r\n\t\tShadowFullOpacity = 0f;\r\n\t\tShadowSpriteDirty = true;\r\n\r\n\t\tPushStrength = 55f;\r\n\t\tAggroRange = 2.25f;\r\n\t\t\r\n\t\tDamageToPlayer = 5f;\r\n\r\n\t\tCoinChance = 1f;\r\n\t\tCoinValueMin = 4;\r\n\t\tCoinValueMax = 8;\r\n\r\n\t\tSprite.Tint = Color.White.WithAlpha( FullOpacity );\r\n\r\n\t\tMoveSpeed = 0.35f;\r\n\r\n\t\tJUMP_DELAY_MIN = 4f;\r\n\t\tJUMP_DELAY_MAX = 10f;\r\n\t}\r\n\r\n\tprotected override void SpawnLandingClouds()\r\n\t{\r\n\t\tfor ( int i = 0; i < Game.Random.Int( 2, 3 ); i++ )\r\n\t\t{\r\n\t\t\tvar dir = Utils.GetRandomVector();\r\n\t\t\tSpawnCloudClient( Position2D + dir * Game.Random.Float(0.4f, 0.6f), dir * Game.Random.Float( 0.3f, 1.5f ) );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void DealDamage()\r\n\t{\r\n\t\tFlash( 0.1f, Color.Red.WithAlpha( 0.25f ) );\r\n\t}\r\n\r\n\tpublic override void Celebrate()\r\n\t{\r\n\t\tif ( IsSpawning )\r\n\t\t\tFinishSpawning();\r\n\r\n\t\tif ( _isJumping )\r\n\t\t\treturn;\r\n\r\n\t\tCelebrateAsync();\r\n\t}\r\n\r\n\tasync void CelebrateAsync()\r\n\t{\r\n\t\tawait Task.Delay( Game.Random.Int( 0, 500 ) );\r\n\r\n\t\tSprite.PlaybackSpeed = Game.Random.Float( 2f, 4f );\r\n\r\n\t\tSprite.Tint = Color.Red.WithAlpha( 0.25f );\r\n\r\n\t\tSprite.PlayAnimation( \"cheer\" );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "things/enemies/Spiker.cs",
            "FileName": "Spiker.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\nusing static Sandbox.VertexLayout;\r\n\r\npublic class Spiker : Enemy\r\n{\r\n\tprivate TimeSince _damageTime;\r\n\tprivate const float DAMAGE_TIME = 0.75f;\r\n\r\n\tprivate float _shootDelayTimer;\r\n\tprivate const float SHOOT_DELAY_MIN = 2f;\r\n\tprivate const float SHOOT_DELAY_MAX = 4f;\r\n\r\n\tpublic bool IsShooting { get; private set; }\r\n\tprivate float _shotTimer;\r\n\tprivate const float SHOOT_TIME = 4f;\r\n\tprivate bool _hasShot;\r\n\tprivate TimeSince _prepareStartTime;\r\n\tprivate bool _hasReversed;\r\n\r\n\tpublic override float HeightVariance => 0.02f;\r\n\tpublic override float WidthVariance => 0.01f;\r\n\r\n\tprivate bool _moveClockwise;\r\n\tpublic static int SpikerNum { get; set; }\r\n\tprivate float _perpendicularMaxDist;\r\n\r\n\tprivate float _digDelayTimer;\r\n\tprivate const float DIG_DELAY_MIN = 4f;\r\n\tprivate const float DIG_DELAY_MAX = 13f;\r\n\tpublic bool IsDigging { get; private set; }\r\n\tprivate TimeSince _timeSinceStartDigging;\r\n\tprivate const float DIG_TIME = 1.2f;\r\n\r\n\tpublic float ShootRange { get; set; }\r\n\r\n\tpublic float MoveSpeed { get; set; }\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\t//OffsetY = -0.58f;\r\n\t\tShadowScale = 1.2f;\r\n\t\tShadowFullOpacity = 0.8f;\r\n\t\tShadowOpacity = 0f;\r\n\r\n\t\tScale = 1.4f;\r\n\r\n\t\tbase.OnAwake();\r\n\r\n\t\t//AnimSpeed = 4f;\r\n\t\t//Sprite.Texture = Texture.Load(\"textures/sprites/spiker.vtex\");\r\n\r\n\t\tPushStrength = 8f;\r\n\t\tDeceleration = 2.57f;\r\n\t\tDecelerationAttacking = 2.35f;\r\n\t\tAggroRange = 0.75f;\r\n\r\n\t\tRadius = 0.27f;\r\n\r\n\t\tHealth = 80f;\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\tHealth = 70f;\r\n\r\n\t\tMaxHealth = Health;\r\n\t\tDamageToPlayer = 14f;\r\n\r\n\t\tCoinValueMin = 2;\r\n\t\tCoinValueMax = 4;\r\n\t\tCoinChance = 0.75f;\r\n\r\n\t\tSprite.PlayAnimation( AnimSpawnPath );\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\t\t\r\n\t\tCollideWith.Add( typeof( Enemy ) );\r\n\t\tCollideWith.Add( typeof( Player ) );\r\n\r\n\t\t_damageTime = DAMAGE_TIME;\r\n\t\t_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );\r\n\t\t_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );\r\n\r\n\t\t_moveClockwise = SpikerNum % 2 == 0;\r\n\t\tSpikerNum++;\r\n\t\t_perpendicularMaxDist = Game.Random.Float( 3.5f, 6.5f );\r\n\r\n\t\tShootRange = 4.5f;\r\n\r\n\t\tMoveSpeed = 0.9f;\r\n\t}\r\n\r\n\tprotected override void UpdatePosition( float dt )\r\n\t{\r\n\t\tbase.UpdatePosition( dt );\r\n\r\n\t\t//if(!IsDigging)\r\n\t\t//{\r\n\t\t//\tGizmo.Draw.Color = Color.White;\r\n\t\t//\tGizmo.Draw.Text( $\"{_digDelayTimer}\", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );\r\n\t\t//}\r\n\r\n\t\tvar targetPos = GetTargetPos();\r\n\r\n\t\tif ( IsShooting )\r\n\t\t{\r\n\t\t\tVelocity *= (1f - dt * (IsAttacking ? DecelerationAttacking : Deceleration));\r\n\t\t\tif ( !_hasShot && _prepareStartTime > 1f )\r\n\t\t\t{\r\n\t\t\t\tCreateSpike();\r\n\t\t\t\t_hasShot = true;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !_hasReversed && _prepareStartTime > 3f )\r\n\t\t\t{\r\n\t\t\t\t_hasReversed = true;\r\n\t\t\t\tSprite.PlayAnimation( \"shoot_reverse\" );\r\n\t\t\t}\r\n\r\n\t\t\tVelocity *= (1f - dt * 4f);\r\n\r\n\t\t\t_shotTimer -= dt;\r\n\t\t\tif ( _shotTimer < 0f )\r\n\t\t\t{\r\n\t\t\t\tFinishShooting();\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( IsDigging )\r\n\t\t{\r\n\t\t\tVelocity *= (1f - dt * 6f);\r\n\r\n\t\t\tif ( _timeSinceStartDigging > DIG_TIME )\r\n\t\t\t{\r\n\t\t\t\tVector2 pos;\r\n\t\t\t\tif ( Target.IsValid() )\r\n\t\t\t\t{\r\n\t\t\t\t\tpos = targetPos + Target.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float( 1.5f, 5.5f);\r\n\t\t\t\t\tif ( (Position2D - pos).LengthSquared < MathF.Pow( 0.5f, 2f ) )\r\n\t\t\t\t\t\tpos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 5f );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tpos = Position2D + Game.Random.Float( 4f, 5f );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tFinishDigging( Manager.Instance.ClampToBounds( pos ) );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tfloat progress = Utils.Map( _timeSinceStartDigging, 0f, DIG_TIME, 0f, 1f );\r\n\t\t\t\t//ZPos = Utils.Map( progress, 0f, 1f, 0f, DIG_DEPTH );\r\n\t\t\t\t//PlaybackRate = Utils.Map( progress, 0f, 1f, 0f, 1f ) * _personalSpeedScale;\r\n\r\n\t\t\t\tfloat shadowOpacity = Utils.Map( progress, 0f, 1f, ShadowOpacity, 0f, EasingType.QuadIn );\r\n\t\t\t\tShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );\r\n\r\n\t\t\t\tVfxOpacity = Utils.Map( progress, 0f, 1f, 1f, 0f, EasingType.QuadIn );\r\n\r\n\t\t\t\t//Gizmo.Draw.Color = Color.White;\r\n\t\t\t\t//Gizmo.Draw.Text( $\"{progress}\", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );\r\n\r\n\t\t\t\tIgnoreCollision = progress > 0.3f;\r\n\r\n\t\t\t\tif ( _spawnCloudTime > (0.3f / TimeScale) )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar cloud = Manager.Instance.SpawnCloud( Position2D + new Vector2( 0f, 0.05f ) );\r\n\t\t\t\t\tcloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );\r\n\t\t\t\t\t_spawnCloudTime = Game.Random.Float( 0f, 0.15f );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tVector2 toTarget = (targetPos - Position2D).Normal;\r\n\r\n\t\t\tif(Manager.Instance.Difficulty >= 1)\r\n\t\t\t{\r\n\t\t\t\tif ( (targetPos - Position2D).LengthSquared < MathF.Pow( _perpendicularMaxDist, 2f ) )\r\n\t\t\t\t\ttoTarget = Vector2.Lerp( toTarget, new Vector2( toTarget.y, -toTarget.x ) * (_moveClockwise ? -1f : 1f), Utils.Map( (targetPos - Position2D).LengthSquared, MathF.Pow( _perpendicularMaxDist, 2f ), MathF.Pow( 1.5f, 2f ), 0f, 1f ) );\r\n\t\t\t}\r\n\r\n\t\t\tVelocity += toTarget * 1.0f * dt * (IsFeared ? -1f : 1f);\r\n\t\t}\r\n\r\n\t\tfloat speed = MoveSpeed * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\tspeed *= 0.85f;\r\n\r\n\t\tWorldPosition += (Vector3)Velocity * speed * dt;\r\n\r\n\t\tif ( !IsShooting && !IsDigging && (!IsAttacking || IsCharmed) && !Manager.Instance.IsGameOver )\r\n\t\t{\r\n\t\t\tvar target_dist_sqr = ((Target.IsValid() ? Target.Position2D : targetPos) - Position2D).LengthSquared;\r\n\t\t\tvar rangeSqr = ShootRange * ShootRange;\r\n\r\n\t\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\t\trangeSqr *= 0.7f;\r\n\r\n\t\t\tif ( target_dist_sqr < rangeSqr )\r\n\t\t\t{\r\n\t\t\t\t_shootDelayTimer -= dt;\r\n\t\t\t\tif ( _shootDelayTimer < 0f )\r\n\t\t\t\t\tStartShooting();\r\n\t\t\t}\r\n\r\n\t\t\tif ( target_dist_sqr < MathF.Pow( 12f, 2f ) && Manager.Instance.Difficulty > 0 )\r\n\t\t\t{\r\n\t\t\t\t_digDelayTimer -= dt;\r\n\t\t\t\tif ( _digDelayTimer < 0f )\r\n\t\t\t\t\tStartDigging();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected virtual Vector2 GetTargetPos()\r\n\t{\r\n\t\treturn Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);\r\n\t}\r\n\r\n\tprotected override void UpdateSprite( Thing target )\r\n\t{\r\n\t\tif ( Sprite.CurrentAnimation.Name.Contains( \"shoot\" ) || IsDigging ) \r\n\t\t\treturn;\r\n\r\n\t\tbase.UpdateSprite( target );\r\n\t}\r\n\r\n\tpublic void StartShooting()\r\n\t{\r\n\t\t_shotTimer = SHOOT_TIME;\r\n\t\tIsShooting = true;\r\n\t\tCanAttack = false;\r\n\t\tCanTurn = false;\r\n\t\tCanAttackAnim = false;\r\n\t\t_hasShot = false;\r\n\t\t_hasReversed = false;\r\n\t\t_prepareStartTime = 0f;\r\n\t\tVelocity *= 0.25f;\r\n\t\tDontChangeAnimSpeed = true;\r\n\t\tAnimSpeed = 1f;\r\n\t\tSprite.PlayAnimation( \"shoot\" );\r\n\r\n\t\tShouldUpdateAfterGameOver = true;\r\n\t}\r\n\r\n\tpublic virtual void CreateSpike()\r\n\t{\r\n\t\tvar target_pos = Target.IsValid()\r\n\t\t\t? Target.Position2D + Target.Velocity * Game.Random.Float( 0.2f, 2f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.8f\r\n\t\t\t: Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 6f );\r\n\r\n\t\tvar BUFFER = 0.1f;\r\n\r\n\t\tvar spike = Manager.Instance.SpawnEnemySpike( new Vector2( Math.Clamp( target_pos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), Math.Clamp( target_pos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) ) );\r\n\r\n\t\tif(IsCharmed)\r\n\t\t{\r\n\t\t\tspike.BecomeCharmed();\r\n\t\t}\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"spike.prepare\", target_pos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1.5f, maxDist: 5f );\r\n\t}\r\n\r\n\tpublic void FinishShooting()\r\n\t{\r\n\t\t_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 2.5f : 1f);\r\n\t\tIsShooting = false;\r\n\t\tCanAttack = true;\r\n\t\tCanAttackAnim = true;\r\n\t\tCanTurn = true;\r\n\t\tDontChangeAnimSpeed = false;\r\n\t\tSprite.PlayAnimation( AnimIdlePath );\r\n\r\n\t\tShouldUpdateAfterGameOver = false;\r\n\t\tif ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )\r\n\t\t\tCelebrate();\r\n\t}\r\n\r\n\tpublic override void Colliding( Thing other, float percent, float dt )\r\n\t{\r\n\t\tbase.Colliding( other, percent, dt );\r\n\r\n\t\tif ( other is Enemy enemy && !enemy.IsDying )\r\n\t\t{\r\n\t\t\tvar spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );\r\n\t\t\tVelocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;\r\n\r\n\t\t\tif ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )\r\n\t\t\t{\r\n\t\t\t\tvar dmg = DamageToPlayer;\r\n\t\t\t\tif ( IsCharmed )\r\n\t\t\t\t\tdmg *= CharmDamageDealtMultiplier;\r\n\r\n\t\t\t\tenemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );\r\n\t\t\t\tenemy.Target = this;\r\n\t\t\t\t_damageTime = 0f;\r\n\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );\r\n\t\t\t}\r\n\t\t}\r\n\t\t// todo: move collision check to player instead to prevent laggy hits?\r\n\t\telse if ( other is Player player )\r\n\t\t{\r\n\t\t\tif ( !player.IsDead )\r\n\t\t\t{\r\n\t\t\t\tVelocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;\r\n\r\n\t\t\t\tif ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );\r\n\r\n\t\t\t\t\tif ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );\r\n\r\n\t\t\t\t\t\tplayer.Damage( dmg );\r\n\r\n\t\t\t\t\t\tif( dmg > 0f )\r\n\t\t\t\t\t\t\tOnDamagePlayer( player, dmg );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t_damageTime = 0f;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )\r\n\t{\r\n\t\tbase.Damage( damage, player, addVel, addTempWeight, isCrit, damageType );\r\n\r\n\t\tif ( Game.Random.Float( 0f, 1f ) < Utils.Map( damage, 1f, 20f, 0.1f, 0.7f ) )\r\n\t\t\t_digDelayTimer *= Game.Random.Float( 0.6f, 0.95f );\r\n\t}\r\n\r\n\tpublic void StartDigging()\r\n\t{\r\n\t\tIsDigging = true;\r\n\t\t_timeSinceStartDigging = 0f;\r\n\t\tCanAttack = false;\r\n\t\tCanAttackAnim = false;\r\n\t\tCanTurn = false;\r\n\t\tVelocity *= 0.5f;\r\n\t\tSprite.PlayAnimation( \"dig\" );\r\n\t\tManager.Instance.PlaySfxNearby( \"zombie.dirt\", Position2D, pitch: Game.Random.Float( 0.5f, 0.55f ), volume: 0.6f, maxDist: 7.5f );\r\n\t\t//SS2Game.PlaySfx( \"zombie.dirt\", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );\r\n\t\t//SS2Game.Current.DustCloudClient( Position2D );\r\n\r\n\t\tShouldUpdateAfterGameOver = true;\r\n\t}\r\n\r\n\tvoid FinishDigging( Vector2 pos )\r\n\t{\r\n\t\tPosition2D = pos;\r\n\t\t//WorldPosition = ((Vector3)Position2D).WithZ( ZPos );\r\n\r\n\t\tTransform.ClearInterpolation();\r\n\r\n\t\tIsDigging = false;\r\n\t\tCanAttack = true;\r\n\t\tCanAttackAnim = true;\r\n\t\tCanTurn = true;\r\n\t\tIgnoreCollision = false;\r\n\t\t//SetAnim( \"Attack1\" );\r\n\r\n\t\t_moveClockwise = !_moveClockwise;\r\n\r\n\t\t_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );\r\n\r\n\t\t//SS2Game.PlaySfx( \"zombie.dirt\", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );\r\n\t\t//SS2Game.Current.DustCloudClient( Position2D );\r\n\r\n\t\tIsSpawning = true;\r\n\t\t//_hasDug = true;\r\n\t\tTimeSinceSpawn = 0f;\r\n\t\t//SpawnProgress = 0f;\r\n\r\n\t\t//ShadowRadiusModifier = 1.5f;\r\n\t\t//ShadowOpacityModifier = 0f;\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"zombie.dirt\", Position2D, pitch: Game.Random.Float( 0.85f, 0.9f ), volume: 0.6f, maxDist: 7.5f );\r\n\r\n\t\tShadowSprite.Tint = Color.Black.WithAlpha( 0f );\r\n\t\tVfxOpacity = 0f;\r\n\t}\r\n\r\n\tpublic override void Celebrate()\r\n\t{\r\n\t\tbase.Celebrate();\r\n\r\n\t\tif ( IsShooting || IsDigging )\r\n\t\t\treturn;\r\n\r\n\t\tCelebrateAsync();\r\n\t}\r\n\r\n\tasync void CelebrateAsync()\r\n\t{\r\n\t\tawait Task.Delay( Game.Random.Int( 0, 500 ) );\r\n\r\n\t\tSprite.PlaybackSpeed = Game.Random.Float( 1.5f, 3.5f );\r\n\r\n\t\tSprite.PlayAnimation( \"cheer_start\" );\r\n\r\n\t\tawait Task.Delay( Game.Random.Int( 200, 400 ) );\r\n\r\n\t\tSprite.PlayAnimation( \"cheer\" );\r\n\t}\r\n\r\n\tprotected override void FinishSpawning()\r\n\t{\r\n\t\tbase.FinishSpawning();\r\n\r\n\t\tShouldUpdateAfterGameOver = false;\r\n\t\tif ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )\r\n\t\t\tCelebrate();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "facepunch.ss1",
            "Path": "things/Player.cs",
            "FileName": "Player.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 421898,
            "IsPrivate": false,
            "Code": "using static Manager;\r\n\r\npublic enum ModifierType { Set, Add, Mult }\r\npublic class ModifierData\r\n{\r\n\tpublic float value;\r\n\tpublic ModifierType type;\r\n\tpublic float priority;\r\n\r\n\tpublic ModifierData( float _value, ModifierType _type, float _priority = 0f )\r\n\t{\r\n\t\tvalue = _value;\r\n\t\ttype = _type;\r\n\t\tpriority = _priority;\r\n\t}\r\n}\r\n\r\npublic enum PlayerStat\r\n{\r\n\tAttackTime, AttackSpeed, ReloadTime, ReloadSpeed, MaxAmmoCount, BulletDamage, BulletForce, Recoil, MoveSpeed, NumProjectiles, BulletSpread, BulletInaccuracy, BulletSpeed, BulletLifetime,\r\n\tBulletNumPiercing, CritChance, CritMultiplier, LowHealthDamageMultiplier, NumUpgradeChoices, HealthRegen, HealthRegenStill, DamageReductionPercent, PushStrength, CoinAttractRange, CoinAttractStrength, Luck, MaxHp,\r\n\tNumDashes, DashInvulnTime, DashCooldown, DashProgress, DashStrength, ThornsPercent, ShootFireIgniteChance, FireDamage, FireLifetime, FireSpreadChance, ShootFreezeChance, FreezeLifetime,\r\n\tFreezeTimeScale, FreezeOnMeleeChance, FreezeFireDamageMultiplier, LastAmmoDamageMultiplier, FearLifetime, FearDamageMultiplier, FearOnMeleeChance, BulletDamageGrow, BulletDamageShrink,\r\n\tBulletDistanceDamage, NumRerollsPerLevel, FullHealthDamageMultiplier, DamagePerEarlierShot, DamageForSpeed, OverallDamageMultiplier, ExplosionSizeMultiplier, GrenadeVelocity, ExplosionDamageMultiplier,\r\n\tBulletDamageMultiplier, ExplosionDamageReductionPercent, NonExplosionDamageIncreasePercent, GrenadeStickyPercent, GrenadeFearChance, FearDrainPercent, FearPainPercent, CrateChanceAdditional,\r\n\tAttackSpeedStill, FearDropGrenadeChance, FrozenShardsNum, NoDashInvuln, BulletFlatDamageAddition, GrenadesCanCrit, BulletHealTeammateAmount, HomingBulletChance, PauseWhileChoosing,\r\n\tBulletNumBouncing, DashCharm, CharmedEnemyDmgTakenMultiplier, CharmedEnemyDmgDealtMultiplier, BossArrivalTime, RadiusMultiplier, SpecialistStatusAmount, ZoomAmount, MaxFireStacks, XpRepel, HealthPackAmount,\r\n\tMaxBulletSpread, ShootRandomDirChance, DashRandomDirChance, MoveSelfDmg, MoveSelfDmgReqDist, MoveSelfDmgAmount, SelfDmgDistanceMoved, IncreasedDmgTaken, ReverseControls, SelfCritChance,\r\n}\r\n\r\npublic enum DamageType { Melee, Ranged, Explosion, Fire, PlayerBullet, Self, Generic, LavaPuddle, FearPain, }\r\npublic enum PlayerDamageType { Enemy, Self, Grenade }\r\n\r\npublic struct CamShakeData\r\n{\r\n\tpublic float strength;\r\n\tpublic float startTime;\r\n\tpublic float time;\r\n\tpublic EasingType easingType;\r\n\tpublic bool useRealTime;\r\n\r\n\tpublic CamShakeData( float _strength, float _startTime, float _time, EasingType _easingType, bool _useRealTime )\r\n\t{\r\n\t\tstrength = _strength;\r\n\t\tstartTime = _startTime;\r\n\t\ttime = _time;\r\n\t\teasingType = _easingType;\r\n\t\tuseRealTime = _useRealTime;\r\n\t}\r\n}\r\n\r\npublic class Player : Thing\r\n{\r\n\t[Property] public GameObject Body { get; set; }\r\n\t[Property] public GameObject ArrowAimerPrefab { get; set; }\r\n\t[Property] public GameObject BulletPrefab { get; set; }\r\n\t[Property] public Sprite EasySprite { get; set; }\r\n\t[Property] public Sprite Difficulty1Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty2Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty3Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty4Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty5Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty6Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty7Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty8Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty9Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty10Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty11Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty12Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty13Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty14Sprite { get; set; }\r\n\t[Property] public Sprite Difficulty15Sprite { get; set; }\r\n\r\n\r\n\t[Sync] public float Health { get; set; }\r\n\tprivate float _regenHpAccumulated;\r\n\tprivate float _drainHpAccumulated;\r\n\r\n\t[Sync] public Vector2 InputVector { get; set; }\r\n\r\n\tpublic GameObject ArrowAimer { get; private set; }\r\n\tpublic Sprite2D ArrowSprite { get; private set; }\r\n\tpublic Vector2 AimDir { get; private set; }\r\n\tprivate Vector2 _controllerAimTarget;\r\n\tprivate const float CONTROLLER_AIM_DEADZONE = 0.25f;\r\n\r\n\t[Sync] public bool IsDead { get; private set; }\r\n\tpublic float Timer { get; protected set; }\r\n\t[Sync] public bool IsReloading { get; protected set; }\r\n\t[Sync] public float ReloadProgress { get; protected set; }\r\n\r\n\tpublic const float BASE_MOVE_SPEED = 15f;\r\n\tprivate int _shotNum;\r\n\r\n\t[Sync] public int Level { get; protected set; }\r\n\tpublic int ExperienceTotal { get; protected set; }\r\n\tpublic int ExperienceCurrent { get; protected set; }\r\n\tpublic int ExperienceRequired { get; protected set; }\r\n\tpublic bool IsChoosingLevelUpReward { get; protected set; }\r\n\tpublic TimeSince TimeSinceLevelUp { get; set; }\r\n\tpublic RealTimeSince RealTimeSinceChoseUpgrade { get; set; }\r\n\tpublic List<Status> LevelUpChoices { get; private set; }\r\n\r\n\t[Sync] public float DashTimer { get; private set; }\r\n\t[Sync] public bool IsDashing { get; private set; }\r\n\t[Sync] public Vector2 DashVelocity { get; private set; }\r\n\t[Sync] public float DashInvulnTimer { get; private set; }\r\n\tprivate TimeSince _dashCloudTime;\r\n\tpublic float DashProgress { get; protected set; }\r\n\t[Sync] public float DashRechargeProgress { get; protected set; }\r\n\t[Sync] public int NumDashesAvailable { get; set; }\r\n\tpublic int AmmoCount { get; protected set; }\r\n\r\n\tpublic bool IsMoving => Velocity.LengthSquared > 0.05f && !IsDashing;\r\n\t//public bool IsMoving => (Position2D - _lastPos2D).LengthSquared > 0.000001f;\r\n\tpublic bool IsInputtingMove => Input.AnalogMove.LengthSquared > 0.01f;\r\n\tpublic bool IsInvulnerable => IsDashing && Stats[PlayerStat.NoDashInvuln] <= 0f;\r\n\tpublic bool IsTimePausedForChoosing => IsChoosingLevelUpReward && Stats[PlayerStat.PauseWhileChoosing] > 0f;\r\n\r\n\r\n\tprivate float _flashTimer;\r\n\tprivate bool _isFlashing;\r\n\tpublic TimeSince TimeSinceHurt { get; private set; }\r\n\tpublic TimeSince TimeSinceChangeHP { get; set; }\r\n\r\n\tprivate GameObject _shieldVfx;\r\n\r\n\t[Sync] public int NumRerollAvailable { get; set; }\r\n\r\n\t[Sync] public NetDictionary<PlayerStat, float> Stats { get; private set; } = new();\r\n\r\n\tpublic Dictionary<int, Status> Statuses { get; private set; }\r\n\r\n\tprivate Dictionary<Status, Dictionary<PlayerStat, ModifierData>> _modifiers_stat = new Dictionary<Status, Dictionary<PlayerStat, ModifierData>>();\r\n\tprivate Dictionary<PlayerStat, float> _original_properties_stat = new Dictionary<PlayerStat, float>();\r\n\r\n\tprivate bool _doneFirstUpdate;\r\n\tprivate TimeSince _timeSinceShoot;\r\n\tprivate TimeSince _timeSinceSpawn;\r\n\r\n\tprivate Vector2 _lastPos2D;\r\n\tprivate TimeSince _timeSinceTouchLeftSide;\r\n\tprivate bool _hasUnlockedSprinterAchievement;\r\n\r\n\tprivate bool _hasUnlockedExperiencedAchievement;\r\n\r\n\tpublic TimeSince TimeSinceInputMove { get; set; }\r\n\tprivate bool _hasUnlockedNoMoveAchievement;\r\n\r\n\tprivate List<CamShakeData> _camShakeDatas = new();\r\n\tpublic float CamShakeAmount { get; set; }\r\n\r\n\tpublic int ChoiceHash { get; set; }\r\n\r\n\tpublic TimeSince TimeSinceHurtLava { get; set; }\r\n\r\n\tpublic RealTimeSince RealTimeSinceDeath { get; set; }\r\n\tprivate float _arrowDeathAlphaStart;\r\n\tprivate bool _hasPlayedDeathSfx;\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\tScale = 1f;\r\n\r\n\t\tShadowOpacity = 0.8f;\r\n\t\tShadowScale = 1.12f;\r\n\r\n\t\tStatuses = new Dictionary<int, Status>();\r\n\t\tLevelUpChoices = new List<Status>();\r\n\t\tInitializeStats();\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tCollideWith.Add( typeof( Enemy ) );\r\n\t\tCollideWith.Add( typeof( Player ) );\r\n\r\n\t\tArrowAimer = ArrowAimerPrefab.Clone( WorldPosition );\r\n\t\tArrowAimer.SetParent( GameObject );\r\n\t\tArrowAimer.NetworkMode = NetworkMode.Never;\r\n\t\tArrowSprite = ArrowAimer.Components.Get<Sprite2D>();\r\n\t\tArrowSprite.Tint = Color.White.WithAlpha( 0f );\r\n\r\n\t\t_timeSinceShoot = 999f;\r\n\t\t_timeSinceSpawn = 0f;\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 0 ) Sprite.Sprite = EasySprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 1 ) Sprite.Sprite = Difficulty1Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 2 ) Sprite.Sprite = Difficulty2Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 3 ) Sprite.Sprite = Difficulty3Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 4 ) Sprite.Sprite = Difficulty4Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 5 ) Sprite.Sprite = Difficulty5Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 6 ) Sprite.Sprite = Difficulty6Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 7 ) Sprite.Sprite = Difficulty7Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 8 ) Sprite.Sprite = Difficulty8Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 9 ) Sprite.Sprite = Difficulty9Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 10 ) Sprite.Sprite = Difficulty10Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 11 ) Sprite.Sprite = Difficulty11Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 12 ) Sprite.Sprite = Difficulty12Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 13 ) Sprite.Sprite = Difficulty13Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 14 ) Sprite.Sprite = Difficulty14Sprite;\r\n\t\telse if ( Manager.Instance.Difficulty == 15 ) Sprite.Sprite = Difficulty15Sprite;\r\n\r\n\t\t//Sprite.LocalScale = 0.5f * Globals.SPRITE_SCALE;\r\n\t}\r\n\r\n\tpublic void InitializeStats()\r\n\t{\r\n\t\t_original_properties_stat.Clear();\r\n\r\n\t\tif ( Network.Active )\r\n\t\t{\r\n\t\t\tRemoveShieldVfx();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tif ( _shieldVfx != null )\r\n\t\t\t{\r\n\t\t\t\t_shieldVfx.Destroy();\r\n\t\t\t\t_shieldVfx = null;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tLevel = 0;\r\n\t\tExperienceRequired = GetExperienceReqForLevel( Level + 1 );\r\n\t\tExperienceTotal = 0;\r\n\t\tExperienceCurrent = 0;\r\n\t\tStats[PlayerStat.AttackTime] = 0.15f;\r\n\t\tAmmoCount = 5;\r\n\t\tStats[PlayerStat.MaxAmmoCount] = AmmoCount;\r\n\t\tStats[PlayerStat.ReloadTime] = 1.5f;\r\n\t\tStats[PlayerStat.ReloadSpeed] = 1f;\r\n\t\tStats[PlayerStat.AttackSpeed] = 1f;\r\n\t\tStats[PlayerStat.BulletDamage] = 5f;\r\n\t\tStats[PlayerStat.BulletForce] = 0.55f;\r\n\t\tStats[PlayerStat.Recoil] = 0f;\r\n\t\tStats[PlayerStat.MoveSpeed] = 1f;\r\n\t\tStats[PlayerStat.NumProjectiles] = 1f;\r\n\t\tStats[PlayerStat.BulletSpread] = 35f;\r\n\t\tStats[PlayerStat.BulletInaccuracy] = 5f;\r\n\t\tStats[PlayerStat.BulletSpeed] = 4.5f;\r\n\t\tStats[PlayerStat.BulletLifetime] = 0.8f;\r\n\t\tStats[PlayerStat.Luck] = 1f;\r\n\t\tStats[PlayerStat.CritChance] = 0.05f;\r\n\t\tStats[PlayerStat.CritMultiplier] = 1.5f;\r\n\t\tStats[PlayerStat.LowHealthDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.FullHealthDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.ThornsPercent] = 0f;\r\n\r\n\t\tStats[PlayerStat.NumDashes] = 1f;\r\n\t\tNumDashesAvailable = (int)MathF.Round( Stats[PlayerStat.NumDashes] );\r\n\t\tStats[PlayerStat.DashCooldown] = 3f;\r\n\t\tStats[PlayerStat.DashInvulnTime] = 0.25f;\r\n\t\tStats[PlayerStat.DashStrength] = 3f;\r\n\t\tStats[PlayerStat.BulletNumPiercing] = 0f;\r\n\t\tStats[PlayerStat.BulletNumBouncing] = 0f;\r\n\t\tStats[PlayerStat.DashCharm] = 0f;\r\n\t\tStats[PlayerStat.CharmedEnemyDmgTakenMultiplier] = 1f;\r\n\t\tStats[PlayerStat.CharmedEnemyDmgDealtMultiplier] = 1f;\r\n\t\tStats[PlayerStat.BossArrivalTime] = 15 * 60f;\r\n\t\t//Stats[PlayerStat.BossArrivalTime] = (Manager.Instance.Difficulty >= 5 ? 10 : 15) * 60f;\r\n\t\tStats[PlayerStat.RadiusMultiplier] = 1f;\r\n\t\tStats[PlayerStat.SpecialistStatusAmount] = 0f;\r\n\t\tStats[PlayerStat.ZoomAmount] = 0f;\r\n\t\tStats[PlayerStat.MaxFireStacks] = 0f;\r\n\t\tStats[PlayerStat.XpRepel] = 0f;\r\n\t\tStats[PlayerStat.HealthPackAmount] = 20f;\r\n\t\tStats[PlayerStat.MaxBulletSpread] = 0f;\r\n\t\tStats[PlayerStat.ShootRandomDirChance] = 0f;\r\n\t\tStats[PlayerStat.DashRandomDirChance] = 0f;\r\n\t\tStats[PlayerStat.MoveSelfDmg] = 0f;\r\n\t\tStats[PlayerStat.MoveSelfDmgReqDist] = 0f;\r\n\t\tStats[PlayerStat.MoveSelfDmgAmount] = 0f;\r\n\t\tStats[PlayerStat.SelfDmgDistanceMoved] = 0f;\r\n\t\tStats[PlayerStat.IncreasedDmgTaken] = 0f;\r\n\t\tStats[PlayerStat.ReverseControls] = 0f;\r\n\t\tStats[PlayerStat.SelfCritChance] = 0f;\r\n\r\n\t\tHealth = 100f;\r\n\t\tStats[PlayerStat.MaxHp] = 100f;\r\n\t\t//Health = 1f;\r\n\t\t_regenHpAccumulated = 0f;\r\n\t\t_drainHpAccumulated = 0f;\r\n\r\n\t\tIsDead = false;\r\n\t\tRadius = 0.10f;\r\n\t\tGridPos = Manager.Instance.GetGridSquareForPos( Position2D );\r\n\t\tAimDir = new Vector2( 0f, 1f );\r\n\t\t_controllerAimTarget = Vector2.Zero;\r\n\t\tNumRerollAvailable = Manager.Instance.Difficulty < 0 ? 4 : (Manager.Instance.Difficulty >= 3 ? 3 : 2);\r\n\r\n\t\tStats[PlayerStat.FireDamage] = 1f;\r\n\t\tStats[PlayerStat.FireLifetime] = 3f;\r\n\t\tStats[PlayerStat.ShootFireIgniteChance] = 0f;\r\n\t\tStats[PlayerStat.FireSpreadChance] = 0f;\r\n\t\tStats[PlayerStat.ShootFreezeChance] = 0f;\r\n\t\tStats[PlayerStat.FreezeLifetime] = 4f;\r\n\t\tStats[PlayerStat.FreezeTimeScale] = 0.55f;\r\n\t\tStats[PlayerStat.FreezeOnMeleeChance] = 0f;\r\n\t\tStats[PlayerStat.FreezeFireDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.FearLifetime] = 4.5f;\r\n\t\tStats[PlayerStat.FearDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.FearOnMeleeChance] = 0f;\r\n\r\n\t\tStats[PlayerStat.CoinAttractRange] = 1.7f;\r\n\t\tStats[PlayerStat.CoinAttractStrength] = 3.5f;\r\n\r\n\t\tStats[PlayerStat.NumUpgradeChoices] = 3f;\r\n\t\tStats[PlayerStat.HealthRegen] = Manager.Instance.Difficulty == -1 ? 0.4f : 0f;\r\n\t\tStats[PlayerStat.HealthRegenStill] = 0f;\r\n\t\t//Stats[PlayerStat.HealthDrain] = 0f;\r\n\t\tStats[PlayerStat.DamageReductionPercent] = 0f;\r\n\t\tStats[PlayerStat.PushStrength] = 30f;\r\n\t\tStats[PlayerStat.LastAmmoDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.BulletDamageGrow] = 0f;\r\n\t\tStats[PlayerStat.BulletDamageShrink] = 0f;\r\n\t\tStats[PlayerStat.BulletDistanceDamage] = 0f;\r\n\t\tStats[PlayerStat.NumRerollsPerLevel] = 1f;\r\n\t\tStats[PlayerStat.DamagePerEarlierShot] = 0f;\r\n\t\tStats[PlayerStat.DamageForSpeed] = 0f;\r\n\t\tStats[PlayerStat.OverallDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.ExplosionSizeMultiplier] = 1f;\r\n\t\tStats[PlayerStat.GrenadeVelocity] = 8f;\r\n\t\tStats[PlayerStat.ExplosionDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.BulletDamageMultiplier] = 1f;\r\n\t\tStats[PlayerStat.ExplosionDamageReductionPercent] = 0f;\r\n\t\tStats[PlayerStat.NonExplosionDamageIncreasePercent] = 0f;\r\n\t\tStats[PlayerStat.GrenadeStickyPercent] = 0f;\r\n\t\tStats[PlayerStat.GrenadeFearChance] = 0f;\r\n\t\tStats[PlayerStat.FearDrainPercent] = 0f;\r\n\t\tStats[PlayerStat.FearPainPercent] = 0f;\r\n\t\tStats[PlayerStat.CrateChanceAdditional] = 0f;\r\n\t\tStats[PlayerStat.AttackSpeedStill] = 1f;\r\n\t\tStats[PlayerStat.FearDropGrenadeChance] = 0f;\r\n\t\tStats[PlayerStat.FrozenShardsNum] = 0f;\r\n\t\tStats[PlayerStat.NoDashInvuln] = 0f;\r\n\t\tStats[PlayerStat.BulletFlatDamageAddition] = 0f;\r\n\t\tStats[PlayerStat.GrenadesCanCrit] = 0f;\r\n\t\tStats[PlayerStat.BulletHealTeammateAmount] = 0f;\r\n\t\tStats[PlayerStat.HomingBulletChance] = 0f;\r\n\t\tStats[PlayerStat.PauseWhileChoosing] = 0f;\r\n\r\n\t\tStatuses.Clear();\r\n\t\t_modifiers_stat.Clear();\r\n\r\n\t\t_isFlashing = false;\r\n\t\tSprite.FlashTint = Color.White.WithAlpha( 0f );\r\n\t\tSprite.Tint = Color.White;\r\n\t\tIsChoosingLevelUpReward = false;\r\n\t\tIsDashing = false;\r\n\t\tIsReloading = true;\r\n\t\tTimer = Stats[PlayerStat.ReloadTime];\r\n\t\tReloadProgress = 0f;\r\n\t\tDashProgress = 0f;\r\n\t\tDashRechargeProgress = 1f;\r\n\t\tTempWeight = 0f;\r\n\t\t_shotNum = 0;\r\n\t\tTimeSinceHurt = 999f;\r\n\t\tTimeSinceChangeHP = 999f;\r\n\t\tTimeSinceHurtLava = 999f;\r\n\t\tShadowOpacity = 0.8f;\r\n\t\tShadowSpriteDirty = true;\r\n\r\n\t\t_timeSinceTouchLeftSide = 999f;\r\n\t\tTimeSinceInputMove = 0f;\r\n\r\n\t\tTimeSinceLevelUp = 999f;\r\n\t\tRealTimeSinceChoseUpgrade = 999f;\r\n\r\n\t\t_camShakeDatas.Clear();\r\n\r\n\t\tChoiceHash = 0;\r\n\t}\r\n\r\n\tpublic Vector2 AverageVelocity { get; private set; }\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\tAverageVelocity += Velocity * Time.Delta * 8f;\r\n\t\t//AverageVelocity = Utils.DynamicEaseTo( AverageVelocity, Vector2.Zero, 0.02f, Time.Delta );\r\n\t\tAverageVelocity *= (1f - Time.Delta * 2.7f);\r\n\r\n\t\t//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );\r\n\t\t//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );\r\n\r\n\t\t//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );\r\n\t\t//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );\r\n\r\n\t\tVector2 anchor = Position2D + AverageVelocity * 0.1f;\r\n\t\tVector2 perp = Utils.GetPerpendicularVector( AverageVelocity ).Normal;\r\n\t\tvar perpA = anchor - perp * 10f;\r\n\t\tvar perpB = anchor + perp * 10f;\r\n\r\n\t\t//Gizmo.Draw.Color = Color.Green.WithAlpha( 0.3f );\r\n\t\t//Gizmo.Draw.Line( perpA, perpB );\r\n\r\n\t\t//Gizmo.Draw.Color = Color.White.WithAlpha( 0.03f );\r\n\t\t//Gizmo.Draw.LineSphere( (Vector3)Position2D, 2, 16 );\r\n\r\n\t\t//Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );\r\n\t\t//Gizmo.Draw.Text( $\"{Stats[PlayerStat.FearPainPercent]}\", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );\r\n\r\n\t\tif ( !_doneFirstUpdate )\r\n\t\t{\r\n\t\t\tSpawnShadow( ShadowScale, ShadowOpacity );\r\n\t\t\tManager.Instance.Camera2D.SetPos( Position2D );\r\n\r\n\t\t\t_doneFirstUpdate = true;\r\n\t\t}\r\n\r\n\t\tInputVector = new Vector2( -Input.AnalogMove.y, Input.AnalogMove.x );\r\n\r\n\t\tif ( Stats[PlayerStat.ReverseControls] > 0f )\r\n\t\t\tInputVector *= -1f;\r\n\r\n\t\t//if ( Input.Pressed( \"Menu\" ) )\r\n\t\t//{\r\n\t\t//\tManager.Instance.Restart();\r\n\t\t//\treturn;\r\n\t\t//}\r\n\r\n\t\tHandleCamShaking();\r\n\r\n\t\tif ( IsDead )\r\n\t\t{\r\n\t\t\tSprite.FlashTint = RealTimeSinceDeath < 0.1f\r\n\t\t\t\t? Color.Red\r\n\t\t\t\t: Color.White.WithAlpha( 0f );\r\n\r\n\t\t\tShadowOpacity = Utils.Map( RealTimeSinceDeath, 0f, Manager.FINAL_PANEL_WAIT_TIME, 0.8f, 0f, EasingType.Linear );\r\n\t\t\tShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );\r\n\r\n\t\t\tArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( RealTimeSinceDeath, 0f, 0.4f, _arrowDeathAlphaStart, 0f, EasingType.SineOut ) );\r\n\r\n\t\t\tif ( !_hasPlayedDeathSfx && RealTimeSinceDeath > 0.7f )\r\n\t\t\t{\r\n\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Game.Random.Float( 0.55f, 0.65f ), volume: 1f, maxDist: 5.5f );\r\n\t\t\t\t_hasPlayedDeathSfx = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !Manager.Instance.ShouldUpdatePlayer )\r\n\t\t\treturn;\r\n\r\n\t\tfloat dt = Time.Delta;\r\n\r\n\t\tif ( MathF.Abs( Velocity.x ) > 0.01f )\r\n\t\t\tSprite.SpriteFlags = Velocity.x > 0f ? SpriteFlags.HorizontalFlip : SpriteFlags.None;\r\n\r\n\t\tbool hurting = TimeSinceHurt < 0.25f;\r\n\t\tbool attacking = !IsReloading;\r\n\t\tbool moving = Velocity.LengthSquared > 0.01f && InputVector.LengthSquared > 0.1f;\r\n\r\n\t\tstring stateStr = \"\";\r\n\t\tif ( IsDead )\r\n\t\t\tstateStr = \"ghost_\";\r\n\t\telse if ( hurting && attacking )\r\n\t\t\tstateStr = \"hurt_attack_\";\r\n\t\telse if ( hurting )\r\n\t\t\tstateStr = \"hurt_\";\r\n\t\telse if ( attacking )\r\n\t\t\tstateStr = \"attack_\";\r\n\r\n\t\tSprite.PlayAnimation( $\"{stateStr}{(moving ? \"walk\" : \"idle\")}\" );\r\n\t\tSprite.PlaybackSpeed = moving ? Utils.Map( Velocity.Length, 0f, 2f, 1.5f, 2f ) : 0.66f;\r\n\r\n\t\tSprite.LocalRotation = new Angles( 0f, -90f + (Velocity.Length * Utils.FastSin( Time.Now * MathF.PI * 6f ) * 1.6f) * (Sprite.SpriteFlags.HasFlag( SpriteFlags.HorizontalFlip ) ? -1f : 1f), 0f );\r\n\r\n\t\tif ( !IsDead )\r\n\t\t{\r\n\t\t\tHandleFlashing( dt );\r\n\t\t}\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\t// ACHIEVEMENTS\r\n\t\tif ( Manager.Instance.Difficulty >= 0 )\r\n\t\t{\r\n\t\t\tif ( _lastPos2D.x < -15.7f && WorldPosition.x >= -15.7f )\r\n\t\t\t{\r\n\t\t\t\t_timeSinceTouchLeftSide = 0f;\r\n\t\t\t}\r\n\r\n\t\t\tif ( _lastPos2D.x < 15.7f && WorldPosition.x >= 15.7f )\r\n\t\t\t{\r\n\t\t\t\tLog.Info( $\"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}\" );\r\n\r\n\t\t\t\tif ( _timeSinceTouchLeftSide < 5.5f )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( !_hasUnlockedSprinterAchievement )\r\n\t\t\t\t\t\tSandbox.Services.Achievements.Unlock( \"sprinter\" );\r\n\r\n\t\t\t\t\t_hasUnlockedSprinterAchievement = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( !_hasUnlockedNoMoveAchievement )\r\n\t\t\t{\r\n\t\t\t\tif ( IsInputtingMove )\r\n\t\t\t\t{\r\n\t\t\t\t\tTimeSinceInputMove = 0f;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( TimeSinceInputMove > 60f * 10f )\r\n\t\t\t\t{\r\n\t\t\t\t\tSandbox.Services.Achievements.Unlock( \"stand_ground\" );\r\n\t\t\t\t\t_hasUnlockedNoMoveAchievement = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t//Gizmo.Draw.Color = Color.White;\r\n\t\t//Gizmo.Draw.Text( $\"ArrowAimer: {ArrowAimer}\", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );\r\n\r\n\t\t//Gizmo.Draw.ScreenText( $\"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}\", new Vector2( 50, 50 ) );\r\n\r\n\t\t_lastPos2D = Position2D;\r\n\r\n\t\tvar velocity = Velocity + (IsDashing ? DashVelocity : Vector2.Zero);\r\n\t\tPosition2D += velocity * dt;\r\n\r\n\t\tif ( Stats[PlayerStat.MoveSelfDmg] > 0f )\r\n\t\t{\r\n\t\t\tStats[PlayerStat.SelfDmgDistanceMoved] += velocity.Length * dt;\r\n\r\n\t\t\tif ( !IsInvulnerable && Stats[PlayerStat.SelfDmgDistanceMoved] > Stats[PlayerStat.MoveSelfDmgReqDist] )\r\n\t\t\t{\r\n\t\t\t\tStats[PlayerStat.SelfDmgDistanceMoved] -= Stats[PlayerStat.MoveSelfDmgReqDist];\r\n\t\t\t\tDamage( Stats[PlayerStat.MoveSelfDmgAmount], PlayerDamageType.Self );\r\n\t\t\t\tManager.Instance.PlaySfxNearby( \"zombie.attack.player\", Position2D, pitch: Game.Random.Float( 1.25f, 1.45f ), volume: 0.9f, maxDist: 3f );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tWorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );\r\n\r\n\t\t//Velocity = Utils.DynamicEaseTo( Velocity, Vector2.Zero, 0.2f, dt );\r\n\t\tVelocity *= Math.Max( 1f - dt * 12.9f, 0f );\r\n\r\n\t\tif ( Velocity.LengthSquared < 0.001f )\r\n\t\t\tVelocity = Vector2.Zero;\r\n\r\n\t\tTempWeight *= (1f - dt * 4.7f);\r\n\r\n\t\tif ( InputVector.LengthSquared > 0f )\r\n\t\t{\r\n\t\t\tVelocity += InputVector.Normal * Stats[PlayerStat.MoveSpeed] * BASE_MOVE_SPEED * dt;\r\n\t\t\t//Log.Info( $\"dt: {dt}\" );\r\n\t\t}\r\n\r\n\t\tHandleBounds();\r\n\r\n\t\tManager.Instance.Camera2D.TargetPos = Position2D;\r\n\r\n\t\tif ( Input.UsingController )\r\n\t\t{\r\n\t\t\t// raw stick rather than AnalogLook, which is scaled by look sensitivity and affected by invert-y (SDL stick y is positive downward)\r\n\t\t\tvar stick = new Vector2( Input.GetAnalog( InputAnalog.RightStickX ), -Input.GetAnalog( InputAnalog.RightStickY ) );\r\n\t\t\tif ( stick.Length > CONTROLLER_AIM_DEADZONE )\r\n\t\t\t\t_controllerAimTarget = stick.Normal;\r\n\r\n\t\t\tif ( _controllerAimTarget.LengthSquared > 0f )\r\n\t\t\t{\r\n\t\t\t\t// reverse here rather than when storing the target, so it still flips when ReverseControls toggles while the stick is released\r\n\t\t\t\tvar target = Stats[PlayerStat.ReverseControls] > 0f ? -_controllerAimTarget : _controllerAimTarget;\r\n\r\n\t\t\t\t// rotate by angle, since a linear lerp toward the exact opposite direction never changes direction\r\n\t\t\t\t// keeps turning after the stick is released, so a quick flick still reaches the target\r\n\t\t\t\tfloat currentDegrees = Utils.VectorToDegrees( AimDir );\r\n\t\t\t\tfloat deltaDegrees = ((Utils.VectorToDegrees( target ) - currentDegrees + 540f) % 360f) - 180f;\r\n\t\t\t\tAimDir = Utils.DegreesToVector( currentDegrees + deltaDegrees * MathF.Min( Time.Delta * 25f, 1f ) );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tAimDir = (Manager.Instance.MouseWorldPos - (Position2D + new Vector2( 0f, 0.5f ))).Normal;\r\n\r\n\t\t\tif ( Stats[PlayerStat.ReverseControls] > 0f )\r\n\t\t\t\tAimDir *= -1f;\r\n\r\n\t\t\t// forget the last stick direction, so switching back to the controller keeps the mouse aim instead of swinging back to it\r\n\t\t\t_controllerAimTarget = Vector2.Zero;\r\n\t\t}\r\n\r\n\t\tif ( ArrowAimer != null && !Manager.Instance.IsPauseMenuOpen && !IsTimePausedForChoosing )\r\n\t\t{\r\n\t\t\tArrowAimer.LocalRotation = new Angles( 0f, MathF.Atan2( AimDir.y, AimDir.x ) * (180f / MathF.PI) - 180f, 0f );\r\n\t\t\tArrowAimer.LocalPosition = new Vector2( 0f, 0.4f ) + AimDir * Utils.Map( _timeSinceShoot, 0f, 0.25f, 0.6f, 0.55f, EasingType.QuadOut );\r\n\t\t\tArrowAimer.LocalScale = new Vector3( Utils.Map( _timeSinceShoot, 0f, 0.25f, 1.25f, 0.75f, EasingType.QuadOut ), 1f, 1f ) * 0.005f;\r\n\t\t\tArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceShoot, 0f, 0.3f, 1f, 0.3f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, 1f, 0f, 1f, EasingType.Linear ) );\r\n\t\t}\r\n\r\n\t\tfor ( int dx = -1; dx <= 1; dx++ )\r\n\t\t{\r\n\t\t\tfor ( int dy = -1; dy <= 1; dy++ )\r\n\t\t\t{\r\n\t\t\t\tManager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !IsDead )\r\n\t\t{\r\n\t\t\tHandleDashing( dt );\r\n\t\t\tHandleStatuses( dt );\r\n\t\t\tHandleShooting( dt );\r\n\t\t\tHandleRegen( dt );\r\n\t\t}\r\n\r\n\t\tif ( IsChoosingLevelUpReward && !Manager.Instance.IsPauseMenuOpen )\r\n\t\t{\r\n\t\t\tif ( Input.Pressed( \"reload\" ) ) UseReroll();\r\n\t\t\telse if ( Input.Pressed( \"Slot1\" ) ) UseChoiceHotkey( 1 );\r\n\t\t\telse if ( Input.Pressed( \"Slot2\" ) ) UseChoiceHotkey( 2 );\r\n\t\t\telse if ( Input.Pressed( \"Slot3\" ) ) UseChoiceHotkey( 3 );\r\n\t\t\telse if ( Input.Pressed( \"Slot4\" ) ) UseChoiceHotkey( 4 );\r\n\t\t\telse if ( Input.Pressed( \"Slot5\" ) ) UseChoiceHotkey( 5 );\r\n\t\t\telse if ( Input.Pressed( \"Slot6\" ) ) UseChoiceHotkey( 6 );\r\n\t\t}\r\n\r\n\t\tif ( Input.Pressed( \"use\" ) )\r\n\t\t{\r\n\t\t\t//AddExperience( 10 );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid HandleRegen( float dt )\r\n\t{\r\n\t\tif ( Math.Abs( Stats[PlayerStat.HealthRegen] ) > 0f )\r\n\t\t\tRegenHealth( Stats[PlayerStat.HealthRegen] * dt );\r\n\r\n\t\t//if ( Math.Abs( Stats[PlayerStat.HealthDrain] ) > 0f )\r\n\t\t//\tRegenHealth( Stats[PlayerStat.HealthDrain] * dt );\r\n\r\n\t\tif ( Stats[PlayerStat.HealthRegenStill] > 0f && !IsMoving )\r\n\t\t{\r\n\t\t\tRegenHealth( Stats[PlayerStat.HealthRegenStill] * dt );\r\n\r\n\t\t\tif ( !IsDashing && Health < Stats[PlayerStat.MaxHp] )\r\n\t\t\t\tTimeSinceChangeHP = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void RegenHealth( float amount )\r\n\t{\r\n\t\tfloat maxHp = Stats[PlayerStat.MaxHp];\r\n\t\tfloat hpMissing = maxHp - Health;\r\n\r\n\t\tif ( amount > 0f )\r\n\t\t{\r\n\t\t\tif ( hpMissing <= 0f )\r\n\t\t\t\treturn;\r\n\r\n\t\t\t_regenHpAccumulated += amount;\r\n\r\n\t\t\tif ( _regenHpAccumulated < 0.85f && hpMissing > _regenHpAccumulated )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tfloat hpRecovered = Math.Min( _regenHpAccumulated, hpMissing );\r\n\r\n\t\t\tHealth += hpRecovered;\r\n\r\n\t\t\t//var particle = DamageNumbersLegacy.Create( hpRecovered, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.8f );\r\n\t\t\t//Vector3 velocity = new Vector3( Game.Random.Float(-0.5f, 0.5f), 0f, 0f );\r\n\t\t\t//Vector3 gravity = new Vector3( 0f, 1.5f, 0f );\r\n\t\t\t//particle.SetVector( 1, velocity );\r\n\t\t\t//particle.SetNamedValue( \"Gravity\", gravity );\r\n\r\n\t\t\tvar pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );\r\n\t\t\tfloat size = 1.1f;\r\n\t\t\tManager.Instance.SpawnDamageNumber( pos, amount, Color.Green, size, FloaterType.Heal );\r\n\r\n\t\t\t_regenHpAccumulated = 0f;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_drainHpAccumulated += amount;\r\n\t\t\tif ( _drainHpAccumulated < -1f )\r\n\t\t\t{\r\n\t\t\t\tvar dmgAmount = MathF.Truncate( _drainHpAccumulated );\r\n\t\t\t\t_drainHpAccumulated -= dmgAmount;\r\n\r\n\t\t\t\tDamage( MathF.Abs( dmgAmount ), PlayerDamageType.Self );\r\n\r\n\t\t\t\tManager.Instance.PlaySfxNearby( \"lava_puddle_03\", Position2D, pitch: Game.Random.Float( 1.7f, 1.75f ), volume: 0.15f, maxDist: 4f );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tvoid HandleDashing( float dt )\r\n\t{\r\n\t\tint numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );\r\n\t\tif ( NumDashesAvailable < numDashes )\r\n\t\t{\r\n\t\t\tDashTimer -= dt;\r\n\t\t\tDashRechargeProgress = Utils.Map( DashTimer, Stats[PlayerStat.DashCooldown], 0f, 0f, 1f );\r\n\t\t\tif ( DashTimer <= 0f )\r\n\t\t\t{\r\n\t\t\t\tDashRecharged();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( DashInvulnTimer > 0f )\r\n\t\t{\r\n\t\t\tDashInvulnTimer -= dt;\r\n\t\t\tDashProgress = Utils.Map( DashInvulnTimer, Stats[PlayerStat.DashInvulnTime], 0f, 0f, 1f );\r\n\t\t\tif ( DashInvulnTimer <= 0f )\r\n\t\t\t{\r\n\t\t\t\tIsDashing = false;\r\n\t\t\t\t//Sprite.Tint = Color.White;\r\n\t\t\t\tSprite.FlashTint = Color.White.WithAlpha( 0f );\r\n\r\n\t\t\t\tDashFinished();\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tif ( Stats[PlayerStat.DashCharm] > 0f )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( IsInvulnerable )\r\n\t\t\t\t\t\tSprite.FlashTint = new Color( Game.Random.Float( 0.8f, 1f ), Game.Random.Float( 0f, 0.1f ), Game.Random.Float( 0.8f, 1f ), 0.9f );\r\n\t\t\t\t\telse if ( !_isFlashing )\r\n\t\t\t\t\t\tSprite.FlashTint = new Color( Game.Random.Float( 0.9f, 1f ), Game.Random.Float( 0.1f, 0.3f ), Game.Random.Float( 0.9f, 1f ), 0.8f );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( IsInvulnerable )\r\n\t\t\t\t\t\tSprite.FlashTint = new Color( Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0.8f, 1f ), 0.9f );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( _dashCloudTime > Game.Random.Float( 0.1f, 0.2f ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tSpawnDashCloudClient();\r\n\t\t\t\t\t_dashCloudTime = 0f;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tbool pressedDash = Input.Pressed( \"Dash\" ) && !Gamepad.IsAClickingUi;\r\n\t\tif ( (pressedDash ||Input.Pressed( \"RB\" )) && !Manager.Instance.IsPauseMenuOpen )\r\n\t\t{\r\n\t\t\t//Position2D = Manager.Instance.MouseWorldPos;\r\n\t\t\t////Manager.Instance.Camera2D.SetPos( Position2D );\r\n\t\t\t//Transform.ClearInterpolation();\r\n\t\t\t//return;\r\n\r\n\t\t\tDash();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Dash()\r\n\t{\r\n\t\tif ( NumDashesAvailable <= 0 || IsTimePausedForChoosing )\r\n\t\t\treturn;\r\n\r\n\t\tVector2 dashDir = Velocity.LengthSquared > 0f ? Velocity.Normal : AimDir;\r\n\r\n\t\tif ( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.DashRandomDirChance] )\r\n\t\t\tdashDir = Utils.GetRandomVector();\r\n\r\n\t\tDashVelocity = dashDir * Stats[PlayerStat.DashStrength];\r\n\t\tTempWeight = 2f;\r\n\r\n\t\tif ( NumDashesAvailable == (int)Stats[PlayerStat.NumDashes] )\r\n\t\t\tDashTimer = Stats[PlayerStat.DashCooldown];\r\n\r\n\t\tNumDashesAvailable--;\r\n\t\tIsDashing = true;\r\n\t\tDashInvulnTimer = Stats[PlayerStat.DashInvulnTime];\r\n\t\tDashProgress = 0f;\r\n\t\tDashRechargeProgress = 0f;\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"player.dash\", Position2D + dashDir * 0.5f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 1f, maxDist: 4f );\r\n\t\tSpawnDashCloudClient();\r\n\t\t_dashCloudTime = 0f;\r\n\r\n\t\tForEachStatus( status => status.OnDashStarted() );\r\n\t}\r\n\r\n\tpublic void DashFinished()\r\n\t{\r\n\t\tForEachStatus( status => status.OnDashFinished() );\r\n\t}\r\n\r\n\tpublic void DashRecharged()\r\n\t{\r\n\t\tNumDashesAvailable++;\r\n\t\tvar numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );\r\n\t\tif ( NumDashesAvailable > numDashes )\r\n\t\t\tNumDashesAvailable = numDashes;\r\n\r\n\t\tif ( NumDashesAvailable < numDashes )\r\n\t\t{\r\n\t\t\tDashTimer = Stats[PlayerStat.DashCooldown];\r\n\t\t\tDashRechargeProgress = 0f;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tDashRechargeProgress = 1f;\r\n\t\t}\r\n\r\n\t\tForEachStatus( status => status.OnDashRecharged() );\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"player.dash.recharge\", Position2D, pitch: Utils.Map( NumDashesAvailable, 1, numDashes, 1f, 1.2f ), volume: 0.2f, maxDist: 5f );\r\n\t}\r\n\r\n\tvoid HandleBounds()\r\n\t{\r\n\t\tvar x_min = Manager.Instance.BOUNDS_MIN.x + Radius;\r\n\t\tvar x_max = Manager.Instance.BOUNDS_MAX.x - Radius;\r\n\t\tvar y_min = Manager.Instance.BOUNDS_MIN.y;\r\n\t\tvar y_max = Manager.Instance.BOUNDS_MAX.y - Radius;\r\n\r\n\t\tif ( Position2D.x < x_min )\r\n\t\t\tPosition2D = new Vector2( x_min, Position2D.y );\r\n\t\telse if ( Position2D.x > x_max )\r\n\t\t\tPosition2D = new Vector2( x_max, Position2D.y );\r\n\r\n\t\tif ( Position2D.y < y_min )\r\n\t\t\tPosition2D = new Vector2( Position2D.x, y_min );\r\n\t\telse if ( Position2D.y > y_max )\r\n\t\t\tPosition2D = new Vector2( Position2D.x, y_max );\r\n\t}\r\n\r\n\tpublic int GetExperienceReqForLevel( int level )\r\n\t{\r\n\t\tswitch ( Manager.Instance.Difficulty )\r\n\t\t{\r\n\t\t\tcase -1:\r\n\t\t\t\treturn (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 240f, EasingType.SineIn ) );\r\n\t\t\tcase 0:\r\n\t\t\tdefault:\r\n\t\t\t\treturn (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 320f, EasingType.SineIn ) );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Flash( float time )\r\n\t{\r\n\t\tif ( _isFlashing )\r\n\t\t\treturn;\r\n\r\n\t\t//Sprite.Tint = new Color( 1f, 0f, 0f );\r\n\t\tSprite.FlashTint = new Color( 1f, 0f, 0f, 1f );\r\n\t\t_isFlashing = true;\r\n\t\t_flashTimer = time;\r\n\t}\r\n\r\n\tpublic void Heal( float amount, float flashTime )\r\n\t{\r\n\t\t//Sprite.Tint = new Color( 0f, 1f, 0f );\r\n\t\tSprite.FlashTint = new Color( 0f, 1f, 0f, 1f );\r\n\t\t_isFlashing = true;\r\n\t\t_flashTimer = flashTime;\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tif ( Health < Stats[PlayerStat.MaxHp] )\r\n\t\t\tTimeSinceChangeHP = 0f;\r\n\r\n\t\tHealth += amount;\r\n\t\tif ( Health > Stats[PlayerStat.MaxHp] )\r\n\t\t\tHealth = Stats[PlayerStat.MaxHp];\r\n\t}\r\n\r\n\tvoid HandleFlashing( float dt )\r\n\t{\r\n\t\tif ( _isFlashing )\r\n\t\t{\r\n\t\t\t_flashTimer -= dt;\r\n\t\t\tif ( _flashTimer < 0f )\r\n\t\t\t{\r\n\t\t\t\t_isFlashing = false;\r\n\t\t\t\t//Sprite.Tint = Color.White;\r\n\t\t\t\tSprite.FlashTint = Color.White.WithAlpha( 0f );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t[ConCmd( \"give_status\" )]\r\n\tpublic static void GiveStatus( string name )\r\n\t{\r\n\t\t// Cheat only works in the editor\r\n\t\tif ( !Game.IsEditor )\r\n\t\t\treturn;\r\n\r\n\t\tvar type = TypeLibrary.GetType( name );\r\n\t\tif ( type == null )\r\n\t\t{\r\n\t\t\tLog.Info( $\"No status with name '{name}' found!\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tManager.Instance?.GetLocalPlayer()?.AddStatus( type );\r\n\t}\r\n\r\n\tpublic void AddStatus( TypeDescription type )\r\n\t{\r\n\t\tStatus status = null;\r\n\t\tvar typeIdentity = type.Identity;\r\n\r\n\t\tForEachStatus( status => status.OnAddStatus( typeIdentity ) );\r\n\r\n\t\tif ( Statuses.ContainsKey( typeIdentity ) )\r\n\t\t{\r\n\t\t\tstatus = Statuses[typeIdentity];\r\n\t\t\tstatus.Level++;\r\n\t\t}\r\n\r\n\t\tif ( status == null )\r\n\t\t{\r\n\t\t\tstatus = StatusManager.CreateStatus( type );\r\n\t\t\tStatuses.Add( typeIdentity, status );\r\n\t\t\tstatus.Init( this );\r\n\t\t}\r\n\r\n\t\t//Sandbox.Services.Stats.Increment( Client, \"status\", 1, $\"{type.Name.ToLowerInvariant()}\", new { Status = type.Name.ToLowerInvariant(), Level = status.Level } );\r\n\r\n\t\tstatus.Refresh();\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"click\", Position2D, 0.9f, 0.75f, 5f );\r\n\r\n\t\tLevelUpChoices.Clear();\r\n\t\tIsChoosingLevelUpReward = false;\r\n\t\tRealTimeSinceChoseUpgrade = 0f;\r\n\r\n\t\tCheckForLevelUp();\r\n\t}\r\n\r\n\tpublic bool HasStatus( TypeDescription type )\r\n\t{\r\n\t\treturn Statuses.ContainsKey( type.Identity );\r\n\t}\r\n\r\n\tpublic Status GetStatus( TypeDescription type )\r\n\t{\r\n\t\tif ( Statuses.ContainsKey( type.Identity ) )\r\n\t\t\treturn Statuses[type.Identity];\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\tpublic int GetStatusLevel( TypeDescription type )\r\n\t{\r\n\t\tif ( Statuses.ContainsKey( type.Identity ) )\r\n\t\t\treturn Statuses[type.Identity].Level;\r\n\r\n\t\treturn 0;\r\n\t}\r\n\r\n\tpublic void Modify( Status caller, PlayerStat statType, float value, ModifierType type, float priority = 0f, bool update = true )\r\n\t{\r\n\t\tif ( !_modifiers_stat.ContainsKey( caller ) )\r\n\t\t\t_modifiers_stat.Add( caller, new Dictionary<PlayerStat, ModifierData>() );\r\n\r\n\t\t_modifiers_stat[caller][statType] = new ModifierData( value, type, priority );\r\n\r\n\t\tif ( update )\r\n\t\t\tUpdateProperty( statType );\r\n\t}\r\n\r\n\tpublic void AdjustBaseStat( PlayerStat statType, float amount, bool update = true )\r\n\t{\r\n\t\tif ( !_original_properties_stat.ContainsKey( statType ) )\r\n\t\t\t_original_properties_stat.Add( statType, Stats[statType] );\r\n\r\n\t\t_original_properties_stat[statType] += amount;\r\n\r\n\t\tif ( update )\r\n\t\t\tUpdateProperty( statType );\r\n\t}\r\n\r\n\tvoid UpdateProperty( PlayerStat statType )\r\n\t{\r\n\t\tif ( !_original_properties_stat.ContainsKey( statType ) )\r\n\t\t{\r\n\t\t\t_original_properties_stat.Add( statType, Stats[statType] );\r\n\t\t}\r\n\r\n\t\tfloat curr_value = _original_properties_stat[statType];\r\n\t\tfloat curr_set = curr_value;\r\n\t\tbool should_set = false;\r\n\t\tfloat curr_priority = 0f;\r\n\t\tfloat total_add = 0f;\r\n\t\tfloat total_mult = 1f;\r\n\r\n\t\tforeach ( Status caller in _modifiers_stat.Keys )\r\n\t\t{\r\n\t\t\tvar dict = _modifiers_stat[caller];\r\n\t\t\tif ( dict.ContainsKey( statType ) )\r\n\t\t\t{\r\n\t\t\t\tvar mod_data = dict[statType];\r\n\t\t\t\tswitch ( mod_data.type )\r\n\t\t\t\t{\r\n\t\t\t\t\tcase ModifierType.Set:\r\n\t\t\t\t\t\tif ( mod_data.priority >= curr_priority )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tcurr_set = mod_data.value;\r\n\t\t\t\t\t\t\tcurr_priority = mod_data.priority;\r\n\t\t\t\t\t\t\tshould_set = true;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase ModifierType.Add:\r\n\t\t\t\t\t\ttotal_add += mod_data.value;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase ModifierType.Mult:\r\n\t\t\t\t\t\ttotal_mult *= mod_data.value;\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( should_set )\r\n\t\t\tcurr_value = curr_set;\r\n\r\n\t\tcurr_value += total_add;\r\n\t\tcurr_value *= total_mult;\r\n\r\n\t\tStats[statType] = curr_value;\r\n\r\n\t\tif ( statType == PlayerStat.MaxHp )\r\n\t\t\tStats[statType] = Math.Max( curr_value, 1f );\r\n\t}\r\n\r\n\tpublic void AddExperience( int xp )\r\n\t{\r\n\t\tExperienceTotal += xp;\r\n\t\tExperienceCurrent += xp;\r\n\r\n\t\t//var particle = DamageNumbersLegacy.Create( xp, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: new Color(0.1f, 0.1f, 1f), sizeMultiplier: 0.8f );\r\n\t\t//Vector3 velocity = new Vector3( 0f, 0f, 0f );\r\n\t\t//Vector3 gravity = new Vector3( 0f, 1f, 0f );\r\n\t\t//particle.SetVector( 1, velocity );\r\n\t\t//particle.SetNamedValue( \"Gravity\", gravity );\r\n\r\n\t\tvar pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );\r\n\t\tfloat size = Utils.Map( xp, 1f, 4f, 0.95f, 1.1f, EasingType.Linear );\r\n\t\tvar color = new Color( 0.4f, 0.4f, 1f );\r\n\t\tManager.Instance.SpawnDamageNumber( pos, xp, color, size, FloaterType.Xp );\r\n\r\n\t\tForEachStatus( status => status.OnGainExperience( xp ) );\r\n\t\tif ( !IsChoosingLevelUpReward )\r\n\t\t\tCheckForLevelUp();\r\n\t}\r\n\r\n\tpublic void LoseExperience( int amount )\r\n\t{\r\n\t\tExperienceCurrent = Math.Max( ExperienceCurrent - amount, 0 );\r\n\t}\r\n\r\n\tpublic void CheckForLevelUp()\r\n\t{\r\n\t\t//Log.Info(\"CheckForLevelUp: \" + ExperienceCurrent + \" / \" + ExperienceRequired + \" IsServer: \" + Sandbox.Game.IsServer + \" Level: \" + Level);\r\n\t\tif ( ExperienceCurrent >= ExperienceRequired && Manager.Instance.ShouldUpdatePlayer )\r\n\t\t\tLevelUp();\r\n\t}\r\n\r\n\tpublic void LevelUp()\r\n\t{\r\n\t\tExperienceCurrent -= ExperienceRequired;\r\n\r\n\t\tLevel++;\r\n\t\tExperienceRequired = GetExperienceReqForLevel( Level + 1 );\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 3 )\r\n\t\t\tNumRerollAvailable += (int)Stats[PlayerStat.NumRerollsPerLevel];\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"levelup\", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.5f, 5f );\r\n\r\n\t\tForEachStatus( status => status.OnLevelUp() );\r\n\r\n\t\tGenerateLevelUpChoices();\r\n\t\tIsChoosingLevelUpReward = true;\r\n\t\tTimeSinceLevelUp = 0f;\r\n\r\n\t\tif ( Manager.Instance.Difficulty >= 0 && !_hasUnlockedExperiencedAchievement && Level >= 85 )\r\n\t\t{\r\n\t\t\tSandbox.Services.Achievements.Unlock( \"experienced\" );\r\n\t\t\t_hasUnlockedExperiencedAchievement = true;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void UseReroll()\r\n\t{\r\n\t\tif ( NumRerollAvailable <= 0 )\r\n\t\t{\r\n\t\t\t// todo: sfx\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"reroll\", Position2D, Utils.Map( NumRerollAvailable, 0, 20, 0.9f, 1.4f, EasingType.QuadIn ), 0.6f, 5f );\r\n\r\n\t\tNumRerollAvailable--;\r\n\r\n\t\tGenerateLevelUpChoices();\r\n\r\n\t\tForEachStatus( status => status.OnReroll() );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Number of level up choices that have a hotkey (Slot1..Slot6 actions).\r\n\t/// </summary>\r\n\tpublic const int NUM_CHOICE_HOTKEYS = 6;\r\n\r\n\tpublic void UseChoiceHotkey( int num )\r\n\t{\r\n\t\tvar index = num - 1;\r\n\r\n\t\tif ( !IsChoosingLevelUpReward || index >= LevelUpChoices.Count )\r\n\t\t\treturn;\r\n\r\n\t\tAddStatus( TypeLibrary.GetType( LevelUpChoices[index].GetType() ) );\r\n\t}\r\n\r\n\tpublic float CheckDamageAmount( float damage, DamageType damageType )\r\n\t{\r\n\t\tif ( IsInvulnerable )\r\n\t\t{\r\n\t\t\treturn 0f;\r\n\t\t}\r\n\r\n\t\tif ( HasStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) && damageType != DamageType.LavaPuddle )\r\n\t\t{\r\n\t\t\tvar shieldStatus = GetStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) as ShieldStatus;\r\n\t\t\tif ( shieldStatus != null && shieldStatus.IsShielded )\r\n\t\t\t{\r\n\t\t\t\tshieldStatus.LoseShield();\r\n\t\t\t\treturn 0f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( Stats[PlayerStat.DamageReductionPercent] > 0f )\r\n\t\t\tdamage *= (1f - MathX.Clamp( Stats[PlayerStat.DamageReductionPercent], 0f, 1f ));\r\n\r\n\t\tif ( Stats[PlayerStat.IncreasedDmgTaken] > 0f )\r\n\t\t\tdamage *= (1f + MathX.Clamp( Stats[PlayerStat.IncreasedDmgTaken], 0f, 1f ));\r\n\r\n\t\tif ( damageType == DamageType.Explosion && Stats[PlayerStat.ExplosionDamageReductionPercent] > 0f )\r\n\t\t\tdamage *= (1f - MathX.Clamp( Stats[PlayerStat.ExplosionDamageReductionPercent], 0f, 1f ));\r\n\r\n\t\tif ( damageType != DamageType.Explosion && Stats[PlayerStat.NonExplosionDamageIncreasePercent] > 0f )\r\n\t\t\tdamage *= (1f + Stats[PlayerStat.NonExplosionDamageIncreasePercent]);\r\n\r\n\t\tif ( Manager.Instance.Difficulty < 0 )\r\n\t\t\tdamage *= 0.571429f; // so zombie's 7 dmg becomes 4 dmg\r\n\r\n\t\treturn damage;\r\n\t}\r\n\r\n\tpublic void Damage( float damage, PlayerDamageType playerDamageType = PlayerDamageType.Enemy )\r\n\t{\r\n\t\tif ( !Manager.Instance.ShouldUpdatePlayer )\r\n\t\t\treturn;\r\n\r\n\t\tTimeSinceHurt = 0f;\r\n\r\n\t\tbool isCrit = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.SelfCritChance];\r\n\r\n\t\tif ( isCrit )\r\n\t\t\tdamage *= 2f;\r\n\r\n\t\tif ( damage > 0f )\r\n\t\t{\r\n\t\t\tTimeSinceChangeHP = 0f;\r\n\t\t\tFlash( 0.125f );\r\n\t\t}\r\n\r\n\t\tvar offset = new Vector2(\r\n\t\t\tGame.Random.Float( -0.1f, 0.1f ),\r\n\t\t\tRadius * 3f + Game.Random.Float( -0.2f, 0.3f ) + (Health - damage < 0f ? -0.7f : 0f)\r\n\t\t);\r\n\r\n\t\t//DamageNumbers.Add( (int)damage, Position2D + Vector2.Up * Radius * 3f + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.2f, color: Color.Red );\r\n\t\t//DamageNumbersLegacy.Create( damage, Position2D + offset, color: isCrit ? Color.Orange : Color.Red );\r\n\r\n\t\tvar pos = Position2D + offset;\r\n\r\n\t\tfloat size;\r\n\t\tif ( damage < 5f ) size = Utils.Map( damage, 1f, 5f, 1.1f, 1.25f, EasingType.QuadOut );\r\n\t\telse if ( damage < 20f ) size = Utils.Map( damage, 5f, 20f, 1.25f, 1.6f, EasingType.Linear );\r\n\t\telse size = Utils.Map( damage, 20f, 100f, 1.6f, 1.8f, EasingType.Linear );\r\n\r\n\t\tvar color = isCrit ? Color.Orange : Color.Red;\r\n\r\n\t\tManager.Instance.SpawnDamageNumber( pos, damage, color, size );\r\n\r\n\t\tForEachStatus( status => status.OnHurt( damage ) );\r\n\r\n\t\tHealth -= damage;\r\n\r\n\t\t//if ( damage > 3f )\r\n\t\t//\tShakeCam( Utils.Map( damage, 3f, 25f, 0f, 0.1f ), Utils.Map( damage, 3f, 20f, 0.1f, 0.25f ), EasingType.QuadOut );\r\n\r\n\t\tif ( Health <= 0f )\r\n\t\t{\r\n\t\t\tDie();\r\n\t\t\tSpawnBlood( damage, sizeMultiplier: Game.Random.Float( 2.5f, 3f ), playbackSpeed: Game.Random.Float( 20f, 25f ), shouldUseRealTime: true );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tSpawnBlood( damage );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void AddVelocity( Vector2 vel )\r\n\t{\r\n\t\tif ( !Manager.Instance.ShouldUpdatePlayer )\r\n\t\t\treturn;\r\n\r\n\t\tVelocity += vel;\r\n\t}\r\n\r\n\tpublic void SpawnBlood( float damage, float sizeMultiplier = 1f, float playbackSpeed = 0f, bool shouldUseRealTime = false )\r\n\t{\r\n\t\tvar blood = Manager.Instance.SpawnBloodSplatter( Position2D );\r\n\r\n\t\tif ( blood != null )\r\n\t\t{\r\n\t\t\tblood.LocalScale *= Utils.Map( damage, 1f, 20f, 0.5f, 1.2f, EasingType.QuadIn ) * Game.Random.Float( 0.8f, 1.2f ) * sizeMultiplier;\r\n\t\t\tblood.Lifetime *= 0.3f;\r\n\t\t\tblood.ShouldUseRealTime = shouldUseRealTime;\r\n\r\n\t\t\tif ( playbackSpeed > 0f )\r\n\t\t\t\tblood.Sprite.PlaybackSpeed = playbackSpeed;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Die()\r\n\t{\r\n\t\tif ( IsDead )\r\n\t\t\treturn;\r\n\r\n\t\tIsDead = true;\r\n\t\t_hasPlayedDeathSfx = false;\r\n\t\tSprite.Tint = new Color( 1f, 1f, 1f, 1f );\r\n\t\tSprite.FlashTint = new Color( 1f, 1f, 1f, 0f );\r\n\t\t//ShadowOpacity = 0.2f;\r\n\t\t_isFlashing = false;\r\n\t\tIsReloading = false;\r\n\r\n\t\tRealTimeSinceDeath = 0f;\r\n\t\t_arrowDeathAlphaStart = ArrowSprite.Tint.a;\r\n\r\n\t\tvar pitch = Game.Random.Float( 1.25f, 1.3f ) * (Manager.Instance.Difficulty < 0 ? 2f : 1f);\r\n\t\tManager.Instance.PlaySfxNearby( \"die\", Position2D, pitch, volume: 1.5f, maxDist: 12f );\r\n\r\n\t\tSprite.LocalScale *= 2f;\r\n\r\n\t\tSprite.PlayAnimation( $\"death\" );\r\n\t\t//Sprite.PlayAnimation( \"ghost_idle\" );\r\n\r\n\t\tShakeCam( Game.Random.Float( 0.025f, 0.065f ), Game.Random.Float( 0.3f, 0.5f ), EasingType.SineOut, useRealTime: true );\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tManager.Instance.PlayerDied( this );\r\n\t}\r\n\r\n\tpublic void Revive()\r\n\t{\r\n\t\tif ( !IsDead )\r\n\t\t\treturn;\r\n\r\n\t\tIsDead = false;\r\n\t\tIsChoosingLevelUpReward = false;\r\n\t\tIsDashing = false;\r\n\t\tIsReloading = true;\r\n\t\tSprite.Tint = Color.White;\r\n\t\tShadowOpacity = 0.8f;\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tTimer = Stats[PlayerStat.ReloadTime];\r\n\t\tReloadProgress = 0f;\r\n\t\tDashProgress = 0f;\r\n\t\tExperienceCurrent = 0;\r\n\r\n\t\tHealth = Stats[PlayerStat.MaxHp] * 0.33f;\r\n\t}\r\n\r\n\tpublic void ForEachStatus( Action<Status> action )\r\n\t{\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var (_, status) in Statuses )\r\n\t\t{\r\n\t\t\taction( status );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid HandleStatuses( float dt )\r\n\t{\r\n\t\tforeach ( KeyValuePair<int, Status> pair in Statuses )\r\n\t\t{\r\n\t\t\tStatus status = pair.Value;\r\n\t\t\tif ( status.ShouldUpdate )\r\n\t\t\t\tstatus.Update( dt );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid HandleShooting( float dt )\r\n\t{\r\n\t\tif ( IsReloading )\r\n\t\t{\r\n\t\t\tReloadProgress = Utils.Map( Timer, Stats[PlayerStat.ReloadTime], 0f, 0f, 1f );\r\n\t\t\tTimer -= dt * Stats[PlayerStat.ReloadSpeed];\r\n\t\t\tif ( Timer <= 0f )\r\n\t\t\t{\r\n\t\t\t\tReload();\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tTimer -= dt * Stats[PlayerStat.AttackSpeed] * (IsMoving ? 1f : Stats[PlayerStat.AttackSpeedStill]);\r\n\t\t\tif ( Timer <= 0f )\r\n\t\t\t{\r\n\t\t\t\tShoot( isLastAmmo: AmmoCount == 1 );\r\n\t\t\t\tAmmoCount--;\r\n\r\n\t\t\t\tif ( AmmoCount <= 0 )\r\n\t\t\t\t{\r\n\t\t\t\t\tIsReloading = true;\r\n\r\n\t\t\t\t\tTimer += Stats[PlayerStat.ReloadTime];\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tTimer += Stats[PlayerStat.AttackTime];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Shoot( bool isLastAmmo = false )\r\n\t{\r\n\t\tint num_bullets_int = (int)Stats[PlayerStat.NumProjectiles];\r\n\r\n\t\tvar aimDir = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootRandomDirChance]\r\n\t\t\t? Utils.GetRandomVector()\r\n\t\t\t: AimDir;\r\n\r\n\t\tvar pos = Position2D + AimDir * 0.3f;\r\n\r\n\t\tif ( Stats[PlayerStat.MaxBulletSpread] > 0f )\r\n\t\t{\r\n\t\t\tfloat increment = 360f / num_bullets_int;\r\n\r\n\t\t\tfor ( int i = 0; i < num_bullets_int; i++ )\r\n\t\t\t{\r\n\t\t\t\tvar dir = Utils.RotateVector( aimDir, i * increment );\r\n\t\t\t\tSpawnBullet( pos, dir, isLastAmmo );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfloat start_angle = MathF.Sin( -_shotNum * 2f ) * Stats[PlayerStat.BulletInaccuracy];\r\n\r\n\t\t\tvar spread = Stats[PlayerStat.BulletSpread] * num_bullets_int;\r\n\r\n\t\t\tfloat currAngleOffset = num_bullets_int == 1 ? 0f : -spread * 0.5f;\r\n\t\t\tfloat increment = num_bullets_int == 1 ? 0f : spread / (float)(num_bullets_int - 1);\r\n\r\n\t\t\tfor ( int i = 0; i < num_bullets_int; i++ )\r\n\t\t\t{\r\n\t\t\t\tvar dir = Utils.RotateVector( aimDir, start_angle + currAngleOffset + increment * i );\r\n\t\t\t\tSpawnBullet( pos, dir, isLastAmmo );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"shoot\", pos, pitch: Utils.Map( _shotNum, 0f, (float)Stats[PlayerStat.MaxAmmoCount], 1f, 1.25f ), volume: 1f, maxDist: 4f );\r\n\r\n\t\tVelocity -= aimDir * Stats[PlayerStat.Recoil];\r\n\r\n\t\t_shotNum++;\r\n\t\t_timeSinceShoot = 0f;\r\n\t}\r\n\r\n\tvoid SpawnBullet( Vector2 pos, Vector2 dir, bool isLastAmmo = false, float damageMult = 1f )\r\n\t{\r\n\t\tvar damage = (Stats[PlayerStat.BulletDamage] * Stats[PlayerStat.BulletDamageMultiplier] + Stats[PlayerStat.BulletFlatDamageAddition]) * GetDamageMultiplier() * damageMult;\r\n\t\tif ( isLastAmmo )\r\n\t\t\tdamage *= Stats[PlayerStat.LastAmmoDamageMultiplier];\r\n\r\n\t\tif ( Stats[PlayerStat.DamagePerEarlierShot] > 0f )\r\n\t\t\tdamage += _shotNum * Stats[PlayerStat.DamagePerEarlierShot];\r\n\r\n\t\tif ( Stats[PlayerStat.DamageForSpeed] > 0f )\r\n\t\t{\r\n\t\t\tdamage += Stats[PlayerStat.DamageForSpeed] * Velocity.Length;\r\n\r\n\t\t\tif ( IsDashing )\r\n\t\t\t\tdamage += Stats[PlayerStat.DamageForSpeed] * DashVelocity.Length;\r\n\t\t}\r\n\r\n\t\tvar bulletObj = BulletPrefab.Clone( (Vector3)pos );\r\n\t\tvar bullet = bulletObj.Components.Get<Bullet>();\r\n\r\n\t\tbullet.Velocity = dir * Stats[PlayerStat.BulletSpeed];\r\n\t\tbullet.Shooter = this;\r\n\t\tbullet.TempWeight = 3f;\r\n\r\n\t\tbullet.Stats[BulletStat.Damage] = damage;\r\n\t\tbullet.Stats[BulletStat.Force] = Stats[PlayerStat.BulletForce];\r\n\t\tbullet.Stats[BulletStat.Lifetime] = Stats[PlayerStat.BulletLifetime];\r\n\t\tbullet.Stats[BulletStat.NumPiercing] = (int)MathF.Round( Stats[PlayerStat.BulletNumPiercing] );\r\n\t\tbullet.Stats[BulletStat.NumBouncing] = (int)MathF.Round( Stats[PlayerStat.BulletNumBouncing] );\r\n\t\tbullet.Stats[BulletStat.WillIgnite] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFireIgniteChance] ? 1f : 0f;\r\n\t\tbullet.Stats[BulletStat.WillFreeze] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFreezeChance] ? 1f : 0f;\r\n\t\tbullet.Stats[BulletStat.GrowDamageAmount] = Stats[PlayerStat.BulletDamageGrow];\r\n\t\tbullet.Stats[BulletStat.ShrinkDamageAmount] = Stats[PlayerStat.BulletDamageShrink];\r\n\t\tbullet.Stats[BulletStat.DistanceDamageAmount] = Stats[PlayerStat.BulletDistanceDamage];\r\n\t\tbullet.Stats[BulletStat.HealTeammateAmount] = Stats[PlayerStat.BulletHealTeammateAmount];\r\n\t\tbullet.IsHoming = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.HomingBulletChance];\r\n\r\n\t\tif ( Stats[PlayerStat.GrenadesCanCrit] <= 0f )\r\n\t\t{\r\n\t\t\tbullet.Stats[BulletStat.CriticalChance] = Stats[PlayerStat.CritChance];\r\n\t\t\tbullet.Stats[BulletStat.CriticalMultiplier] = Stats[PlayerStat.CritMultiplier];\r\n\t\t}\r\n\r\n\t\tbullet.Init();\r\n\r\n\t\t//bullet.GameObject.NetworkSpawn( Network.Owner );\r\n\t}\r\n\r\n\tvoid Reload()\r\n\t{\r\n\t\tAmmoCount = (int)Stats[PlayerStat.MaxAmmoCount];\r\n\t\tIsReloading = false;\r\n\t\t_shotNum = 0;\r\n\t\tReloadProgress = 0f;\r\n\r\n\t\tForEachStatus( status => status.OnReload() );\r\n\t}\r\n\r\n\tpublic float GetDamageMultiplier()\r\n\t{\r\n\t\tfloat damageMultiplier = Stats[PlayerStat.OverallDamageMultiplier];\r\n\r\n\t\tif ( Stats[PlayerStat.LowHealthDamageMultiplier] > 1f )\r\n\t\t\tdamageMultiplier *= Utils.Map( Health, Stats[PlayerStat.MaxHp], 0f, 1f, Stats[PlayerStat.LowHealthDamageMultiplier] );\r\n\r\n\t\tif ( Stats[PlayerStat.FullHealthDamageMultiplier] > 1f && !(Health < Stats[PlayerStat.MaxHp]) )\r\n\t\t\tdamageMultiplier *= Stats[PlayerStat.FullHealthDamageMultiplier];\r\n\r\n\t\treturn damageMultiplier;\r\n\t}\r\n\r\n\tpublic override void Colliding( Thing other, float percent, float dt )\r\n\t{\r\n\t\tbase.Colliding( other, percent, dt );\r\n\r\n\t\tif ( IsDead )\r\n\t\t\treturn;\r\n\r\n\t\tForEachStatus( status => status.Colliding( other, percent, dt ) );\r\n\r\n\t\tif ( other is Enemy enemy && !enemy.IsDying )\r\n\t\t{\r\n\t\t\tif ( !Position2D.Equals( other.Position2D ) )\r\n\t\t\t{\r\n\t\t\t\tvar spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );\r\n\t\t\t\tVelocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * spawnFactor * dt;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( other is Player player )\r\n\t\t{\r\n\t\t\tif ( !player.IsDead && !Position2D.Equals( other.Position2D ) )\r\n\t\t\t{\r\n\t\t\t\tVelocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * dt;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void SpawnDashCloudClient()\r\n\t{\r\n\t\tManager.Instance.SpawnCloud( Position2D + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.05f );\r\n\t}\r\n\r\n\tpublic void GenerateLevelUpChoices()\r\n\t{\r\n\t\tLevelUpChoices.Clear();\r\n\r\n\t\t//if( Level == 1)\r\n\t\t//{\r\n\t\t//\tLevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );\r\n\t\t//\tLevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( MysteryBoxStatus ) ) ) );\r\n\t\t//\tLevelUpChoices.Add( CreateStatus( GetRandomStartingPerk() ) );\r\n\r\n\t\t//\tLevelUpChoices.Shuffle();\r\n\r\n\t\t//\treturn;\r\n\t\t//}\r\n\r\n\t\tbool offerCurses = false;\r\n\t\tif ( Manager.Instance.Difficulty >= 6 )\r\n\t\t\tofferCurses = IsLevelCursed( Level );\r\n\r\n\t\tint numChoices = Math.Clamp( (int)MathF.Round( Stats[PlayerStat.NumUpgradeChoices] ), 1, 6 );\r\n\t\tList<TypeDescription> statusTypes = StatusManager.GetRandomStatuses( this, numChoices, offerCurses );\r\n\r\n\t\tfor ( int i = 0; i < statusTypes.Count; i++ )\r\n\t\t\tLevelUpChoices.Add( CreateStatus( statusTypes[i] ) );\r\n\r\n\t\tif ( Level == 1 )\r\n\t\t{\r\n\t\t\tbool alreadyOffered = false;\r\n\r\n\t\t\tforeach ( var status in LevelUpChoices )\r\n\t\t\t{\r\n\t\t\t\tif ( status is PauseWhileChoosingStatus )\r\n\t\t\t\t{\r\n\t\t\t\t\talreadyOffered = true;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( !alreadyOffered )\r\n\t\t\t{\r\n\t\t\t\tLevelUpChoices.RemoveAt( 1 );\r\n\t\t\t\tLevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );\r\n\t\t\t\tLevelUpChoices.Shuffle();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tChoiceHash++;\r\n\t}\r\n\r\n\tpublic bool IsLevelCursed( int level )\r\n\t{\r\n\t\tif ( Manager.Instance.Difficulty < 6 || level <= 1 )\r\n\t\t\treturn false;\r\n\r\n\t\tswitch ( Manager.Instance.Difficulty )\r\n\t\t{\r\n\t\t\tcase 6: return level % 10 == 0;\r\n\t\t\tcase 7: return level % 9 == 0;\r\n\t\t\tcase 8: return level % 8 == 0;\r\n\t\t\tcase 9: return level % 7 == 0;\r\n\t\t\tcase 10: return level % 6 == 0;\r\n\t\t\tcase 11: return level % 5 == 0;\r\n\t\t\tcase 12: return level % 4 == 0;\r\n\t\t\tcase 13: return level % 3 == 0;\r\n\t\t\tcase 14: return level % 2 == 0;\r\n\t\t\tcase 15: return level % 3 != 1;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tTypeDescription GetRandomStartingPerk()\r\n\t{\r\n\t\tList<(TypeDescription Type, float Weight)> perks = new List<(TypeDescription, float)>\r\n\t\t{\r\n\t\t\t(TypeLibrary.GetType( typeof( DamageStatus ) ), 5f),\r\n\t\t\t(TypeLibrary.GetType( typeof( MovespeedStatus ) ), 3f),\r\n\t\t\t(TypeLibrary.GetType( typeof( AttackSpeedStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( NumProjectileStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( PiercingStatus ) ), 3f),\r\n\t\t\t(TypeLibrary.GetType( typeof( GrenadeShootReloadStatus ) ), 1f),\r\n\t\t\t(TypeLibrary.GetType( typeof( NumDashesStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( FireIgniteStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( FreezeShootStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( FullHealthDamageStatus ) ), 2f),\r\n\t\t\t(TypeLibrary.GetType( typeof( MoreRerollsStatus ) ), 4f),\r\n\t\t\t(TypeLibrary.GetType( typeof( MoreChoicesStatus ) ), 3f),\r\n\t\t\t(TypeLibrary.GetType( typeof( ReloadSpeedStatus ) ), 1f),\r\n\t\t\t(TypeLibrary.GetType( typeof( XpDamageStatus ) ), 1f),\r\n\t\t\t(TypeLibrary.GetType( typeof( HomingBulletStatus ) ), 4f),\r\n\t\t\t(TypeLibrary.GetType( typeof( ThornsStatus ) ), 1f),\r\n\t\t\t(TypeLibrary.GetType( typeof( BouncingBulletStatus ) ), 3f),\r\n\t\t};\r\n\r\n\t\tTypeDescription chosenPerk = null;\r\n\r\n\t\tfloat totalWeight = perks.Sum( x => x.Weight );\r\n\t\tvar rand = Game.Random.Float( 0f, totalWeight );\r\n\r\n\t\tfor ( int i = perks.Count - 1; i >= 0; i-- )\r\n\t\t{\r\n\t\t\tvar (type, weight) = perks[i];\r\n\t\t\trand -= weight;\r\n\r\n\t\t\tif ( rand < 0f )\r\n\t\t\t{\r\n\t\t\t\tchosenPerk = type;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn chosenPerk;\r\n\t}\r\n\r\n\tStatus CreateStatus( TypeDescription type )\r\n\t{\r\n\t\tvar status = StatusManager.CreateStatus( type );\r\n\t\tvar currLevel = GetStatusLevel( type );\r\n\t\tstatus.Level = currLevel + 1;\r\n\t\treturn status;\r\n\t}\r\n\r\n\tpublic void Restart()\r\n\t{\r\n\t\tSprite.PlayAnimation( \"idle\" );\r\n\t\tSprite.PlaybackSpeed = 0.66f;\r\n\r\n\t\tSprite.Tint = new Color( 1f, 1f, 1f, 1f );\r\n\r\n\t\tSprite.LocalScale = new Vector3( Globals.SPRITE_SCALE );\r\n\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tPosition2D = new Vector3( Game.Random.Float( -3f, 3f ), Game.Random.Float( -3f, 3f ) );\r\n\t\tManager.Instance.Camera2D.SetPos( Position2D );\r\n\r\n\t\tInitializeStats();\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"restart\", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.66f, 4f );\r\n\t}\r\n\r\n\tpublic void SpawnBulletRing( Vector2 pos, int numBullets, Vector2 aimDir, float damageMultMin = 1f, float damageMultMax = 1f )\r\n\t{\r\n\t\tfloat increment = 360f / numBullets;\r\n\r\n\t\tfor ( int i = 0; i < numBullets; i++ )\r\n\t\t{\r\n\t\t\tvar dir = Utils.RotateVector( aimDir, i * increment );\r\n\t\t\tfloat damageMult = Game.Random.Float( damageMultMin, damageMultMax );\r\n\t\t\tSpawnBullet( pos, dir, false, damageMult );\r\n\t\t}\r\n\r\n\t\tManager.Instance.PlaySfxNearby( \"shoot\", pos, pitch: 1f, volume: 1f, maxDist: 3f );\r\n\t}\r\n\r\n\tpublic Grenade SpawnGrenade( Vector2 pos, Vector2 vel )\r\n\t{\r\n\t\tvar grenadeObj = Manager.Instance.GrenadePrefab.Clone();\r\n\r\n\t\tvar grenade = grenadeObj.Components.Get<Grenade>();\r\n\t\tgrenade.Velocity = vel;\r\n\t\tgrenade.ExplosionSizeMultiplier = Stats[PlayerStat.ExplosionSizeMultiplier];\r\n\t\tgrenade.Player = this;\r\n\t\tgrenade.StickyPercent = Stats[PlayerStat.GrenadeStickyPercent];\r\n\t\tgrenade.FearChance = Stats[PlayerStat.GrenadeFearChance];\r\n\r\n\t\tif ( Stats[PlayerStat.GrenadesCanCrit] > 0f )\r\n\t\t{\r\n\t\t\tgrenade.CriticalChance = Stats[PlayerStat.CritChance];\r\n\t\t\tgrenade.CriticalMultiplier = Stats[PlayerStat.CritMultiplier];\r\n\t\t}\r\n\r\n\t\t//grenadeObj.NetworkSpawn( Network.Owner );\r\n\t\tgrenadeObj.WorldPosition = new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) );\r\n\r\n\t\tManager.Instance.AddThing( grenade );\r\n\r\n\t\treturn grenade;\r\n\t}\r\n\r\n\tpublic void CreateShieldVfx()\r\n\t{\r\n\t\t_shieldVfx = Manager.Instance.ShieldVfxPrefab.Clone( WorldPosition );\r\n\t\t_shieldVfx.Parent = GameObject;\r\n\t\t_shieldVfx.LocalPosition = new Vector3( 0f, 0f, 0.1f );\r\n\t\t_shieldVfx.LocalScale = new Vector3( 1f ) * 1.8f * Globals.SPRITE_SCALE;\r\n\t\t_shieldVfx.LocalRotation = new Angles( 0f, -90f, 0f );\r\n\t}\r\n\r\n\tpublic void RemoveShieldVfx()\r\n\t{\r\n\t\tif ( _shieldVfx != null )\r\n\t\t{\r\n\t\t\t_shieldVfx.Destroy();\r\n\t\t\t_shieldVfx = null;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void PlaySfx( string name, Vector2 pos, float pitch, float volume )\r\n\t{\r\n\t\tvar sfx = Sound.Play( name, new Vector3( pos.x, pos.y, Globals.SFX_DEPTH ) );\r\n\r\n\t\tif ( sfx != null )\r\n\t\t{\r\n\t\t\tsfx.Volume = volume;\r\n\t\t\tsfx.Pitch = pitch;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tbase.OnStart();\r\n\r\n\t\tManager.Instance.AddPlayer( this );\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tbase.OnDestroy();\r\n\r\n\t\tManager.Instance.RemovePlayer( this );\r\n\t}\r\n\r\n\tvoid HandleCamShaking()\r\n\t{\r\n\t\tCamShakeAmount = 0f;\r\n\r\n\t\tfor ( int i = _camShakeDatas.Count - 1; i >= 0; i-- )\r\n\t\t{\r\n\t\t\tvar data = _camShakeDatas[i];\r\n\t\t\tvar time = data.useRealTime ? RealTime.Now : Time.Now;\r\n\r\n\t\t\tif ( time > data.startTime + data.time )\r\n\t\t\t{\r\n\t\t\t\t_camShakeDatas.RemoveAt( i );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tfloat amount = Utils.Map( time, data.startTime, data.startTime + data.time, data.strength, 0f, data.easingType );\r\n\t\t\t\tCamShakeAmount = MathF.Max( amount, CamShakeAmount );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = false )\r\n\t{\r\n\t\tvar timeNow = useRealTime ? RealTime.Now : Time.Now;\r\n\t\t_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );\r\n\t}\r\n}\r\n"
        }
    ]
}