--[[Copyright (C) Mr Chow Modding, 2025]]
--===============================================================
     -- Helicopter script For FS25 --
--===============================================================
-- Made by Mr Chow Modding
-- Controls:
-- W / A / S / D – Tilt forward, left, backward, right
-- Q / R – Yaw left / right
-- LMB / RMB – Increase / decrease vertical thrust
-- Left Shift – Increase / decrease pitch (boost)
-- Left Ctrl – Toggle auto hover
-- I – Cycle HUD modes (first-person view only)
-- Shift + G - Thrust booster mode (will make your helicopter go up with more force, adjust depending on your weight
-- T - Increase boost
-- G - Decrease boost

MrChowHelicopter1 = {}

function MrChowHelicopter1.prerequisitesPresent(specializations)
    return true
end

function MrChowHelicopter1.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad",       MrChowHelicopter1)
    SpecializationUtil.registerEventListener(vehicleType, "onStartMotor", MrChowHelicopter1)
    SpecializationUtil.registerEventListener(vehicleType, "onStopMotor",  MrChowHelicopter1)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate",     MrChowHelicopter1)
    SpecializationUtil.registerEventListener(vehicleType, "onDraw",       MrChowHelicopter1)
end

function MrChowHelicopter1:onLoad(savegame)
    
    self.setHelicopterInput     = MrChowHelicopter1.setHelicopterInput
    self._getFuelLiters         = MrChowHelicopter1._getFuelLiters
    self._getFuelUnitAndType    = MrChowHelicopter1._getFuelUnitAndType

    
    self.heli_inputW, self.heli_inputS = false, false
    self.heli_inputA, self.heli_inputD = false, false 
    self.heli_inputQ, self.heli_inputR = false, false 
    self.heli_inputLMB, self.heli_inputRMB = false, false
    self.lastInputs = {w=false,s=false,a=false,d=false,q=false,r=false,lmb=false,rmb=false}

    
    self.heli_pitch      = 0
    self.heli_yForce     = 0
    self.motorStarted    = false
    self.engineTurningOn = false
    self.motorStartTimer = 0
    self.motorStartDuration = 3  
	
	
    
    
    
    self.heli_extraRotorEnabled   = (self.heli_extraRotorEnabled == true)
    self.heli_extraRotorLevel     = math.max(1, math.floor(self.heli_extraRotorLevel or 1))
    self.heli_extraRotorMaxLevel  = self.heli_extraRotorMaxLevel or 50  

    
    self._extraRotorLastToggleKey = self._extraRotorLastToggleKey or false
    self._extraRotorLastDownKey   = self._extraRotorLastDownKey   or false
    self._extraRotorLastUpKey     = self._extraRotorLastUpKey     or false

    
    self.heli_hudMode     = 1 
    self.lastCamToggleKey = false

    
    self.hudToggleMode    = 1   
    self.lastHudToggleKey = false

    
    self.hudInteriorMode        = self.hudInteriorMode        or "OFF"  
    
    
    
    
    self.motorActiveVisualsMode = self.motorActiveVisualsMode or "OFF"  

    
    if self.components and self.components[1] then
        setCenterOfMass(self.components[1].node, 0, -0.8, 0)
    end

    
    MrChowHelicopter1._initMotorActiveNodes(self)

    
    
    
    self._autoHoverEnabled, self._autoHoverTarget = false, nil
    self._yawHold = nil

    
    
    
    
    self._wheelOffsetFuelOut   = self._wheelOffsetFuelOut or 0.07

    self.strafeCfg = self.strafeCfg or {
        deadzone        = 0.18,  
        expo            = 1.8,   
        accel           = 3.5,   
        decel           = 5.0,   
        accelNearZero   = 1.2,   
        decelNearZero   = 2.5,   
        crossZeroBrake  = 0.55,  
        followTime      = 0.08   
    }

    
    self._wheelOffsetEnabled  = true
    self._wheelOffsetGround   = -0.037   
    self._wheelOffsetAir      = 0.034    
    self._frontAirOffsetScale = 0.5      
    self._wheelOffsetSmoothT  = 0.65     
    self._wheelOffsetCur      = self._wheelOffsetGround

    
    
    
    
    self.hudInteriorOffset   = self.hudInteriorOffset   or { x = 0.40, y = 2.0000, z = 2.10 }

    
    self.hudInteriorNdcOffset = self.hudInteriorNdcOffset or { x = 0.025, y = -0.060 }

    
    
    self.hudHeliDist  = self.hudHeliDist  or 1800   
    self.hudHeliRight = self.hudHeliRight or  0.00  
    self.hudHeliUp    = self.hudHeliUp    or -0.15  

    
    self.hudInteriorScale       = self.hudInteriorScale       or 0.75
    self.hudInteriorLineSpacing = self.hudInteriorLineSpacing or 0.008
    self.hudInteriorBaseYOffset = self.hudInteriorBaseYOffset or -0.004

    
    self.hudRollSlantMode  = self.hudRollSlantMode  or "ON"   
    self.hudRollSlantScale = self.hudRollSlantScale or 1.0    

    
    self.hudRollSpreadMode   = self.hudRollSpreadMode   or "ON"  
    self.hudRollSpreadGain   = self.hudRollSpreadGain   or 0.25  
    self.hudRollSpreadMaxDeg = self.hudRollSpreadMaxDeg or 60    

    
    
    
    
    self.fuelCfg = self.fuelCfg or {
        tickPeriod   = 1.0,  
        idleRateLps  = 0.2,  
        rpmScaleLps  = 0.4,  
        loadScaleLps = 0.35,  
        
    }

    
    
    
    
    self.liftCfg = self.liftCfg or {
        maxSupportedMassKg = 12000, 
        hoverHeadroom      = 1.10,
        baseHoverFactor    = 0.82,
        maxUpVspd          = 5.0,   
        maxUpAccel         = 18.0
    }
end

function MrChowHelicopter1:onStartMotor()
    self.engineTurningOn  = true
    self.motorStarted     = false
    self.motorStartTimer  = 0

    local node = self.components and self.components[1] and self.components[1].node
    if node then
        local _, y, _ = getWorldTranslation(node)
        self._altZeroBase = y
    end

    self._hoverI = 0
end

function MrChowHelicopter1:onStopMotor()
    self.engineTurningOn   = false
    self.motorStarted      = false
    self.motorStartTimer   = 0
    self._autoHoverEnabled = false
    self._autoHoverTarget  = nil
    self._yawHold          = nil
    self._altZeroBase      = nil

    
    MrChowHelicopter1._applyMotorActiveScale(self, false)
end

function MrChowHelicopter1:setHelicopterInput(w, s, q, r, a, d, lmb, rmb)
    
    self.heli_inputW, self.heli_inputS = w, s
    self.heli_inputA, self.heli_inputD = a, d  
    self.heli_inputQ, self.heli_inputR = q, r  
    self.heli_inputLMB, self.heli_inputRMB = lmb, rmb
end





function MrChowHelicopter1._getFuelLiters(self)
    local idx
    if self.spec_motorized and self.spec_motorized.fuelFillUnitIndex then
        idx = self.spec_motorized.fuelFillUnitIndex
    end
    if not idx and self.spec_motorized and self.spec_motorized.consumers then
        for _, consumer in pairs(self.spec_motorized.consumers) do
            if consumer.fillUnitIndex then idx = consumer.fillUnitIndex break end
        end
    end
    local fuSpec = self.spec_fillUnit
    if not idx and fuSpec and fuSpec.fillUnits then
        local dieselIx = g_fillTypeManager and (g_fillTypeManager:getFillTypeIndexByName("DIESEL") or g_fillTypeManager:getFillTypeIndexByName("diesel")) or nil
        if dieselIx then
            for i, fu in ipairs(fuSpec.fillUnits) do
                if fu.fillTypes and fu.fillTypes[dieselIx] then idx = i break end
            end
        end
        if not idx then
            for i, fu in ipairs(fuSpec.fillUnits) do if fu.showOnHud then idx = i break end end
        end
        if not idx and fuSpec.primaryFillUnitIndex then idx = fuSpec.primaryFillUnitIndex end
        if not idx then idx = 1 end
    end
    if idx and self.getFillUnitFillLevel and self.getFillUnitCapacity then
        local liters = self:getFillUnitFillLevel(idx) or 0
        local cap    = self:getFillUnitCapacity(idx) or 0
        return liters, cap, idx
    end
    local cap = (fuSpec and fuSpec.fillUnits and fuSpec.fillUnits[1] and fuSpec.fillUnits[1].capacity) or (self._fuelCapacityLiters or 200)
    local pct = (self._fuelPercent or 0) / 100
    return pct * cap, cap, nil
end

function MrChowHelicopter1._getFuelUnitAndType(self)
    local fuIdx = nil
    if self.spec_motorized and self.spec_motorized.fuelFillUnitIndex then fuIdx = self.spec_motorized.fuelFillUnitIndex end
    if not fuIdx and self.spec_motorized and self.spec_motorized.consumers then
        for _, c in pairs(self.spec_motorized.consumers) do if c.fillUnitIndex then fuIdx = c.fillUnitIndex break end end
    end
    if not fuIdx and self.spec_fillUnit and self.spec_fillUnit.fillUnits then
        local dieselIx = g_fillTypeManager and (g_fillTypeManager:getFillTypeIndexByName("DIESEL") or g_fillTypeManager:getFillTypeIndexByName("diesel")) or nil
        if dieselIx then
            for i, fu in ipairs(self.spec_fillUnit.fillUnits) do if fu.fillTypes and fu.fillTypes[dieselIx] then fuIdx = i break end end
        end
        if not fuIdx then fuIdx = self.spec_fillUnit.primaryFillUnitIndex or 1 end
    end
    if not fuIdx then return nil, nil end
    local fuelType = self.getFillUnitFillType and self:getFillUnitFillType(fuIdx) or nil
    if not fuelType and self.spec_fillUnit and self.spec_fillUnit.fillUnits and self.spec_fillUnit.fillUnits[fuIdx] then
        local fu = self.spec_fillUnit.fillUnits[fuIdx]
        fuelType = fu.fillType or fu.lastFillType or fu.defaultFillType
        if not fuelType and fu.fillTypes and g_fillTypeManager then
            local dieselIx = g_fillTypeManager:getFillTypeIndexByName("DIESEL") or g_fillTypeManager:getFillTypeIndexByName("diesel")
            if dieselIx and fu.fillTypes[dieselIx] then fuelType = dieselIx
            else for ix, ok in pairs(fu.fillTypes) do if ok then fuelType = ix break end end end
        end
    end
    return fuIdx, fuelType
end




function MrChowHelicopter1._updateRpm(self, dt)
    local dt_s = math.max(((type(dt)=="number" and dt or 16)/1000), 0.0005)
    local IDLE, HIGH, LOW = 2800, 3200, 2250
    local RATE_UP, RATE_DOWN = 600, 600
    local target = IDLE
    if self.heli_inputLMB then target = HIGH
    elseif self.heli_inputRMB then target = LOW end
    self._currentRpm = self._currentRpm or IDLE
    local rate    = (target > self._currentRpm) and RATE_UP or RATE_DOWN
    local maxStep = rate * dt_s
    local delta   = target - self._currentRpm
    if delta >  maxStep then delta =  maxStep elseif delta < -maxStep then delta = -maxStep end
    self._currentRpm = self._currentRpm + delta
end




function MrChowHelicopter1._getSeaLevelY(self, x, z) return 65 end

function MrChowHelicopter1._getAltitudes(self)
    local node = self.components and self.components[1] and self.components[1].node
    if not node then return 0, 0, 0, 0 end
    local x, y, z = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z)
    local agl = math.max(0, y - groundY)
    local seaY = MrChowHelicopter1._getSeaLevelY(self, x, z)
    local asl = y - seaY
    return agl, asl, y, groundY
