372 lines
11 KiB
Lua
372 lines
11 KiB
Lua
local PLUGIN = PLUGIN
|
||
|
||
ix.util.Include("sh_config.lua")
|
||
|
||
-- Цветовая схема (как в арсенале)
|
||
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(174, 213, 129)
|
||
local COLOR_DANGER = Color(229, 57, 53)
|
||
local COLOR_WARNING = Color(255, 193, 7)
|
||
local COLOR_BORDER = Color(46, 125, 50, 100)
|
||
|
||
surface.CreateFont("ixShopPriceFont", {
|
||
font = "Roboto",
|
||
size = 28,
|
||
weight = 900,
|
||
antialias = true
|
||
})
|
||
|
||
surface.CreateFont("ixShopNameFont", {
|
||
font = "Roboto",
|
||
size = 20,
|
||
weight = 700,
|
||
antialias = true
|
||
})
|
||
|
||
-- Вспомогательная функция для рисования кругов
|
||
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
|
||
|
||
-- Получение меню магазина
|
||
net.Receive("ixShop_OpenMenu", function()
|
||
local shopType = net.ReadString()
|
||
local shopData = net.ReadTable()
|
||
|
||
PLUGIN:OpenShopMenu(shopType, shopData)
|
||
end)
|
||
|
||
-- Создание меню магазина
|
||
function PLUGIN:OpenShopMenu(shopType, shopData)
|
||
if IsValid(self.shopFrame) then
|
||
self.shopFrame:Remove()
|
||
end
|
||
|
||
local scrW, scrH = ScrW(), ScrH()
|
||
local frame = vgui.Create("DFrame")
|
||
frame:SetSize(math.min(1200, scrW - 100), math.min(800, scrH - 100))
|
||
frame:Center()
|
||
frame:SetTitle("")
|
||
frame:SetDraggable(true)
|
||
frame:ShowCloseButton(false)
|
||
frame:MakePopup()
|
||
frame:SetDeleteOnClose(true)
|
||
self.shopFrame = frame
|
||
|
||
-- Градиентный фон с зеленым оттенком (как в арсенале)
|
||
frame.Paint = function(s, w, h)
|
||
-- Основной фон
|
||
surface.SetDrawColor(COLOR_BG_DARK)
|
||
surface.DrawRect(0, 0, w, h)
|
||
|
||
-- Градиент сверху
|
||
local gradHeight = 200
|
||
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, 80)
|
||
|
||
-- Акцентная линия
|
||
surface.SetDrawColor(COLOR_PRIMARY)
|
||
surface.DrawRect(0, 80, w, 3)
|
||
|
||
-- Декоративные элементы
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
for i = 0, w, 150 do
|
||
surface.DrawRect(i, 0, 1, 80)
|
||
end
|
||
|
||
-- Заголовок
|
||
draw.SimpleText("◆ " .. (shopData.name or "МАГАЗИН") .. " ◆", "ixMenuButtonFont", w/2, 28, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
draw.SimpleText((shopData.description or ""), "ixSmallFont", w/2, 55, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
end
|
||
|
||
-- Кнопка закрытия
|
||
local closeBtn = vgui.Create("DButton", frame)
|
||
closeBtn:SetText("")
|
||
closeBtn:SetSize(38, 38)
|
||
closeBtn:SetPos(frame:GetWide() - 50, 12)
|
||
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 sidePanel = vgui.Create("DPanel", frame)
|
||
sidePanel:SetSize(250, frame:GetTall() - 110)
|
||
sidePanel:SetPos(20, 90)
|
||
sidePanel.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 categoryList = vgui.Create("DScrollPanel", sidePanel)
|
||
categoryList:Dock(FILL)
|
||
categoryList:DockMargin(5, 5, 5, 5)
|
||
|
||
-- Панель товаров (основная)
|
||
local contentPanel = vgui.Create("DPanel", frame)
|
||
contentPanel:SetSize(frame:GetWide() - 300, frame:GetTall() - 110)
|
||
contentPanel:SetPos(280, 90)
|
||
contentPanel.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 itemList = vgui.Create("DScrollPanel", contentPanel)
|
||
itemList:SetSize(contentPanel:GetWide() - 20, contentPanel:GetTall() - 20)
|
||
itemList:SetPos(10, 10)
|
||
|
||
local function LoadCategory(catID)
|
||
itemList:Clear()
|
||
local data = PLUGIN.shops[catID]
|
||
if not data then return end
|
||
|
||
local items = data.items or {}
|
||
if table.Count(items) == 0 then
|
||
local noItems = vgui.Create("DLabel", itemList)
|
||
noItems:SetText("В этой категории пока нет товаров.")
|
||
noItems:SetFont("ixMediumFont")
|
||
noItems:SetTextColor(Color(200, 50, 50))
|
||
noItems:SizeToContents()
|
||
noItems:SetPos(itemList:GetWide()/2 - noItems:GetWide()/2, 100)
|
||
return
|
||
end
|
||
|
||
for itemID, itemData in SortedPairs(items) do
|
||
local ixItem = ix.item.list[itemID]
|
||
if not ixItem then continue end
|
||
|
||
local itemPanel = vgui.Create("DPanel", itemList)
|
||
itemPanel:SetSize(itemList:GetWide() - 20, 100)
|
||
itemPanel:Dock(TOP)
|
||
itemPanel:DockMargin(0, 0, 10, 8)
|
||
|
||
local isHovered = false
|
||
itemPanel.Paint = function(s, w, h)
|
||
local bgColor = COLOR_BG_LIGHT
|
||
if isHovered then
|
||
bgColor = Color(bgColor.r + 10, bgColor.g + 15, bgColor.b + 10)
|
||
end
|
||
draw.RoundedBox(6, 0, 0, w, h, bgColor)
|
||
draw.RoundedBoxEx(6, 0, 0, 5, h, COLOR_PRIMARY, true, false, true, false)
|
||
surface.SetDrawColor(COLOR_BORDER)
|
||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||
end
|
||
itemPanel.OnCursorEntered = function() isHovered = true end
|
||
itemPanel.OnCursorExited = function() isHovered = false end
|
||
|
||
-- Иконка
|
||
local itemIcon = vgui.Create("DModelPanel", itemPanel)
|
||
itemIcon:SetSize(80, 80)
|
||
itemIcon:SetPos(15, 10)
|
||
itemIcon:SetFOV(30)
|
||
|
||
function itemIcon:LayoutEntity(ent)
|
||
return
|
||
end
|
||
|
||
if ixItem.model then
|
||
itemIcon:SetModel(ixItem.model)
|
||
|
||
itemIcon.LayoutEntity = function() end
|
||
|
||
local ent = itemIcon:GetEntity()
|
||
if IsValid(ent) then
|
||
local mins, maxs = ent:GetRenderBounds()
|
||
local size = math.max(maxs.x - mins.x, maxs.y - mins.y, maxs.z - mins.z)
|
||
local center = (mins + maxs) / 2
|
||
|
||
if shopType == "consumables" then
|
||
itemIcon:SetFOV(18)
|
||
itemIcon:SetCamPos(center + Vector(size * 0.6, size * 0.6, size * 0.4))
|
||
itemIcon:SetLookAt(center)
|
||
|
||
elseif shopType == "ammo_basic" or shopType == "ammo_special" then
|
||
itemIcon:SetFOV(45)
|
||
itemIcon:SetCamPos(center + Vector(size * 2.2, size * 2.2, size * 1.2))
|
||
itemIcon:SetLookAt(center)
|
||
|
||
else
|
||
itemIcon:SetFOV(30)
|
||
itemIcon:SetCamPos(center + Vector(size * 1.4, size * 1.4, size * 0.8))
|
||
itemIcon:SetLookAt(center)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Название
|
||
local nameLabel = vgui.Create("DLabel", itemPanel)
|
||
nameLabel:SetText(ixItem.name or itemID)
|
||
nameLabel:SetFont("ixShopNameFont")
|
||
nameLabel:SetTextColor(COLOR_TEXT_PRIMARY)
|
||
nameLabel:SetPos(110, 25)
|
||
nameLabel:SizeToContents()
|
||
|
||
-- Цена
|
||
local price = itemData.price or 0
|
||
local priceLabel = vgui.Create("DLabel", itemPanel)
|
||
priceLabel:SetText(price .. " ₽")
|
||
priceLabel:SetFont("ixShopPriceFont")
|
||
priceLabel:SetTextColor(COLOR_ACCENT)
|
||
priceLabel:SetPos(110, 55)
|
||
priceLabel:SizeToContents()
|
||
|
||
-- Кнопка покупки
|
||
local buyButton = vgui.Create("DButton", itemPanel)
|
||
buyButton:SetSize(120, 40)
|
||
buyButton:SetPos(itemPanel:GetWide() - 135, 30)
|
||
buyButton:SetText("")
|
||
buyButton.Paint = function(s, w, h)
|
||
local col = s:IsHovered() and COLOR_ACCENT or COLOR_PRIMARY
|
||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||
draw.SimpleText("КУПИТЬ ►", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||
end
|
||
buyButton.DoClick = function()
|
||
net.Start("ixShop_BuyItem")
|
||
net.WriteString(catID)
|
||
net.WriteString(itemID)
|
||
net.SendToServer()
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Заполнение списка категорий
|
||
for id, data in SortedPairsByMemberValue(PLUGIN.shops, "name") do
|
||
local catBtn = categoryList:Add("DButton")
|
||
catBtn:SetText(data.name or id)
|
||
catBtn:SetFont("ixSmallFont")
|
||
catBtn:Dock(TOP)
|
||
catBtn:SetTall(45)
|
||
catBtn:DockMargin(0, 0, 0, 5)
|
||
catBtn:SetTextColor(COLOR_TEXT_PRIMARY)
|
||
|
||
catBtn.Paint = function(s, w, h)
|
||
local active = (shopType == id)
|
||
local col = active and COLOR_PRIMARY or COLOR_BG_LIGHT
|
||
if s:IsHovered() then col = COLOR_ACCENT end
|
||
|
||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||
if active then
|
||
draw.RoundedBox(0, 0, 0, 4, h, COLOR_TEXT_PRIMARY)
|
||
end
|
||
end
|
||
|
||
catBtn.DoClick = function()
|
||
shopType = id
|
||
LoadCategory(id)
|
||
end
|
||
end
|
||
|
||
-- Загружаем начальную категорию
|
||
LoadCategory(shopType)
|
||
end
|
||
|
||
-- Добавление опций в C-Menu (свойства сущности)
|
||
properties.Add("ixShopModel", {
|
||
MenuLabel = "Изменить модель NPC",
|
||
Order = 999,
|
||
MenuIcon = "icon16/user_edit.png",
|
||
|
||
Filter = function(self, ent, ply)
|
||
if (ent:GetClass() != "ix_shop_npc") then return false end
|
||
if (!ply:IsAdmin()) then return false end
|
||
return true
|
||
end,
|
||
|
||
Action = function(self, ent)
|
||
Derma_StringRequest(
|
||
"Изменить модель NPC",
|
||
"Введите путь к модели:",
|
||
ent:GetModel(),
|
||
function(text)
|
||
net.Start("ixShop_UpdateModel")
|
||
net.WriteEntity(ent)
|
||
net.WriteString(text)
|
||
net.SendToServer()
|
||
end
|
||
)
|
||
end
|
||
})
|
||
|
||
properties.Add("ixShopType", {
|
||
MenuLabel = "Изменить тип магазина",
|
||
Order = 1000,
|
||
MenuIcon = "icon16/cart_edit.png",
|
||
|
||
Filter = function(self, ent, ply)
|
||
if (ent:GetClass() != "ix_shop_npc") then return false end
|
||
if (!ply:IsAdmin()) then return false end
|
||
return true
|
||
end,
|
||
|
||
MenuOpen = function(self, option, ent, ply)
|
||
local plugin = ix.plugin.Get("shop")
|
||
if (not plugin) then return end
|
||
|
||
local subMenu = option:AddSubMenu()
|
||
for typeID, data in pairs(plugin.shops) do
|
||
local target = subMenu:AddOption(data.name or typeID, function()
|
||
net.Start("ixShop_UpdateType")
|
||
net.WriteEntity(ent)
|
||
net.WriteString(typeID)
|
||
net.SendToServer()
|
||
end)
|
||
target:SetIcon("icon16/tag_blue.png")
|
||
end
|
||
end,
|
||
|
||
Action = function(self, ent)
|
||
-- Опции обрабатываются в MenuOpen
|
||
end
|
||
})
|