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


script_name('HUD Bars')
script_author('Claude')
script_version('2.0')

require 'lib.moonloader'
local inicfg   = require 'inicfg'
local encoding = require 'encoding'
local imgui    = require 'mimgui'
local sampev   = require 'samp.events'

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

local CFG_NAME = 'hudbars.ini'
local cfg = inicfg.load({
    main = {
        enabled = true,
        interval = 45,   -- как часто спрашивать голод (сек)
        low = 20,        -- порог предупреждения о голоде (%)
        offx = 30,       -- отступ от правого края экрана
        offy = 140,      -- отступ от верха экрана
        w = 210,         -- ширина полоски
        h = 24,          -- высота полоски
        scale = 1.2,     -- размер текста
    }
}, CFG_NAME)

local hunger = nil      -- 0..100, пока нет данных: nil
local waitUntil = 0     -- до какого момента ждём ответ на /satiety
local nextPoll = 0
local debugMode = false
local lowWarned = false

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

local function msg(text)
    sampAddChatMessage(t('{00BFFF}[HUD]{FFFFFF} ' .. text), -1)
end

-- ===== Разбор ответа на /satiety =====
-- Ответ вида: "Ваша сытость: 39.5/100.0."
local function parseHunger(raw)
    local clean = raw:gsub('{%x%x%x%x%x%x}', '')
    local text = u8:encode(clean)   -- cp1251 -> utf8 для поиска слов

    local hasWord = text:find('сыт', 1, true) or text:find('Сыт', 1, true)
        or text:find('голод', 1, true) or text:find('Голод', 1, true)
    if not hasWord then return nil end

    local a, b = clean:match('(%d+[%.,]?%d*)%s*/%s*(%d+[%.,]?%d*)')
    local value
    if a and b then
        a = tonumber((a:gsub(',', '.')))
        b = tonumber((b:gsub(',', '.')))
        if a and b and b > 0 then value = a / b * 100 end
    else
        local n = clean:match('(%d+[%.,]?%d*)')
        value = n and tonumber((n:gsub(',', '.')))
    end
    if not value then return nil end
    return math.max(0, math.min(100, value))
end

local function short(s)
    return (s:gsub('{%x%x%x%x%x%x}', ''):gsub('[\r\n]+', ' '):sub(1, 70))
end

-- Ответ приходит окном «Информация»: ловим его, читаем число и прячем окно
function sampev.onShowDialog(id, style, title, button1, button2, text)
    if os.clock() > waitUntil then return end

    local value = parseHunger(text)
    if value then
        hunger = value
        waitUntil = 0
        if debugMode then
            msg(('Поймал диалог id=%d, голод: {FFD700}%.1f%%'):format(id, value))
        end
        sampSendDialogResponse(id, 1, -1, '')
        return false
    elseif debugMode then
        sampAddChatMessage('[HUD] Диалог не подошёл: id=' .. id .. ' | ' .. short(title) ..
            ' | ' .. short(text), -1)
    end
end

-- Запасной вариант: если ответ придёт в чат
function sampev.onServerMessage(color, text)
    if os.clock() > waitUntil then return end

    local value = parseHunger(text)
    if value then
        hunger = value
        waitUntil = 0
        if debugMode then
            msg(('Поймал сообщение, голод: {FFD700}%.1f%%'):format(value))
        end
        return false
    elseif debugMode then
        sampAddChatMessage('[HUD] Сообщение не подошло: ' .. short(text), -1)
    end
end

-- ===== Отрисовка в стиле GTA =====
local COL_HP     = { 0.90, 0.12, 0.12 }
local COL_HUNGER = { 1.00, 0.82, 0.00 }
local COL_ARMOR  = { 0.55, 0.55, 0.55 }
local COL_LOW    = { 0.90, 0.12, 0.12 }

local function col(r, g, b, a)
    return imgui.ColorConvertFloat4ToU32(imgui.ImVec4(r, g, b, a or 1))
end

local function drawText(dl, x, y, label)
    local black = col(0, 0, 0, 1)
    for _, o in ipairs({ { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } }) do
        dl:AddText(imgui.ImVec2(x + o[1], y + o[2]), black, label)
    end
    dl:AddText(imgui.ImVec2(x, y), col(1, 1, 1, 1), label)
end

local function drawBar(dl, x, y, w, h, frac, color, label)
    local f = 3  -- толщина рамки
    -- тёмная рамка
    dl:AddRectFilled(imgui.ImVec2(x, y), imgui.ImVec2(x + w, y + h), col(0.05, 0.05, 0.05, 0.95))

    local ix1, iy1, ix2, iy2 = x + f, y + f, x + w - f, y + h - f
    -- пустая часть: тёмный оттенок цвета
    dl:AddRectFilled(imgui.ImVec2(ix1, iy1), imgui.ImVec2(ix2, iy2),
        col(color[1] * 0.35, color[2] * 0.35, color[3] * 0.35, 1))

    -- заполненная часть
    frac = math.max(0, math.min(1, frac or 0))
    if frac > 0 then
        dl:AddRectFilled(imgui.ImVec2(ix1, iy1),
            imgui.ImVec2(ix1 + (ix2 - ix1) * frac, iy2),
            col(color[1], color[2], color[3], 1))
    end

    -- процент по центру
    local size = imgui.CalcTextSize(label)
    drawText(dl, x + (w - size.x) / 2, y + (h - size.y) / 2, label)
