Загрузка данных
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