Diagnostics/TiltProbe.cs

A diagnostic helper for measuring view tilt requested by recoil vs what was actually rendered. It tracks a configured number of shots, logs the requested kick and requested tilt gains when a probe is started, records per-shot requested values via Asked, samples the shown lean each Frame to record the peak, and logs warnings about common failure modes.

File Access
using Sandbox;
using System;

namespace NZombies;

/// <summary>
/// `nz_tilt_probe [shots]` — what the model lean was ASKED for, and what it actually reached.
///
/// ⛔ IT EXISTS BECAUSE "THE VERTICAL DOES BASICALLY NOTHING" HAS AT LEAST FOUR CAUSES AND THEY ARE
/// INDISTINGUISHABLE FROM IN FRONT OF THE MONITOR: the kick's pitch arriving near zero, the gain
/// being multiplied out somewhere, the spring never reaching the target, or the rotation being
/// applied about an axis that does not read on screen. Reasoning narrowed it to "all four look
/// correct", which is the point at which INSTRUCTIONS.md §7 applies and guessing stops paying.
///
/// ⚠️ IT PRINTS BOTH ENDS OF THE CHAIN, WHICH IS THE WHOLE DESIGN. One line at the shot says what
/// was requested; one line when the lean dies says what was reached. If the request is 1.2° and the
/// peak is 1.2°, the maths is fine and the problem is that a pitch of that size simply does not
/// read against a camera that is pitching too — a different fix entirely from a number being lost.
///
/// ⚠️ COUNTED IN SHOTS, NOT SECONDS. A burst is the only way to judge a lean, and a timed probe
/// either stops mid-burst or buries the interesting lines under a hundred idle frames.
/// </summary>
public static class TiltProbe
{
	static int _left;
	static Vector3 _peak;
	static bool _watching;

	public static bool On => _left > 0 || _watching;

	/// <summary>Log the next few shots' lean, both requested and delivered.</summary>
	[ConCmd( "nz_tilt_probe" )]
	public static void Cmd( int shots = 5 )
	{
		_left = shots < 1 ? 5 : shots;
		_peak = Vector3.Zero;
		_watching = false;

		Log.Info( $"[nz-tilt] probing the next {_left} shot(s)" );
		Log.Info( "[nz-tilt]   'asked' is the lean the shot requested, in degrees" );
		Log.Info( "[nz-tilt]   'peak' is the most the spring actually reached before it died" );
		Log.Info( $"[nz-tilt]   gains: up ×{GlobalHandling.VisualFollowUp:0.##}"
			+ $" side ×{GlobalHandling.VisualFollowSide:0.##}"
			+ $" roll ×{GlobalHandling.VisualFollowRoll:0.##}"
			+ $"   soften {GlobalHandling.VisualTiltSoften:0.###}s" );
	}

	/// <summary>Called at the shot, with the kick that drove it and the lean it produced.</summary>
	public static void Asked( Angles kick, float up, float side, float roll, bool following )
	{
		if ( _left <= 0 ) return;

		_left--;
		_watching = true;
		_peak = Vector3.Zero;

		Log.Info( $"[nz-tilt] kick pitch {kick.pitch:0.000} yaw {kick.yaw:0.000}"
			+ $"  ->  asked up {up:0.000}° side {side:0.000}° roll {roll:0.000}°"
			+ (following ? "" : "   (NOT following — authored path)") );

		// ⚠️ A ZERO KICK IS CALLED OUT RATHER THAN LEFT TO BE SPOTTED IN THE NUMBERS, because it is
		// the one cause with a completely different fix: nothing downstream can make a lean out of
		// a shot that did not push the view.
		if ( MathF.Abs( kick.pitch ) < 0.0001f )
			Log.Warning( "[nz-tilt]   ⛔ the shot's PITCH is zero — the vertical lean has nothing to"
				+ " scale. Check nz_recoil (vertical base, RecoilScale) before touching the tilt." );
	}

	/// <summary>Called every frame the lean is alive, with what is being drawn.</summary>
	public static void Frame( Vector3 shown )
	{
		if ( !_watching ) return;

		_peak.x = MathF.Max( _peak.x, MathF.Abs( shown.x ) );
		_peak.y = MathF.Max( _peak.y, MathF.Abs( shown.y ) );
		_peak.z = MathF.Max( _peak.z, MathF.Abs( shown.z ) );

		if ( shown.LengthSquared > 1e-8f ) return;

		// the lean has died — report what it managed
		_watching = false;

		Log.Info( $"[nz-tilt]   peak up {_peak.x:0.000}° side {_peak.y:0.000}° roll {_peak.z:0.000}°" );

		if ( _peak.x < 0.02f && _peak.y > 0.05f )
			Log.Warning( "[nz-tilt]   ⚠ the vertical reached nothing while the horizontal did —"
				+ " the loss is upstream of the spring, not in it." );
	}
}