Загрузка данных


script_name('FOV First Person')
script_author('Claude')
script_version('1.0')

require 'lib.moonloader'
local inicfg   = require 'inicfg'
local encoding = require 'encoding'

-- Файл должен быть сохранён в UTF-8
encoding.default = 'CP1251'
local u8 = encoding.UTF8
local function t(s) return u8:decode(s) end

local MIN_FOV, MAX_FOV, DEFAULT_FOV = 30, 120, 70
local GAME_FOV = 70.0
local CFG_NAME = 'fov_firstperson.ini'

local cfg = inicfg.load({
    main = { fov = 90, enabled = true, dist = 1.3 }
}, CFG_NAME)

local DLG_MENU, DLG_FOV, DLG_DIST, DLG_PRESET = 7200, 7201, 7202, 7203
local presets = { 70, 80, 90, 100, 110, 120 }

local inFirstPerson = false

local function save() inicfg.save(cfg, CFG_NAME) end

local function msg(text)
    sampAddChatMessage(t('{00BFFF}[FOV 1-е лицо]{FFFFFF} ' .. text), -1)
end

-- Первое лицо определяем по расстоянию от камеры до игрока:
-- в третьем лице камера всегда заметно дальше, чем в первом.
local function cameraDistance()
    local cx, cy, cz = getActiveCameraCoordinates()
    local px, py, pz = getCharCoordinates(PLAYER_PED)
    local dx, dy, dz = cx - px, cy - py, cz - pz
    return math.sqrt(dx * dx + dy * dy + dz * dz)
end

local debugMode = false
local lastDebug = 0

local function isFirstPerson()
    return cameraDistance() < cfg.main.dist
end

local function setFov(value)
    value = tonumber(value)
    if not value then
        msg('Введите число, например: /fpfov 90')
        return
    end
    if value < MIN_FOV or value > MAX_FOV then
        msg(('Допустимый диапазон: %d-%d'):format(MIN_FOV, MAX_FOV))
        return
    end
    cfg.main.fov = value
    cfg.main.enabled = true
    save()
    msg('FOV для первого лица: {FFD700}' .. value)
end

local function setDist(value)
    value = tonumber(value)
    if not value or value < 0.5 or value > 3.0 then
        msg('Порог должен быть числом от 0.5 до 3.0')
        return
    end
    cfg.main.dist = value
    save()
    msg('Порог определения первого лица: {FFD700}' .. value)
end

local function openMenu()
    local status = cfg.main.enabled and '{00FF00}включён' or '{FF4444}выключен'
    local text = table.concat({
        'Скрипт: ' .. status,
        'Задать FOV (сейчас: {FFD700}' .. cfg.main.fov .. '{FFFFFF})',
        'Выбрать готовый пресет',
        'Порог определения 1-го лица (сейчас: {FFD700}' .. cfg.main.dist .. '{FFFFFF})',
        'Сбросить настройки',
    }, '\n')
    sampShowDialog(DLG_MENU, t('{00BFFF}FOV от первого лица'), t(text),
        t('Выбрать'), t('Закрыть'), 2)
end

local function openFovInput()
    sampShowDialog(DLG_FOV, t('{00BFFF}Значение FOV'),
        t(('Введите значение от %d до %d:'):format(MIN_FOV, MAX_FOV)),
        t('Применить'), t('Назад'), 1)
end

local function openDistInput()
    sampShowDialog(DLG_DIST, t('{00BFFF}Порог определения'),
        t('Если скрипт путает виды, подберите значение от 0.5 до 3.0.\n' ..
          'Больше значение, тем шире зона, где считается «первое лицо».'),
        t('Применить'), t('Назад'), 1)
end

local function openPresets()
    local lines = {}
    for _, p in ipairs(presets) do
        local mark = (p == cfg.main.fov) and ' {00FF00}(текущий)' or ''
        lines[#lines + 1] = 'FOV ' .. p .. mark
    end
    sampShowDialog(DLG_PRESET, t('{00BFFF}Пресеты FOV'), t(table.concat(lines, '\n')),
        t('Применить'), t('Назад'), 2)
end

local function handleDialogs()
    local ok, button, list = sampHasDialogRespond(DLG_MENU)
    if ok then
        if button == 1 then
            if list == 0 then
                cfg.main.enabled = not cfg.main.enabled
                save()
                msg(cfg.main.enabled and 'Скрипт включён.' or 'Скрипт выключен.')
                openMenu()
            elseif list == 1 then
                openFovInput()
            elseif list == 2 then
                openPresets()
            elseif list == 3 then
                openDistInput()
            elseif list == 4 then
                cfg.main.fov, cfg.main.enabled, cfg.main.dist = 90, true, 1.3
                save()
                msg('Настройки сброшены.')
                openMenu()
            end
        end
        return
    end

    local ok2, button2, _, input2 = sampHasDialogRespond(DLG_FOV)
    if ok2 then
        if button2 == 1 then setFov(input2) end
        openMenu()
        return
    end

    local ok3, button3, _, input3 = sampHasDialogRespond(DLG_DIST)
    if ok3 then
        if button3 == 1 then setDist(input3) end
        openMenu()
        return
    end

    local ok4, button4, list4 = sampHasDialogRespond(DLG_PRESET)
    if ok4 then
        if button4 == 1 and presets[list4 + 1] then setFov(presets[list4 + 1]) end
        openMenu()
    end
end

function main()
    while not isSampAvailable() do wait(100) end

    sampRegisterChatCommand('fpfov', function(arg)
        arg = (arg or ''):match('^%s*(.-)%s*$')

        if arg == '' then
            openMenu()
        elseif arg == 'on' then
            cfg.main.enabled = true
            save()
            msg('Скрипт включён.')
        elseif arg == 'off' then
            cfg.main.enabled = false
            save()
            msg('Скрипт выключен.')
        elseif arg == 'debug' then
            debugMode = not debugMode
            msg(debugMode and 'Отладка включена.' or 'Отладка выключена.')
        elseif arg:match('^dist%s+') then
            setDist(arg:match('^dist%s+(.+)$'))
        else
            setFov(arg)
        end
    end)

    msg('Загружен. Меню: {FFD700}/fpfov{FFFFFF}, быстрая смена: {FFD700}/fpfov [число]')

    while true do
        wait(0)

        if debugMode and os.clock() - lastDebug > 1.0 then
            lastDebug = os.clock()
            msg(('Дистанция: {FFD700}%.2f{FFFFFF} | порог: %.2f | 1-е лицо: %s'):format(
                cameraDistance(), cfg.main.dist, tostring(isFirstPerson())))
        end

        local firstPerson = cfg.main.enabled and isFirstPerson()

        if firstPerson then
            cameraSetLerpFov(cfg.main.fov, cfg.main.fov, 1000, true)
            inFirstPerson = true
        elseif inFirstPerson then
            -- вышли из первого лица: возвращаем стандартный FOV один раз
            cameraSetLerpFov(GAME_FOV, GAME_FOV, 1000, true)
            inFirstPerson = false
        end

        handleDialogs()
    end
end