menu/MenuEnemy.cs

A menu enemy component used in the main menu. It controls sprite animation, scaling, tinting and attack state over time, and randomizes per-instance animation offsets, size variance and 'anger' behavior.

File Access
public class MenuEnemy : Component
{
	[Property] public Sprite2D Sprite { get; set; }


	public Vector3 Velocity { get; set; }

	public bool IsAttacking { get; set; }

	public bool IsFlipped { get; set; }

	public float SpeedModifier { get; set; }

	float _heightMod;
	float _widthMod;

	public virtual float HeightVariance => 0f;
	public virtual float WidthVariance => 0f;

	private TimeSince _timeSinceSpawn;

	private float _angerThreshold;
	private float _angerChangeSpeed;

	public float AnimSpeedOffset = Game.Random.Float( 0f, 99f );

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

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

		_heightMod = Game.Random.Float( 1f - HeightVariance, 1f + HeightVariance );
		_widthMod = Game.Random.Float( 1f - WidthVariance, 1f + WidthVariance );

		//Sprite.Tint = Color.White.WithAlpha( 0f );

		_timeSinceSpawn = 0f;

		_angerThreshold = Utils.Map( Game.Random.Float( 0f, 1f ), 0f, 1f, 0f, 1f, EasingType.QuadIn );
		_angerChangeSpeed = Game.Random.Float( 0.2f, 0.8f );

		LocalScale = Vector3.Zero;
	}

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

		LocalRotation = new Angles( 0f, -90f, 0f );
	}

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

		//Sprite.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceSpawn, 0f, 1f, 0f, 1f ) );

		WorldPosition = WorldPosition.WithZ( Utils.Map( WorldPosition.y, MenuManager.Y_MIN, MenuManager.Y_MAX, MenuManager.Z_NEAR, MenuManager.Z_FAR ) );

		var shouldAttack = _timeSinceSpawn > 0.5f && (0.5f + Utils.FastSin( AnimSpeedOffset + Time.Now * _angerChangeSpeed )) > _angerThreshold;

		//if ( !IsAttacking && shouldAttack )
		//{
		//	var sfx = Sound.Play( "zombie.spawn0", WorldPosition.WithZ(100f) );
		//	if ( sfx != null )
		//	{
		//		sfx.Volume = Utils.Map( WorldPosition.y, MenuManager.Y_MIN, MenuManager.Y_MAX, 0.9f, 0.25f) * Utils.Map(MenuManager.Instance.ElapsedTime, 0f, 1.2f, 0f, 1f);
		//		sfx.Pitch = Game.Random.Float( 0.9f, 1.1f );
		//	}
		//}

		IsAttacking = shouldAttack;
		Sprite.PlayAnimation( IsAttacking ? "attack" : "walk" );

		var scale = Utils.Map( WorldPosition.z, MenuManager.Z_NEAR, MenuManager.Z_FAR, 0.6f, 0.1f );
		LocalScale = new Vector3( scale * _heightMod, scale * _widthMod, scale * _heightMod );

		var blend = Utils.Map( WorldPosition.z, MenuManager.Z_NEAR, MenuManager.Z_FAR, 0f, 1f, EasingType.QuadOut );
		var remaining = 1f - blend;
		// The shader computes tint * lerp(texture, overlay, overlayAlpha). Using
		// identical tint/overlay RGB avoids its mismatched RGBE exponent bug.
		// Solve t*t - remaining*t = background*blend, giving the final RGB:
		// texture * (1-blend) + background * blend, with unchanged pixel alpha.
		var tint = (remaining + MathF.Sqrt( remaining * remaining + 4f * MenuCamera.BackgroundLinear * blend )) * 0.5f;
		var overlayAlpha = 1f - remaining / tint;
		Sprite.Tint = new Color( tint, tint, tint, 1f );
		Sprite.FlashTint = new Color( tint, tint, tint, overlayAlpha );
		Sprite.SpriteFlags = IsFlipped ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
	}
}