local socket		= require "socket"
local maf			= require "maf"
local vec3, quat	= maf.vec3, maf.quat
local samem			= require "SAMemory"

samem.require "CVehicle"
samem.require "tHandlingData"
samem.require "CPlane"
samem.require 'CTransmission'

-- Constants for controlling plane steering behavior
local PITCH_UP_THRESHOLD	= -15		-- Pitch angle threshold in degrees
local FINE_STEERING_ANGLE	= 15		-- Angle for fine-steering adjustments in degrees
local YAW_FACTOR			= 1			-- Multiplicative factor for yaw adjustment
local ROLL_FACTOR			= 0.75			-- Multiplicative factor for roll adjustment
local STEERING_SPEED		= 25.0		-- Speed of steering control in units per second

-- Direction vectors
local DIRECTION = {
    FORWARD		= vec3(0, 1, 0),
    BACKWARD	= vec3(0, -1, 0),
    RIGHT		= vec3(1, 0, 0),
    LEFT		= vec3(-1, 0, 0),
    UP			= vec3(0, 0, 1),
    DOWN		= vec3(0, 0, -1)
}

function math.sign(x)
    if x > 0 then
        return 1
    elseif x < 0 then
        return -1
    else
        return 0
    end
end

-- Calculate the signed angle between two vectors with respect to a reference axis
local function calculateSignedAngle(from, to, axis)
    local fromNormalized, toNormalized = from:clone():normalize(), to:clone():normalize()
    local dotProduct = fromNormalized:dot(toNormalized)
    local angle = math.acos(math.max(-1, math.min(1, dotProduct)))    -- Clamp the dot product
    local crossProduct = fromNormalized:cross(toNormalized)
    local sign = crossProduct:dot(axis) < 0 and -1 or 1
    return angle * sign
end

-- Calculate the angle between two vectors in degrees
local function calculateAngleBetweenVectors(v1, v2)
    local normV1, normV2 = v1:normalize(), v2:normalize()
    local dotProduct = math.max(-1, math.min(1, normV1:dot(normV2)))    -- Clamp the dot product
    return math.deg(math.acos(dotProduct))
end

-- Obtain the conjugate of a quaternion
local function conjugateQuaternion(q)
    return quat(-q.x, -q.y, -q.z, q.w)
end

-- Smooth step function
local function smoothstep(edge0, edge1, x)
    x = math.max(0, math.min(1, (x - edge0) / (edge1 - edge0)))
    return x * x * (3 - 2 * x)
end

local function dxDrawRing(posX, posY, radius, width, startAngle, amount, color, absoluteAmount, anglesPerLine)
    if type(posX) ~= "number" or type(posY) ~= "number" or type(startAngle) ~= "number" or type(amount) ~= "number" then
        print("Error: One of the required parameters (posX, posY, startAngle, amount) is not a number.")
        return false
    end

    radius = type(radius) == "number" and radius or 50
    width = type(width) == "number" and width or 5
    color = color or 0xFFFFFFFF
    absoluteAmount = type(absoluteAmount) == "boolean" and absoluteAmount or false
    anglesPerLine = type(anglesPerLine) == "number" and anglesPerLine or 1

    local stopAngle = absoluteAmount and (amount + startAngle) or ((amount * 360) + startAngle)

    for angle = startAngle, stopAngle, anglesPerLine do
        local radAngle = math.rad(angle)
        local startX = math.cos(radAngle) * (radius - width)
        local startY = math.sin(radAngle) * (radius - width)
        local endX = math.cos(radAngle) * (radius + width)
        local endY = math.sin(radAngle) * (radius + width)

        renderDrawLine(startX + posX, startY + posY, endX + posX, endY + posY, width, color)
    end

    return math.floor((stopAngle - startAngle) / anglesPerLine)
end

