local PLUGIN = PLUGIN -- Хранение выбранного loadout для каждой техники local selectedLoadouts = {} -- Цветовая схема (такая же как в арсенале) local COLOR_BG_DARK = Color(3, 5, 4) local COLOR_BG_MEDIUM = Color(8, 12, 10) local COLOR_BG_LIGHT = Color(12, 18, 14) local COLOR_PRIMARY = Color(27, 94, 32) local COLOR_PRIMARY_DARK = Color(15, 60, 18) local COLOR_ACCENT = Color(56, 102, 35) local COLOR_TEXT_PRIMARY = Color(165, 214, 167) local COLOR_TEXT_SECONDARY = Color(102, 187, 106) local COLOR_DANGER = Color(136, 14, 14) local COLOR_WARNING = Color(191, 130, 0) local COLOR_BORDER = Color(15, 60, 18, 60) -- Вспомогательная функция для рисования кругов if not draw.Circle then function draw.Circle(x, y, radius, color) local segmentCount = math.max(16, radius) surface.SetDrawColor(color or color_white) local circle = {} for i = 0, segmentCount do local angle = math.rad((i / segmentCount) * 360) table.insert(circle, { x = x + math.cos(angle) * radius, y = y + math.sin(angle) * radius }) end surface.DrawPoly(circle) end end if not surface.DrawCircle then function surface.DrawCircle(x, y, radius, color) draw.Circle(x, y, radius, color) end end PLUGIN.clientFactionPoints = {} PLUGIN.selectedLoadouts = {} -- Глобальное хранилище выбранных loadout'ов -- Проверка доступности техники для игрока function PLUGIN:CanPlayerAccessVehicle(vehicleConfig) local character = LocalPlayer():GetCharacter() if not character then return false end local playerSubdivision = character:GetPodr() -- Если нет ограничений по подразделениям - доступно всем if not vehicleConfig.allowedPodr or not istable(vehicleConfig.allowedPodr) then return true end -- Проверяем доступность для подразделения игрока return vehicleConfig.allowedPodr[playerSubdivision] ~= nil end function PLUGIN:GetFactions() if not self.cachedFactions then if not self.config then -- Fallback to global if self.config is missing for some reason self.config = PLUGIN.config or {} end if not self.config or not istable(self.config) then self.cachedFactions = {} else self.cachedFactions = self.config.getFactions and self.config.getFactions() or self.config.factions or {} end end return self.cachedFactions end -- Сетевые сообщения net.Receive("LVS_SyncFactionPoints", function() local faction = net.ReadUInt(8) local points = net.ReadUInt(32) PLUGIN.clientFactionPoints[faction] = points end) -- Открытие меню наземной техники net.Receive("LVS_OpenGroundMenu", function() PLUGIN:OpenVehicleMenu("ground") end) -- Открытие меню воздушной техники net.Receive("LVS_OpenAirMenu", function() PLUGIN:OpenVehicleMenu("air") end) local function UpdateVehicleInfo(vehicleConfig, vehicleID, infoContent, modelPanel, vehicleName, vehicleDesc, vehiclePrice, vehicleCooldown, spawnBtn, factionPoints, vehicleType, returnBtn, loadoutPanel, updateLoadoutCallback) if not vehicleConfig then return end -- Инициализируем выбранный loadout если его нет (дефолтный = 1) if not PLUGIN.selectedLoadouts[vehicleID] then PLUGIN.selectedLoadouts[vehicleID] = 1 end local faction = LocalPlayer():Team() local factions = PLUGIN:GetFactions() local factionConfig = factions and factions[faction] local character = LocalPlayer():GetCharacter() local playerSubdivision = character and character:GetPodr() or 1 -- Проверяем доступность по подразделению local canSpawnBySubdivision = true local subdivisionMessage = "" -- Проверяем разрешенные подразделения и специализации if vehicleConfig.allowedPodr and istable(vehicleConfig.allowedPodr) then local playerSpec = character and character:GetSpec() or 1 local podrConfig = vehicleConfig.allowedPodr[playerSubdivision] if podrConfig == nil then -- Подразделение не в списке canSpawnBySubdivision = false elseif podrConfig == true then -- Разрешено всем специализациям этого подразделения canSpawnBySubdivision = true elseif istable(podrConfig) then -- Проверяем специализацию canSpawnBySubdivision = table.HasValue(podrConfig, playerSpec) end if not canSpawnBySubdivision then -- Получаем названия разрешенных подразделений и специализаций local podrNames = {} local factionTable = ix.faction.Get(faction) if factionTable and factionTable.Podr and factionTable.Spec then for podrID, config in pairs(vehicleConfig.allowedPodr) do local podrData = factionTable.Podr[podrID] if podrData and podrData.name then if config == true then table.insert(podrNames, podrData.name) elseif istable(config) then local specNames = {} for _, specID in ipairs(config) do local specData = factionTable.Spec[specID] if specData and specData.name then table.insert(specNames, specData.name) end end if #specNames > 0 then table.insert(podrNames, podrData.name .. " (" .. table.concat(specNames, ", ") .. ")") end end end end end if #podrNames > 0 then subdivisionMessage = "Только: " .. table.concat(podrNames, "; ") else subdivisionMessage = "Недоступно для вашего подразделения" end end end -- Показываем элементы modelPanel:SetVisible(true) vehicleName:SetVisible(true) vehicleDesc:SetVisible(true) vehiclePrice:SetVisible(true) vehicleCooldown:SetVisible(true) spawnBtn:SetVisible(true) returnBtn:SetVisible(true) -- Устанавливаем модель modelPanel:SetModel(vehicleConfig.model) local ent = modelPanel.Entity if IsValid(ent) then local center = ent:OBBCenter() local distance = ent:BoundingRadius() * 1.5 modelPanel:SetLookAt(center) modelPanel:SetCamPos(center + Vector(distance, distance, distance)) modelPanel:SetFOV(50) end -- Устанавливаем текстовую информацию vehicleName:SetText(vehicleConfig.name) vehicleDesc:SetText(vehicleConfig.description) vehiclePrice:SetText("Стоимость: " .. vehicleConfig.price .. " очков техники") -- Проверяем кд и доступность local vehiclesData = character and character:GetData("lvs_vehicles", {}) or {} local lastSpawn = vehiclesData[vehicleID] local cooldown = lastSpawn and lastSpawn > os.time() and (lastSpawn - os.time()) or 0 -- Проверяем, есть ли активная техника этого типа local hasActiveVehicle = PLUGIN:HasActiveVehicle(vehicleID, vehicleType) local refundAmount = math.floor(vehicleConfig.price * 0.8) -- Настраиваем кнопку возврата if hasActiveVehicle then returnBtn:SetEnabled(true) returnBtn:SetText("") returnBtn.Paint = function(s, w, h) local col = s:IsHovered() and Color(185, 60, 60) or Color(165, 85, 60) draw.RoundedBox(6, 0, 0, w, h, col) if s:IsHovered() then surface.SetDrawColor(255, 255, 255, 30) draw.RoundedBox(6, 2, 2, w-4, h-4, Color(255, 255, 255, 30)) end surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("◄ ВЕРНУТЬ (" .. refundAmount .. " очков)", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end -- Отправляем запрос на возврат техники при нажатии кнопки returnBtn.DoClick = function() local veh = PLUGIN:GetActiveVehicle(vehicleID, vehicleType) if IsValid(veh) then net.Start("LVS_RequestReturn") net.WriteUInt(veh:EntIndex(), 16) net.SendToServer() else LocalPlayer():Notify("Активная техника не найдена") end end else returnBtn:SetEnabled(false) returnBtn:SetText("") returnBtn.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, Color(60, 60, 60)) surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("НЕТ АКТИВНОЙ ТЕХНИКИ", "ixSmallFont", w/2, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end end if not canSpawnBySubdivision then vehicleCooldown:SetText(subdivisionMessage) vehicleCooldown:SetTextColor(COLOR_DANGER) spawnBtn:SetEnabled(false) spawnBtn:SetText("") spawnBtn.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, Color(60, 60, 60)) surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("НЕДОСТУПНО", "ixSmallFont", w/2, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end elseif cooldown > 0 then local minutes = math.floor(cooldown / 60) local seconds = math.floor(cooldown % 60) vehicleCooldown:SetText("Перезарядка: " .. string.format("%02d:%02d", minutes, seconds)) vehicleCooldown:SetTextColor(COLOR_WARNING) spawnBtn:SetEnabled(false) spawnBtn:SetText("") spawnBtn.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, Color(60, 60, 60)) surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("В КД", "ixSmallFont", w/2, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end elseif factionPoints < vehicleConfig.price then vehicleCooldown:SetText("Недостаточно очков техники") vehicleCooldown:SetTextColor(COLOR_WARNING) spawnBtn:SetEnabled(false) spawnBtn:SetText("") spawnBtn.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, Color(60, 60, 60)) surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("НЕТ ОЧКОВ", "ixSmallFont", w/2, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end else vehicleCooldown:SetText("Готов к вызову") vehicleCooldown:SetTextColor(COLOR_ACCENT) spawnBtn:SetEnabled(true) spawnBtn:SetText("") spawnBtn.Paint = function(s, w, h) local col = s:IsHovered() and COLOR_ACCENT or COLOR_PRIMARY draw.RoundedBox(6, 0, 0, w, h, col) if s:IsHovered() then surface.SetDrawColor(255, 255, 255, 30) draw.RoundedBox(6, 2, 2, w-4, h-4, Color(255, 255, 255, 30)) end surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("ВЫЗВАТЬ ТЕХНИКУ ►", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end spawnBtn.DoClick = function() local loadoutID = PLUGIN.selectedLoadouts[vehicleID] or 1 print("[LVS] Запрос спавна техники " .. vehicleConfig.name .. " с loadout ID: " .. loadoutID) PLUGIN:RequestSpawnVehicle(vehicleID, vehicleType, loadoutID) end end -- Обновляем панель loadout если она передана if IsValid(loadoutPanel) and updateLoadoutCallback then updateLoadoutCallback(vehicleConfig, vehicleID, loadoutPanel) end end function PLUGIN:GetActiveVehicle(vehicleID, vehicleType) local ply = LocalPlayer() if not IsValid(ply) then return nil end for _, ent in ipairs(ents.GetAll()) do if IsValid(ent) then local nwID = ent:GetNWString("LVS_VehicleID", "") local nwType = ent:GetNWString("LVS_VehicleType", "") local nwOwner = ent:GetNWString("LVS_OwnerSteamID", "") if nwID == tostring(vehicleID) and nwType == tostring(vehicleType) then if ply:IsAdmin() or ply:SteamID() == nwOwner then return ent end end end end return nil end -- Функция обновления кд function PLUGIN:UpdateVehicleCooldown(panel, vehicleID) local character = LocalPlayer():GetCharacter() if not character then return end local vehicles = character:GetData("lvs_vehicles", {}) local lastSpawn = vehicles[vehicleID] if lastSpawn and lastSpawn > os.time() then panel.cooldown = lastSpawn - os.time() timer.Create("LVS_Cooldown_" .. vehicleID, 1, 0, function() if not IsValid(panel) then timer.Remove("LVS_Cooldown_" .. vehicleID) return end panel.cooldown = panel.cooldown - 1 if panel.cooldown <= 0 then panel.cooldown = 0 timer.Remove("LVS_Cooldown_" .. vehicleID) end end) else panel.cooldown = 0 end end -- Основная функция создания меню в стиле Military RP function PLUGIN:OpenVehicleMenu(vehicleType) -- Если окно уже открыто — закроем старое, чтобы не было дубликатов if IsValid(PLUGIN._vehicleMenuFrame) then PLUGIN._vehicleMenuFrame:Remove() PLUGIN._vehicleMenuFrame = nil end local scrW, scrH = ScrW(), ScrH() local faction = LocalPlayer():Team() local factions = PLUGIN:GetFactions() local factionConfig = factions and factions[faction] if not factionConfig then return end local vehicles = vehicleType == "ground" and factionConfig.groundVehicles or factionConfig.airVehicles -- Фильтруем технику по доступности local availableVehicles = {} for vehicleID, vehicleConfig in pairs(vehicles) do if self:CanPlayerAccessVehicle(vehicleConfig) then availableVehicles[vehicleID] = vehicleConfig end end -- Если нет доступной техники - не открываем меню if table.Count(availableVehicles) == 0 then LocalPlayer():Notify("Техника вам не доступна") return end local frame = vgui.Create("DFrame") frame:SetSize(scrW, scrH) frame:SetPos(0, 0) frame:SetTitle("") frame:SetDraggable(false) frame:ShowCloseButton(false) frame:MakePopup() local factionPoints = self.clientFactionPoints[faction] or 0 -- Градиентный фон с зеленым оттенком frame.Paint = function(s, w, h) -- Основной фон surface.SetDrawColor(COLOR_BG_DARK) surface.DrawRect(0, 0, w, h) -- Градиент сверху local gradHeight = 300 for i = 0, gradHeight do local alpha = (1 - i/gradHeight) * 40 surface.SetDrawColor(COLOR_PRIMARY.r, COLOR_PRIMARY.g, COLOR_PRIMARY.b, alpha) surface.DrawRect(0, i, w, 1) end -- Верхняя панель surface.SetDrawColor(COLOR_BG_MEDIUM) surface.DrawRect(0, 0, w, 100) -- Акцентная линия surface.SetDrawColor(COLOR_PRIMARY) surface.DrawRect(0, 100, w, 3) -- Декоративные элементы surface.SetDrawColor(COLOR_BORDER) for i = 0, w, 200 do surface.DrawRect(i, 0, 1, 100) end -- Заголовок local title = vehicleType == "ground" and "◆ НАЗЕМНАЯ ТЕХНИКА ◆" or "◆ ВОЗДУШНАЯ ТЕХНИКА ◆" local subtitle = vehicleType == "ground" and "СИСТЕМА ВЫЗОВА ТЕХНИКИ" or "СИСТЕМА ВЫЗОВА АВИАЦИИ" draw.SimpleText(title, "ixMenuButtonFont", w/2, 35, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) draw.SimpleText(subtitle, "ixSmallFont", w/2, 68, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end -- Кнопка закрытия local closeBtn = vgui.Create("DButton", frame) closeBtn:SetText("") closeBtn:SetSize(42, 42) closeBtn:SetPos(scrW - 60, 18) closeBtn.Paint = function(s, w, h) local col = s:IsHovered() and COLOR_DANGER or Color(COLOR_DANGER.r * 0.7, COLOR_DANGER.g * 0.7, COLOR_DANGER.b * 0.7) draw.RoundedBox(4, 0, 0, w, h, col) surface.SetDrawColor(COLOR_BG_DARK) surface.DrawOutlinedRect(0, 0, w, h, 2) surface.SetDrawColor(COLOR_TEXT_PRIMARY) surface.DrawLine(w*0.3, h*0.3, w*0.7, h*0.7) surface.DrawLine(w*0.7, h*0.3, w*0.3, h*0.7) if s:IsHovered() then surface.SetDrawColor(255, 255, 255, 30) draw.RoundedBox(4, 0, 0, w, h, Color(255, 255, 255, 30)) end end closeBtn.DoClick = function() frame:Close() end -- Панель информации об очках local infoPanel = vgui.Create("DPanel", frame) infoPanel:SetSize(300, 70) infoPanel:SetPos(40, 115) infoPanel.Paint = function(s, w, h) draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_LIGHT) surface.SetDrawColor(COLOR_PRIMARY) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("ОЧКИ ТЕХНИКИ", "ixSmallFont", 20, 12, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) draw.SimpleText(factionPoints, "ixMenuButtonFont", 20, 32, COLOR_ACCENT, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) -- Иконка surface.SetDrawColor(COLOR_PRIMARY) draw.NoTexture() surface.DrawCircle(w - 35, h/2, 12, COLOR_PRIMARY) end local colWidth = (scrW - 120) / 2 local startY = 205 -- Панель списка техники local vehiclesPanel = vgui.Create("DPanel", frame) vehiclesPanel:SetSize(colWidth, scrH - startY - 40) vehiclesPanel:SetPos(40, startY) vehiclesPanel.Paint = function(s, w, h) draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_MEDIUM) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 2) end local vehiclesHeader = vgui.Create("DPanel", vehiclesPanel) vehiclesHeader:SetSize(vehiclesPanel:GetWide(), 45) vehiclesHeader:SetPos(0, 0) vehiclesHeader.Paint = function(s, w, h) draw.RoundedBoxEx(8, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false) draw.SimpleText("▣ ДОСТУПНАЯ ТЕХНИКА", "ixSmallFont", 15, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) surface.SetDrawColor(COLOR_ACCENT) surface.DrawRect(0, h-2, w, 2) end -- Панель информации о технике local infoPanel2 = vgui.Create("DPanel", frame) infoPanel2:SetSize(colWidth, scrH - startY - 40) infoPanel2:SetPos(scrW - colWidth - 40, startY) infoPanel2.Paint = function(s, w, h) draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_MEDIUM) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 2) end local infoHeader = vgui.Create("DPanel", infoPanel2) infoHeader:SetSize(infoPanel2:GetWide(), 45) infoHeader:SetPos(0, 0) infoHeader.Paint = function(s, w, h) draw.RoundedBoxEx(8, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false) draw.SimpleText("⬢ ИНФОРМАЦИЯ", "ixSmallFont", 15, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) surface.SetDrawColor(COLOR_ACCENT) surface.DrawRect(0, h-2, w, 2) end -- Список техники local vehiclesScroll = vgui.Create("DScrollPanel", vehiclesPanel) vehiclesScroll:SetSize(vehiclesPanel:GetWide() - 20, vehiclesPanel:GetTall() - 60) vehiclesScroll:SetPos(10, 50) local sbar = vehiclesScroll:GetVBar() sbar:SetHideButtons(true) function sbar:Paint(w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_DARK) end function sbar.btnGrip:Paint(w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY) end -- Панель информации local infoContent = vgui.Create("DPanel", infoPanel2) infoContent:SetSize(infoPanel2:GetWide() - 20, infoPanel2:GetTall() - 60) infoContent:SetPos(10, 50) infoContent.selectedVehicle = nil infoContent.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_LIGHT) if not s.selectedVehicle then draw.SimpleText("⚠ ВЫБЕРИТЕ ТЕХНИКУ", "ixSmallFont", w/2, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end end -- Модель техники local modelPanel = vgui.Create("DModelPanel", infoContent) modelPanel:SetSize(400, 200) modelPanel:SetPos(10, 10) modelPanel:SetVisible(false) -- Информация о технике local vehicleName = vgui.Create("DLabel", infoContent) vehicleName:SetFont("ixSmallFont") vehicleName:SetTextColor(color_white) vehicleName:SetPos(10, 220) vehicleName:SetSize(400, 20) vehicleName:SetVisible(false) local vehicleDesc = vgui.Create("DLabel", infoContent) vehicleDesc:SetFont("ixSmallFont") vehicleDesc:SetTextColor(Color(200, 200, 200)) vehicleDesc:SetPos(10, 245) vehicleDesc:SetSize(400, 40) vehicleDesc:SetVisible(false) local vehiclePrice = vgui.Create("DLabel", infoContent) vehiclePrice:SetFont("ixSmallFont") vehiclePrice:SetTextColor(color_white) vehiclePrice:SetPos(10, 290) vehiclePrice:SetSize(400, 20) vehiclePrice:SetVisible(false) local vehicleCooldown = vgui.Create("DLabel", infoContent) vehicleCooldown:SetFont("ixSmallFont") vehicleCooldown:SetTextColor(color_white) vehicleCooldown:SetPos(10, 315) vehicleCooldown:SetSize(400, 20) vehicleCooldown:SetVisible(false) -- Кнопка вызова local spawnBtn = vgui.Create("DButton", infoContent) spawnBtn:SetSize(200, 35) spawnBtn:SetPos(10, 350) spawnBtn:SetText("") spawnBtn:SetVisible(false) -- Кнопка возврата local returnBtn = vgui.Create("DButton", infoContent) returnBtn:SetSize(200, 35) returnBtn:SetPos(220, 350) returnBtn:SetText("") returnBtn:SetVisible(false) -- Используем глобальное хранилище selectedLoadouts local selectedLoadouts = PLUGIN.selectedLoadouts -- Создаем панель выбора loadout (скрыта по умолчанию) local loadoutPanel = vgui.Create("DPanel", infoContent) loadoutPanel:SetSize(infoContent:GetWide() - 20, 120) loadoutPanel:SetPos(10, 400) loadoutPanel:SetVisible(false) loadoutPanel.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, ColorAlpha(COLOR_BG_DARK, 200)) surface.SetDrawColor(COLOR_PRIMARY) surface.DrawOutlinedRect(0, 0, w, h, 2) draw.SimpleText("⚙ ВООРУЖЕНИЕ:", "ixSmallFont", 10, 8, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) end -- Функция обновления панели loadout local function UpdateLoadoutPanel(vehicleConfig, vehicleID, panel) if not IsValid(panel) then return end for _, child in pairs(panel:GetChildren()) do child:Remove() end local loadoutConfig = vehicleConfig.loadout if not istable(loadoutConfig) or table.Count(loadoutConfig) == 0 then panel:SetVisible(false) return end panel:SetVisible(true) local btnWidth = 130 local btnHeight = 80 local gap = 10 local startX = 10 local startY = 30 -- соберём пары в массив и отсортируем по ID, чтобы кнопки шли в правильном порядке local entries = {} for loadoutName, loadoutID in pairs(loadoutConfig) do if isnumber(loadoutID) then table.insert(entries, {name = loadoutName, id = loadoutID}) end end table.sort(entries, function(a, b) return a.id < b.id end) -- вставим пустые слоты для пропущенных ID, чтобы кнопки располагались на своих позициях local filled = {} local nextIdx = 1 local maxID = entries[#entries] and entries[#entries].id or 0 for id = 1, maxID do if entries[nextIdx] and entries[nextIdx].id == id then table.insert(filled, entries[nextIdx]) nextIdx = nextIdx + 1 else table.insert(filled, {name = "", id = id}) end end for idx, entry in ipairs(filled) do local btn = vgui.Create("DButton", panel) btn:SetSize(btnWidth, btnHeight) btn:SetPos(startX + (idx - 1) * (btnWidth + gap), startY) btn:SetText("") btn.variantIndex = idx btn.loadoutID = entry.id btn.loadoutName = tostring(entry.name) btn.Paint = function(s, w, h) local isSelected = PLUGIN.selectedLoadouts[vehicleID] == s.loadoutID local bgColor = isSelected and COLOR_PRIMARY or COLOR_BG_MEDIUM if s:IsHovered() and not isSelected then bgColor = ColorAlpha(COLOR_PRIMARY, 100) end draw.RoundedBox(4, 0, 0, w, h, bgColor) if isSelected then surface.SetDrawColor(COLOR_ACCENT) surface.DrawOutlinedRect(0, 0, w, h, 3) else surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) end draw.SimpleText(s.loadoutName or ("Вариант " .. s.variantIndex), "ixSmallFont", w/2, 12, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) draw.SimpleText("ID: " .. tostring(s.loadoutID), "DermaDefault", w/2, 50, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER) if isSelected then draw.SimpleText("✓", "ixMenuButtonFont", w/2, h - 20, COLOR_ACCENT, TEXT_ALIGN_CENTER) end end btn.DoClick = function() print("[LVS] Пользователь выбрал loadout '" .. btn.loadoutName .. "' (ID=" .. btn.loadoutID .. ") для " .. vehicleConfig.name) PLUGIN.selectedLoadouts[vehicleID] = btn.loadoutID for _, child in pairs(panel:GetChildren()) do child:Remove() end UpdateLoadoutPanel(vehicleConfig, vehicleID, panel) end end end -- Привяжем frame к плагину, чтобы можно было предотвращать дубликаты PLUGIN._vehicleMenuFrame = frame -- Функция создания строки техники local function CreateVehicleRow(parent, vehicleID, vehicleConfig) local row = vgui.Create("DButton", parent) row:SetSize(parent:GetWide() - 10, 70) row:SetText("") row.vehicleID = vehicleID row.vehicleConfig = vehicleConfig local isHovered = false row.Paint = function(s, w, h) local isSelected = infoContent.selectedVehicle == vehicleID local canAfford = factionPoints >= vehicleConfig.price local cooldown = s.cooldown or 0 local hasActive = PLUGIN:HasActiveVehicle(vehicleID, vehicleType) -- Фон строки local bgColor = isSelected and COLOR_PRIMARY_DARK or COLOR_BG_LIGHT if isHovered and not isSelected then bgColor = Color(bgColor.r + 10, bgColor.g + 15, bgColor.b + 10) end draw.RoundedBox(6, 0, 0, w, h, bgColor) -- Боковая полоска local statusColor = hasActive and Color(56, 142, 200) or (canAfford and COLOR_PRIMARY or COLOR_DANGER) draw.RoundedBoxEx(6, 0, 0, 5, h, statusColor, true, false, true, false) -- Рамка surface.SetDrawColor(isSelected and COLOR_ACCENT or COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 2) -- Акцентная линия при наведении if isHovered then surface.SetDrawColor(COLOR_ACCENT) surface.DrawRect(5, h-3, w-10, 3) end -- Название техники draw.SimpleText(vehicleConfig.name, "ixSmallFont", 15, 12, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT) -- Цена local priceColor = canAfford and COLOR_ACCENT or COLOR_WARNING draw.SimpleText(vehicleConfig.price .. " очков", "ixSmallFont", 15, 35, priceColor, TEXT_ALIGN_LEFT) -- Статус local statusText = "" local statusTextColor = COLOR_TEXT_SECONDARY if hasActive then statusText = "● АКТИВНА" statusTextColor = Color(100, 180, 255) elseif cooldown > 0 then local minutes = math.floor(cooldown / 60) local seconds = math.floor(cooldown % 60) statusText = "КД: " .. string.format("%02d:%02d", minutes, seconds) statusTextColor = COLOR_WARNING elseif not canAfford then statusText = "НЕТ ОЧКОВ" statusTextColor = COLOR_WARNING else statusText = "● ГОТОВ" statusTextColor = COLOR_ACCENT end draw.SimpleText(statusText, "ixSmallFont", w - 15, h/2, statusTextColor, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER) end row.OnCursorEntered = function() isHovered = true end row.OnCursorExited = function() isHovered = false end row.DoClick = function(s) infoContent.selectedVehicle = vehicleID UpdateVehicleInfo(vehicleConfig, vehicleID, infoContent, modelPanel, vehicleName, vehicleDesc, vehiclePrice, vehicleCooldown, spawnBtn, factionPoints, vehicleType, returnBtn, loadoutPanel, UpdateLoadoutPanel) end -- Обновляем кд PLUGIN:UpdateVehicleCooldown(row, vehicleID) return row end -- Создаем строки с техникой (только доступной) for vehicleID, vehicleConfig in SortedPairsByMemberValue(availableVehicles, "price") do local row = CreateVehicleRow(vehiclesScroll, vehicleID, vehicleConfig) row:Dock(TOP) row:DockMargin(0, 0, 0, 5) end -- Функция обновления интерфейса local function UpdateInterface() if not IsValid(frame) then return end local currentPoints = PLUGIN.clientFactionPoints[faction] or 0 factionPoints = currentPoints -- Обновляем отображение очков if IsValid(pointsPanel) then pointsPanel.Paint = function(s, w, h) surface.SetDrawColor(Color(23, 23, 23)) surface.DrawRect(0, 0, w, h) surface.SetDrawColor(Color(1, 67, 29)) surface.DrawOutlinedRect(0, 0, w, h, 1) draw.SimpleText("ОЧКИ ТЕХНИКИ", "ixSmallFont", w/2, 11, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) draw.SimpleText(currentPoints, "ixSmallFont", w/2, 28, Color(100, 255, 100), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end end -- Обновляем информацию о выбранной технике if infoContent.selectedVehicle then local vehicleConfig = availableVehicles[infoContent.selectedVehicle] if vehicleConfig then UpdateVehicleInfo(vehicleConfig, infoContent.selectedVehicle, infoContent, modelPanel, vehicleName, vehicleDesc, vehiclePrice, vehicleCooldown, spawnBtn, currentPoints, vehicleType, returnBtn) end end end -- Таймер для обновления интерфейса timer.Create("LVS_InterfaceUpdate_" .. vehicleType, 1, 0, function() if not IsValid(frame) then timer.Remove("LVS_InterfaceUpdate_" .. vehicleType) return end UpdateInterface() end) -- Автоматически выбираем первую технику timer.Simple(0.1, function() if IsValid(vehiclesScroll) and vehiclesScroll:GetCanvas():GetChildren()[1] then local firstRow = vehiclesScroll:GetCanvas():GetChildren()[1] if firstRow.DoClick then firstRow:DoClick() end end end) frame.OnCursorEntered = function(s) s:MoveToFront() end frame.OnClose = function() timer.Remove("LVS_InterfaceUpdate_" .. vehicleType) if PLUGIN._vehicleMenuFrame == frame then PLUGIN._vehicleMenuFrame = nil end end end -- Запрос спавна техники function PLUGIN:RequestSpawnVehicle(vehicleID, vehicleType, loadoutVariant) print("[LVS] Отправка запроса спавна на сервер - vehicleID: " .. vehicleID .. ", loadoutVariant: " .. (loadoutVariant or 0)) net.Start("LVS_RequestSpawn") net.WriteString(vehicleID) net.WriteString(vehicleType) net.WriteUInt(loadoutVariant or 0, 8) net.SendToServer() end -- Запрос возврата техники function PLUGIN:RequestReturnVehicle(vehicleID, vehicleType) local vehicle = self:GetActiveVehicle(vehicleID, vehicleType) if IsValid(vehicle) then net.Start("LVS_RequestReturn") net.WriteUInt(vehicle:EntIndex(), 16) net.SendToServer() else LocalPlayer():Notify("Активная техника не найдена") end end net.Receive("LVS_SyncVehicleInfo", function() local vehicle = net.ReadEntity() local ownerSteamID = net.ReadString() local vehicleID = net.ReadString() local vehicleType = net.ReadString() if IsValid(vehicle) then vehicle.lvsVehicleInfo = { ownerSteamID = ownerSteamID, vehicleID = vehicleID, vehicleType = vehicleType } end end) -- Получаем таблицу моделей от сервера и подставляем в конфиг при отсутствии net.Receive("LVS_SyncVehicleModels", function() local mapping = net.ReadTable() if not istable(mapping) then return end for factionID, data in pairs(mapping) do local factionConfig = PLUGIN:GetFactions()[factionID] if not factionConfig then continue end if istable(data.ground) then for id, mdl in pairs(data.ground) do if factionConfig.groundVehicles and factionConfig.groundVehicles[id] then if not factionConfig.groundVehicles[id].model or factionConfig.groundVehicles[id].model == "" then factionConfig.groundVehicles[id].model = mdl end end end end if istable(data.air) then for id, mdl in pairs(data.air) do if factionConfig.airVehicles and factionConfig.airVehicles[id] then if not factionConfig.airVehicles[id].model or factionConfig.airVehicles[id].model == "" then factionConfig.airVehicles[id].model = mdl end end end end end end)