end

local function getVitals()
    local hp, armor
    local ok, id = sampGetPlayerIdByCharHandle(PLAYER_PED)
    if ok then
        hp = sampGetPlayerHealth(id)
        armor = sampGetPlayerArmor(id)
    end
    return hp or getCharHealth(PLAYER_PED), armor or getCharArmour(PLAYER_PED)
end

local winFlags = imgui.WindowFlags.NoTitleBar + imgui.WindowFlags.NoResize
    + imgui.WindowFlags.NoMove + imgui.WindowFlags.NoScrollbar
    + imgui.WindowFlags.NoSavedSettings + imgui.WindowFlags.NoInputs
    + imgui.WindowFlags.NoBackground

imgui.OnFrame(
    function() return cfg.main.enabled and isSampAvailable() end,
    function(self)
        self.HideCursor = true
        local sw, sh = getScreenResolution()

        imgui.SetNextWindowPos(imgui.ImVec2(0, 0))
        imgui.SetNextWindowSize(imgui.ImVec2(sw, sh))
        imgui.Begin('##hudbars', nil, winFlags)
        imgui.SetWindowFontScale(cfg.main.scale)
        local dl = imgui.GetWindowDrawList()

        local w, h = cfg.main.w, cfg.main.h
        local x = sw - w - cfg.main.offx
        local y = cfg.main.offy
        local step = h + 8

        local hp, armor = getVitals()

        -- красная: здоровье
        drawBar(dl, x, y, w, h, hp / 100, COL_HP, ('%.1f%%'):format(hp))

        -- жёлтая: голод (мигает красным при низком уровне)
        local hungerColor = COL_HUNGER
        local hungerLabel = '--'
        if hunger then
            hungerLabel = ('%.1f%%'):format(hunger)
            if hunger <= cfg.main.low and math.floor(os.clock() * 2) % 2 == 0 then
                hungerColor = COL_LOW
            end
        end
        drawBar(dl, x, y + step, w, h, hunger and hunger / 100 or 0, hungerColor, hungerLabel)

        -- серая: броня
        drawBar(dl, x, y + step * 2, w, h, armor / 100, COL_ARMOR, ('%.1f%%'):format(armor))

        imgui.End()
    end
)

-- ===== Основной цикл =====
local function setNumber(key, arg, pattern, min, max, text)
    local v = tonumber(arg:match(pattern))
    if not v or v < min or v > max then
        msg(('Значение должно быть от %s до %s'):format(min, max))
        return
    end
    cfg.main[key] = v
    save()
    msg(text .. ': {FFD700}' .. v)
end

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

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

        if arg == 'on' then
            cfg.main.enabled = true
            save()
            msg('Полоски включены.')
        elseif arg == 'off' then
            cfg.main.enabled = false
            save()
            msg('Полоски выключены.')
        elseif arg == 'now' then
            nextPoll = 0
            msg('Обновляю голод...')
        elseif arg == 'debug' then
            debugMode = not debugMode
            msg(debugMode and 'Отладка включена.' or 'Отладка выключена.')
        elseif arg:match('^pos%s+%-?%d+%s+%-?%d+$') then
            local ox, oy = arg:match('^pos%s+(%-?%d+)%s+(%-?%d+)$')
            cfg.main.offx, cfg.main.offy = tonumber(ox), tonumber(oy)
            save()
            msg(('Позиция: отступ справа {FFD700}%s{FFFFFF}, сверху {FFD700}%s'):format(ox, oy))
        elseif arg:match('^w%s+%d+$') then
            setNumber('w', arg, '(%d+)', 80, 600, 'Ширина полосок')
        elseif arg:match('^scale%s+[%d%.]+$') then
            setNumber('scale', arg, '([%d%.]+)', 0.6, 3, 'Размер текста')
        elseif arg:match('^low%s+%d+$') then
            setNumber('low', arg, '(%d+)', 1, 99, 'Порог предупреждения о голоде')
        elseif arg:match('^time%s+%d+$') then
            setNumber('interval', arg, '(%d+)', 15, 3600, 'Интервал проверки голода (сек)')
        else
            msg('Команды: /hudbars on | off | now | debug | pos [справа] [сверху] | w [ширина] | scale [размер] | low [%] | time [сек]')
        end
    end)

    msg('Загружен. Настройки: {FFD700}/hudbars')

    while true do
        wait(0)

        if cfg.main.enabled and sampIsLocalPlayerSpawned() and os.clock() >= nextPoll then
            nextPoll = os.clock() + cfg.main.interval
            waitUntil = os.clock() + 4
            sampSendChat('/satiety')
        end

        -- предупреждение в чат, когда голод упал ниже порога
        if hunger then
            if hunger <= cfg.main.low and not lowWarned then
                lowWarned = true
                msg('{FF4444}Голод ниже ' .. cfg.main.low .. '%! Пора покормить персонажа.')
            elseif hunger > cfg.main.low + 5 then
                lowWarned = false
            end
        end
    end
end