-- Compute the steering input for the plane to move towards a target position
local function computeSteeringInput(plane, dt, targetPosition)
    local planePosition = vec3(getCarCoordinates(plane))
    local planeRotation = quat(getVehicleQuaternion(plane))

    local positionError = targetPosition - planePosition
    local distanceError = positionError:length()
    local localError = conjugateQuaternion(planeRotation) * positionError

    local errorDir = localError:normalize()
    local pitchError, rollError, yawError
    pitchError = vec3(0, localError.y, localError.z):normalize()
    rollError = vec3(localError.x, localError.y, 0):normalize()
    yawError = vec3(localError.x, 0, localError.z):normalize()

    local targetInput = vec3()
    local pitch = calculateSignedAngle(DIRECTION.FORWARD, pitchError, DIRECTION.RIGHT)

    if -pitch < PITCH_UP_THRESHOLD then
        pitch = (pitch + 360) % 360
    end
    targetInput.x = pitch

    local angleError = calculateAngleBetweenVectors(DIRECTION.FORWARD, rollError)
    local minAngle, maxAngle = -5, FINE_STEERING_ANGLE

    local angleError = calculateAngleBetweenVectors(DIRECTION.FORWARD, rollError)
    local dynamicYawFactor = smoothstep(minAngle, maxAngle, angleError) * YAW_FACTOR

    if angleError < FINE_STEERING_ANGLE then
        local yaw = -calculateSignedAngle(DIRECTION.FORWARD, yawError, DIRECTION.UP)
        targetInput.y = yaw * dynamicYawFactor
    else
        local roll = calculateSignedAngle(DIRECTION.UP, rollError, DIRECTION.FORWARD)
        targetInput.z = roll * ROLL_FACTOR
    end

    local function clampInput(value)
        return math.max(-1, math.min(1, value))
    end

    targetInput.x, targetInput.y, targetInput.z = clampInput(targetInput.x), clampInput(targetInput.y), clampInput(targetInput.z)

    --local input = vec3()
    --input.x = clampInput(input.x + (targetInput.x - input.x) * STEERING_SPEED * dt)
    --input.y = clampInput(input.y + (targetInput.y - input.y) * STEERING_SPEED * dt)
    --input.z = clampInput(input.z + (targetInput.z - input.z) * STEERING_SPEED * dt)

    return targetInput
end

function main()
    local lastTime = socket.gettime()

    while true do
        if isCharInAnyCar(playerPed) then

			local currentTime = socket.gettime()
			local dt = currentTime - lastTime
			lastTime = currentTime

			local plane = storeCarCharIsInNoSave(playerPed)
			local planePosition = vec3(getCarCoordinates(plane))
			local camPosition = vec3(getActiveCameraCoordinates())
			camPosition.z = camPosition.z - 0.1
			local camDirection = (vec3(getActiveCameraPointAt()) - camPosition):normalize() * 10
			local targetPosition = planePosition + camDirection

			local wposX, wposY = convert3DCoordsToScreen(targetPosition:unpack())
			dxDrawRing(wposX, wposY - 65, 15, 0.75, 0, 1, 285212671, false, 1)

			local pitchInput, yawInput, rollInput = computeSteeringInput(plane, dt * 10, targetPosition):unpack()

			if doesVehicleExist(plane) then
				local carPointer = getCarPointer(plane)
				local vehicle = samem.cast("CVehicle *", carPointer)

				if vehicle ~= samem.nullptr and vehicle.nVehicleClass == 0 then
					local plane = samem.cast('CPlane *', vehicle)
					plane.field_988 = yawInput
					plane.field_98C = pitchInput
					plane.field_990 = rollInput
					printStringNow(yawInput, 1)
				end
			end
		end
		
		wait(0)
    end
end

как мне сделать самолет лучше в управлении?
мне нужна максимально точная и максимально отзывчивая управляемость по всем осям



можешь подсмотреть отсюда
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Windows;