end

function MrChowHelicopter1._altReadout(self)
    local agl, asl = MrChowHelicopter1._getAltitudes(self)
    if agl < 50 then return "GL", agl else return "SL", math.max(0, asl) end
end




local function _safeLinearVelocity(node)
    local vx, vy, vz = getLinearVelocity(node)
    return tonumber(vx) or 0, tonumber(vy) or 0, tonumber(vz) or 0
end

local function _safeAngularVelocity(node)
    local avx, avy, avz = getAngularVelocity(node)
    return tonumber(avx) or 0, tonumber(avy) or 0, tonumber(avz) or 0
end


local function _isOnFlag(v)
    if v == nil then return true end             
    if type(v) == "boolean" then return v end
    if type(v) == "string" then
        v = v:lower()
        return (v == "on" or v == "true" or v == "yes" or v == "1")
    end
    return false
end




function MrChowHelicopter1._initMotorActiveNodes(self)
    
    if not _isOnFlag(self.motorActiveVisualsMode) then
        self._motorScaleNodes = nil
        return
    end

    if self._motorScaleNodes ~= nil then return end
    self._motorScaleNodes = {}

    
    if not (self.components and I3DUtil and I3DUtil.indexToObject) then
        return
    end

    local function addNode(indexStr)
        local node = I3DUtil.indexToObject(self.components, indexStr, self.i3dMappings)
        if node ~= nil then
            local sx, sy, sz = getScale(node)
            table.insert(self._motorScaleNodes, {
                node    = node,
                offX    = sx or 1,
                offY    = sy or 1,
                offZ    = sz or 1,
                onScale = 0.001
            })
        end
    end

    
    addNode("0>1|0|0|0")
    addNode("0>1|0|0|0|0")
end

function MrChowHelicopter1._applyMotorActiveScale(self, active)
    
    if not _isOnFlag(self.motorActiveVisualsMode) then
        return
    end

    if not self._motorScaleNodes then return end
    if self._motorScaleState == active then return end 

    self._motorScaleState = active

    for _, e in ipairs(self._motorScaleNodes) do
        if active then
            local s = e.onScale or 0.001
            setScale(e.node, s, s, s)
        else
            setScale(e.node, e.offX or 1, e.offY or 1, e.offZ or 1)
        end
    end
end




