add sborka
This commit is contained in:
371
garrysmod/gamemodes/militaryrp/plugins/shop/cl_plugin.lua
Normal file
371
garrysmod/gamemodes/militaryrp/plugins/shop/cl_plugin.lua
Normal file
@@ -0,0 +1,371 @@
|
||||
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
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.PrintName = "Магазин NPC"
|
||||
ENT.Category = "[FT] Магазины"
|
||||
ENT.Spawnable = true
|
||||
ENT.AdminOnly = true
|
||||
ENT.RenderGroup = RENDERGROUP_BOTH
|
||||
ENT.bNoPersist = true
|
||||
ENT.AutomaticFrameAdvance = true
|
||||
|
||||
function ENT:SetupDataTables()
|
||||
self:NetworkVar("String", 0, "ShopType")
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/humans/group01/male_01.mdl")
|
||||
self:PhysicsInit(SOLID_BBOX)
|
||||
self:SetMoveType(MOVETYPE_NONE)
|
||||
self:SetSolid(SOLID_BBOX)
|
||||
self:SetUseType(SIMPLE_USE)
|
||||
self:SetCollisionGroup(COLLISION_GROUP_NPC)
|
||||
|
||||
-- Установка bbox для корректной коллизии
|
||||
self:SetCollisionBounds(Vector(-16, -16, 0), Vector(16, 16, 72))
|
||||
|
||||
-- Дефолтный тип магазина
|
||||
self:SetShopType("consumables")
|
||||
|
||||
local phys = self:GetPhysicsObject()
|
||||
if IsValid(phys) then
|
||||
phys:EnableMotion(false)
|
||||
end
|
||||
|
||||
-- Анимация простоя
|
||||
local sequence = self:LookupSequence("idle_subtle")
|
||||
if sequence > 0 then
|
||||
self:ResetSequence(sequence)
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
if not IsValid(activator) or not activator:IsPlayer() then return end
|
||||
|
||||
local shopType = self:GetShopType()
|
||||
local plugin = ix.plugin.Get("shop")
|
||||
local shopData = plugin and plugin.shops[shopType]
|
||||
|
||||
if not shopData then
|
||||
activator:Notify("Этот магазин не настроен или не имеет товаров (ID: " .. tostring(shopType) .. ")")
|
||||
return
|
||||
end
|
||||
|
||||
net.Start("ixShop_OpenMenu")
|
||||
net.WriteString(shopType)
|
||||
net.WriteTable(shopData)
|
||||
net.Send(activator)
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
self:NextThink(CurTime())
|
||||
return true
|
||||
end
|
||||
else
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
|
||||
local pos = self:GetPos() + Vector(0, 0, 85)
|
||||
local ang = Angle(0, LocalPlayer():EyeAngles().y - 90, 90)
|
||||
|
||||
local distance = LocalPlayer():GetPos():Distance(self:GetPos())
|
||||
if distance > 300 then return end
|
||||
|
||||
local shopType = self:GetShopType()
|
||||
local plugin = ix.plugin.Get("shop")
|
||||
local shopData = plugin and plugin.shops[shopType]
|
||||
|
||||
if not shopData then return end
|
||||
|
||||
cam.Start3D2D(pos, ang, 0.1)
|
||||
-- Название магазина
|
||||
draw.SimpleText(shopData.name, "DermaLarge", 0, 0, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Подсказка
|
||||
draw.SimpleText("Нажмите E для взаимодействия", "DermaDefault", 0, 25, Color(200, 200, 200), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
cam.End3D2D()
|
||||
end
|
||||
end
|
||||
298
garrysmod/gamemodes/militaryrp/plugins/shop/sh_config.lua
Normal file
298
garrysmod/gamemodes/militaryrp/plugins/shop/sh_config.lua
Normal file
@@ -0,0 +1,298 @@
|
||||
PLUGIN.shops = PLUGIN.shops or {}
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
-- МАГАЗИНЫ ОБВЕСОВ TACRP
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
|
||||
-- Магазин оптики и прицелов
|
||||
PLUGIN.shops["optics"] = {
|
||||
name = "Оптика и прицелы",
|
||||
description = "Прицелы, коллиматоры и тактические устройства",
|
||||
items = {
|
||||
-- Оптика
|
||||
["optic_rds"] = {price = 400},
|
||||
["optic_holographic"] = {price = 500},
|
||||
["optic_rmr"] = {price = 500},
|
||||
["optic_rds2"] = {price = 450},
|
||||
["optic_okp7"] = {price = 600},
|
||||
["optic_rmr_rifle"] = {price = 550},
|
||||
["optic_shortdot"] = {price = 700},
|
||||
["optic_elcan"] = {price = 700},
|
||||
["optic_acog"] = {price = 800},
|
||||
["optic_8x"] = {price = 1000},
|
||||
["optic_irons"] = {price = 300},
|
||||
-- Специфичные прицелы
|
||||
["optic_ak_kobra"] = {price = 700},
|
||||
["optic_galil"] = {price = 650},
|
||||
["optic_m16a2_colt"] = {price = 700},
|
||||
["optic_ar_colt"] = {price = 900},
|
||||
["optic_pso1"] = {price = 1200},
|
||||
["optic_ak_pso1"] = {price = 1100},
|
||||
["optic_k98_zf42"] = {price = 1000},
|
||||
["optic_xm8_4x"] = {price = 1100},
|
||||
["optic_xm8_6x"] = {price = 1300},
|
||||
["optic_xm8_8x"] = {price = 1500},
|
||||
["optic_m1_scope"] = {price = 850},
|
||||
["optic_delisle_scope"] = {price = 950},
|
||||
-- Тактические устройства
|
||||
["tac_flashlight"] = {price = 200},
|
||||
["tac_laser"] = {price = 300},
|
||||
["tac_combo"] = {price = 450},
|
||||
["tac_dmic"] = {price = 500},
|
||||
["tac_rangefinder"] = {price = 1200},
|
||||
["tac_spreadgauge"] = {price = 800},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин дульных устройств
|
||||
PLUGIN.shops["muzzle"] = {
|
||||
name = "Дульные устройства",
|
||||
description = "Глушители, компенсаторы и тормоза",
|
||||
items = {
|
||||
-- Глушители
|
||||
["muzz_silencer"] = {price = 800},
|
||||
["muzz_supp_compact"] = {price = 750},
|
||||
["muzz_supp_weighted"] = {price = 750},
|
||||
["muzz_supp_pbs"] = {price = 850},
|
||||
["muzz_supp_assassin"] = {price = 1500},
|
||||
-- Компенсаторы
|
||||
["muzz_pistol_comp"] = {price = 500},
|
||||
["muzz_comp_mac10"] = {price = 550},
|
||||
["muzz_comp_usp"] = {price = 550},
|
||||
["muzz_ak_comp"] = {price = 600},
|
||||
["muzz_comp_io_m14"] = {price = 650},
|
||||
-- Стволы
|
||||
["muzz_hbar"] = {price = 600},
|
||||
["muzz_lbar"] = {price = 500},
|
||||
["muzz_ak_booster"] = {price = 550},
|
||||
-- Тормоза
|
||||
["muzz_brake_aggressor"] = {price = 600},
|
||||
["muzz_brake_breaching"] = {price = 700},
|
||||
["muzz_brake_concussive"] = {price = 700},
|
||||
-- Специфичные
|
||||
["muzz_tec9_shroud"] = {price = 550},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин рукояток и аксессуаров
|
||||
PLUGIN.shops["accessories"] = {
|
||||
name = "Рукоятки и аксессуары",
|
||||
description = "Рукоятки, приклады, сошки и другие аксессуары",
|
||||
items = {
|
||||
-- Рукоятки
|
||||
["acc_ergo"] = {price = 450},
|
||||
["acc_dual_ergo"] = {price = 550},
|
||||
["acc_dual_quickdraw"] = {price = 500},
|
||||
["acc_quickdraw"] = {price = 450},
|
||||
["acc_skel"] = {price = 400},
|
||||
["acc_dual_skel"] = {price = 650},
|
||||
-- Сошки и приклады
|
||||
["acc_bipod"] = {price = 500},
|
||||
["acc_brace"] = {price = 600},
|
||||
["acc_foldstock"] = {price = 600},
|
||||
["acc_foldstock2"] = {price = 550},
|
||||
["acc_cheekrest"] = {price = 400},
|
||||
["acc_pad"] = {price = 400},
|
||||
["acc_sling"] = {price = 350},
|
||||
-- Прочее
|
||||
["acc_conceal"] = {price = 300},
|
||||
["acc_duffelbag"] = {price = 800},
|
||||
["acc_extendedbelt"] = {price = 900},
|
||||
["acc_ak74_poly"] = {price = 500},
|
||||
-- Расширенные магазины
|
||||
["acc_extmag_pistol"] = {price = 500},
|
||||
["acc_extmag_pistol2"] = {price = 550},
|
||||
["acc_extmag_rifle"] = {price = 550},
|
||||
["acc_extmag_rifle2"] = {price = 600},
|
||||
["acc_extmag_smg"] = {price = 500},
|
||||
["acc_extmag_shotgun"] = {price = 600},
|
||||
["acc_extmag_shotgun_mag"] = {price = 700},
|
||||
["acc_extmag_shotgun_tube"] = {price = 650},
|
||||
["acc_extmag_dual"] = {price = 700},
|
||||
["acc_extmag_dual2"] = {price = 750},
|
||||
["acc_extmag_dualsmg"] = {price = 700},
|
||||
["acc_extmag_auto5"] = {price = 650},
|
||||
["acc_extmag_sniper"] = {price = 800},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин патронов (основные)
|
||||
PLUGIN.shops["ammo_basic"] = {
|
||||
name = "Боеприпасы - Основные",
|
||||
description = "Универсальные патроны для пистолетов, винтовок и дробовиков",
|
||||
items = {
|
||||
-- Универсальные
|
||||
["ammo_pistol_ap"] = {price = 600},
|
||||
["ammo_pistol_hollowpoints"] = {price = 500},
|
||||
["ammo_pistol_match"] = {price = 400},
|
||||
["ammo_pistol_headshot"] = {price = 1500},
|
||||
["ammo_rifle_match"] = {price = 450},
|
||||
["ammo_rifle_jhp"] = {price = 500},
|
||||
["ammo_magnum"] = {price = 700},
|
||||
["ammo_subsonic"] = {price = 550},
|
||||
["ammo_surplus"] = {price = 200},
|
||||
["ammo_tmj"] = {price = 450},
|
||||
-- Дробовик
|
||||
["ammo_shotgun_bird"] = {price = 300},
|
||||
["ammo_shotgun_slugs"] = {price = 500},
|
||||
["ammo_shotgun_slugs2"] = {price = 550},
|
||||
["ammo_shotgun_mag"] = {price = 550},
|
||||
["ammo_shotgun_breach"] = {price = 400},
|
||||
["ammo_shotgun_dragon"] = {price = 1100},
|
||||
["ammo_shotgun_frag"] = {price = 900},
|
||||
["ammo_shotgun_triple"] = {price = 1000},
|
||||
["ammo_shotgun_triple2"] = {price = 1100},
|
||||
["ammo_shotgun_minishell"] = {price = 600},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин патронов (специальные)
|
||||
PLUGIN.shops["ammo_special"] = {
|
||||
name = "Боеприпасы - Специальные",
|
||||
description = "Гранаты 40mm, RPG, Stinger и экзотические патроны",
|
||||
items = {
|
||||
-- 40mm гранаты
|
||||
["ammo_40mm_3gl"] = {price = 1200},
|
||||
["ammo_40mm_buck"] = {price = 800},
|
||||
["ammo_40mm_gas"] = {price = 900},
|
||||
["ammo_40mm_heat"] = {price = 1500},
|
||||
["ammo_40mm_impact"] = {price = 1000},
|
||||
["ammo_40mm_lvg"] = {price = 950},
|
||||
["ammo_40mm_ratshot"] = {price = 700},
|
||||
["ammo_40mm_smoke"] = {price = 600},
|
||||
["ammo_40mm_heal"] = {price = 2000},
|
||||
-- AMR патроны
|
||||
["ammo_amr_hv"] = {price = 1500},
|
||||
["ammo_amr_ratshot"] = {price = 1200},
|
||||
["ammo_amr_saphe"] = {price = 1800},
|
||||
-- RPG
|
||||
["ammo_rpg_improvised"] = {price = 800},
|
||||
["ammo_rpg_mortar"] = {price = 1400},
|
||||
["ammo_rpg_ratshot"] = {price = 1000},
|
||||
["ammo_rpg_harpoon"] = {price = 1100},
|
||||
-- Stinger ракеты
|
||||
["ammo_stinger_saam"] = {price = 2500},
|
||||
["ammo_stinger_qaam"] = {price = 2800},
|
||||
["ammo_stinger_4aam"] = {price = 3500},
|
||||
["ammo_stinger_apers"] = {price = 2000},
|
||||
-- Специфичные
|
||||
["ammo_ak12_762"] = {price = 600},
|
||||
["ammo_usp_9mm"] = {price = 700},
|
||||
["ammo_r700_300winmag"] = {price = 900},
|
||||
["ammo_star15_300blk"] = {price = 850},
|
||||
["ammo_star15_50beo"] = {price = 1200},
|
||||
["ammo_1858_45colt"] = {price = 750},
|
||||
["ammo_1858_36perc"] = {price = 600},
|
||||
-- Экзотические
|
||||
["ammo_roulette"] = {price = 3000},
|
||||
["ammo_buckshotroulette"] = {price = 2500},
|
||||
["ammo_ks23_flashbang"] = {price = 1200},
|
||||
["ammo_ks23_flashbang_top"] = {price = 1300},
|
||||
["ammo_gyrojet_ratshot"] = {price = 900},
|
||||
["ammo_gyrojet_pipe"] = {price = 1200},
|
||||
["ammo_gyrojet_lv"] = {price = 800},
|
||||
["ammo_gyrojet_he"] = {price = 1400},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин болтов и триггеров
|
||||
PLUGIN.shops["modifications"] = {
|
||||
name = "Модификации оружия",
|
||||
description = "Болты, триггеры и специальные модификации",
|
||||
items = {
|
||||
-- Болты
|
||||
["bolt_fine"] = {price = 500},
|
||||
["bolt_greased"] = {price = 400},
|
||||
["bolt_heavy"] = {price = 600},
|
||||
["bolt_light"] = {price = 450},
|
||||
["bolt_tactical"] = {price = 550},
|
||||
["bolt_refurbished"] = {price = 300},
|
||||
["bolt_rough"] = {price = 250},
|
||||
["bolt_surplus"] = {price = 200},
|
||||
["bolt_af2011_alt"] = {price = 800},
|
||||
-- Триггеры базовые
|
||||
["trigger_burst"] = {price = 600},
|
||||
["trigger_burst2"] = {price = 700},
|
||||
["trigger_burstauto"] = {price = 850},
|
||||
["trigger_semi"] = {price = 400},
|
||||
["trigger_comp"] = {price = 700},
|
||||
["trigger_comp2"] = {price = 800},
|
||||
["trigger_hair"] = {price = 650},
|
||||
["trigger_heavy"] = {price = 500},
|
||||
["trigger_heavy2"] = {price = 550},
|
||||
["trigger_straight"] = {price = 450},
|
||||
["trigger_wide"] = {price = 400},
|
||||
["trigger_wide2"] = {price = 500},
|
||||
["trigger_tactical"] = {price = 550},
|
||||
["trigger_slam"] = {price = 800},
|
||||
["trigger_slam2"] = {price = 900},
|
||||
["trigger_dualstage"] = {price = 750},
|
||||
["trigger_frcd"] = {price = 900},
|
||||
["trigger_frcd2"] = {price = 950},
|
||||
-- Триггеры специальные
|
||||
["trigger_akimbo"] = {price = 1500},
|
||||
["trigger_hair_akimbo"] = {price = 1600},
|
||||
["trigger_vp70_auto"] = {price = 1200},
|
||||
["trigger_vp70_semi"] = {price = 600},
|
||||
["trigger_dual_uzis_semi"] = {price = 1500},
|
||||
-- Прочее
|
||||
["toploader_stripper_clip"] = {price = 400},
|
||||
["tac_1858_spin"] = {price = 700},
|
||||
}
|
||||
}
|
||||
|
||||
-- Магазин перков
|
||||
PLUGIN.shops["perks"] = {
|
||||
name = "Навыки и перки",
|
||||
description = "Улучшения персонажа и холодного оружия",
|
||||
items = {
|
||||
-- Перки персонажа
|
||||
["perk_aim"] = {price = 1000},
|
||||
["perk_hipfire"] = {price = 800},
|
||||
["perk_reload"] = {price = 900},
|
||||
["perk_speed"] = {price = 850},
|
||||
["perk_melee"] = {price = 700},
|
||||
["perk_shock"] = {price = 950},
|
||||
["perk_throw"] = {price = 600},
|
||||
["perk_blindfire"] = {price = 750},
|
||||
["perk_mlg"] = {price = 2000},
|
||||
-- Улучшения холодного оружия
|
||||
["melee_boost_all"] = {price = 1500},
|
||||
["melee_boost_str"] = {price = 800},
|
||||
["melee_boost_agi"] = {price = 750},
|
||||
["melee_boost_int"] = {price = 700},
|
||||
["melee_boost_lifesteal"] = {price = 1200},
|
||||
["melee_boost_momentum"] = {price = 900},
|
||||
["melee_boost_afterimage"] = {price = 1000},
|
||||
["melee_boost_shock"] = {price = 1100},
|
||||
}
|
||||
}
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
-- МАГАЗИН РАСХОДНИКОВ (CONSUMABLES)
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
-- МАГАЗИН ПО УМОЛЧАНИЮ
|
||||
-- ═══════════════════════════════════════════════════════════
|
||||
|
||||
PLUGIN.shops["drones"] = {
|
||||
name = "Дроны",
|
||||
description = "Различные модели дронов и аксессуары к ним",
|
||||
items = {
|
||||
["swep_drone"] = {price = 30000},
|
||||
["swep_drone_grenade"] = {price = 25000},
|
||||
}
|
||||
}
|
||||
|
||||
PLUGIN.shops["consumables"] = {
|
||||
name = "Расходники",
|
||||
description = "Еда, напитки, медикаменты и прочие расходные материалы",
|
||||
items = {
|
||||
["bread"] = {price = 20},
|
||||
["canned"] = {price = 40},
|
||||
["milk"] = {price = 40},
|
||||
["sausage"] = {price = 35},
|
||||
["water"] = {price = 20},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
PLUGIN.name = "Shop System"
|
||||
PLUGIN.author = "RefoselTeamWork"
|
||||
PLUGIN.description = "Система магазинов с NPC"
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
ix.util.Include("sh_config.lua")
|
||||
170
garrysmod/gamemodes/militaryrp/plugins/shop/sv_plugin.lua
Normal file
170
garrysmod/gamemodes/militaryrp/plugins/shop/sv_plugin.lua
Normal file
@@ -0,0 +1,170 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
util.AddNetworkString("ixShop_OpenMenu")
|
||||
util.AddNetworkString("ixShop_BuyItem")
|
||||
util.AddNetworkString("ixShop_UpdateModel")
|
||||
util.AddNetworkString("ixShop_UpdateType")
|
||||
|
||||
ix.util.Include("sh_config.lua")
|
||||
|
||||
function PLUGIN:SaveData()
|
||||
local data = {}
|
||||
|
||||
for _, npc in ipairs(ents.FindByClass("ix_shop_npc")) do
|
||||
if IsValid(npc) then
|
||||
data[#data + 1] = {
|
||||
pos = npc:GetPos(),
|
||||
angles = npc:GetAngles(),
|
||||
model = npc:GetModel(),
|
||||
shopType = npc:GetShopType()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
self:SetData(data)
|
||||
end
|
||||
|
||||
function PLUGIN:LoadData()
|
||||
for _, v in ipairs(self:GetData() or {}) do
|
||||
local npc = ents.Create("ix_shop_npc")
|
||||
if IsValid(npc) then
|
||||
npc:SetPos(v.pos)
|
||||
npc:SetAngles(v.angles)
|
||||
npc:SetModel(v.model or "models/humans/group01/male_01.mdl")
|
||||
npc:Spawn()
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
if IsValid(npc) then
|
||||
npc:SetShopType(v.shopType or "general")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnUnloaded()
|
||||
self:SaveData()
|
||||
end
|
||||
|
||||
-- Обработка изменения типа магазина
|
||||
net.Receive("ixShop_UpdateType", function(len, client)
|
||||
if not client:IsAdmin() then return end
|
||||
|
||||
local npc = net.ReadEntity()
|
||||
local shopType = net.ReadString()
|
||||
|
||||
if not IsValid(npc) or npc:GetClass() != "ix_shop_npc" then return end
|
||||
if client:GetPos():Distance(npc:GetPos()) > 300 then return end
|
||||
|
||||
npc:SetShopType(shopType)
|
||||
PLUGIN:SaveData()
|
||||
|
||||
client:Notify("Тип магазина изменен на: " .. shopType)
|
||||
end)
|
||||
|
||||
-- Обработка покупки предмета
|
||||
net.Receive("ixShop_BuyItem", function(len, client)
|
||||
local shopType = net.ReadString()
|
||||
local itemID = net.ReadString()
|
||||
|
||||
local shopData = PLUGIN.shops[shopType]
|
||||
if not shopData or not shopData.items[itemID] then
|
||||
client:Notify("Предмет не найден!")
|
||||
return
|
||||
end
|
||||
|
||||
local itemData = shopData.items[itemID]
|
||||
local character = client:GetCharacter()
|
||||
|
||||
if not character then return end
|
||||
|
||||
-- Проверка денег
|
||||
if not character:HasMoney(itemData.price) then
|
||||
client:Notify("Недостаточно денег! Требуется: " .. itemData.price .. "₽")
|
||||
return
|
||||
end
|
||||
|
||||
-- Проверка наличия предмета в базе ix.item
|
||||
local ixItem = ix.item.list[itemID]
|
||||
if not ixItem then
|
||||
client:Notify("Предмет не существует в базе данных!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Попытка дать предмет
|
||||
if shopType == "drones" then
|
||||
local weaponClass = ixItem.class or itemID
|
||||
|
||||
-- Проверка, нет ли уже такого дрона у игрока
|
||||
if client:HasWeapon(weaponClass) then
|
||||
client:Notify("У вас уже есть этот дрон!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Если это магазин дронов, даем оружие напрямую (SWEP)
|
||||
client:Give(weaponClass)
|
||||
client:SelectWeapon(weaponClass)
|
||||
else
|
||||
-- Для остальных магазинов добавляем в инвентарь
|
||||
local inventory = character:GetInventory()
|
||||
|
||||
if not inventory or isnumber(inventory) then
|
||||
client:Notify("Инвентарь еще загружается, подождите немного...")
|
||||
return
|
||||
end
|
||||
|
||||
if not inventory:Add(itemID) then
|
||||
client:Notify("Недостаточно места в инвентаре!")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Списание денег
|
||||
character:TakeMoney(itemData.price)
|
||||
client:Notify("Вы купили " .. (ixItem.name or itemID) .. " за " .. itemData.price .. "₽")
|
||||
|
||||
-- Логирование покупки
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if serverlogsPlugin then
|
||||
local message = string.format("%s купил %s за %d₽ (магазин: %s)",
|
||||
client:GetName(), (ixItem.name or itemID), itemData.price, shopData.name)
|
||||
serverlogsPlugin:AddLog("SHOP_PURCHASE", message, client, {
|
||||
itemID = itemID,
|
||||
itemName = (ixItem.name or itemID),
|
||||
price = itemData.price,
|
||||
shopType = shopType
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Обработка изменения модели NPC
|
||||
net.Receive("ixShop_UpdateModel", function(len, client)
|
||||
if not client:IsAdmin() then return end
|
||||
|
||||
local npc = net.ReadEntity()
|
||||
local model = net.ReadString()
|
||||
|
||||
if not IsValid(npc) or npc:GetClass() != "ix_shop_npc" then return end
|
||||
if client:GetPos():Distance(npc:GetPos()) > 300 then return end
|
||||
|
||||
npc:SetModel(model)
|
||||
PLUGIN:SaveData()
|
||||
|
||||
client:Notify("Модель NPC изменена на: " .. model)
|
||||
end)
|
||||
|
||||
-- Команда для сохранения магазинов
|
||||
ix.command.Add("ShopSave", {
|
||||
description = "Сохранить все магазины",
|
||||
superAdminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
local plugin = ix.plugin.Get("shop")
|
||||
if not plugin then
|
||||
return "@commandNoExist"
|
||||
end
|
||||
|
||||
plugin:SaveData()
|
||||
local count = #ents.FindByClass("ix_shop_npc")
|
||||
client:Notify("Магазины сохранены (" .. count .. " шт.)")
|
||||
end
|
||||
})
|
||||
Reference in New Issue
Block a user