public class AIController : MonoBehaviour {
    [SerializeField]
    Plane plane;
    [SerializeField]
    float steeringSpeed;
    [SerializeField]
    float minSpeed;
    [SerializeField]
    float maxSpeed;
    [SerializeField]
    float recoverSpeedMin;
    [SerializeField]
    float recoverSpeedMax;
    [SerializeField]
    LayerMask groundCollisionMask;
    [SerializeField]
    float groundCollisionDistance;
    [SerializeField]
    float groundAvoidanceAngle;
    [SerializeField]
    float groundAvoidanceMinSpeed;
    [SerializeField]
    float groundAvoidanceMaxSpeed;
    [SerializeField]
    float pitchUpThreshold;
    [SerializeField]
    float fineSteeringAngle;
    [SerializeField]
    float rollFactor;
    [SerializeField]
    float yawFactor;
    [SerializeField]
    bool canUseMissiles;
    [SerializeField]
    bool canUseCannon;
    [SerializeField]
    float missileLockFiringDelay;
    [SerializeField]
    float missileFiringCooldown;
    [SerializeField]
    float missileMinRange;
    [SerializeField]
    float missileMaxRange;
    [SerializeField]
    float missileMaxFireAngle;
    [SerializeField]
    float bulletSpeed;
    [SerializeField]
    float cannonRange;
    [SerializeField]
    float cannonMaxFireAngle;
    [SerializeField]
    float cannonBurstLength;
    [SerializeField]
    float cannonBurstCooldown;
    [SerializeField]
    float minMissileDodgeDistance;
    [SerializeField]
    float reactionDelayMin;
    [SerializeField]
    float reactionDelayMax;
    [SerializeField]
    float reactionDelayDistance;

    Target selfTarget;
    Plane targetPlane;
    Vector3 lastInput;
    bool isRecoveringSpeed;

    float missileDelayTimer;
    float missileCooldownTimer;

    bool cannonFiring;
    float cannonBurstTimer;
    float cannonCooldownTimer;

    struct ControlInput {
        public float time;
        public Vector3 targetPosition;
    }

    Queue<ControlInput> inputQueue;

    bool dodging;
    Vector3 lastDodgePoint;
    List<Vector3> dodgeOffsets;
    const float dodgeUpdateInterval = 0.25f;
    float dodgeTimer;

    void Start() {
        selfTarget = plane.GetComponent<Target>();

        if (plane.Target != null) {
            targetPlane = plane.Target.GetComponent<Plane>();
        }

        dodgeOffsets = new List<Vector3>();
        inputQueue = new Queue<ControlInput>();
    }

    Vector3 AvoidGround() {
        //roll level and pull up
        var roll = plane.Rigidbody.rotation.eulerAngles.z;
        if (roll > 180f) roll -= 360f;
        return new Vector3(-1, 0, Mathf.Clamp(-roll * rollFactor, -1, 1));
    }

    Vector3 RecoverSpeed() {
        //roll and pitch level
        var roll = plane.Rigidbody.rotation.eulerAngles.z;
        var pitch = plane.Rigidbody.rotation.eulerAngles.x;
        if (roll > 180f) roll -= 360f;
        if (pitch > 180f) pitch -= 360f;
        return new Vector3(Mathf.Clamp(-pitch, -1, 1), 0, Mathf.Clamp(-roll * rollFactor, -1, 1));
    }

    Vector3 GetTargetPosition() {
        if (plane.Target == null) {
            return plane.Rigidbody.position;
        }

        var targetPosition = plane.Target.Position;

        if (Vector3.Distance(targetPosition, plane.Rigidbody.position) < cannonRange) {
            return Utilities.FirstOrderIntercept(plane.Rigidbody.position, plane.Rigidbody.velocity, bulletSpeed, targetPosition, plane.Target.Velocity);
        }

        return targetPosition;
    }

