Player/BallInput.cs
using System;

/// <summary>
/// 输入辅助:**仅键盘 + 手柄**(2026-09-05 用户决策,鼠标操控已移除)。
/// 数据源 Input.AnalogMove——引擎已把 WASD(模板 Input.config 预定义的
/// Forward/Backward/Left/Right)与手柄左摇杆叠加为 x=前(+)/后(-)、y=左(+)/右(-)。
/// 顶视相机下:前 → 相机 Up(屏幕上),左 → -相机 Right(屏幕左)。
/// M2 起真人客户端本地计算(owner 模拟),bot 由 BotBrain 直接给方向,不走这里。
/// </summary>
public static class BallInput
{
	/// <summary> 摇杆死区(防模拟量中心漂移) </summary>
	const float DeadZone = 0.2f;

	/// <summary>
	/// 是否正在文本输入(聊天框/输入框持有键盘焦点)——此时任何裸 Input 读取都必须让位。
	/// 根因(已对引擎源码核实):`Input.Actions.OnButton` 把按键**无差别累积进所有 InputContext**
	/// (sbox-public/engine/Sandbox.Engine/Systems/Input/Input.Actions.cs:229-232),
	/// 聊天框也不设 `Input.Suppressed`,所以 `Input.Keyboard.Pressed("P")` 在打字时同样为真
	/// → 聊天里打 "P" 会直接退局。
	/// 判定用 `Sandbox.UI.InputFocus.Current`(引擎 UI 焦点面板;无输入框聚焦时为 null)。
	/// 注意:更贴近的 `InputRouter.KeyboardFocusPanel` 是 engine internal,游戏程序集不可见。
	/// </summary>
	public static bool Typing
	{
		get
		{
			try
			{
				return Sandbox.UI.InputFocus.Current is not null;
			}
			catch
			{
				// UI 系统未就绪(启动早期/无面板)——按"没在打字"处理,绝不每帧抛异常
				return false;
			}
		}
	}

	public static Vector2 GetMoveDir( CameraComponent camera )
	{
		if ( !camera.IsValid() ) return Vector2.Zero;

		var analog = Input.AnalogMove;
		if ( analog.Length < DeadZone ) return Vector2.Zero;

		var rot = camera.GameObject.WorldRotation;
		var world = rot * ( Vector3.Right * ( -analog.y ) + Vector3.Up * analog.x );

		var v = new Vector2( world.x, world.y );
		var len = v.Length;
		return len > 0.001f ? new Vector2( v.x / len, v.y / len ) : Vector2.Zero;
	}

	/// <summary>
	/// 瞄准方向(v0.7.0.0 用户需求):光标世界位置相对球的单位方向——分裂/吐孢子指向这里。
	/// 正交俯视相机下 ScreenPixelToRay 的射线原点 XY 就是光标的世界坐标。
	/// 光标正贴球心时返回 Zero(调用方回退移动方向)。
	/// </summary>
	public static Vector2 GetAimDir( CameraComponent camera, Vector3 ballPos )
	{
		if ( !camera.IsValid() ) return Vector2.Zero;

		var ray = camera.ScreenPixelToRay( Mouse.Position );
		var dx = ray.Position.x - ballPos.x;
		var dy = ray.Position.y - ballPos.y;
		var len = MathF.Sqrt( dx * dx + dy * dy );
		if ( len < 0.001f ) return Vector2.Zero;

		return new Vector2( dx / len, dy / len );
	}

	/// <summary>
	/// 光标世界坐标(v0.7.8.7 手感):分裂/吐孢子传"点"不传"方向"——host 对主球和每个分身
	/// 各自算指向(分身也朝光标吐,agar 标准),正贴某颗身体时该身体回退移动方向。
	/// </summary>
	public static Vector2 GetAimPoint( CameraComponent camera )
	{
		if ( !camera.IsValid() ) return Vector2.Zero;

		var ray = camera.ScreenPixelToRay( Mouse.Position );
		return new Vector2( ray.Position.x, ray.Position.y );
	}
}