things/enemies/Zombie.cs

Enemy subclass representing a Zombie NPC. It sets stats, randomizes appearance and animation parameters, controls movement/attack/flinch/jump animations, and computes a walk speed factor based on animation progress.

NetworkingFile Access
using System;
using Sandbox;

public class Zombie : Enemy
{
	public override EnemyType EnemyType => EnemyType.Zombie;

	public override float MeleeForce => 20f;
	public override float MeleeRagdollForce => Game.Random.Float( 0.4f, 1.25f );
	public override float MeleeUpwardForceAmount => Game.Random.Float( 0f, 0.3f );

	private int _bodyGroupIndex;
	protected override bool HasLeftArm => _bodyGroupIndex != 4;
	public override float GetMaxHealth()
	{
		switch( Manager.Instance.Difficulty )
		{
			case 0: default: return 30f;
			case 1: return 35f;
			case 2: return 37f;
		}
	}

	public override Vector3 SpawnScale => new Vector3( 1f );
	//public override float SpawnZPos => -10f;

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

		CoinValueMin = 1;
		CoinValueMax = 1;
		CoinChance = 0.55f;

		PushStrength = 5000f;

		_personalSpeedScale = Game.Random.Float( 0.85f, 1.25f );
		_personalSpeedFreq = Game.Random.Float( 6f, 12f );
		_fullMeleeAttackAnimSpeed = 2.5f;

		_bodyGroupIndex = Game.Random.Int( 0, 4 );
		ModelRenderer.SetBodyGroup( 0, _bodyGroupIndex ); // value for part 0: 3 has right eye hanging out, 4 has no left arm

		var blueTintAmount = this is ZombieTemporary ? 0f : Game.Random.Float( 0f, 0.2f );
		TintFullHp = new Color( TintFullHp.r - blueTintAmount, TintFullHp.g - blueTintAmount, TintFullHp.b );
		TintZeroHp = Color.Lerp( TintZeroHp, TintZeroHp.WithBlue( TintZeroHp.b + blueTintAmount ), 0.25f );
		ModelRenderer.Tint = TintFullHp.WithAlpha( ModelRenderer.Tint.a );

		if ( IsProxy )
			return;

		AggroRange = 100f;
		DetectTargetRange = 250f;
		LoseTargetRange = 700f;
		LoseTargetTime = 3f;
		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 5f, 7f, 8f );
		DamageTargetDelay = 0.5f;
		//_personalTurnSpeed = Game.Random.Float( 1.5f, 4f );
		_personalTurnSpeed = Game.Random.Float( 3.5f, 5f );
		//Acceleration = 110f;
		//AccelerationAttacking = 130f;
		Acceleration = 350f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.1f );
		AccelerationAttacking = 600f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.25f );
		Deceleration = 1.95f;
		DecelerationAttacking = 1.75f;
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"CanAnimate: {CanAnimate} - {ModelRenderer.PlaybackRate.ToString("N2")}", new global::Transform( WorldPosition ) );
		//Gizmo.Draw.Text( $"{ModelRenderer.SceneModel.CurrentSequence.Name}\nspeed: {ModelRenderer.SceneModel.PlaybackRate}\n_hitstopOldPlaybackSpeed: {_hitstopOldPlaybackSpeed}", new global::Transform( WorldPosition ) );
		//Gizmo.Draw.Text( $"{ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString("N2")}", new global::Transform( WorldPosition ) );

		//var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
		//var animName = "Hurt";

		//if ( progress > 0.3f && progress < 0.6f )
		//	animName = "Hurt_Left";
		//else if ( progress < 0.2f || progress > 0.7f )
		//	animName = "Hurt_Right";

		//Gizmo.Draw.Text( $"{ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString( "N2" )}\n{animName}", new global::Transform( WorldPosition ) );
	}

	protected override float GetMoveSpeedFactor()
	{
		var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
		return Zombie.GetZombieMoveSpeedFactor( progress );
	}

	public static float GetZombieMoveSpeedFactor( float animProgress )
	{
		var leftFootStart = 0.18f;
		var leftFootEnd = 0.28f;
		var rightFootStart = 0.66f;
		var rightFootEnd = 0.8f;

		//if ( progress > leftFootStart && progress < leftFootEnd )
		//	return Utils.MapReturn( progress, leftFootStart, leftFootEnd, 0f, 1f, EasingType.Linear );
		//else if( progress > rightFootStart && progress < rightFootEnd )
		//	return Utils.MapReturn( progress, rightFootStart, rightFootEnd, 0f, 1f, EasingType.Linear );

		if ( animProgress > leftFootStart && animProgress < leftFootEnd )
			return Utils.Map( animProgress, leftFootStart, leftFootEnd, 0f, 1f, EasingType.QuadOut );
		else if ( animProgress > rightFootStart && animProgress < rightFootEnd )
			return Utils.Map( animProgress, rightFootStart, rightFootEnd, 0f, 1f, EasingType.QuadOut );

		return 0f;

		//return (0.5f + Utils.FastSin( MoveTimeOffset + Manager.Instance.ElapsedTime * _personalSpeedFreq * (IsAttacking ? 2f : 1f) ) * 0.5f) * _personalSpeedScale; // todo: should "(IsAttacking ? 2f : 1f)" be there?
	}

	protected override void PlayWalkAnim()
	{
		//SetPlaybackRate( Game.Random.Float( 0.8f, 1.2f ) );

		if( !IsStunned )
			SetPlaybackRate( _personalSpeedScale );

		SetAnim( "Walk" );
	}

	protected override void PlayAttackAnim()
	{
		SetPlaybackRate( _personalSpeedScale );

		SetAnim( "Attack" );
	}

	protected override void PlayFlinchAnim()
	{
		var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;

		var currAnimName = ModelRenderer.SceneModel.CurrentSequence?.Name ?? string.Empty;
		string animName;
		if ( !string.IsNullOrEmpty( currAnimName ) && currAnimName.Contains( "Hurt" ) )
		{
			animName = ModelRenderer.SceneModel.CurrentSequence.Name;
		}
		else
		{
			if ( progress > 0.3f && progress < 0.6f )
				animName = "Hurt_Left";
			else if ( progress < 0.2f || progress > 0.7f )
				animName = "Hurt_Right";
			else
				animName = "Hurt";
		}

		SetAnim( animName, forceRestart: true );
		//SetPlaybackRate( 1f );
	}

	protected override void PlayJumpAnim()
	{
		SetAnim( "Jump" );
		//SetPlaybackRate( Game.Random.Float(0.2f, 0.8f) );
	}

	//protected override void PlayCelebrateAnim( bool victory )
	//{
	//	if ( victory )
	//	{
	//		//SetPlaybackRate( Game.Random.Float( 0.5f, 0.9f ) );
	//		SetAnim( "HeadPokeLoop" );
	//	}
	//	else
	//	{
	//		//SetPlaybackRate( Game.Random.Float( 0.8f, 1.4f ) );
	//		SetAnim( "Attack3" );
	//	}
	//}
}