    Vector3 CalculateSteering(float dt, Vector3 targetPosition) {
        if (plane.Target != null && targetPlane.Health == 0) {
            return new Vector3();
        }

        var error = targetPosition - plane.Rigidbody.position;
        error = Quaternion.Inverse(plane.Rigidbody.rotation) * error;   //transform into local space

        var errorDir = error.normalized;
        var pitchError = new Vector3(0, error.y, error.z).normalized;
        var rollError = new Vector3(error.x, error.y, 0).normalized;
        var yawError = new Vector3(error.x, 0, error.z).normalized;

        var targetInput = new Vector3();

        var pitch = Vector3.SignedAngle(Vector3.forward, pitchError, Vector3.right);
        if (-pitch < pitchUpThreshold) pitch += 360f;
        targetInput.x = pitch;

        if (Vector3.Angle(Vector3.forward, errorDir) < fineSteeringAngle) {
            var yaw = Vector3.SignedAngle(Vector3.forward, yawError, Vector3.up);
            targetInput.y = yaw * yawFactor;
        } else {
            var roll = Vector3.SignedAngle(Vector3.up, rollError, Vector3.forward);
            targetInput.z = roll * rollFactor;
        }

        targetInput.x = Mathf.Clamp(targetInput.x, -1, 1);
        targetInput.y = Mathf.Clamp(targetInput.y, -1, 1);
        targetInput.z = Mathf.Clamp(targetInput.z, -1, 1);

        var input = Vector3.MoveTowards(lastInput, targetInput, steeringSpeed * dt);
        lastInput = input;

        return input;
    }

    Vector3 GetMissileDodgePosition(float dt, Missile missile) {
        dodgeTimer = Mathf.Max(0, dodgeTimer - dt);
        var missilePos = missile.Rigidbody.position;

        var dist = Mathf.Max(minMissileDodgeDistance, Vector3.Distance(missilePos, plane.Rigidbody.position));

        //calculate dodge points
        if (dodgeTimer == 0) {
            var missileForward = missile.Rigidbody.rotation * Vector3.forward;
            dodgeOffsets.Clear();

            //4 dodge points: up, down, left, right

            dodgeOffsets.Add(new Vector3(0, dist, 0));
            dodgeOffsets.Add(new Vector3(0, -dist, 0));
            dodgeOffsets.Add(Vector3.Cross(missileForward, Vector3.up) * dist);
            dodgeOffsets.Add(Vector3.Cross(missileForward, Vector3.up) * -dist);

            dodgeTimer = dodgeUpdateInterval;
        }

        //select nearest dodge point
        float min = float.PositiveInfinity;
        Vector3 minDodge = missilePos + dodgeOffsets[0];

        foreach (var offset in dodgeOffsets) {
            var dodgePosition = missilePos + offset;
            var offsetDist = Vector3.Distance(dodgePosition, lastDodgePoint);

            if (offsetDist < min) {
                minDodge = dodgePosition;
                min = offsetDist;
            }
        }

        lastDodgePoint = minDodge;
        return minDodge;
    }

    float CalculateThrottle(float minSpeed, float maxSpeed) {
        float input = 0;

        if (plane.LocalVelocity.z < minSpeed) {
            input = 1;
        } else if (plane.LocalVelocity.z > maxSpeed) {
            input = -1;
        }

        return input;
    }

    void CalculateWeapons(float dt) {
        if (plane.Target == null) return;

        if (canUseMissiles) {
            CalculateMissiles(dt);
        }

        if (canUseCannon) {
            CalculateCannon(dt);
        }
    }

    void CalculateMissiles(float dt) {
        missileDelayTimer = Mathf.Max(0, missileDelayTimer - dt);
        missileCooldownTimer = Mathf.Max(0, missileCooldownTimer - dt);

        var error = plane.Target.Position - plane.Rigidbody.position;
        var range = error.magnitude;
        var targetDir = error.normalized;
        var targetAngle = Vector3.Angle(targetDir, plane.Rigidbody.rotation * Vector3.forward);

        if (!plane.MissileLocked || !(targetAngle < missileMaxFireAngle || (180f - targetAngle) < missileMaxFireAngle)) {
            //don't fire if not locked or target is too off angle
            //can fire if angle is close to 0 (chasing) or 180 (head on)
            missileDelayTimer = missileLockFiringDelay;
        }

        if (range < missileMaxRange && range > missileMinRange && missileDelayTimer == 0 && missileCooldownTimer == 0) {
            plane.TryFireMissile();
            missileCooldownTimer = missileFiringCooldown;
        }
    }

