script_name('Weapon Draw Text') script_version('1.0') script_description('Shows "*достает оружие M4*" above a player after a weapon switch') --[[ Weapon Draw Text для SA-MP / MoonLoader (Gambit RP) Когда игрок (или ты сам) переключает оружие колесиком/клавишами, над его головой появляется надпись «*достает оружие M4*» и держится несколько секунд, затем плавно исчезает. Установка: положить файл в GTA San Andreas\moonloader\ Нужны: MoonLoader, SAMPFUNCS, библиотека encoding (идет с MoonLoader). Команды: /wtext - включить / выключить /wtext self - показывать / не показывать над своей головой /wtext time [сек] - сколько секунд держится надпись (1-60) /wtext dist [м] - дальность показа у других игроков (5-100) /wtext help - список команд Надпись рисуется только на твоем экране (это клиентский скрипт). ]] local encoding = require 'encoding' encoding.default = 'CP1251' local u8 = encoding.UTF8 -- Значения по умолчанию. Команды меняют их и сохраняют в config. local settings = { enabled = true, showSelf = true, duration = 6, -- секунд maxDistance = 25 -- метров } local TEXT_FORMAT = '*достает оружие %s*' local TEXT_COLOR_RGB = 0xC2A2DA -- стандартный цвет /me local FADE_MS = 600 -- плавное исчезновение в конце local SWITCH_CONFIRM_MS = 150 -- при быстрой прокрутке показывается только итоговое оружие local GRACE_MS = 1500 -- после стрима, спавна и выхода из машины смена оружия не считается local HEAD_OFFSET_Z = 1.15 -- высота над центром педа (над ником и полоской HP) local SCREEN_OFFSET_Y = -18 local configPath = getWorkingDirectory() .. '\\config\\weapon_draw_text.json' -- Только то, что действительно «достают». Фотоаппарат, парашют, очки, -- детонатор, баллончик и огнетушитель намеренно не показываются. local WEAPON_NAMES = { [1] = 'Brass Knuckles', [2] = 'Golf Club', [3] = 'Nightstick', [4] = 'Knife', [5] = 'Baseball Bat', [6] = 'Shovel', [7] = 'Pool Cue', [8] = 'Katana', [9] = 'Chainsaw', [10] = 'Purple Dildo', [11] = 'Dildo', [12] = 'Vibrator', [13] = 'Silver Vibrator', [14] = 'Flowers', [15] = 'Cane', [16] = 'Grenade', [17] = 'Tear Gas', [18] = 'Molotov', [22] = 'Colt 45', [23] = 'Silenced Pistol', [24] = 'Desert Eagle', [25] = 'Shotgun', [26] = 'Sawed-Off Shotgun', [27] = 'Combat Shotgun', [28] = 'Micro UZI', [29] = 'MP5', [30] = 'AK-47', [31] = 'M4', [32] = 'TEC-9', [33] = 'Country Rifle', [34] = 'Sniper Rifle', [35] = 'RPG', [36] = 'Heat-Seeking RPG', [37] = 'Flamethrower', [38] = 'Minigun', [39] = 'Satchel Charge' } local font = nil local tracked = {} -- playerId или 'self' -> состояние оружия и надписи local frame = 0 local function clamp(value, minimum, maximum) if value < minimum then return minimum end if value > maximum then return maximum end return value end local function chat(text) sampAddChatMessage(u8:decode('[Weapon Text] ' .. text), 0xFFC2A2DA) end local function loadSettings() local file = io.open(configPath, 'r') if not file then return end local contents = file:read('*a') file:close() local ok, data = pcall(decodeJson, contents) if not ok or type(data) ~= 'table' then return end if type(data.enabled) == 'boolean' then settings.enabled = data.enabled end if type(data.showSelf) == 'boolean' then settings.showSelf = data.showSelf end if tonumber(data.duration) then settings.duration = math.floor(clamp(tonumber(data.duration), 1, 60)) end if tonumber(data.maxDistance) then settings.maxDistance = math.floor(clamp(tonumber(data.maxDistance), 5, 100)) end end local function saveSettings() local directory = getWorkingDirectory() .. '\\config' if not doesDirectoryExist(directory) then createDirectory(directory) end local file = io.open(configPath, 'w') if not file then return end file:write(encodeJson(settings)) file:close() end local function observePed(key, ped, now) local weapon = getCurrentCharWeapon(ped) local state = tracked[key] if not state or state.ped ~= ped then state = { ped = ped, weapon = weapon, graceUntil = now + GRACE_MS } tracked[key] = state end state.seenFrame = frame -- Смерть, спавн и транспорт меняют оружие без «доставания». local busy = isCharDead(ped) or isCharInAnyCar(ped) or (key == 'self' and not sampIsLocalPlayerSpawned()) if busy then state.notice = nil state.candidate = nil state.weapon = weapon state.graceUntil = now + GRACE_MS return end if now < state.graceUntil or weapon == state.weapon then state.weapon = weapon state.candidate = nil return end if state.candidate ~= weapon then state.candidate = weapon state.candidateSince = now return end if now - state.candidateSince < SWITCH_CONFIRM_MS then return end state.weapon = weapon state.candidate = nil -- Новое оружие заменяет старую надпись, а кулаки просто убирают ее. local name = WEAPON_NAMES[weapon] if name then state.notice = { text = u8:decode(TEXT_FORMAT:format(name)), expiresAt = now + settings.duration * 1000 } else state.notice = nil end end local function updateTracking(now) frame = frame + 1 for _, ped in ipairs(getAllChars()) do if doesCharExist(ped) then if ped == PLAYER_PED then if settings.showSelf then observePed('self', ped, now) end else local ok, playerId = sampGetPlayerIdByCharHandle(ped) if ok then observePed(playerId, ped, now) end end end end -- Игроки, ушедшие из зоны стрима, при возвращении начнут с чистого листа. for key, state in pairs(tracked) do if state.seenFrame ~= frame then tracked[key] = nil end end end local function isNoticeVisible(ped, isSelf) if not isCharOnScreen(ped) then return false end if isSelf then return true end local myX, myY, myZ = getCharCoordinates(PLAYER_PED) local x, y, z = getCharCoordinates(ped) if getDistanceBetweenCoords3d(myX, myY, myZ, x, y, z) > settings.maxDistance then return false end -- Сквозь стены и объекты надпись не видна. local camX, camY, camZ = getActiveCameraCoordinates() local blocked = processLineOfSight( camX, camY, camZ, x, y, z + 0.7, true, false, false, true, false, false, false, false ) return not blocked end local function drawNotices(now) for key, state in pairs(tracked) do local notice = state.notice if notice and now >= notice.expiresAt then state.notice = nil elseif notice and isNoticeVisible(state.ped, key == 'self') then local x, y, z = getCharCoordinates(state.ped) local screenX, screenY = convert3DCoordsToScreen(x, y, z + HEAD_OFFSET_Z) local remaining = notice.expiresAt - now local alpha = 255 if remaining < FADE_MS then alpha = 255 * remaining / FADE_MS end local width = renderGetFontDrawTextLength(font, notice.text, true) renderFontDrawText( font, notice.text, screenX - width / 2, screenY + SCREEN_OFFSET_Y, math.floor(alpha) * 0x1000000 + TEXT_COLOR_RGB, true ) end end end local function showHelp() chat('/wtext - вкл/выкл, /wtext self - над своей головой') chat('/wtext time [1-60] - секунд держится надпись, /wtext dist [5-100] - дальность') chat(string.format( 'Сейчас: %s, над собой: %s, %d сек., %d м', settings.enabled and 'включено' or 'выключено', settings.showSelf and 'да' or 'нет', settings.duration, settings.maxDistance )) end local function onCommand(arg) local command, value = arg:match('^%s*(%S*)%s*(.-)%s*$') command = command:lower() if command == '' then settings.enabled = not settings.enabled if not settings.enabled then tracked = {} end chat(settings.enabled and 'Надписи включены.' or 'Надписи выключены.') elseif command == 'self' then settings.showSelf = not settings.showSelf tracked.self = nil chat(settings.showSelf and 'Надпись над своей головой включена.' or 'Надпись над своей головой выключена.') elseif command == 'time' and tonumber(value) then settings.duration = math.floor(clamp(tonumber(value), 1, 60)) chat(string.format('Надпись держится %d сек.', settings.duration)) elseif command == 'dist' and tonumber(value) then settings.maxDistance = math.floor(clamp(tonumber(value), 5, 100)) chat(string.format('Дальность показа: %d м.', settings.maxDistance)) else showHelp() return end saveSettings() end function main() if not isSampLoaded() or not isSampfuncsLoaded() then return end while not isSampAvailable() do wait(100) end loadSettings() local _, screenHeight = getScreenResolution() font = renderCreateFont('Arial', math.max(9, math.floor(screenHeight / 90)), 5) sampRegisterChatCommand('wtext', onCommand) while true do wait(0) if settings.enabled and not isPauseMenuActive() then local now = getGameTimer() updateTracking(now) drawNotices(now) end end end function onScriptTerminate(script) if script == thisScript() and font then renderReleaseFont(font) font = nil end end