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;
}
}