function sign(x)
    return x > 0 and 1 or x < 0 and -1 or 0
end

function adjustCamSlot(mouseDelta, fastModifier, preciseModifier)
    local step = fastModifier and mouseDelta * 3 or preciseModifier and sign(mouseDelta) or mouseDelta
    addOneOffSound(0, 0, 0, SOUND_SCROLL)
    camSlot = math.max(1, camSlot + step)
end

function drawText(text, x, y, size, color, isHovered)
    local style = mad.font_style.FONT_SUBTITLES
    local width = size * 0.58
    local height = size * 1.02
    local align = mad.font_align.LEFT
    local wrap = MENU_WIDTH
    local proportional = true
    local justify = false  

    local textColor = isHovered and CURSOR_HOVER_COLOR or color
    mad.draw_text(text, x, y, style, width, height, align, wrap, proportional, justify, 
                  textColor.r, textColor.g, textColor.b, textColor.a, 
                  1, 0, 0, 0, 0, 255, 0, 0, 0, 0)
end

function isPointInArea(pointX, pointY, rectX, rectY, rectWidth, rectHeight)
    return pointX >= rectX and pointX <= rectX + rectWidth and
           pointY >= rectY and pointY <= rectY + rectHeight
end

function displayMenu(menu)
	if not isMenuActive then return end

    local cursorX, cursorY		= getCursorPos()		
    local isLMBDown				= isKeyDown(1)  		
    local isRMBDown				= isKeyDown(2)  		
    local currentDescription	= ""  					
    local isArrowKeyDown		= false  				
	local isAnyItemHovered		= false
    local mouseWheelDelta		= getMousewheelDelta()  
    local isUArrowDown			= isKeyDown(38)  		
    local isDArrowDown			= isKeyDown(40)  		
    local isLArrowDown			= isKeyDown(37)  		
    local isRArrowDown			= isKeyDown(39)  		
    local isEnterDown			= isKeyDown(13)  		
	local isShiftDown			= isKeyDown(16)
	local isCtrlDown			= isKeyDown(18)			
    local isBackspaceDown		= isKeyDown(8)			

    local isNotInRestrictedMode = not isSampfuncsConsoleActive() and not sampIsChatInputActive() and not sampIsDialogActive() and not isPauseMenuActive()

    drawText(menu.title, MENU_X, MENU_Y, MENU_TITLE_SIZE, MENU_COLOR, false)

    for i, item in ipairs(menu.items) do
        local itemY = MENU_Y + 10 + (MENU_ITEM_HEIGHT + MENU_ITEM_SPACING) * i

        if isNotInRestrictedMode then
            local isHovered = isPointInArea(cursorX, cursorY, MENU_X, itemY, MENU_ITEM_WIDTH, MENU_ITEM_HEIGHT)
            if isHovered then isAnyItemHovered = true end
			
            if isHovered and not isArrowKeyDown then
                focusIndex = i
            end
        end

        local isFocused = (i == focusIndex)
        drawText(item.label, MENU_X, itemY, 1, TEXT_COLOR, isFocused or (isNotInRestrictedMode and isPointInArea(cursorX, cursorY, MENU_X, itemY, MENU_ITEM_WIDTH, MENU_ITEM_HEIGHT)))

        if isFocused then
            currentDescription = item.description

            local currentTime = socket.gettime()

			if mouseWheelDelta ~= 0 and item.scrollAction then
				item.scrollAction(mouseWheelDelta, isShiftDown, isCtrlDown)
			end

            if isLArrowDown then
                if not wasLArrowDown then
                    holdStartTimeL = currentTime
                    if item.scrollAction then item.scrollAction(-1, isShiftDown, isCtrlDown) end
                elseif currentTime - holdStartTimeL >= holdDelay then
                    if item.scrollAction then item.scrollAction(-1, isShiftDown, isCtrlDown) end
                    holdStartTimeL = currentTime - (holdDelay - holdInterval)
                end
            else
                holdStartTimeL = nil
            end

            if isRArrowDown then
                if not wasRArrowDown then
                    holdStartTimeR = currentTime
					print("f", holdStartTimeR)
                    if item.scrollAction then item.scrollAction(1, isShiftDown, isCtrlDown) end
                elseif currentTime - holdStartTimeR >= holdDelay then
                    if item.scrollAction then item.scrollAction(1, isShiftDown, isCtrlDown) end
                    holdStartTimeR = currentTime - (holdDelay - holdInterval)
                end
				print(currentTime - holdStartTimeR)
            else
                holdStartTimeR = nil
            end

            --- Obrabotka nazatia ?LKM? ili Enter
			if isNotInRestrictedMode and (
				(isLMBDown and not wasLMBDown and isAnyItemHovered) or
				(isEnterDown and not wasEnterDown)
			) then
				item.action()
			end
        end
    end
	
	local currentTime = socket.gettime()
	if isUArrowDown and not isAnyItemHovered then
		if not wasUArrowDown then
			holdStartTimeU = currentTime
			repeat
				if focusIndex == 1 then
					focusIndex = #menu.items  --- Go to the last element
				else
					focusIndex = focusIndex - 1
				end
			until menu.items[focusIndex].label ~= ""
		elseif currentTime - holdStartTimeU >= holdDelay then
			repeat
				if focusIndex == 1 then
					focusIndex = #menu.items  --- Go to the last element
				else
					focusIndex = focusIndex - 1
				end
			until menu.items[focusIndex].label ~= ""
			holdStartTimeU = currentTime - (holdDelay - holdInterval)
		end
	else
		holdStartTimeU = nil
	end

	if isDArrowDown and not isAnyItemHovered then
		if not wasDArrowDown then
			holdStartTimeD = currentTime
			repeat
				if focusIndex == #menu.items then
					focusIndex = 1  --- Go to the first element
				else
					focusIndex = focusIndex + 1
				end
			until menu.items[focusIndex].label ~= ""
		elseif currentTime - holdStartTimeD >= holdDelay then
			repeat
				if focusIndex == #menu.items then
					focusIndex = 1  --- Go to the first element
				else
					focusIndex = focusIndex + 1
				end
			until menu.items[focusIndex].label ~= ""
			holdStartTimeD = currentTime - (holdDelay - holdInterval)
		end
	else
		holdStartTimeD = nil
	end
	
    local descriptionY = MENU_Y + 15 + (MENU_ITEM_HEIGHT + MENU_ITEM_SPACING) * (#menu.items + 1)
    drawText(currentDescription, MENU_X, descriptionY, DESCRIPTION_FONT_SIZE, DESCRIPTION_COLOR, false)

    if isNotInRestrictedMode and ((isRMBDown and not wasRMBDown and isAnyItemHovered) or (isBackspaceDown and not wasBackspaceDown)) then
		if menu.parent then
			currentMenu = menu.parent
		else
			isMenuActive = false
			showCursor(false, false)
		end
		addOneOffSound(0, 0, 0, SOUND_BACK)
		if not isAnyItemHovered then	--- obnulenie fokusa na pervyj ?item
			focusIndex = 1				--- esli ni odin iz ?ajtemov? ne ?hovered?
		end
    end

    --- Obnovlenie peremennyh sostoania klavis
    wasUArrowDown 		= isUArrowDown
    wasDArrowDown 		= isDArrowDown
    wasLArrowDown	 	= isLArrowDown
    wasRArrowDown 		= isRArrowDown
    wasLMBDown			= isLMBDown
    wasRMBDown			= isRMBDown
    wasEnterDown		= isEnterDown
    wasBackspaceDown	= isBackspaceDown
end

function checkKeyState()
    if isKeyDown(0x12) and camHack.isFlyModeActive then  --- VK_MENU 0x12 ALT key
        --- Check Q key
        if isKeyDown(0x51) then  --- 0x51 Q key
            if not qWasDown then
                printStringNow("~r~q ~w~action", 900)
				
				currentCamPositions.cameraX = camHack.cameraX
				currentCamPositions.cameraY = camHack.cameraY
				currentCamPositions.cameraZ = camHack.cameraZ
				
				currentCamPositions.pointX	= camHack.pointX				
				currentCamPositions.pointY	= camHack.pointY				
				currentCamPositions.pointZ	= camHack.pointZ
				
                qWasDown = true
            end
        else
            qWasDown = false
        end
        
        --- Check E key
        if isKeyDown(0x45) then  --- 0x45 E key
            if not eWasDown then
                printStringNow("~g~e ~w~action", 900)
				
				currentCamPositions.cameraX = camHack.cameraX
				currentCamPositions.cameraY = camHack.cameraY
				currentCamPositions.cameraZ = camHack.cameraZ
				
                eWasDown = true
            end
        else
            eWasDown = false
        end

        --- Check R key
        if isKeyDown(0x52) then  --- 0x52 R key
            if not rWasDown then
                printStringNow("~b~r ~w~action", 900)
				
				currentCamPositions.pointX = camHack.cameraX
				currentCamPositions.pointY = camHack.cameraY
				currentCamPositions.pointZ = camHack.cameraZ
				
                rWasDown = true
            end
        else
            rWasDown = false
        end
    end
end

function writeToFile(filePath, content)
    local file = io.open(filePath, "w")
    if file then
        file:write(content)
        file:close()
    else
        print("Невозможно открыть файл для записи: " .. filePath)
    end
end

function readFromFile(filePath)
    local file = io.open(filePath, "r")
    if file then
        local content = file:read("*a")
        file:close()
        return content
    else
        print("Невозможно открыть файл для чтения: " .. filePath)
    end
end

function saveCamToSlot(slot)
    --- Проверка наличия всех необходимых координат
    if currentCamPositions.cameraX and currentCamPositions.cameraY and currentCamPositions.cameraZ and 
       currentCamPositions.pointX and currentCamPositions.pointY and currentCamPositions.pointZ then
       
        local dir = getWorkingDirectory() .. "/prometheus/profiles/" .. profile .. "/cam" .. slot .. ".json"
        local json = encodeJson(currentCamPositions)
        writeToFile(dir, json)
        printStringNow("Camera position saved to slot " .. slot, 900)
    else
        printStringNow("Camera position data is incomplete, cannot save.", 900)
    end
end

function loadCamFromSlot(slot)
    local dir = getWorkingDirectory() .. "/prometheus/profiles/" .. profile .. "/cam" .. slot .. ".json"
    local json = readFromFile(dir)
    if json then
        local data = decodeJson(json)
        if data then
            currentCamPositions.cameraX	= data.cameraX
            currentCamPositions.cameraY	= data.cameraY
            currentCamPositions.cameraZ	= data.cameraZ
            currentCamPositions.pointX	= data.pointX
            currentCamPositions.pointY	= data.pointY
            currentCamPositions.pointZ	= data.pointZ


            printStringNow("Camera position loaded from slot " .. slot, 900)
        else
            printStringNow("Failed to decode slot data", 900)
        end
    else
        printStringNow("No saved camera positions in slot " .. slot, 900)
    end
end

function toggleCurrentPreview()
    if not previewMode then      
        local cam = currentCamPositions
        if cam.cameraX and cam.cameraY and cam.cameraZ and cam.pointX and cam.pointY and cam.pointZ then
		
			initialFlyModeActive = camHack.isFlyModeActive 
			
			camHack.isFlyModeActive = false
			camHack.enableWithoutReset = true
            camHack.isFlyModeToggleRequested = false

            setFixedCameraPosition(cam.cameraX, cam.cameraY, cam.cameraZ, 0.0, 0.0, 0.0)
            pointCameraAtPoint(cam.pointX, cam.pointY, cam.pointZ, 2)
			displayRadar(false)
			displayHud(false)
			lockPlayerControl(true)
            previewMode = true
            printStringNow("Preview mode activated.", 900)
        else
            printStringNow("Current camera data incomplete.", 900)
        end
    else
        if initialFlyModeActive then
            camHack.isFlyModeToggleRequested = true
		else
			camHack.enableWithoutReset = false
			
			lockPlayerControl(false)
			restoreCameraJumpcut()
			setCameraBehindPlayer()
			displayRadar(true)
			displayHud(true)
        end
		
        previewMode = false
        printStringNow("Exited preview mode.", 900)
    end
end

function previewCamFromSlot(slot)
    local dir = getWorkingDirectory() .. "/prometheus/profiles/" .. profile .. "/cam" .. slot .. ".json"
    local json = readFromFile(dir)
    if json then
        local data = decodeJson(json)
        if data then
            setFixedCameraPosition(data.cameraX, data.cameraY, data.cameraZ, 0.0, 0.0, 0.0)
            pointCameraAtPoint(data.pointX, data.pointY, data.pointZ, 2)
            printStringNow("Preview of slot " .. slot, 900)
        else
            printStringNow("Failed to decode slot data", 900)
        end
    else
        printStringNow("No data to preview in slot " .. slot, 900)
    end
end

 
 function sampev.onPlayerChatBubble(id, col, dist, dur, msg)
     if camHack.isFlyModeActive == 1 then return {id, col, 1488, dur, msg} end
 end






как это отрефакторить