Core/GunnerGameplayPreview.cs
namespace Prototype;
/// <summary>Reversible visual experiment on the first Gunner of each wave.</summary>
public sealed class GunnerGameplayPreview : Component
{
public Vector3 MuzzlePosition => _renderer.IsValid() && _renderer.TryGetBoneTransform( "attach_muzzle", out var muzzle )
? muzzle.Position : WorldPosition + Vector3.Up * 42f + WorldRotation.Forward * 35f;
private GameObject _visual;
private SkinnedModelRenderer _renderer;
private ModelHitboxes _hitboxes;
private float _playedAttackAt = -100f;
private int _playedDamage;
private Vector3 _previous;
private string _clip;
private float _hitUntil;
private int _damage;
private float _originalEnd;
private bool _rejected;
protected override void OnStart()
{
_previous = WorldPosition;
_originalEnd = Components.Get<CapsuleCollider>()?.End.z ?? 0f;
}
protected override void OnUpdate()
{
var brain = Components.Get<EnemyBrain>();
if ( !brain.IsValid() ) return;
bool active = PrototypeGame.Instance?.ReviewGunnerInGame == true;
if ( active && !_visual.IsValid() )
{
if ( _rejected ) return;
var model = Model.Load( "models/review/gunner_relic_v001/gunner.vmdl" );
if ( model is null || model.Bounds.Size.z < 60 || model.Bounds.Size.z > 90 || model.Bounds.Size.x > 85 || model.Bounds.Size.y > 85 || model.BoneCount != 20 )
{
_rejected = true;
Log.Error( "GUNNER_GAMEPLAY: candidate rejected by bounds/bone check; keeping original visual" );
return;
}
_visual = new GameObject( true, "Gunner relic v001" );
_visual.NetworkMode = NetworkMode.Never;
_visual.SetParent( GameObject, false );
_visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale + 2f);
_renderer = _visual.Components.Create<SkinnedModelRenderer>();
_renderer.Model = model; _renderer.UseAnimGraph = false;
_renderer.Sequence.Blending = true;
_visual.Tags.Add( "enemy" );
_hitboxes = _visual.Components.Create<ModelHitboxes>();
_hitboxes.Renderer = _renderer;
_hitboxes.Target = GameObject;
}
if ( _hitboxes.IsValid() ) _hitboxes.Enabled = active && brain.Vitality?.IsDead != true;
if ( active && _hitboxes.IsValid() ) GameObject.Tags.Add( "enemy_model_proxy" );
else GameObject.Tags.Remove( "enemy_model_proxy" );
if ( brain.Vitality?.IsDead == true )
{
var deadCapsule = Components.Get<CapsuleCollider>();
if ( deadCapsule.IsValid() ) deadCapsule.Enabled = false;
}
if ( _visual.IsValid() )
{
_visual.Enabled = active;
_visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale + 2f);
}
foreach ( var child in GameObject.Children )
if ( child.Name == "Gunner Visual" )
{
var oldRenderer = child.Components.Get<ModelRenderer>();
if ( oldRenderer is not null ) oldRenderer.Enabled = !active;
var oldAnimator = child.Components.Get<EnemyVisualAnimator>();
if ( oldAnimator is not null ) oldAnimator.Enabled = !active;
child.Enabled = !active;
}
var capsule = Components.Get<CapsuleCollider>();
if ( capsule.IsValid() && brain.Vitality?.IsDead != true ) capsule.End = Vector3.Up * (active ? 75.8071f - (27f * brain.Spec.Scale + 2f) - capsule.Radius : _originalEnd);
var speed = (WorldPosition - _previous).WithZ(0).Length / System.MathF.Max(Time.Delta, .001f);
_previous = WorldPosition;
if ( !active || _renderer is null ) return;
if ( brain.DamageSequence != _damage ) { _damage = brain.DamageSequence; _hitUntil = Time.Now + .6f; }
var clip = brain.Vitality?.IsDead == true ? "death" : Time.Now < _hitUntil ? "hit" : Time.Now - brain.LastAttackAt < .9f ? "attack" : speed > 10 ? "run" : "idle";
var repeatedAttack = (clip is "attack" or "cast") && _playedAttackAt != brain.LastAttackAt;
var repeatedHit = clip == "hit" && _playedDamage != brain.DamageSequence;
if ( clip == _clip && !repeatedAttack && !repeatedHit ) return;
_playedAttackAt = brain.LastAttackAt;
_playedDamage = brain.DamageSequence;
_clip = clip; _renderer.Sequence.Name = clip; _renderer.Sequence.Time = 0;
_renderer.Sequence.PlaybackRate = clip == "attack"
? System.MathF.Max( 1f, _renderer.Sequence.Duration / System.MathF.Max( .1f, System.MathF.Min( .9f, brain.Spec.AttackCooldown ) ) )
: 1f;
_renderer.Sequence.Looping = clip is "idle" or "run";
}
}