function MrChowHelicopter1._setupWheelOffsetNodes(self)
    if self._wheelOffsetSetup then return end
    self._wheelOffsetSetup = true
    self._wheelOffsetNodes = {}

    local ws = self.spec_wheels
    if ws and ws.wheels then
        for i, w in ipairs(ws.wheels) do
            
            local node = w.visualNode or w.visualWheelNode or w.repr or w.reprNode
            if node ~= nil then
                local rx, ry, rz = getRotation(node)
                table.insert(self._wheelOffsetNodes, {
                    node    = node,
                    applied = 0,
                    baseRot = { x = rx or 0, y = ry or 0, z = rz or 0 },
                    isLeft  = (w.isLeft == true),
                })
            end
        end
    end
    
    do
        local root = self.components and self.components[1] and self.components[1].node
        if root and self._wheelOffsetNodes and #self._wheelOffsetNodes > 0 then
            local first, second = nil, nil
            for _, g in ipairs(self._wheelOffsetNodes) do
                local wx, wy, wz = getWorldTranslation(g.node)
                local lx, ly, lz = worldToLocal(root, wx, wy, wz)
                g._zLocal = lz or 0
                if (first == nil) or (g._zLocal > (first._zLocal or -1e9)) then
                    second = first
                    first = g
                elseif (second == nil) or (g._zLocal > (second._zLocal or -1e9)) then
                    second = g
                end
            end
            if first  then first._isFrontWheel  = true end
            if second then second._isFrontWheel = true end
        end
    end
end

function MrChowHelicopter1._applyWheelOffsets(self, targetOffsetY, grounded)
    if not self._wheelOffsetNodes then return end
    local t = targetOffsetY or 0
    local airborne = not grounded
    local frontScale = self._frontAirOffsetScale or 0.5

    for _, g in ipairs(self._wheelOffsetNodes) do
        local perWheel = t
        if airborne and g._isFrontWheel then
            perWheel = t * frontScale 
        end

        local x, y, z = getTranslation(g.node)
        local delta = perWheel - (g.applied or 0)
        if math.abs(delta) > 1e-9 then
            setTranslation(g.node, x, y + delta, z)
            g.applied = (g.applied or 0) + delta
        end
    end
end




function MrChowHelicopter1._isOutOfFuel(self)
    local liters = select(1, MrChowHelicopter1._getFuelLiters(self))
    return (liters or 0) <= 0.01
end

function MrChowHelicopter1._isTaxiMode(self)
    local node = self.components and self.components[1] and self.components[1].node
    if not node then return false end
    local x, y, z = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z)
    local altitudeAGL = y - groundY
    local _, vy, _ = _safeLinearVelocity(node)
    return (altitudeAGL < 1.0) and (vy < 0.8) and not self.heli_inputLMB
end

