CameraModes.cs

Camera mode helper for the HorizonRP roleplay game. Cycles the local player's view between first person, third person behind and third person front on the View key, skips front view while seated in a vehicle, and positions the front camera ahead of the eyes with a trace to avoid clipping into walls. Also lowers the scene camera's near plane.

namespace HorizonRP;

/// <summary>
/// La touche « Vue » (C) fait le tour de trois vues : première personne, troisième personne
/// de derrière, troisième personne de face (le personnage vu de devant), puis de nouveau la
/// première. Le basculement d'origine du PlayerController (première / troisième) est coupé
/// (ToggleCameraModeButton vide) : c'est nous qui posons ThirdPerson.
///
/// Dans un véhicule, la vue de face est sautée (la caméra se mettrait dans le capot) : C passe
/// de la première personne à la vue de derrière, que le véhicule recule à sa taille.
/// </summary>
public static class CameraModes
{
	/// <summary>Le plan proche de la caméra du joueur (pouces).</summary>
	public const float NearPlane = 3f;

	public enum Mode { FirstPerson, Behind, Front }

	public static Mode Current { get; private set; } = Mode.Behind;

	/// <summary>Distance de la caméra de face, devant les yeux (pouces), et sa hauteur.</summary>
	public static float FrontDistance { get; set; } = 85f;
	public static float FrontHeight { get; set; } = 4f;

	/// <summary>Chez le joueur local, à chaque image, avant le PlayerController.</summary>
	public static void Update( RoleplayPlayer me, PlayerController controller )
	{
		controller.ToggleCameraModeButton = "";

		// Le plan proche de la caméra de la scène était à 10 pouces : avec le champ de 121°, ses
		// bords débordaient d'une trentaine de pouces de chaque côté, bien au-delà de la capsule
		// (16) — collé à un mur, on voyait à travers (Julien, 30/10/2026). À 3 pouces, moins de 10.
		if ( me.Scene.Camera is { } camera && camera.ZNear > NearPlane )
			camera.ZNear = NearPlane;

		// Une vue posée ailleurs (montée en véhicule, création du personnage) : on la suit.
		if ( Current == Mode.FirstPerson && controller.ThirdPerson )
			Current = Mode.Behind;
		else if ( Current != Mode.FirstPerson && !controller.ThirdPerson )
			Current = Mode.FirstPerson;

		if ( Input.Pressed( "View" ) && !UiLock.IsLocked && !me.NeedsCharacter )
			Next( me, controller );
	}

	/// <summary>La vue suivante : première → derrière → face → première.</summary>
	public static void Next( RoleplayPlayer me, PlayerController controller )
	{
		var seated = me.GameObject.GetComponentInParent<Vehicle>() is not null;
		Current = Current switch
		{
			Mode.FirstPerson => Mode.Behind,
			Mode.Behind when !seated => Mode.Front,
			_ => Mode.FirstPerson,
		};
		controller.ThirdPerson = Current != Mode.FirstPerson;
	}

	/// <summary>
	/// Chez le joueur local, après le placement de la caméra par le PlayerController : en vue de
	/// face, la caméra passe devant le visage et le regarde (sans traverser un mur).
	/// </summary>
	public static void PreRender( RoleplayPlayer me, PlayerController controller )
	{
		if ( Current != Mode.Front || !controller.ThirdPerson || me.GameObject.GetComponentInParent<Vehicle>() is not null )
			return;
		var camera = me.Scene.Camera;
		if ( !camera.IsValid() )
			return;

		var eye = controller.EyePosition;
		var forward = Rotation.FromYaw( controller.EyeAngles.yaw ).Forward;
		var wanted = eye + forward * FrontDistance + Vector3.Up * FrontHeight;
		var trace = me.Scene.Trace.Ray( eye, wanted ).IgnoreGameObjectHierarchy( me.GameObject ).WithoutTags( "trigger" ).Radius( 6 ).Run();
		var position = trace.Hit ? trace.EndPosition : wanted;

		camera.WorldPosition = position;
		camera.WorldRotation = Rotation.LookAt( eye - position, Vector3.Up );
	}
}