AI/EnemyVisualAnimator.cs
using System;

namespace Prototype;

/// <summary>Presentation-only motion for generated, unrigged enemy proxies.</summary>
public sealed class EnemyVisualAnimator : Component
{
	public GameObject Root { get; set; }
	public float GroundOffset { get; set; } = 27f;
	public bool ProductionOrientation { get; set; }
	private Vector3 _previousPosition;
	private float _stride;
	private int _damageSequence;
	private float _hitImpulse;

	protected override void OnStart()
	{
		_previousPosition = Root.IsValid() ? Root.WorldPosition : WorldPosition;
	}

	protected override void OnUpdate()
	{
		if ( !Root.IsValid() ) return;
		var delta = (Root.WorldPosition - _previousPosition).WithZ( 0f );
		var speed = Time.Delta > 0f ? delta.Length / Time.Delta : 0f;
		var moving = Math.Clamp( speed / 180f, 0f, 1f );
		_stride += Time.Delta * (3.5f + moving * 7.5f);

		var brain = Root.Components.Get<EnemyBrain>();
		if ( brain is not null && brain.DamageSequence != _damageSequence )
		{
			_damageSequence = brain.DamageSequence;
			_hitImpulse = 1f;
		}
		_hitImpulse = MathF.Max( 0f, _hitImpulse - Time.Delta * 5.5f );

		var bob = MathF.Abs( MathF.Sin( _stride ) ) * 3.2f * moving;
		var weightShift = MathF.Sin( _stride * .5f ) * 2.2f * moving;
		WorldPosition = Root.WorldPosition + Vector3.Down * GroundOffset + Vector3.Up * bob;
		// ModelDoc converts the production FBX forward axis into S&box +X itself.
		// Any additional quarter-turn makes the enemy approach with one of its sides.
		var authoredRotation = ProductionOrientation ? Rotation.Identity : Rotation.FromPitch( -90f );
		WorldRotation = Root.WorldRotation
			* authoredRotation
			* Rotation.FromPitch( -moving * 2.5f - _hitImpulse * 8f )
			* Rotation.FromRoll( weightShift + _hitImpulse * MathF.Sin( _stride * 3f ) * 5f );
		_previousPosition = Root.WorldPosition;
	}
}