function MrChowHelicopter1:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local controlled = self:getIsControlled()
    if not controlled then
        if not (self.isServer and self._autoHoverEnabled and self.motorStarted) then return end
    end

    if self.engineTurningOn and not self.motorStarted then
        self.motorStartTimer = self.motorStartTimer + (dt / 1000)
        if self.motorStartTimer >= self.motorStartDuration then
            self.engineTurningOn = false
            self.motorStarted    = true
        end
    end

    
    
    local motorActive = self.engineTurningOn or self.motorStarted
    MrChowHelicopter1._applyMotorActiveScale(self, motorActive)

    MrChowHelicopter1._updateRpm(self, dt)

    local camKey = Input.isKeyPressed(Input.KEY_c)
    if camKey and not self.lastCamToggleKey then
        self.heli_hudMode = (self.heli_hudMode + 1) % 2
    end
    self.lastCamToggleKey = camKey

    
    
    
    if self.spec_enterable then
        local cam = self.spec_enterable.cameras and self.spec_enterable.cameras[self.spec_enterable.camIndex]
        local isInterior = cam and cam.isInside

        
        if isInterior and (self.lastInteriorState ~= true) then
        self.hudToggleMode = 2  
        end
        self.lastInteriorState = isInterior
    end

    
    
    
    if self.spec_enterable then
        local cam = self.spec_enterable.cameras and self.spec_enterable.cameras[self.spec_enterable.camIndex]
        local isInterior = cam and cam.isInside
        local hudKey = Input.isKeyPressed(Input.KEY_i)

        if isInterior and hudKey and not self.lastHudToggleKey then
            
            if self.hudToggleMode == 1 then
                self.hudToggleMode = 2
            elseif self.hudToggleMode == 2 then
                self.hudToggleMode = 3
            else
                self.hudToggleMode = 1
            end

            local modeName = (self.hudToggleMode == 1 and "INTERIOR HUD") or (self.hudToggleMode == 2 and "EXTERIOR HUD") or "HUD OFF"
            print(string.format("[HELI HUD] Toggled mode = %s", modeName))
        end

        self.lastHudToggleKey = hudKey
    end

    if self.isClient and self:getIsActiveForInput(true) then
        local outOfFuel  = MrChowHelicopter1._isOutOfFuel(self)
        local allowInput = self.motorStarted or outOfFuel

        local w  = allowInput and Input.isKeyPressed(Input.KEY_w) or false
        local s  = allowInput and Input.isKeyPressed(Input.KEY_s) or false
        local a  = allowInput and Input.isKeyPressed(Input.KEY_a) or false
        local d  = allowInput and Input.isKeyPressed(Input.KEY_d) or false
        local q  = allowInput and Input.isKeyPressed(Input.KEY_q) or false
        local r  = allowInput and Input.isKeyPressed(Input.KEY_r) or false
        
        self._shiftBoostActive = allowInput and (Input.isKeyPressed(Input.KEY_lshift) or Input.isKeyPressed(Input.KEY_rshift)) or false

        local lmb = (allowInput and not outOfFuel) and Input.isMouseButtonPressed(Input.MOUSE_BUTTON_LEFT)  or false
        local rmb = (allowInput and not outOfFuel) and Input.isMouseButtonPressed(Input.MOUSE_BUTTON_RIGHT) or false

        if outOfFuel and self._autoHoverEnabled then
            self._autoHoverEnabled, self._autoHoverTarget = false, nil
        end

        local ctrl = Input.isKeyPressed(Input.KEY_lctrl)
        if ctrl and not self._lastCtrl then
            if self._autoHoverEnabled then
                self._autoHoverEnabled, self._autoHoverTarget = false, nil
            else
                local _, yNow, _ = getWorldTranslation(self.components[1].node)
                self._autoHoverEnabled, self._autoHoverTarget = true, yNow
                self._hoverI = 0
            end
        end
        self._lastCtrl = ctrl
		
		
        
        
        
        
        
        local shiftHeld = allowInput and (Input.isKeyPressed(Input.KEY_lshift) or Input.isKeyPressed(Input.KEY_rshift)) or false
        local gBase     = allowInput and Input.isKeyPressed(Input.KEY_g) or false
        local tKey      = allowInput and Input.isKeyPressed(Input.KEY_t) or false

        
        local toggleKey = gBase and shiftHeld
        
        local downKey   = gBase and (not shiftHeld)

        
        if toggleKey and not self._extraRotorLastToggleKey then
            self.heli_extraRotorEnabled = not self.heli_extraRotorEnabled
        end

        
        if downKey and not self._extraRotorLastDownKey then
            local lvl = (self.heli_extraRotorLevel or 1) - 1
            if lvl < 1 then lvl = 1 end
            self.heli_extraRotorLevel = lvl
        end

        
        if tKey and not self._extraRotorLastUpKey then
            local maxLvl = self.heli_extraRotorMaxLevel or 10
            local lvl    = (self.heli_extraRotorLevel or 1) + 1
            if lvl > maxLvl then lvl = maxLvl end
            self.heli_extraRotorLevel = lvl
        end

        self._extraRotorLastToggleKey = toggleKey
        self._extraRotorLastDownKey   = downKey
        self._extraRotorLastUpKey     = tKey

        if lmb or rmb then
            self._autoHoverEnabled, self._autoHoverTarget = false, nil
        end

        if w~=self.lastInputs.w or s~=self.lastInputs.s or
           a~=self.lastInputs.a or d~=self.lastInputs.d or
           q~=self.lastInputs.q or r~=self.lastInputs.r or
           lmb~=self.lastInputs.lmb or rmb~=self.lastInputs.rmb then

            self.lastInputs = {w=w, s=s, a=a, d=d, q=q, r=r, lmb=lmb, rmb=rmb}

            
            self:setHelicopterInput(w, s, q, r, a, d, lmb, rmb)

            local sc = g_client and g_client.getServerConnection and g_client:getServerConnection() or nil
            if HelicopterInputEvent ~= nil and HelicopterInputEvent.new ~= nil and sc ~= nil then
                sc:sendEvent(HelicopterInputEvent.new(self, w, s, a, d, q, r, lmb, rmb))
            end
        end
    end

    
    if self._wheelOffsetEnabled then
        MrChowHelicopter1._setupWheelOffsetNodes(self)

        local dt_s = math.max(((type(dt)=="number" and dt or 16)/1000), 0.0005)

        local grounded = MrChowHelicopter1._isTaxiMode(self)

        
        do
            local outOfFuel = MrChowHelicopter1._isOutOfFuel(self)
            if outOfFuel then
                local agl = select(1, MrChowHelicopter1._getAltitudes(self))
                if agl < 1.5 then grounded = true end
            end
        end

        
        local outNow = MrChowHelicopter1._isOutOfFuel(self)
        local target
        if outNow then
            target = (self._wheelOffsetFuelOut ~= nil) and self._wheelOffsetFuelOut or 0.07
        else
            target = grounded and (self._wheelOffsetGround or 0) or (self._wheelOffsetAir or 0)
        end

        local tau    = self._wheelOffsetSmoothT or 0.65
        local alpha  = 1 - math.exp(-dt_s / math.max(0.01, tau))

        self._wheelOffsetCur = (self._wheelOffsetCur ~= nil) and self._wheelOffsetCur or target
        self._wheelOffsetCur = self._wheelOffsetCur + (target - self._wheelOffsetCur) * alpha
        MrChowHelicopter1._applyWheelOffsets(self, self._wheelOffsetCur, grounded)
    end

    if not self.isServer then return end

    
    do
        local motorOn = (self.spec_motorized and self.spec_motorized.isMotorStarted) or self.motorStarted
        if motorOn then
            local FUEL_STEP_PERIOD = (self.fuelCfg and self.fuelCfg.tickPeriod) or 1.0

            self._fuelDrainTimer = (self._fuelDrainTimer or 0) + (dt / 1000)
            while self._fuelDrainTimer >= FUEL_STEP_PERIOD do
                local fuIdx, fuelType = MrChowHelicopter1._getFuelUnitAndType(self)
                if (not fuelType) and g_fillTypeManager then
                    fuelType = g_fillTypeManager:getFillTypeIndexByName("DIESEL") or g_fillTypeManager:getFillTypeIndexByName("diesel")
                end

                if fuIdx and self.getFillUnitFillLevel then
                    local before = self:getFillUnitFillLevel(fuIdx) or 0
                    if before > 0 then
                        local farmId = (self.getOwnerFarmId and self:getOwnerFarmId()) or 1

                        local IDLE_RPM, MAX_RPM = 2800, 3200
                        local rpmNow  = (self._currentRpm or IDLE_RPM)
                        local rpmFrac = (rpmNow - IDLE_RPM) / math.max(1, (MAX_RPM - IDLE_RPM))
                        if rpmFrac < 0 then rpmFrac = 0 elseif rpmFrac > 1 then rpmFrac = 1 end

                        local pitchFrac = math.abs(self._pitchTarget or 0) / 32
                        if pitchFrac > 1 then pitchFrac = 1 end
                        local rawFwd = (self.heli_inputW and 1 or 0) - (self.heli_inputS and 1 or 0)
                        local rawStr = (self.heli_inputA and 1 or 0) - (self.heli_inputD and 1 or 0)
                        local moveFrac = math.min(1, math.abs(rawFwd) * 1.0 + math.abs(rawStr) * 0.5)
                        local loadFrac = math.min(1, 0.75 * pitchFrac + 0.25 * moveFrac)

                        local cfg   = self.fuelCfg or {}
                        local idle  = cfg.idleRateLps  or 0.6
                        local rpmS  = cfg.rpmScaleLps  or 1.2
                        local loadS = cfg.loadScaleLps or 1.0
                        local scale = (cfg.globalScale ~= nil) and cfg.globalScale or 1.0
                        local burnLps = (idle + rpmS * rpmFrac + loadS * loadFrac) * scale
                        local litersThisTick = burnLps * FUEL_STEP_PERIOD

                        local target  = math.max(0, before - litersThisTick)
                        local changed = false

                        if self.setFillUnitFillLevel then
                            self:setFillUnitFillLevel(farmId, target, fuelType, fuIdx, true, false)
                            local after = self:getFillUnitFillLevel(fuIdx) or before
                            if after < before then changed = true end
                        end

                        if (not changed) and self.addFillUnitFillLevel then
                            local cur2  = self:getFillUnitFillLevel(fuIdx) or before
                            local delta = math.max(-litersThisTick, target - cur2)
                            if delta ~= 0 then
                                self:addFillUnitFillLevel(farmId, delta, fuelType, nil, nil, fuIdx, nil, nil, nil, true, false)
                            end
                            local after2 = self:getFillUnitFillLevel(fuIdx) or cur2
                            if after2 < cur2 then changed = true end
                        end

                        if (not changed) and self.spec_fillUnit and self.spec_fillUnit.fillUnits and self.spec_fillUnit.fillUnits[fuIdx] then
                            self.spec_fillUnit.fillUnits[fuIdx].fillLevel = target
                        end
                    end
                end

                self._fuelDrainTimer = self._fuelDrainTimer - FUEL_STEP_PERIOD
            end
        end
    end

    
    local node = self.components[1].node
    if node == nil then return end
    local dt_s = math.max((type(dt)=="number" and dt or 16)/1000, 0.0005)

    local shift = (self._shiftBoostActive == true)
    local shiftMul = shift and 1.2 or 1 
    local speed = 200 * shiftMul
    local rawFwd        = (self.heli_inputW and 1 or 0) - (self.heli_inputS and 1 or 0)
    local rawStr        = (self.heli_inputA and 1 or 0) - (self.heli_inputD and 1 or 0)
    local yawMultiplier = 1
    local rawYaw        = ((self.heli_inputQ and 1 or 0) - (self.heli_inputR and 1 or 0)) * yawMultiplier

    local outOfFuel_server = MrChowHelicopter1._isOutOfFuel(self)
    local allowMove = self.motorStarted or outOfFuel_server
    if not allowMove then rawFwd, rawStr, rawYaw = 0,0,0 end

    if outOfFuel_server then
        self.heli_inputLMB, self.heli_inputRMB = false, false
        if self._autoHoverEnabled then self._autoHoverEnabled, self._autoHoverTarget = false, nil end
    end

    if (self.heli_inputLMB or self.heli_inputRMB) and self._autoHoverEnabled then
        self._autoHoverEnabled, self._autoHoverTarget = false, nil
    end

    
    self._descendRamp = self._descendRamp or 0
    if self.heli_inputRMB then
        self._descendRamp = math.min(1, self._descendRamp + dt_s / 1.1)
    else
        self._descendRamp = math.max(0, self._descendRamp - dt_s / 1.2)
    end

    self._pitchTarget = self._pitchTarget or 0
    local minPitch, maxPitch = -16, 32
    local pitchRateUp, pitchRateDown = 50, 90
    local returnRate, followTime     = 24, 0.09
    local alpha = 1 - math.exp(-dt_s / followTime)

    if self.heli_inputLMB then
        self._pitchTarget = math.min(self._pitchTarget + pitchRateUp   * dt_s, maxPitch)
    elseif self.heli_inputRMB then
        self._pitchTarget = math.max(self._pitchTarget - pitchRateDown * (0.2 + 0.8*self._descendRamp) * dt_s, minPitch)
    else
        if     self._pitchTarget > 0 then self._pitchTarget = math.max(0, self._pitchTarget - returnRate * dt_s)
        elseif self._pitchTarget < 0 then self._pitchTarget = math.min(0, self._pitchTarget + returnRate * dt_s) end
    end

    self.heli_pitch = self.heli_pitch + (self._pitchTarget - self.heli_pitch) * alpha
    self.heli_pitch = math.max(minPitch, math.min(maxPitch, self.heli_pitch))

    
    self._strafeState = self._strafeState or 0
    do
        local cfg = self.strafeCfg or {}
        local function shapeInput(v, dz, expo)
            local a = math.abs(v)
            if a <= (dz or 0) then return 0 end
            local t = (a - (dz or 0)) / math.max(1e-6, 1 - (dz or 0))
            t = t ^ (expo or 1.0)
            return (v >= 0 and 1 or -1) * math.min(1, t)
        end

        local targetStr = shapeInput(rawStr, cfg.deadzone, cfg.expo)

        local state   = self._strafeState
        local crossing= (targetStr ~= 0 and state ~= 0 and (targetStr * state) < 0)
        local nearCtr = (math.abs(state) < 0.35)

        local accel = nearCtr and (cfg.accelNearZero or cfg.accel or 4.0) or (cfg.accel or 4.0)
        local decel = nearCtr and (cfg.decelNearZero or cfg.decel or 6.0) or (cfg.decel or 6.0)

        if crossing then
            local k = cfg.crossZeroBrake or 0.6
            accel = accel * k
            decel = decel * k
        end

        local rate
        if targetStr == 0 then
            rate = decel
        elseif math.abs(targetStr) > math.abs(state) then
            rate = accel
        else
            rate = decel
        end

        local maxStep = rate * dt_s
        local delta   = targetStr - state
        if delta >  maxStep then delta =  maxStep
        elseif delta < -maxStep then delta = -maxStep end

        self._strafeState = state + delta

        
        local ft = math.max(0.01, cfg.followTime or 0.08)
        local fa = 1 - math.exp(-dt_s / ft)
        self._strafeFiltered = (self._strafeFiltered or 0) + (self._strafeState - (self._strafeFiltered or 0)) * fa
    end

    
    local fwd = rawFwd * shiftMul
    local str = (self._strafeFiltered ~= nil) and self._strafeFiltered or self._strafeState
    local zSteer = speed * fwd
    local xSteer = speed * str

    local dX, _, dZ = localDirectionToWorld(node, 1, 0, 0)
    local sinDir = (dX or 0) / 15
    local cosDir = (dZ or 0) / -15

    local xDirX, _, _ = localDirectionToWorld(node, xSteer * 0.125, 0, 0)
    local rawXForce   = zSteer * cosDir + (xDirX or 0)
    self.prevXForce = self.prevXForce or 0
    local xForce = self.prevXForce * 0.75 + rawXForce * 0.25
    self.prevXForce = xForce

    local _, _, xDirZ2 = localDirectionToWorld(node, xSteer * 0.125, 0, 0)
    local zForce = zSteer * sinDir + (xDirZ2 or 0)

    xForce = xForce * 1.25
    zForce = zForce * 1.25
    if self._autoHoverEnabled then
        xForce = xForce * 1.10
        zForce = zForce * 1.10
    end

    local moveActive = (rawFwd ~= 0) or (math.abs(self._strafeState) > 0.02)
    local effectivePitch = self.heli_pitch
    if moveActive and not (self.heli_inputLMB or self.heli_inputRMB) and not self._autoHoverEnabled then
        effectivePitch = math.max(-4, math.min(4, effectivePitch * 0.2))
    end


