Player/DasherBodyVisual.cs
using System.Linq;
namespace Prototype;
/// <summary>Shared rig for first-person arms and the visible multiplayer body.</summary>
public sealed class DasherBodyVisual : Component
{
private GameObject _body, _weapon;
private SkinnedModelRenderer _renderer;
private ModelRenderer _weaponRenderer;
private string _clip, _weaponId;
private bool _firstPerson;
public bool Ready => _renderer.IsValid() && _weaponRenderer.IsValid();
public Vector3 MuzzlePosition
{
get
{
UpdateWeaponPose();
return _weapon.IsValid() ? _weapon.WorldPosition + _weapon.WorldRotation.Forward * (_weaponId == "scatter" ? 24.66f : 27.64f) + _weapon.WorldRotation.Up * (_weaponId == "scatter" ? 4.91f : 4.72f) : WorldPosition;
}
}
protected override void OnUpdate() => PrepareView();
public void PrepareView()
{
// Networked component must be unique. Old clients/hotload can retain a
// locally-created copy from the previous OnStart setup.
var primary = Components.GetAll<DasherBodyVisual>().FirstOrDefault();
if (primary != this)
{
if (_body.IsValid()) _body.Enabled = false;
if (_weapon.IsValid()) _weapon.Enabled = false;
return;
}
var player = Components.Get<FpsPlayer>();
if ( !player.IsValid() ) return;
// Hide legacy bodies left by hot reload or older replicated hierarchies.
foreach (var child in GameObject.Children)
if (child.Name == "DASHER Visual" || child.Name == "THE NOMAD Visual"
|| (child != _body && (child.Name == "Dasher View Arms" || child.Name == "Dasher Skinned Body"))) child.Enabled = false;
var viewer = PrototypeGame.Instance?.LocalPlayer;
var watched = viewer?.IsSpectating == true && viewer.SpectatorTarget == player;
var firstPerson = watched || (viewer == player && !player.ThirdPersonView);
player.UpdateLegacyVisualVisibility(firstPerson, Ready);
if ( !_renderer.IsValid() || _firstPerson != firstPerson )
{
var model = Model.Load( firstPerson ? "models/review/dasher_v003/arms.vmdl" : "models/review/dasher_v003/body.vmdl" );
if ( model is null || model.BoneCount < (firstPerson ? 10 : 30) ) return;
if ( _body.IsValid() ) _body.Destroy();
_firstPerson = firstPerson;
_body = new GameObject( false, firstPerson ? "Dasher View Arms" : "Dasher Skinned Body" );
_body.NetworkMode = NetworkMode.Never;
_body.SetParent( GameObject, false );
_renderer = _body.Components.Create<SkinnedModelRenderer>();
_renderer.Model = model; _renderer.UseAnimGraph = false;
_renderer.RenderOptions.Game = !firstPerson; _renderer.RenderOptions.Overlay = firstPerson;
_weapon = new GameObject( true, "Dasher Held Weapon" ); _weapon.SetParent( _body, false );
_weapon.NetworkMode = NetworkMode.Never;
_weaponRenderer = _weapon.Components.Create<ModelRenderer>();
_weaponRenderer.RenderOptions.Game = !firstPerson; _weaponRenderer.RenderOptions.Overlay = firstPerson;
_body.Enabled = true;
_clip = null; _weaponId = null;
foreach ( var child in GameObject.Children )
if ( child.Name == "DASHER Visual" ) child.Enabled = false;
}
if ( _weaponId != player.BodyWeaponId )
{
_weaponId = player.BodyWeaponId;
_weaponRenderer.Model = Model.Load( _weaponId == "scatter" ? "models/review/dasher_v003/scatter.vmdl" : "models/review/dasher_v003/rifle.vmdl" );
}
var dead = player.Vitality?.IsDead == true || player.BodyAnimation == "death";
_body.Enabled = !firstPerson || !dead;
_weapon.Enabled = !dead;
var clip = dead ? "death" : player.WeaponSwitchProgress > 0f ? "swap" : player.BodyAnimation ?? "idle";
// Disabled view arms have no scene object; do not write sequence time
// during death/model creation before the renderer has been initialized.
if ( !_body.Enabled || _renderer.SceneObject is null ) return;
if ( clip != _clip )
{
_clip = clip; _renderer.Sequence.Name = clip; _renderer.Sequence.Time = 0;
_renderer.Sequence.Looping = clip is "idle" or "run" or "walk";
_renderer.PlaybackRate = clip.StartsWith( "dash" ) ? (17f / 30f) / player.DashDuration : 1f;
}
if ( clip == "reload" ) _renderer.Sequence.Time = player.BodyReloadProgress * (53f / 30f);
if ( clip == "swap" ) _renderer.Sequence.Time = player.WeaponSwitchProgress * (16f / 30f);
}
protected override void OnPreRender()
{
PrepareView();
UpdateWeaponPose();
}
private void UpdateWeaponPose()
{
if ( !Ready || _renderer.SceneObject is null ) return;
var player = Components.Get<FpsPlayer>();
// Bone poses belong to the renderer's sampled transform, which can lag the
// camera root. Convert to model space before applying this frame's root.
var sampledRoot = _renderer.SceneObject.Transform;
var hasGrip = _renderer.TryGetBoneTransform( "attach_right_grip", out var grip );
var localGrip = hasGrip ? sampledRoot.ToLocal( grip ) : _renderer.Model.GetBoneTransform( "attach_right_grip" );
if ( _firstPerson )
{
var rotation = player.ViewRigRotation;
_body.WorldRotation = rotation;
_body.WorldPosition = player.EyePosition + player.ViewRigOffset - rotation.Up * 59f + rotation.Forward * 20f;
}
if ( _weapon.Parent != _body ) _weapon.SetParent( _body, false );
_weapon.LocalPosition = localGrip.Position;
_weapon.LocalRotation = localGrip.Rotation * _renderer.Model.GetBoneTransform( "attach_right_grip" ).Rotation.Inverse;
}
protected override void OnDestroy()
{
if ( _weapon.IsValid() ) _weapon.Destroy();
if ( _body.IsValid() ) _body.Destroy();
}
}