Game/Entity/TVEntity.cs
/// <summary>
/// A TV screen entity that displays the feed from a linked <see cref="CameraEntity"/>.
/// Use the Linker tool to connect a Camera to this TV.
/// </summary>
public sealed class TVEntity : Component
{
	[Property]
	public string ScreenMaterialName { get; set; } = "screen";

	[Property, Range( 0.5f, 10 ), Step( 0.5f ), ClientEditable, Group( "Screen" )]
	public float Brightness { get; set; } = 1f;

	[Property, ClientEditable, Group( "Screen" )]
	public bool On { get; set; } = true;

	public float MaxRenderDistance { get; set; } = 1024f;

	/// <summary>
	/// True when a linked camera is actively providing a render texture.
	/// </summary>
	public bool HasLinkedCamera => _linkedWeapon is not null && _linkedWeapon.Enabled && _linkedWeapon.RenderTexture is not null;

	private Texture _linkedTexture;
	private Texture _lastTexture;
	private CameraWeapon _linkedWeapon;
	private Material _materialCopy;
	private ModelRenderer _renderer;
	private bool _hasSignal;
	private RealTimeSince _timeSinceSignalChange;

	private static readonly float TransitionDuration = 0.4f;
	private static readonly float FadeStartFraction = 0.75f;

	protected override void OnStart()
	{
		_renderer = GetComponentInChildren<ModelRenderer>( true );
		_renderer?.SceneObject.Batchable = false;
	}

	protected override void OnUpdate()
	{
		FindLinkedTexture();

		// Distance-based fade and RT camera culling
		float distanceToCamera = Vector3.DistanceBetween( WorldPosition, Scene.Camera.WorldPosition );
		float fadeStart = MaxRenderDistance * FadeStartFraction;
		float distanceFade = 1.0f - MathX.Clamp( ( distanceToCamera - fadeStart ) / ( MaxRenderDistance - fadeStart ), 0f, 1f );
		bool tooFar = distanceFade <= 0f;

		// Enable/disable the linked RT camera based on distance
		if ( _linkedWeapon is not null )
		{
			var camera = _linkedWeapon.GetComponentInChildren<CameraComponent>( true );
			camera?.Enabled = !tooFar;
		}

		var newSignal = On && _linkedTexture is not null && !tooFar;

		if ( newSignal != _hasSignal )
		{
			_timeSinceSignalChange = 0;
			_hasSignal = newSignal;
		}

		// Keep the last known texture alive during the off-transition,
		// but only if the linked weapon still has a valid render target.
		if ( _linkedTexture is not null )
		{
			_lastTexture = _linkedTexture;
		}
		else if ( _linkedWeapon is null || !_linkedWeapon.Enabled || _linkedWeapon.RenderTexture is null )
		{
			// Weapon gone or disabled — its texture was disposed, don't use the cached copy.
			_lastTexture = null;
		}

		EnsureMaterialSetup();

		if ( _materialCopy is null || _renderer is null ) return;

		var inTransition = _timeSinceSignalChange < TransitionDuration;
		var textureToUse = _linkedTexture ?? ( inTransition ? _lastTexture : null );

		_renderer.Attributes.Set( "Color", textureToUse is not null ? textureToUse : Texture.Black );

		if ( !_hasSignal && !inTransition )
		{
			_lastTexture = null;
		}

		_renderer.Attributes.Set( "HasSignal", _hasSignal ? 1.0f : 0.0f );
		_renderer.Attributes.Set( "ScreenOn", On ? 1.0f : 0.0f );
		_renderer.Attributes.Set( "TimeSinceSignalChange", (float)_timeSinceSignalChange );
		_renderer.Attributes.Set( "DistanceFade", distanceFade );
		_renderer.Attributes.Set( "Brightness", Brightness );
	}

	protected override void OnDestroy()
	{
		_materialCopy = null;
		_linkedTexture = null;
		base.OnDestroy();
	}

	/// <summary>
	/// Resolves the linked render texture each frame by walking ManualLink components.
	/// Looks for a CameraWeapon on the linked object.
	/// </summary>
	private void FindLinkedTexture()
	{
		_linkedTexture = null;
		_linkedWeapon = null;

		foreach ( var link in GameObject.GetComponentsInChildren<ManualLink>() )
		{
			var target = link.Body?.Root;
			if ( target is null ) continue;

			if ( target.GetComponentInChildren<CameraWeapon>() is CameraWeapon weapon
				&& weapon.RenderTexture is not null )
			{
				_linkedTexture = weapon.RenderTexture;
				_linkedWeapon = weapon;
				return;
			}
		}
	}

	private static readonly string ShaderPath = "entities/sents/tv/materials/tv_crt_screen.shader";

	private void EnsureMaterialSetup()
	{
		if ( _materialCopy is not null && _renderer.IsValid() ) return;
		if ( _renderer is null ) return;

		var materials = _renderer.Model?.Materials;
		if ( materials is not { } mats ) return;

		for ( int i = 0; i < mats.Length; i++ )
		{
			if ( mats[i]?.Name?.Contains( ScreenMaterialName ) is true )
			{
				_materialCopy = Material.FromShader( ShaderPath );
				_renderer.Materials.SetOverride( i, _materialCopy );
				return;
			}
		}
	}
}