if moveActive and self._autoHoverEnabled then
    effectivePitch = math.max(-4, math.min(4, effectivePitch * 0.75))
end

    local mass           = self:getTotalMass(true)
    local downForce      = mass * -9.81
    local hoverForce     = mass * 9.81
    local cfg            = self.liftCfg or {}
    local baseHoverFactor= (cfg.baseHoverFactor ~= nil) and cfg.baseHoverFactor or 0.82
    local lift           = hoverForce * baseHoverFactor
    local _, yPos, _ = getWorldTranslation(node)

    
    local xw, _, zw = getWorldTranslation(node)
    local groundY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, xw, 0, zw)
    local altitudeAGL = yPos - groundY

    if MrChowHelicopter1._isTaxiMode(self) then
        local vx, vy, vz = _safeLinearVelocity(node)
        if vy > 0 then setLinearVelocity(node, vx, 0, vz) end
        local mass2 = self:getTotalMass(true)
        local stickForce = -mass2 * 9.81 * 1.05
        addForce(node, 0, stickForce, 0, 0, 0, 0, true)

        self._yawGroundRamp = self._yawGroundRamp or 0
        local rawYawG = ((self.heli_inputQ and 1 or 0) - (self.heli_inputR and 1 or 0))
        if rawYawG ~= 0 then
            local rampRate = 1.5
            self._yawGroundRamp = self._yawGroundRamp + (rawYawG - self._yawGroundRamp) * rampRate * (dt / 1000)
            local yawTorque = self._yawGroundRamp * 50
            addTorque(node, 0, yawTorque, 0, true)
            setAngularDamping(node, 0.45)
        else
            self._yawGroundRamp = self._yawGroundRamp * 0.92
            setAngularDamping(node, 0.10)
        end
        setLinearDamping(node, 0.65)
    end

    if yPos < 4 then lift = lift * (1 + (4 - yPos) * 0.10) end
    local pitchComp = effectivePitch
    self.heli_yForce = lift + pitchComp

    
    
    
    
    do
        local maxLvl = self.heli_extraRotorMaxLevel or 10
        local level  = math.max(1, math.min(maxLvl, math.floor(self.heli_extraRotorLevel or 1)))

        if self.heli_extraRotorEnabled
           and level > 1
           and self.heli_inputLMB
           and (not outOfFuel_server)
        then
            
            local strength = 1 + (level - 1) * (4 / 9)  
            local baseForce = self.heli_yForce
            self.heli_yForce = baseForce * strength
        end
    end

    
    if outOfFuel_server then
        self.heli_yForce = 0
    end

    if (not outOfFuel_server) and self._autoHoverEnabled and self._autoHoverTarget then
        local _, curY, _ = getWorldTranslation(node)
        local _, vY, _   = _safeLinearVelocity(node)
        local err        = (self._autoHoverTarget - curY)
        self._hoverI = (tonumber(self._hoverI) or 0) + err * dt_s
        if self._hoverI > 5 then self._hoverI = 5 elseif self._hoverI < -5 then self._hoverI = -5 end
        local kP, kD, kI = 180, 55, 30
        local correction = kP*err + kD*(-vY) + kI*self._hoverI
        self.heli_yForce = self.heli_yForce + correction
    end
    
    do
        local vx_l, vy_l, vz_l = _safeLinearVelocity(node)

        
        
        local rawLevel = (self.heli_extraRotorEnabled and (self.heli_extraRotorLevel or 1)) or 1
        local maxLvl   = self.heli_extraRotorMaxLevel or 10
        rawLevel       = math.max(1, math.min(maxLvl, math.floor(rawLevel)))

        
        local boostScale = 1 + (rawLevel - 1) * (4 / 9)

        
        local maxLiftForce = math.max(0,
            ((cfg.maxSupportedMassKg or mass) * 9.81) *
            math.max(1.0, (cfg.hoverHeadroom or 1.0) * boostScale)
        )

        if self.heli_yForce > maxLiftForce then
            self.heli_yForce = maxLiftForce
        end

        
        local baseMaxUpV = (cfg.maxUpVspd ~= nil) and cfg.maxUpVspd or 8.0
        local maxUpV     = baseMaxUpV * boostScale
        if vy_l > maxUpV and self.heli_yForce > hoverForce then
            
            self.heli_yForce = hoverForce
        end

        
        local baseMaxUpA = (cfg.maxUpAccel ~= nil) and cfg.maxUpAccel or 18.0
        local maxUpA     = baseMaxUpA * boostScale
        if self.heli_yForce > hoverForce then
            local netUp  = self.heli_yForce - hoverForce
            local netMax = mass * maxUpA
            if netUp > netMax then
                self.heli_yForce = hoverForce + netMax
            end
        end
    end

    local fwdOrBack = 0
    local lmb = self.heli_inputLMB
    local rmb = self.heli_inputRMB
    if rawFwd > 0 then
        if     shift and lmb then fwdOrBack = 3.0
        elseif shift and rmb then fwdOrBack = 2.25
        elseif shift then          fwdOrBack = 2.5
        elseif lmb then            fwdOrBack = 1.75
        elseif rmb then            fwdOrBack = 1.25
        else                       fwdOrBack = 1.5 end
    elseif rawFwd < 0 then
        if     shift and lmb then fwdOrBack = -3.0
        elseif shift and rmb then fwdOrBack = -2.25
        elseif shift then          fwdOrBack = -2.5
        elseif lmb then            fwdOrBack = -1.75
        elseif rmb then            fwdOrBack = -1.25
        else                       fwdOrBack = -1.5 end
    end

    
    if shift and rawFwd ~= 0 then
        
        fwdOrBack = -fwdOrBack
        
        fwdOrBack = fwdOrBack * 0.35
    end

    if shift then
        fwdOrBack = -fwdOrBack
    end

    
    if outOfFuel_server then
        local vx, vy, vz = _safeLinearVelocity(node)
        local maxDescent = self._fuelOutMaxDescent or 8 

        if vy > 0 then
            setLinearVelocity(node, vx, 0, vz)
        elseif vy < -maxDescent then
            setLinearVelocity(node, vx, -maxDescent, vz)
        end
    end

    
    local zOffset = fwdOrBack
    if shift then
        if rawFwd > 0 then
            zOffset = zOffset + 0.5  
        elseif rawFwd < 0 then
            zOffset = zOffset - 0.5  
        end
    end

    
    addForce(node, xForce, self.heli_yForce, zForce,  0, 1.2, zOffset, true)

    self._yawTarget = self._yawTarget or 0
    local yawAccel, yawReturn, yawFollow = 5.0, 2.2, 0.08
    local yawAlpha = 1 - math.exp(-dt_s / yawFollow)
    if self._autoHoverEnabled then
        if rawYaw ~= 0 then self._yawHold = rawYaw else self._yawHold = nil end
        self._yawTarget = self._yawHold or 0
    else
        if rawYaw ~= 0 then
            self._yawTarget = math.max(-1, math.min(1, self._yawTarget + rawYaw * yawAccel * dt_s))
        else
            if     self._yawTarget > 0 then self._yawTarget = math.max(0, self._yawTarget - yawReturn * dt_s)
            elseif self._yawTarget < 0 then self._yawTarget = math.min(0, self._yawTarget + yawReturn * dt_s) end
        end
    end
    local yTurn = (self._prevYaw or 0) + (self._yawTarget - (self._prevYaw or 0)) * yawAlpha
    self._prevYaw = yTurn
    yTurn = math.max(-1, math.min(1, yTurn))
    setAngularVelocity(node, 0, yTurn, 0)
    setLinearDamping(node, (outOfFuel_server and 0.85) or (self._autoHoverEnabled and 0.34 or 0.22))
    setAngularDamping(node, (outOfFuel_server and 0.10) or (self._autoHoverEnabled and 0.05 or 0.02))