    void CalculateCannon(float dt) {
        if (targetPlane.Health == 0) {
            cannonFiring = false;
            return;
        }

        if (cannonFiring) {
            cannonBurstTimer = Mathf.Max(0, cannonBurstTimer - dt);

            if (cannonBurstTimer == 0) {
                cannonFiring = false;
                cannonCooldownTimer = cannonBurstCooldown;
                plane.SetCannonInput(false);
            }
        } else {
            cannonCooldownTimer = Mathf.Max(0, cannonCooldownTimer - dt);

            var targetPosition = Utilities.FirstOrderIntercept(plane.Rigidbody.position, plane.Rigidbody.velocity, bulletSpeed, plane.Target.Position, plane.Target.Velocity);

            var error = targetPosition - plane.Rigidbody.position;
            var range = error.magnitude;
            var targetDir = error.normalized;
            var targetAngle = Vector3.Angle(targetDir, plane.Rigidbody.rotation * Vector3.forward);

            if (range < cannonRange && targetAngle < cannonMaxFireAngle && cannonCooldownTimer == 0) {
                cannonFiring = true;
                cannonBurstTimer = cannonBurstLength;
                plane.SetCannonInput(true);
            }
        }
    }

    void SteerToTarget(float dt, Vector3 planePosition) {
        bool foundTarget = false;
        Vector3 steering = Vector3.zero;
        Vector3 targetPosition = Vector3.zero;

        var delay = reactionDelayMax;

        if (Vector3.Distance(planePosition, plane.Rigidbody.position) < reactionDelayDistance) {
            delay = reactionDelayMin;
        }

        while (inputQueue.Count > 0) {
            var input = inputQueue.Peek();

            if (input.time + delay <= Time.time) {
                targetPosition = input.targetPosition;
                inputQueue.Dequeue();
                foundTarget = true;
            } else {
                break;
            }
        }

        if (foundTarget) {
            steering = CalculateSteering(dt, targetPosition);
        }

        plane.SetControlInput(steering);
    }

    void FixedUpdate() {
        if (plane.Dead) return;
        var dt = Time.fixedDeltaTime;

        Vector3 steering = Vector3.zero;
        float throttle;
        bool emergency = false;
        Vector3 targetPosition = plane.Target.Position;

        var velocityRot = Quaternion.LookRotation(plane.Rigidbody.velocity.normalized);
        var ray = new Ray(plane.Rigidbody.position, velocityRot * Quaternion.Euler(groundAvoidanceAngle, 0, 0) * Vector3.forward);

        if (Physics.Raycast(ray, groundCollisionDistance + plane.LocalVelocity.z, groundCollisionMask.value)) {
            steering = AvoidGround();
            throttle = CalculateThrottle(groundAvoidanceMinSpeed, groundAvoidanceMaxSpeed);
            emergency = true;
        } else {
            var incomingMissile = selfTarget.GetIncomingMissile();
            if (incomingMissile != null) {
                if (dodging == false) {
                    //start dodging
                    dodging = true;
                    lastDodgePoint = plane.Rigidbody.position;
                    dodgeTimer = 0;
                }

                var dodgePosition = GetMissileDodgePosition(dt, incomingMissile);
                steering = CalculateSteering(dt, dodgePosition);
                emergency = true;
            } else {
                dodging = false;
                targetPosition = GetTargetPosition();
            }

            if (incomingMissile == null && (plane.LocalVelocity.z < recoverSpeedMin || isRecoveringSpeed)) {
                isRecoveringSpeed = plane.LocalVelocity.z < recoverSpeedMax;

                steering = RecoverSpeed();
                throttle = 1;
                emergency = true;
            } else {
                throttle = CalculateThrottle(minSpeed, maxSpeed);
            }
        }

        inputQueue.Enqueue(new ControlInput {
            time = Time.time,
            targetPosition = targetPosition,
        });

        plane.SetThrottleInput(throttle);

        if (emergency) {
            if (isRecoveringSpeed) {
                //reduce steering strength while recovering speed
                steering.x = Mathf.Clamp(steering.x, -0.5f, 0.5f);
            }

            plane.SetControlInput(steering);
        } else {
            SteerToTarget(dt, plane.Target.Position);
        }

        CalculateWeapons(dt);
    }
}'