end




function MrChowHelicopter1:_hudTelemetry()
    local node = self.components and self.components[1] and self.components[1].node or nil
    local alt, spdKmh, vspd = 0, 0, 0
    if node ~= nil then
        local _, y, _ = getWorldTranslation(node)
        local vx, vy, vz = getLinearVelocity(node)
        alt = y or 0
        spdKmh = math.sqrt((vx or 0)^2 + (vz or 0)^2) * 3.6
        vspd = vy or 0
    end
    self._currentRpm  = self._currentRpm or 0
    self._fuelPercent = self._fuelPercent or 100
    return alt, spdKmh, vspd
end

function MrChowHelicopter1:onDraw()
    if not self:getIsActiveForInput(true) then return end

    
    
    
    local isInterior = false
    if self.spec_enterable then
        local cam = self.spec_enterable.cameras and self.spec_enterable.cameras[self.spec_enterable.camIndex]
        isInterior = cam and cam.isInside
    end

    if isInterior then
        
        if self.hudToggleMode < 1 or self.hudToggleMode > 3 then
            self.hudToggleMode = 1
        end

        if self.hudToggleMode == 3 then
            return  
        elseif self.hudToggleMode == 1 then
            self.hudInteriorMode = "ON"   
        elseif self.hudToggleMode == 2 then
            self.hudInteriorMode = "OFF"  
        end
    else
        
        self.hudInteriorMode = "OFF"
        self.hudToggleMode = 2
    end


    local currentTime = getTimeSec()
    self._lastDrawTime = self._lastDrawTime or currentTime
    local dtDraw = currentTime - self._lastDrawTime
    self._lastDrawTime = currentTime

    self._fadeTimer = self._fadeTimer or 0
    if self.motorStarted then
        self._fadeTimer = math.min(self._fadeTimer + dtDraw, 1.5)
    else
        self._fadeTimer = 0
        return
    end

    local fadeProgress = math.min(self._fadeTimer / 1.5, 1.0)

    local function drawThickText(x, y, s, t)
        renderText(x, y, s, t)
        renderText(x + 0.0009, y, s, t)
        renderText(x - 0.0009, y, s, t)
        renderText(x, y + 0.0009, s, t)
        renderText(x, y - 0.0009, s, t)
    end

    local node = self.components[1].node
    if not node then return end

    local _, y, _ = getWorldTranslation(node)
    local vx, vY, vz = getLinearVelocity(node)

    local altAGL_now, altASL_now = MrChowHelicopter1._getAltitudes(self)
    local altLabel, altDisplay    = MrChowHelicopter1._altReadout(self)

    local forwardSpeed = math.sqrt((vx or 0) * (vx or 0) + (vz or 0) * (vz or 0))

    MrChowHelicopter1._smoothedVspd = MrChowHelicopter1._smoothedVspd or (vY or 0)
    local vAlpha = 0.15
    local verticalSpeed = MrChowHelicopter1._smoothedVspd + ((vY or 0) - MrChowHelicopter1._smoothedVspd) * vAlpha
    MrChowHelicopter1._smoothedVspd = verticalSpeed

    local isInterior = false
    if self.spec_enterable then
        local cam = self.spec_enterable.cameras and self.spec_enterable.cameras[self.spec_enterable.camIndex]
        isInterior = cam and cam.isInside
    end

    
    local interiorHudOn = _isOnFlag(self.hudInteriorMode)

    local baseScale = 0.015
    local green = {0.2, 1.0, 0.2, fadeProgress}
    local red   = {1.0, 0.2, 0.2, fadeProgress}
    local amber = {1.0, 1.0, 0.3, fadeProgress}

    local taxiActive = MrChowHelicopter1._isTaxiMode(self)
    local taxiBlinkAlpha = 0.35 + 0.65 * (0.5 + 0.5 * math.sin(getTimeSec() * 5.5))

    setTextBold(true)

    if isInterior and interiorHudOn then
        local hudScale = 0.8
        local scaleMult  = self.hudInteriorScale or 1.0
        local scale      = baseScale * hudScale * scaleMult
        local lineSpace  = self.hudInteriorLineSpacing or 0.008
        local baseYShift = self.hudInteriorBaseYOffset or -0.004

        local fX, fY, fZ = localDirectionToWorld(node, 0, 0, 1)
        local rX, rY, rZ = localDirectionToWorld(node, 1, 0, 0)
        local rawPitch = math.deg(math.atan2(fY, math.sqrt(fX * fX + fZ * fZ)))
        local rawRoll  = math.deg(math.atan2(rY, math.sqrt(rX * rX + rZ * rZ)))
        local alpha = 0.1
        self._lastPitchRaw = self._lastPitchRaw or rawPitch
        self._lastRollRaw  = self._lastRollRaw  or rawRoll
        local pitch = self._lastPitchRaw + alpha * (rawPitch - self._lastPitchRaw)
        local roll  = self._lastRollRaw  + alpha * (rawRoll  - self._lastRollRaw)
        self._lastPitchRaw = pitch
        self._lastRollRaw  = roll

        self._displayRpm = self._displayRpm or 0
        local rpmSmoothAlpha = 0.15
        self._displayRpm = self._displayRpm + ((self._currentRpm or 0) - self._displayRpm) * rpmSmoothAlpha
        local rpm = math.floor(self._displayRpm + 0.5)

        local fuelLiters, fuelCap = MrChowHelicopter1._getFuelLiters(self)
        local fuelPercent = (fuelCap > 0) and (fuelLiters / fuelCap) * 100 or 0
        local battPercent = (self.spec_electric and self.spec_electric.batteryLevel or 1) * 100

        
        local px, py, pz
        local root = self.components and self.components[1] and self.components[1].node or nil
        if root ~= nil then
            local hx, hy, hz = getWorldTranslation(root)
            local fXh, fYh, fZh = localDirectionToWorld(root, 0, 0, 1)  
            local rXh, rYh, rZh = localDirectionToWorld(root, 1, 0, 0)  
            local uXh, uYh, uZh = localDirectionToWorld(root, 0, 1, 0)  

            local dist = self.hudHeliDist  or 1800
            local offR = self.hudHeliRight or 0.0
            local offU = self.hudHeliUp    or 0.0

            local wx = hx + fXh * dist + rXh * offR + uXh * offU
            local wy = hy + fYh * dist + rYh * offR + uYh * offU
            local wz = hz + fZh * dist + rZh * offR + uZh * offU

            px, py, pz = project(wx, wy, wz)
        end

        if px and pz > 0 then
            local ndc = self.hudInteriorNdcOffset or { x = 0, y = 0 }
            px = px + (ndc.x or 0)
            py = py + (ndc.y or 0)

            
            local function isOn(v)
                if type(v)=="boolean" then return v end
                if type(v)=="string" then v=v:lower(); return (v=="on" or v=="true" or v=="yes") end
                return false
            end
            local slantEnabled = isOn(self.hudRollSlantMode)
            local rollRad = slantEnabled and (-math.rad(roll or 0)) * (self.hudRollSlantScale or 1.0) or 0
            local ct, st = math.cos(rollRad), math.sin(rollRad)

            
            local baseYShift   = self.hudInteriorBaseYOffset or -0.004
            local lineSpace    = self.hudInteriorLineSpacing or 0.008

            
            local spreadEnabled = isOn(self.hudRollSpreadMode)
            local absRollDeg    = math.abs(roll or 0)
            local maxDeg        = math.max(1, self.hudRollSpreadMaxDeg or 60)
            local t             = spreadEnabled and math.min(1, absRollDeg / maxDeg) or 0
            t = t * t * (3 - 2*t)  
            local lineSpaceDyn  = lineSpace * (1 + (self.hudRollSpreadGain or 0.25) * t)

            
            local function pos(dx, dy)
                
                return px + dx*ct - dy*st,  py + dx*st + dy*ct
            end
            
            local function flatPos(dx, dy)
                return px + dx, py + dy
            end
            
            local function linePos(n)
                local dy = baseYShift - n * lineSpaceDyn
                return pos(0, dy)
            end

            setTextAlignment(RenderText.ALIGN_RIGHT)
            if taxiActive then setTextColor(table.unpack(amber)) else setTextColor(table.unpack(green)) end

            
            do
                local headingScale = 1.0 * hudScale * scaleMult
                local function norm360(h) h = h % 360 if h < 0 then h = h + 360 end return h end
                local heading = norm360(math.deg(math.atan2(fX, fZ)) + 180)
                local function cardinal8(h)
                    local dirs = {"N","NE","E","SE","S","SW","W","NW"}
                    local idx = math.floor((h + 22.5) / 45) % 8 + 1
                    return dirs[idx]
                end
                local hdgCard = cardinal8(heading)

                setTextAlignment(RenderText.ALIGN_CENTER)
                setTextColor(table.unpack(green))

                local baseN   = -1.2
                local baseDy  = baseYShift - baseN * lineSpaceDyn
                local headingCX, headingBaseY = flatPos(-0.024, baseDy)

                local stepDeg   = 10
                local halfSpan  = 30
                local unitWidth = 0.0074

                
                do
                    local lx, ly = flatPos(-0.024, baseDy + 0.010)
                    renderText(lx, ly, 0.0085 * headingScale * 1.35, string.format("%03d° %s", math.floor(heading + 0.5), hdgCard))
                    lx, ly = flatPos(-0.024, baseDy + 0.001)
                    renderText(lx, ly, 0.0150 * headingScale, "|")
                end

                
                for d = -halfSpan, halfSpan, stepDeg do
                    local dx = (-0.024) + (d / stepDeg) * unitWidth
                    local lx1, ly1 = flatPos(dx, baseDy + 0.003)
                    renderText(lx1, ly1, 0.0075 * headingScale, "|")
                    local lx2, ly2 = flatPos(dx, baseDy - 0.002)
                    renderText(lx2, ly2, 0.0082 * headingScale * 1.35, string.format("%03d", math.floor(norm360(heading + d) + 0.5)))
                end

                
                do
                    local lx, ly = flatPos(-0.024, baseDy + 0.007)
                    renderText(lx, ly, 0.0090 * headingScale, "▼")
                end

                
                setTextAlignment(RenderText.ALIGN_RIGHT)
                if taxiActive then setTextColor(table.unpack(amber)) else setTextColor(table.unpack(green)) end
            end

            
            do
                local x0,y0 = linePos(0.0); renderText(x0, y0, scale, string.format("ALT %s  %.0f m||", altLabel, altDisplay))
                local x1,y1 = linePos(1.0); renderText(x1, y1, scale, string.format("FSPD  %.0f m/s||", forwardSpeed))
                local x2,y2 = linePos(2.0); renderText(x2, y2, scale, string.format("VSPD  %.0f m/s||", verticalSpeed))
                local x3,y3 = linePos(3.0); renderText(x3, y3, scale, string.format("RPM          %d||", rpm))
                local x4,y4 = linePos(4.0); renderText(x4, y4, scale, string.format("PITCH    %.0f°||", pitch))
                local x5,y5 = linePos(5.0); renderText(x5, y5, scale, string.format("ROLL       %.0f°||", roll))
                local x6,y6 = linePos(6.0); renderText(x6, y6, scale, string.format("FUEL   %.0f L / %.0f %%||", fuelLiters, fuelPercent))
                local x7,y7 = linePos(7.0); renderText(x7, y7, scale, string.format("BATT     %.0f%%||", battPercent))
                local x8,y8 = linePos(8.0); renderText(x8, y8, scale, "[=======O=======]")
            end

            
            do
                local baseN = 2.0
                local dy    = baseYShift - baseN * lineSpaceDyn
                local lx, ly = pos(-0.045, dy)
                if verticalSpeed > 0.5 then
                    setTextColor(table.unpack(red)); setTextAlignment(RenderText.ALIGN_LEFT); renderText(lx, ly, scale, "↑")
                elseif verticalSpeed < -0.5 then
                    setTextColor(table.unpack(red)); setTextAlignment(RenderText.ALIGN_LEFT); renderText(lx, ly, scale, "↓")
                else
                    setTextColor(0.8, 0.8, 0.8, fadeProgress); setTextAlignment(RenderText.ALIGN_LEFT); renderText(lx, ly + 0.004, scale, "—")
                end
                setTextColor(table.unpack(green)); setTextAlignment(RenderText.ALIGN_RIGHT)
            end

            
            do
                local baseN = 9.3
                local dy    = baseYShift - baseN * lineSpaceDyn
                local leftShift, ahExtraL, rightNudge = 0.0020, 0.002, -0.015  

                if self._autoHoverEnabled then
                    local flashAlpha = 0.5 + 0.5 * math.sin(getTimeSec() * 8.0)
                    setTextColor(1.0, 0.0, 0.0, flashAlpha)
                    local lx, ly = pos((-0.024 - ahExtraL - leftShift + rightNudge + 0.004), dy + 0.020)
                    drawThickText(lx, ly, 0.010 * hudScale * scaleMult, "AH")
                    setTextColor(table.unpack(green))
                end
				
				
                if self.heli_extraRotorEnabled then
                    local lvl    = math.max(1, math.floor(self.heli_extraRotorLevel or 1))
                    local maxLvl = self.heli_extraRotorMaxLevel or lvl
                    if lvl > maxLvl then lvl = maxLvl end

                    local lxB, lyB = pos((-0.024 - ahExtraL - leftShift + rightNudge + 0.004), dy + 0.010)
                    renderText(lxB, lyB, scale, string.format("BOOST x%02d", lvl))
                end

                if fuelPercent < 25 then
                    setTextColor(table.unpack(red))
                    local lx, ly = pos((-leftShift + rightNudge + 0.009), dy - 0.006)
                    renderText(lx, ly, scale, "FUEL LOW")
                elseif fuelPercent < 45 then
                    setTextColor(table.unpack(amber))
                    local lx, ly = pos((-leftShift + rightNudge + 0.009), dy - 0.006)
                    renderText(lx, ly, scale, "FUEL WARN")
                end

                if (altAGL_now or 99) < 5 and (vY or 0) < -5 then
                    setTextColor(table.unpack(red))
                    local lx, ly = pos((-leftShift + rightNudge + 0.009), dy - 0.016)
                    renderText(lx, ly, scale, "ALT WARNING")
                end

                if taxiActive then
                    setTextColor(1.0, 1.0, 0.3, taxiBlinkAlpha)
                    local lx, ly = pos((-0.0015 - leftShift + rightNudge), dy + 0.002)
                    renderText(lx, ly, scale, "TAXI")
                    setTextColor(table.unpack(green))
                end
            end
        end
    else
        
        setTextAlignment(RenderText.ALIGN_LEFT)
        setTextColor(table.unpack(green))
        local baseX, baseY = 0.18, 0.2
        if taxiActive then
            setTextAlignment(RenderText.ALIGN_LEFT)
            setTextColor(1.0, 1.0, 0.3, taxiBlinkAlpha)
            renderText(baseX, baseY - 0.060, baseScale, "TAXI")
            setTextColor(table.unpack(green))
        end

        renderText(baseX, baseY,         baseScale, string.format("ALT %s  %.0f m", altLabel, altDisplay))
        renderText(baseX, baseY - 0.015, baseScale, string.format("FSPD  %.0f m/s", math.sqrt((vx or 0)^2 + (vz or 0)^2)))
        renderText(baseX, baseY - 0.030, baseScale, string.format("VSPD  %.0f m/s", verticalSpeed))

        if self._autoHoverEnabled then
            local flashAlpha = 0.5 + 0.5 * math.sin(getTimeSec() * 8.0)
            setTextColor(1.0, 0.0, 0.0, flashAlpha)
            drawThickText(baseX, baseY - 0.060, 0.012, "AUTO HOVER")
            setTextColor(table.unpack(green))
        end

        
        if self.heli_extraRotorEnabled then
            local boostLevel = math.max(1, math.floor(self.heli_extraRotorLevel or 1))
            local maxLvl     = self.heli_extraRotorMaxLevel or boostLevel
            if boostLevel > maxLvl then boostLevel = maxLvl end
            renderText(baseX, baseY - 0.075, baseScale, string.format("BOOST x%02d", boostLevel))
        end
    end
end